Need Help Making Collision with Ground Work Correctly

Yes.

Note that they are counted from 0. So the first vertex has index 0, the second vertex has index 1 and so on.

Excellent.

Now that that is clarified, I need help understanding in the code, how to access Sandy’s Transform to set the location of the vertices precisely at where I want them, to match the direction of them, because by default it won’t recognize SandyTransform because I didn’t define it.

I know in other examples I find, you do it like this:

procedure TEnemy.TakeDamageFromBullet(const Bullet: TCastleTransform);
begin
  SoundEngine.Play(NamedSound('HitEnemy'));
  Bullet.Exists := false;

  Dead := true;
  RBody.Exists := false;
end;

Where the important detail is specifying the thing you want to access from the scene, in parenthesis next to the function name.

However, I believe in this case that won’t work, because the RenderUnlitMesh procedures are pre-designed in a very specific way, as we went over earlier, and so it will most likely not work as the same function but rather be treated as something foreign/unknown, if I specify parameters for the CreateMesh function.

Is the solution perhaps to have another function nested inside, that I can define as my own/custom?

Don’t be upset, but I have the impression that this topic is still a little difficult for you at this moment. Visualizing the ray would be a really good thing but maybe it would take up a lot of your time.
Try to think carefully about the use of raycast.
You have a starting point, let’s say it’s the character’s feet, and you have a direction, which is perpendicularly downwards.
You want the character to have some animation while it is flying. while it is on the ground a different animation.
This is where MaxDistance comes to your aid. This parameter must be very very short (starting from the feet). Imagine a “one/two millimeter” line starting from your feet and going downwards.
In your Update() you will write that, if the ray collides with the floor, the animation will be that of Sandy on the ground, if instead the ray does not touch the floor (and this will happen while Sandy is in flight precisely because the ray is very short) the animation will be that of flight.

Basically you have to think of the final tip of the ray as if it were the center of the character’s feet but just slightly below them.
Normally, to work, the ray must come out of the transform, which is why I tell you very short. Try different mini-lengths, we’ll have to see how CGE handles the raycast.

Once you’re able to change the animation according to the character’s position, you’ll probably need to modify the logic of your code a bit.
I’m not sure how PlayAnimation works, but in your code it seems like you call it repeatedly. Could it be that you lose a few frames this way? It’s just a doubt to check, it could be that CGE completes it anyway before starting over from the first frame.

I understand that from earlier in the discussion; my point is about something totally unrelated to this. Namely, that I want to use the correct coordinates you specified like a good coder, but the way the code itself is organized makes it confusing to do.

Because for example, the code for the procedure for DrawVertices is pre-defined in a very specific way, and it doesn’t accept parameters normally.

My question then, is how to get it to accept custom parameters like Sandy’s coordinates that you and I both want? Is it by specifying a new procedure inside it with my own custom parameters? That’s what I assume.

Satisfy my curiosity. If you write:

RayCastResult := AvatarRigidBody.PhysicsRayCast(AvatarTransform.Translation, RayCastDirection, 0.3);

does Sandy have a flying animation when it jumps and idle when he returns to the ground?

I will try that, but I still need help with figuring out how to draw/display the vector correctly so it’s easier to debug, which is why I asked for help in the posts directly before your most recent one.

No luck unfortunately; it still plays the airborne pose while on the ground.

This is why I feel it’s better to be able to see the ray visually, like you were teaching me earlier; so that way I can see exactly what the problem is rather than just having to plug and play random numbers.

But again, my point was, although maybe it wasn’t clear at first,

was that I was concerned that I would get an error about using the wrong number of parameters/inputs if I specified inputs for the CreateMesh procedure in the code to render the mesh, given that there weren’t any,

But I can see now, from reading the API page for TCastleRenderUnlitMesh, that CreateMesh isn’t actually a pre-coded procedure with a specific number of parameters, but rather something the programmer seemed to make as their own custom procedure.

I will try specifying parameters so that way it can access the Sandy Transform in the scene directly in the code, and see what it does.

As mentioned, displaying the ray is a great choice.
Unfortunately, when you do, you’ll end up with the exact same problem.
Your code in Update() is wrong, I had already pointed it out to you, you replied that you understood it, but you left it as is.
Get into the habit of formatting your code, we’ve already said this too.
This way you can see your mistakes better.

What you are doing is saying that when SandyAirborne = 0 the animation should be that of flight.
But your incorrect coding means that after each raycast, in which you set SandyAirborne := 0, you check whether SandyAirborne = 0, and set the flight animation if it is. And since it is, you will always have the flying animation, even when the character hits the ground again.

Okay, now that makes sense, like you were intentional/on purpose about pointing out errors in the code that were more important than getting the ray to display. I will fix those as well.

It’s weird cause I thought I organized it correctly; could you please highlight the more exact snippet so that I can fix it more easily?

I am really sorry for asking for tons of clarification, but I just want to make sure that I can understand using concrete lines of code where the error actually is, rather than just guessing/trial and error,

Yeah, she should have Airborne set to 0 only if you are sucessfully touching the ground, and she should have airborne set to 1 if and only if you aren’t touching the ground; it shouldn’t be setting to 0 in both cases by the way the code is organized.

How did you get that? Again, showing me actual code snippets would help me identify the problem better.

Just post the correctly formatted raycast code and comment it to let me understand what you want to achieve.

Okay, I can format it using indents for readibility, if that is easier for you and more organized; that definitely makes sense.

Here it is, indented and formatted for easier readability: pay attention to how the “if-else” block is organized according to the rules of how it should be organized, but it doesn’t seem to work by what you are telling me, which is what doesn’t make sense.

   if RayCastResult.Hit then
           begin
  SandyAirborne := 0;
  LabelFps.Caption := 'FPS: PLACEHOLDER';
      if SandyIdling = 0 then
      begin
  SandyIdling := 1;
  SandyLegs.PlayAnimation('LEGS_IDLE', true);
  SandyTorso.PlayAnimation('TORSO_IDLE', true);
          end
     end
   else
  SandyIdling := 0;
          begin
  if SandyAirborne = 0 then
      begin
  SandyAirborne := 1;
  SandyTorso.PlayAnimation('TORSO_AIRBORNE', true);
  SandyLegs.PlayAnimation('LEGS_AIRBORNE', true);
     end;
  AvatarTransform.Move(MoveAmount, false, true);
  LabelFps.Caption := 'FPS: ' + Container.Fps.ToString
        end;

Notice how the “if-else” block has the conditional set up correctly according to the rules of how if-else conditionals usually work, but it doesn’t work in practice for whatever reason.

In particular, I set it up so that the airborne pose should only turn to 0 if you are touching the ground, and should not turn to 0 at all if you hit the “else” part and are not touching the ground.

Maybe let’s try formatting the code properly? :slightly_smiling_face:

if RayCastResult.Hit then
begin
  SandyAirborne := 0;
  LabelFps.Caption := 'FPS: PLACEHOLDER';
  if SandyIdling = 0 then
  begin
    SandyIdling := 1;
    SandyLegs.PlayAnimation('LEGS_IDLE', true);
    SandyTorso.PlayAnimation('TORSO_IDLE', true);
  end;
end
else
  SandyIdling := 0;
begin
  if SandyAirborne = 0 then
  begin
    SandyAirborne := 1;
    SandyTorso.PlayAnimation('TORSO_AIRBORNE', true);
    SandyLegs.PlayAnimation('LEGS_AIRBORNE', true);
  end;
  AvatarTransform.Move(MoveAmount, false, true);
  LabelFps.Caption := 'FPS: ' + Container.Fps.ToString
end;

Can you see the error now? Otherwise, you should review the use of begin and end blocks.

Oh, now I understand… it’s cause I ended the if block before actually touching the else conditonal properly. Now it all makes sense!

Now, even though this line is exactly the same as before, and I made sure that all my begin and end blocks are done in the correct number, I am told this line is an error/problem, and an invalid expression in particular, despite it being perfectly fine earlier without any changes:

function TViewPlay.Press(const Event: TInputPressRelease): Boolean;

Full code for reference, in case the problem is elsewhere like I still didn’t do the begin and end blocks immediately before it correctly:

{
  Copyright 2020-2023 Michalis Kamburelis.

  This file is part of "Castle Game Engine".

  "Castle Game Engine" is free software; see the file COPYING.txt,
  included in this distribution, for details about the copyright.

  "Castle Game Engine" is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

  ----------------------------------------------------------------------------
}

{ Main "playing game" view, where most of the game logic takes place. }
unit GameViewPlay;

{ Unit for GameMesh renderer features. }

{.$define CASTLE_UNFINISHED_CHANGE_TRANSFORMATION_BY_FORCE}

interface

uses Classes,
  CastleComponentSerialize, CastleUIControls, CastleControls,
  CastleKeysMouse, CastleViewport, CastleScene, CastleVectors, CastleCameras,
  CastleTransform, CastleInputs, CastleThirdPersonNavigation, CastleDebugTransform,
  CastleSceneCore, SysUtils, CastleRenderPrimitives, CastleBoxes,
  GameEnemy;

type

  { Main "playing game" view, where most of the game logic takes place. }
  TViewPlay = class(TCastleView)
  published
    { Components designed using CGE editor.
      These fields will be automatically initialized at Start. }
    LabelFps: TCastleLabel;
    MainViewport: TCastleViewport;
    AvatarTransform: TCastleTransform;
    SandyLegs, SandyHead, SandyTorso, SceneLevel: TCastleScene;
    AvatarRigidBody: TCastleRigidBody;
    CheckboxCameraFollows: TCastleCheckbox;
    CheckboxAimAvatar: TCastleCheckbox;
    CheckboxDebugAvatarColliders: TCastleCheckbox;
    CheckboxImmediatelyFixBlockedCamera: TCastleCheckbox;
    SliderAirRotationControl: TCastleFloatSlider;
    SliderAirMovementControl: TCastleFloatSlider;
    ButtonChangeTransformationAuto,
      ButtonChangeTransformationDirect,
      ButtonChangeTransformationVelocity,
      ButtonChangeTransformationForce: TCastleButton;
  private
    { Enemies behaviors }
    Enemies: TEnemyList;

    DebugAvatar: TDebugTransform;
    { Change things after ThirdPersonNavigation.ChangeTransformation changed. }
    procedure UpdateAfterChangeTransformation;
    procedure ChangeCheckboxCameraFollows(Sender: TObject);
    procedure ChangeCheckboxAimAvatar(Sender: TObject);
    procedure ChangeCheckboxDebugAvatarColliders(Sender: TObject);
    procedure ChangeCheckboxImmediatelyFixBlockedCamera(Sender: TObject);
    procedure ChangeAirRotationControl(Sender: TObject);
    procedure ChangeAirMovementControl(Sender: TObject);
    procedure ClickChangeTransformationAuto(Sender: TObject);
    procedure ClickChangeTransformationDirect(Sender: TObject);
    procedure ClickChangeTransformationVelocity(Sender: TObject);
    procedure ClickChangeTransformationForce(Sender: TObject);
  public
    constructor Create(AOwner: TComponent); override;
    procedure Start; override;
    procedure Stop; override;
    procedure Update(const SecondsPassed: Single; var HandleInput: Boolean); override;
    function Press(const Event: TInputPressRelease): Boolean; override;
    function Release(const Event: TInputPressRelease): Boolean; override;
  end;

  TMyThirdPersonNavigation = class(TCastleThirdPersonNavigation)
  protected
    procedure SetAnimation(const AnimationNames: array of String); override;
  end;

var
  ViewPlay: TViewPlay;
  MyThirdPersonNavigation: TMyThirdPersonNavigation;
  GroundRayCast: TPhysicsRayCastResult;
  StandOnGround: Boolean;
  SandyInRunning: Boolean;
  MaxDistance: Integer;

implementation

uses Math, StrUtils, CastleRenderContext, CastleColors,
  CastleSoundEngine, CastleLog, CastleStringUtils, CastleFilesUtils,
  GameViewMenu;

{ TViewPlay ----------------------------------------------------------------- }

constructor TViewPlay.Create(AOwner: TComponent);
begin
  inherited;
  DesignUrl := 'castle-data:/gameviewplay.castle-user-interface';
end;

procedure TMyThirdPersonNavigation.SetAnimation(const AnimationNames: array of String);
begin

end;

procedure TViewPlay.Start;
var
  SoldierScene: TCastleScene;
  Enemy: TEnemy;
  I: Integer;
begin
  inherited;
  MyThirdPersonNavigation := TMyThirdPersonNavigation.Create(FreeAtStop);
  { Create TEnemy instances, add them to Enemies list }
  Enemies := TEnemyList.Create(true);
  for I := 1 to 4 do
  begin
    SoldierScene := DesignedComponent('SceneSoldier' + IntToStr(I)) as TCastleScene;
    { Below using nil as Owner of TEnemy, as the Enemies list already "owns"
      instances of this class, i.e. it will free them. }
    Enemy := TEnemy.Create(nil);
    SoldierScene.AddBehavior(Enemy);
    Enemies.Add(Enemy);
  end;

  { synchronize state -> UI }
  SliderAirRotationControl.Value := MyThirdPersonNavigation.AirRotationControl;
  SliderAirMovementControl.Value := MyThirdPersonNavigation.AirMovementControl;
  UpdateAfterChangeTransformation;

  CheckboxCameraFollows.OnChange := {$ifdef FPC}@{$endif} ChangeCheckboxCameraFollows;
  CheckboxAimAvatar.OnChange := {$ifdef FPC}@{$endif} ChangeCheckboxAimAvatar;
  CheckboxDebugAvatarColliders.OnChange := {$ifdef FPC}@{$endif} ChangeCheckboxDebugAvatarColliders;
  CheckboxImmediatelyFixBlockedCamera.OnChange := {$ifdef FPC}@{$endif} ChangeCheckboxImmediatelyFixBlockedCamera;
  SliderAirRotationControl.OnChange := {$ifdef FPC}@{$endif} ChangeAirRotationControl;
  SliderAirMovementControl.OnChange := {$ifdef FPC}@{$endif} ChangeAirMovementControl;
  ButtonChangeTransformationAuto.OnClick := {$ifdef FPC}@{$endif} ClickChangeTransformationAuto;
  ButtonChangeTransformationDirect.OnClick := {$ifdef FPC}@{$endif} ClickChangeTransformationDirect;
  ButtonChangeTransformationVelocity.OnClick := {$ifdef FPC}@{$endif} ClickChangeTransformationVelocity;
  ButtonChangeTransformationForce.OnClick := {$ifdef FPC}@{$endif} ClickChangeTransformationForce;

  {$ifndef CASTLE_UNFINISHED_CHANGE_TRANSFORMATION_BY_FORCE}
  { Hide UI to test ChangeTransformation = ctForce, it is not finished now,
    not really useful for normal usage. }
  ButtonChangeTransformationForce.Exists := false;
  {$endif}

  { This configures SceneAvatar.Middle point, used for shooting.
    In case of old physics (ChangeTransformation = ctDirect) this is also the center
    of SceneAvatar.CollisionSphereRadius. }
  // SceneAvatar.MiddleHeight := 0.9;

  { Configure some parameters of old simple physics,
    these only matter when SceneAvatar.Gravity = true.
    Don't use these deprecated things if you don't plan to use ChangeTransformation = ctDirect! }
  // SceneAvatar.GrowSpeed := 10.0;
  // SceneAvatar.FallSpeed := 10.0;
  { When avatar collides as sphere it can climb stairs,
    because legs can temporarily collide with objects. }
  // SceneAvatar.CollisionSphereRadius := 0.5;

  { Visualize SceneAvatar bounding box, sphere, middle point, direction etc. }
  DebugAvatar := TDebugTransform.Create(FreeAtStop);
  // DebugAvatar.Parent := SceneAvatar;

  { Configure MyThirdPersonNavigation keys (for now, we don't expose doing this in CGE editor). }
  MyThirdPersonNavigation.Input_LeftStrafe.Assign(keyQ);
  MyThirdPersonNavigation.Input_RightStrafe.Assign(keyE);
  MyThirdPersonNavigation.MouseLook := true; // TODO: assigning it from editor doesn't make mouse hidden in mouse look
  MyThirdPersonNavigation.Init;
  MyThirdPersonNavigation.AvatarHierarchy := AvatarTransform;
  SandyLegs.AutoAnimation := 'LEGS_IDLE';
  SandyTorso.AutoAnimation := 'TORSO_IDLE';
end;

procedure TViewPlay.Stop;
begin
  FreeAndNil(Enemies);
  inherited;
end;

procedure TViewPlay.Update(const SecondsPassed: Single; var HandleInput: Boolean);

  // Test: use this to make AimAvatar only when *holding* right mouse button.
  (*
  procedure UpdateAimAvatar;
  begin
    if buttonRight in Container.MousePressed then
      ThirdPersonNavigation.AimAvatar := aaHorizontal
    else
      ThirdPersonNavigation.AimAvatar := aaNone;

    { In this case CheckboxAimAvatar only serves to visualize whether
      the right mouse button is pressed now. }
    CheckboxAimAvatar.Checked := ThirdPersonNavigation.AimAvatar <> aaNone;
  end;
  *)

var
  SandyAnims: array of String;
  RayCastResult: TPhysicsRayCastResult;
  MoveAmount: TVector3;
  RayCastDirection: TVector3;
  MaxDistance: Single;
  SandyAirborne: Integer;
  SandyIdling: Integer;
begin
  inherited;
  { This virtual method is executed every frame (many times per second). }
  SandyIdling := 0;
  SandyAirborne := 0;
  MaxDistance := 0.3;
  MoveAmount := Vector3(0, -5, 0);
  RayCastDirection := Vector3(0, -1, 0);
  RayCastResult := AvatarRigidBody.PhysicsRayCast(
  (AvatarTransform.Translation),
  RayCastDirection,
  0.3
  );
  if RayCastResult.Hit then
  begin
  SandyAirborne := 0;
  LabelFps.Caption := 'FPS: PLACEHOLDER';
      if SandyIdling = 0 then
      begin
  SandyIdling := 1;
  SandyLegs.PlayAnimation('LEGS_IDLE', true);
  SandyTorso.PlayAnimation('TORSO_IDLE', true);
      end
  else
  SandyIdling := 0;
  begin
    if SandyAirborne = 0 then
    begin
  SandyAirborne := 1;
  SandyTorso.PlayAnimation('TORSO_AIRBORNE', true);
  SandyLegs.PlayAnimation('LEGS_AIRBORNE', true);
    end;
  AvatarTransform.Move(MoveAmount, false, true);
  LabelFps.Caption := 'FPS: ' + Container.Fps.ToString
  end;
  // UpdateAimAvatar;
end;

function TViewPlay.Press(const Event: TInputPressRelease): Boolean;
var
  HitByAvatar: TCastleTransform;
  HitEnemy: TEnemy;
begin
  Result := inherited;
  if Result then Exit; // allow the ancestor to handle keys

  { This virtual method is executed when user presses
    a key, a mouse button, or touches a touch-screen.

    Note that each UI control has also events like OnPress and OnClick.
    These events can be used to handle the "press", if it should do something
    specific when used in that UI control.
    The TViewPlay.Press method should be used to handle keys
    not handled in children controls.
  }

  if Event.IsMouseButton(buttonLeft) then
  begin
    SoundEngine.Play(SoundEngine.SoundFromName('shoot_sound'));
  end;

  if Event.IsMouseButton(buttonRight) then
  begin
    MyThirdPersonNavigation.MouseLook := not MyThirdPersonNavigation.MouseLook;
    Exit(true);
  end;

  if Event.IsKey(keyF5) then
  begin
    Container.SaveScreenToDefaultFile;
    Exit(true);
  end;

  if Event.IsKey(keyEscape) then
  begin
    Container.View := ViewMenu;
    Exit(true);
  end;

  if (Event.IsKey(keyArrowUp) and SandyInRunning = false) then
  begin
    SandyInRunning := true;
    SandyLegs.PlayAnimation('LEGS_RUN', true);
    SandyTorso.PlayAnimation('TORSO_RUN', true);
  end;
end;

function TViewPlay.Release(const Event: TInputPressRelease): Boolean;
begin
  Result := inherited;
  if Result then Exit; // allow the ancestor to handle keys

  { This virtual method is executed when user releases
    a key, a mouse button, or touches a touch-screen.

    Note that each UI control has also events like OnPress and OnClick.
    These events can be used to handle the "press", if it should do something
    specific when used in that UI control.
    The TViewPlay.Release method should be used to handle keys
    not handled in children controls.
  }

  if (Event.IsKey(keyArrowUp) and SandyInRunning = true) then
  begin
    SandyInRunning := false;
    SandyLegs.PlayAnimation('LEGS_IDLE', true);
    SandyTorso.PlayAnimation('TORSO_IDLE', true);
  end;
end;

procedure TViewPlay.ChangeCheckboxCameraFollows(Sender: TObject);
begin
  MyThirdPersonNavigation.CameraFollows := CheckboxCameraFollows.Checked;
end;

procedure TViewPlay.ChangeCheckboxAimAvatar(Sender: TObject);
begin
  if CheckboxAimAvatar.Checked then
    MyThirdPersonNavigation.AimAvatar := aaHorizontal
  else
    MyThirdPersonNavigation.AimAvatar := aaNone;

  { The 3rd option, aaFlying, doesn't make sense for this case,
    when avatar walks on the ground and has Gravity = true. }
end;

procedure TViewPlay.ChangeCheckboxDebugAvatarColliders(Sender: TObject);
begin
  DebugAvatar.Exists := CheckboxDebugAvatarColliders.Checked;
end;

procedure TViewPlay.ChangeCheckboxImmediatelyFixBlockedCamera(Sender: TObject);
begin
  MyThirdPersonNavigation.ImmediatelyFixBlockedCamera := CheckboxImmediatelyFixBlockedCamera.Checked;
end;

procedure TViewPlay.ChangeAirRotationControl(Sender: TObject);
begin
  MyThirdPersonNavigation.AirRotationControl := SliderAirRotationControl.Value;
end;

procedure TViewPlay.ChangeAirMovementControl(Sender: TObject);
begin
  MyThirdPersonNavigation.AirMovementControl := SliderAirMovementControl.Value;
end;

procedure TViewPlay.UpdateAfterChangeTransformation;
begin
  ButtonChangeTransformationAuto.Pressed := MyThirdPersonNavigation.ChangeTransformation = ctAuto;
  ButtonChangeTransformationDirect.Pressed := MyThirdPersonNavigation.ChangeTransformation =  ctDirect;
  ButtonChangeTransformationVelocity.Pressed := MyThirdPersonNavigation.ChangeTransformation =  ctVelocity;
  {$ifdef CASTLE_UNFINISHED_CHANGE_TRANSFORMATION_BY_FORCE}
  ButtonChangeTransformationForce.Pressed := ThirdPersonNavigation.ChangeTransformation = ctForce;
  {$endif}

  { ctDirect requires to set up gravity without physics engine,
    using deprecated TCastleTransform.Gravity.
    See https://castle-engine.io/physics#_old_system_for_collisions_and_gravity }
  AvatarRigidBody.Exists := MyThirdPersonNavigation.ChangeTransformation <> ctDirect;

  { Gravity means that object tries to maintain a constant height
    (SceneAvatar.PreferredHeight) above the ground.
    GrowSpeed means that object raises properly (makes walking up the stairs work).
    FallSpeed means that object falls properly (makes walking down the stairs,
    falling down pit etc. work). }
  SandyTorso.Gravity := not AvatarRigidBody.Exists;
  SandyLegs.Gravity := not AvatarRigidBody.Exists;
  SandyHead.Gravity := not AvatarRigidBody.Exists;
end;

procedure TViewPlay.ClickChangeTransformationAuto(Sender: TObject);
begin
  MyThirdPersonNavigation.ChangeTransformation := ctAuto;
  UpdateAfterChangeTransformation;
end;

procedure TViewPlay.ClickChangeTransformationDirect(Sender: TObject);
begin
  MyThirdPersonNavigation.ChangeTransformation := ctDirect;
  UpdateAfterChangeTransformation;
end;

procedure TViewPlay.ClickChangeTransformationVelocity(Sender: TObject);
begin
  MyThirdPersonNavigation.ChangeTransformation := ctVelocity;
  UpdateAfterChangeTransformation;
end;

procedure TViewPlay.ClickChangeTransformationForce(Sender: TObject);
begin
  {$ifdef CASTLE_UNFINISHED_CHANGE_TRANSFORMATION_BY_FORCE}
  ThirdPersonNavigation.ChangeTransformation := ctForce;
  {$endif}
  UpdateAfterChangeTransformation;
end;

end.

The error is obvious, but forgive me, I will not respond to you again until you post properly formatted code. It doesn’t help me to understand your code, but it helps you to understand where you go wrong. Good luck.

Okay, I will do that. Silly me for not formatting this piece of code correctly, cause I know obviously it’s a good habit to get into to understand it better, like you were saying the first time.

Okay, here is not all the code, but only the impotant parts up to the error, this time with the code formatted according to how you did it for easier readibility:

{
  Copyright 2020-2023 Michalis Kamburelis.

  This file is part of "Castle Game Engine".

  "Castle Game Engine" is free software; see the file COPYING.txt,
  included in this distribution, for details about the copyright.

  "Castle Game Engine" is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

  ----------------------------------------------------------------------------
}

{ Main "playing game" view, where most of the game logic takes place. }
unit GameViewPlay;

{ Unit for GameMesh renderer features. }

{.$define CASTLE_UNFINISHED_CHANGE_TRANSFORMATION_BY_FORCE}

interface

uses Classes,
  CastleComponentSerialize, CastleUIControls, CastleControls,
  CastleKeysMouse, CastleViewport, CastleScene, CastleVectors, CastleCameras,
  CastleTransform, CastleInputs, CastleThirdPersonNavigation, CastleDebugTransform,
  CastleSceneCore, SysUtils, CastleRenderPrimitives, CastleBoxes,
  GameEnemy;

type

  { Main "playing game" view, where most of the game logic takes place. }
  TViewPlay = class(TCastleView)
  published
    { Components designed using CGE editor.
      These fields will be automatically initialized at Start. }
    LabelFps: TCastleLabel;
    MainViewport: TCastleViewport;
    AvatarTransform: TCastleTransform;
    SandyLegs, SandyHead, SandyTorso, SceneLevel: TCastleScene;
    AvatarRigidBody: TCastleRigidBody;
    CheckboxCameraFollows: TCastleCheckbox;
    CheckboxAimAvatar: TCastleCheckbox;
    CheckboxDebugAvatarColliders: TCastleCheckbox;
    CheckboxImmediatelyFixBlockedCamera: TCastleCheckbox;
    SliderAirRotationControl: TCastleFloatSlider;
    SliderAirMovementControl: TCastleFloatSlider;
    ButtonChangeTransformationAuto,
    ButtonChangeTransformationDirect,
    ButtonChangeTransformationVelocity,
    ButtonChangeTransformationForce: TCastleButton;
  private
    { Enemies behaviors }
    Enemies: TEnemyList;

    DebugAvatar: TDebugTransform;
    { Change things after ThirdPersonNavigation.ChangeTransformation changed. }
    procedure UpdateAfterChangeTransformation;
    procedure ChangeCheckboxCameraFollows(Sender: TObject);
    procedure ChangeCheckboxAimAvatar(Sender: TObject);
    procedure ChangeCheckboxDebugAvatarColliders(Sender: TObject);
    procedure ChangeCheckboxImmediatelyFixBlockedCamera(Sender: TObject);
    procedure ChangeAirRotationControl(Sender: TObject);
    procedure ChangeAirMovementControl(Sender: TObject);
    procedure ClickChangeTransformationAuto(Sender: TObject);
    procedure ClickChangeTransformationDirect(Sender: TObject);
    procedure ClickChangeTransformationVelocity(Sender: TObject);
    procedure ClickChangeTransformationForce(Sender: TObject);
  public
    constructor Create(AOwner: TComponent); override;
    procedure Start; override;
    procedure Stop; override;
    procedure Update(const SecondsPassed: Single; var HandleInput: Boolean); override;
    function Press(const Event: TInputPressRelease): Boolean; override;
    function Release(const Event: TInputPressRelease): Boolean; override;
  end;

  TMyThirdPersonNavigation = class(TCastleThirdPersonNavigation)
  protected
    procedure SetAnimation(const AnimationNames: array of String); override;
  end;

var
  ViewPlay: TViewPlay;
  MyThirdPersonNavigation: TMyThirdPersonNavigation;
  GroundRayCast: TPhysicsRayCastResult;
  StandOnGround: Boolean;
  SandyInRunning: Boolean;
  MaxDistance: Integer;

implementation

uses Math, StrUtils, CastleRenderContext, CastleColors,
  CastleSoundEngine, CastleLog, CastleStringUtils, CastleFilesUtils,
  GameViewMenu;

{ TViewPlay ----------------------------------------------------------------- }

constructor TViewPlay.Create(AOwner: TComponent);
begin
  inherited;
  DesignUrl := 'castle-data:/gameviewplay.castle-user-interface';
end;

procedure TMyThirdPersonNavigation.SetAnimation(const AnimationNames: array of String);
begin

end;

procedure TViewPlay.Start;
var
  SoldierScene: TCastleScene;
  Enemy: TEnemy;
  I: Integer;
begin
  inherited;
  MyThirdPersonNavigation := TMyThirdPersonNavigation.Create(FreeAtStop);
  { Create TEnemy instances, add them to Enemies list }
  Enemies := TEnemyList.Create(true);
     for I := 1 to 4 do
     begin
    SoldierScene := DesignedComponent('SceneSoldier' + IntToStr(I)) as TCastleScene;
    { Below using nil as Owner of TEnemy, as the Enemies list already "owns"
      instances of this class, i.e. it will free them. }
    Enemy := TEnemy.Create(nil);
    SoldierScene.AddBehavior(Enemy);
    Enemies.Add(Enemy);
     end;

  { synchronize state -> UI }
  SliderAirRotationControl.Value := MyThirdPersonNavigation.AirRotationControl;
  SliderAirMovementControl.Value := MyThirdPersonNavigation.AirMovementControl;
  UpdateAfterChangeTransformation;

  CheckboxCameraFollows.OnChange := {$ifdef FPC}@{$endif} ChangeCheckboxCameraFollows;
  CheckboxAimAvatar.OnChange := {$ifdef FPC}@{$endif} ChangeCheckboxAimAvatar;
  CheckboxDebugAvatarColliders.OnChange := {$ifdef FPC}@{$endif} ChangeCheckboxDebugAvatarColliders;
  CheckboxImmediatelyFixBlockedCamera.OnChange := {$ifdef FPC}@{$endif} ChangeCheckboxImmediatelyFixBlockedCamera;
  SliderAirRotationControl.OnChange := {$ifdef FPC}@{$endif} ChangeAirRotationControl;
  SliderAirMovementControl.OnChange := {$ifdef FPC}@{$endif} ChangeAirMovementControl;
  ButtonChangeTransformationAuto.OnClick := {$ifdef FPC}@{$endif} ClickChangeTransformationAuto;
  ButtonChangeTransformationDirect.OnClick := {$ifdef FPC}@{$endif} ClickChangeTransformationDirect;
  ButtonChangeTransformationVelocity.OnClick := {$ifdef FPC}@{$endif} ClickChangeTransformationVelocity;
  ButtonChangeTransformationForce.OnClick := {$ifdef FPC}@{$endif} ClickChangeTransformationForce;

  {$ifndef CASTLE_UNFINISHED_CHANGE_TRANSFORMATION_BY_FORCE}
  { Hide UI to test ChangeTransformation = ctForce, it is not finished now,
    not really useful for normal usage. }
  ButtonChangeTransformationForce.Exists := false;
  {$endif}

  { This configures SceneAvatar.Middle point, used for shooting.
    In case of old physics (ChangeTransformation = ctDirect) this is also the center
    of SceneAvatar.CollisionSphereRadius. }
  // SceneAvatar.MiddleHeight := 0.9;

  { Configure some parameters of old simple physics,
    these only matter when SceneAvatar.Gravity = true.
    Don't use these deprecated things if you don't plan to use ChangeTransformation = ctDirect! }
  // SceneAvatar.GrowSpeed := 10.0;
  // SceneAvatar.FallSpeed := 10.0;
  { When avatar collides as sphere it can climb stairs,
    because legs can temporarily collide with objects. }
  // SceneAvatar.CollisionSphereRadius := 0.5;

  { Visualize SceneAvatar bounding box, sphere, middle point, direction etc. }
  DebugAvatar := TDebugTransform.Create(FreeAtStop);
  // DebugAvatar.Parent := SceneAvatar;

  { Configure MyThirdPersonNavigation keys (for now, we don't expose doing this in CGE editor). }
  MyThirdPersonNavigation.Input_LeftStrafe.Assign(keyQ);
  MyThirdPersonNavigation.Input_RightStrafe.Assign(keyE);
  MyThirdPersonNavigation.MouseLook := true; // TODO: assigning it from editor doesn't make mouse hidden in mouse look
  MyThirdPersonNavigation.Init;
  MyThirdPersonNavigation.AvatarHierarchy := AvatarTransform;
  SandyLegs.AutoAnimation := 'LEGS_IDLE';
  SandyTorso.AutoAnimation := 'TORSO_IDLE';
end;

procedure TViewPlay.Stop;
begin
  FreeAndNil(Enemies);
  inherited;
end;

procedure TViewPlay.Update(const SecondsPassed: Single; var HandleInput: Boolean);

  // Test: use this to make AimAvatar only when *holding* right mouse button.
  (*
  procedure UpdateAimAvatar;
  begin
    if buttonRight in Container.MousePressed then
      ThirdPersonNavigation.AimAvatar := aaHorizontal
    else
      ThirdPersonNavigation.AimAvatar := aaNone;

    { In this case CheckboxAimAvatar only serves to visualize whether
      the right mouse button is pressed now. }
    CheckboxAimAvatar.Checked := ThirdPersonNavigation.AimAvatar <> aaNone;
  end;
  *)

var
  SandyAnims: array of String;
  RayCastResult: TPhysicsRayCastResult;
  MoveAmount: TVector3;
  RayCastDirection: TVector3;
  MaxDistance: Single;
  SandyAirborne: Integer;
  SandyIdling: Integer;
begin
  inherited;
  { This virtual method is executed every frame (many times per second). }
  SandyIdling := 0;
  SandyAirborne := 0;
  MaxDistance := 0.3;
  MoveAmount := Vector3(0, -5, 0);
  RayCastDirection := Vector3(0, -1, 0);
  RayCastResult := AvatarRigidBody.PhysicsRayCast(
  (AvatarTransform.Translation),
  RayCastDirection,
  0.3
  );
  if RayCastResult.Hit then
  begin
  SandyAirborne := 0;
  LabelFps.Caption := 'FPS: PLACEHOLDER';
      if SandyIdling = 0 then
      begin
  SandyIdling := 1;
  SandyLegs.PlayAnimation('LEGS_IDLE', true);
  SandyTorso.PlayAnimation('TORSO_IDLE', true);
      end
  else
  SandyIdling := 0;
  begin
    if SandyAirborne = 0 then
    begin
  SandyAirborne := 1;
  SandyTorso.PlayAnimation('TORSO_AIRBORNE', true);
  SandyLegs.PlayAnimation('LEGS_AIRBORNE', true);
    end;
  AvatarTransform.Move(MoveAmount, false, true);
  LabelFps.Caption := 'FPS: ' + Container.Fps.ToString
  end;
  // UpdateAimAvatar;
end;

function TViewPlay.Press(const Event: TInputPressRelease): Boolean;

I still cannot for the life of me figure out why “function TViewPlay.Press(const Event: TInputPressRelease): Boolean;” should be considered invalid.