MD3 - improve animations support

I was trying (unsuccesfully) to make a case/point out that reading the documents and examples doesn’t help here, because the details were unclear on the page for the thing I wanted to use (TCastleMouseNavigation specifically and not TThirdPersonNavigation).

I already have TCastleThirdPersonNavigation as my “uses” unit, and yet it still tells me that TCastleMouseNavigation, what I am assigning, is unidentified which is what I am arguing makes no sense if you read the manual on how classes and units work.

To add to this, I understand you’re telling me to start smaller because it seems I don’t understand the basics, but believe me I know already that a procedure is a group of functions, and that the uses unit is what actually makes the code readable, cause I have experience coding so it’s nothing I haven’t seen before; it’s just that the exact details that the manual does not go over are weird and incosistent, which has been my entire point all this time.

On top of that, starting smaller will not help me achieve my goal of making a third person character, as the third person navigation stuff is not covered by the smaller projects, and it is those details I need help with and not the basics. Do my points make more sense to you now?

It’s exactly like I said a few posts ago: we want to immediately see the results of the work we have set ourselves, but this prevents us from doing it well :wink:
We each have our own unique way of learning something. I’ll tell you which one is mine, which isn’t said to be the right one, but it has always brought me luck.

I open an example, tested and definitely working, and then I start modifying it. I add little things, I remove other things, step by step, checking if I’m in the right direction.
In the end, little of the starting code remains, and the example has a completely different aspect, namely the one I wanted to create from scratch.

Open for example third_person_camera, take a look at the .pas files and then start eliminating a single enemy. When your edit is successful delete them all and test again. Then ask yourself: will it be possible to replace the default character with my own? Are the models the same? If yes, you do, otherwise you try to add your character to the scene and move commands to it.

I repeat, this way of improving one’s programming skill brings good luck :crossed_fingers:

I don’t know if the demo I suggested will be suitable for your purposes, but maybe michalis will be able to suggest the ideal demo.

1 Like

Okay, I was stuck on the idea that the examples in the manual didn’t have third person camera stuff explcitily, but your suggestion makes more sense. I will try that example project instead and see how well it works.

And now strangely, the definition of the class of MyThirdPersonNavigation = class(TMouseLookNavigation) works now, even though it didn’t earlier. I am assuming because the third person navigation has units under the “uses” clause that were not covered on the API page,

but again this issue had nothing to do with my ignorance of programming, but rather the fact that the page did not give you all the units that TCastleMouseLookNavigation used, and only TThirdPersonNavigation which was obviously not enough the first time.

Unfortunately, now I am getting the old errors about class override not being accepted, even though I am using the latest version and the same exact line for overriding the code worked earlier.

Here is the error:

And here is the full code, although I mostly pasted the old code to make typing faster, I made sure to check that it matches A: the examples in the manual and B: what other code looks like in the current project (for example, follow the formatting that TViewMain uses, which I am pretty sure is a legitimate way to learn/improve your skills).

{
  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;

{.$define CASTLE_UNFINISHED_CHANGE_TRANSFORMATION_BY_FORCE}

interface

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

type
  TMyThirdPersonNavigation = class(TCastleMouseLookNavigation)
  protected
    procedure SetAnimation(const AnimationNames: array of String); override;
    procedure AssignAvatar;
  published
    ExampleLegsScene, HumanBase1, ExampleTorsoScene: TCastleScene;
  end;

  { 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;
    ThirdPersonNavigation: TCastleThirdPersonNavigation;
    SceneAvatar, 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;
  end;

var
  ViewPlay: TViewPlay;

implementation

uses SysUtils, Math, StrUtils,
  CastleSoundEngine, CastleLog, CastleStringUtils, CastleFilesUtils, CastleUtils,
  GameViewMenu;

{ TMyThirdPersonNavigation ---------------------------------------------------- }

procedure TMyThirdPersonNavigation.SetAnimation(const AnimationNames: array of String);
begin
  if AnimationNames[0] = 'idle' then
  begin
    ExampleLegsScene.PlayAnimation('idle', true);
    ExampleTorsoScene.PlayAnimation('idle', true);
  end else
  if AnimationNames[0] = 'walk' then
  begin
    ExampleLegsScene.PlayAnimation('walk', true);
    ExampleTorsoScene.PlayAnimation('walk', true);
  end;
end;

procedure TMyThirdPersonNavigation.AssignAvatar;
begin
  TMyThirdPersonNavigation:AvatarHierarchy := HumanBase1;
end;

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

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

procedure TViewPlay.Start;
var
  SoldierScene: TCastleScene;
  Enemy: TEnemy;
  I: Integer;
begin
  inherited;

  { 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 := ThirdPersonNavigation.AirRotationControl;
  SliderAirMovementControl.Value := ThirdPersonNavigation.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 ThirdPersonNavigation keys (for now, we don't expose doing this in CGE editor). }
  ThirdPersonNavigation.Input_LeftStrafe.Assign(keyQ);
  ThirdPersonNavigation.Input_RightStrafe.Assign(keyE);
  ThirdPersonNavigation.MouseLook := true; // TODO: assigning it from editor doesn't make mouse hidden in mouse look
  ThirdPersonNavigation.Init;
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;
  *)

begin
  inherited;
  { This virtual method is executed every frame (many times per second). }
  LabelFps.Caption := 'FPS: ' + Container.Fps.ToString;
  // UpdateAimAvatar;
end;

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

  function AvatarRayCast: TCastleTransform;
  var
    RayCastResult: TPhysicsRayCastResult;
  begin
    RayCastResult := AvatarRigidBody.PhysicsRayCast(
      SceneAvatar.Middle,
      SceneAvatar.Direction
    );
    Result := RayCastResult.Transform;

    { Alternative version, using Items.PhysicsRayCast
      (everything in world space coordinates).
      This works equally well, showing it here just for reference.

    RayCastResult := MainViewport.Items.PhysicsRayCast(
      SceneAvatar.Parent.LocalToWorld(SceneAvatar.Middle),
      SceneAvatar.Parent.LocalToWorldDirection(SceneAvatar.Direction),
      MaxSingle,
      AvatarRigidBody
    );
    Result := RayCastResult.Transform;
    }

    (* Alternative versions, using old physics,
       see https://castle-engine.io/physics#_old_system_for_collisions_and_gravity .
       They still work (even when you also use new physics).

    if not AvatarRigidBody.Exists then
    begin
      { SceneAvatar.RayCast tests a ray collision,
        ignoring the collisions with SceneAvatar itself (so we don't detect our own
        geometry as colliding). }
      Result := SceneAvatar.RayCast(SceneAvatar.Middle, SceneAvatar.Direction);
    end else
    begin
      { When physics engine is working, we should not toggle Exists multiple
        times in a single frame, which makes the curent TCastleTransform.RayCast not good.
        So use Items.WorldRayCast, and secure from "hitting yourself" by just moving
        the initial ray point by 0.5 units. }
      Result := MainViewport.Items.WorldRayCast(
        SceneAvatar.Middle + SceneAvatar.Direction * 0.5, SceneAvatar.Direction);
    end;
    *)
  end;

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'));

    { We clicked on enemy if
      - HitByAvatar indicates we hit something
      - It has a behavior of TEnemy. }
    HitByAvatar := AvatarRayCast;
    if (HitByAvatar <> nil) and
       (HitByAvatar.FindBehavior(TEnemy) <> nil) then
    begin
      HitEnemy := HitByAvatar.FindBehavior(TEnemy) as TEnemy;
      HitEnemy.Hurt;
    end;

    Exit(true);
  end;

  if Event.IsMouseButton(buttonRight) then
  begin
    ThirdPersonNavigation.MouseLook := not ThirdPersonNavigation.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;
end;

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

procedure TViewPlay.ChangeCheckboxAimAvatar(Sender: TObject);
begin
  if CheckboxAimAvatar.Checked then
    ThirdPersonNavigation.AimAvatar := aaHorizontal
  else
    ThirdPersonNavigation.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
  ThirdPersonNavigation.ImmediatelyFixBlockedCamera := CheckboxImmediatelyFixBlockedCamera.Checked;
end;

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

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

procedure TViewPlay.UpdateAfterChangeTransformation;
begin
  ButtonChangeTransformationAuto.Pressed := ThirdPersonNavigation.ChangeTransformation = ctAuto;
  ButtonChangeTransformationDirect.Pressed := ThirdPersonNavigation.ChangeTransformation =  ctDirect;
  ButtonChangeTransformationVelocity.Pressed := ThirdPersonNavigation.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 := ThirdPersonNavigation.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). }
  SceneAvatar.Gravity := not AvatarRigidBody.Exists;
end;

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

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

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

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

end.

I am assuming because the third person navigation has units under the “uses” clause that were not covered on the API page,

If this is really the case, now that it works you can investigate, find “the culprit” and define it where it is missing :slightly_smiling_face:

Another small tip (just from my experience) is to always reduce the code to a minimum so you can focus exclusively on what doesn’t work.

In your case, are you getting stuck on character initialization? Erase away anything you don’t need, such as animations. Your target is to load the character on a white background, the animations come later…

:man_facepalming: You are telling me that I should look somewhere else rather than the page that is supposed to spell out for you, explicitly and in every detail, what units the class uses? I can’t imagine how that would ever be weird and totally illogical, frustrating and annoying… again, this kind of stuff has been my point the entire time.

And yes, I do make sure to reduce the code to the smallest portions and test them before adding more, cause I know that’s the sensible/most logical thing to do, but it’s another thing if you’re stuck on even the smallest bits for reasons that don’t make logical sense.

However, I think I am past that point now and just need it explained to me why a code that works one time using the same exact syntax and rules doesn’t work another time.

You are distracted :slightly_smiling_face:
This code is always wrong, why don’t you fix it once and for all? :face_with_raised_eyebrow: :slightly_smiling_face:

Yes, but as always, how to fix it has never been specified. I initially tried it using a period, which is what the manual tells, you, word for word, what you should do, yet I got an error at first about a period being in place of where a colon goes, which I will repeat as many times as necessary makes no sense whatsoever, because the manual tells you that you are supposed to use the period.

However, I remember, again I must stress how weird and illogical this is! That sometimes it would work after I put the period, but why did it sometimes work and sometimes not work when I would put the period? Computer code is supposed to work the same way if you type it the same, and that’s just basic Computer Programming 101.

If you put the period now, does it give you an error?

In either case, that’s a good catch and I put the period cause that’s what the manual says what you should do, but that’s not the error that I am getting now, it’s more about there being no method to override an ancestor class, even though michaelis explained that’s exactly what that particular line of code should do (reading the number and line it’s pointing to).

Why don’t you postpone this problem until later? Animations only make sense if the character who has to undergo them exists.

Are you sure your character is properly initialized? Were you able to see it in the scene next to the default avatar? Only worry about animations after this step.

Okay, that makes sense that that should be the next step I do before trying animations. Thank you!

Again, giving people gentle walkthroughs like this, as opposed to telling them “screw you and RTFM!” are very appreciated. Thank you so much!

Okay, even after I fixed that error, like I made sure “HumanBase1” is actually in the scene and is in the spot where an avatar should go, I still get the same error I posted above about the override not being accepted, so I don’t think that addresses that particular error, sadly.

I think again, it’s cause of weirdness/incosistency where it will accept the override sometimes but not other times, even though I type the same line in the same exact way and I am just following the directions to the best of my ability. What gives?

But the error is about “SetAnimation”, didn’t we say to remove everything related to animations?

Okay, that makes sense when you tell it to me like that. My thinking though, was that because there were no errors that were not related to animations I was safe with that step and could work on animations, but I can’t make assumptions like that?

I repeat: is your character properly initialized and does it appear in the scene? If yes, you can focus on animations, if not, you should delete all animation related code and just worry about visualizing the character.

Okay, really weirdly I do get more errors even though I only deleted the animation related code.

I really think it’s strange those errors didn’t show up in addition to the ones related to animation, because again it’s the same exact lines as earlier just with the additional ones about animation all deleted.

So that’s definitely a good catch you had, cause again I was expecting the engine to tell you all the problems up front instead of telling you only a couple at a time (which is what a good code compiler should do in my experience because it’s weird and incosistent otherwise, may I add).

The output is as follows:

The first error I still call BS on and say is totally illogical the same as when I did the first time, because it’s given to you on the API page I linked above that AvatarHiearchy is a member of TCastleThirdPersonNavigation, so it should in fact be recognized,

and all the rest are warnings that already came with the code by default when I first initialized the program, like they were before that before I added my own code, so I figured I should just ignore them because again, they were like that when I opened the program already and I figure trying to correct them will just add more errors with what has been happening to me so far.

Okay, giving the API page a more critical eye I think maybe it is because AvatarHiearchy references a different type of object, namely a Transform, than Avatar itself does, which references a Scene instead?

I believe the assignment syntax is still the same for both object types from what I can tell reading the examples and looking at the code that comes with the engine, but maybe my problem is that I haven’t defined a Transform of HumanBase1 yet, and only a Scene?

I know obviously if that is the case, I can do my own research/work on how to figure out how to initalize a Transform, instead of expecting someone to hold my hand.