MD3 - improve animations support

By eliminating the animation code you narrowed it down. The animation errors were hiding this from you, that’s why I suggested you to delete the animations. Now you know something is wrong even before the animations. That’s what you need to focus on.

As for the warnings in the yellow band, they inform you that certain symbols have been deprecated, probably because michalis made some changes later to improve the engine. The demo is dated and written in old code. I don’t think it makes sense to get involved, the demo works anyway and your goal would be to upload your own character for study purposes.

I don’t want it to cause you any more problems though.

If your main aim isn’t to spend some time on working code to understand how it works but rather you want to work on your own project, then working here could be a waste at this point.

Your problem is loading the character into a scene. Create a new empty project, or start from a code base, and commit only to that. To do this, however, you must necessarily view the examples of the engine by looking for the code that loads a 3D character into the scene. There should be enough of them. Or try following the links suggested by michalis that explain just how to load a character into the scene.

And only when you see your character magically appear in the scene you can dedicate yourself to animations. At least I think so :slightly_smiling_face:

And again, I am just focusing on loading a character into a scene using the code michaelis gave me that he says will work for loading multi-part models (that’s my goal specifically and not single part models, cause I want legs and torsos to be animated seperately), but the problem is that like in my last two posts, I run into errors that do not make any logical sense even after you read the tutorials.

For example, if it tells you word for word that “AvatarHiearchy” is a property of TCastleThirdPersonNavigation, then wouldn’t it be only reasonable to expect that you can use “TCastleThirdPersonNavigation.AvatarHiearchy := HumanBase1;” without running into any errors?

And I am 100% sure that if I start a project from blank, I will run into the same error again because I would just be using the same exact code written in the same exact way for the TCastleThirdPersonNavigation definition, and the error doesn’t care about anything besides what is going on in those specific lines.

Sure, and that’s exactly what Michalis suggested in one of his responses. The reasoning is correct. But I’m wondering: what if HumanBase1 doesn’t match the type expected by the AvatarHierarchy property? This could throw the compiler into a crisis which, trying to understand what your code wants to do, may erroneously signal you that TCastleThirdPersonNavigation should be of the AvatarHiearchy type and therefore suggest you put a colon. But actually the real problem could be HumanBase1.

Okay, so maybe it is because like I thought after re-reviewing the API page, that TCastleTransform and TCastleScene do not match/overalp exactly, but need to be defined seperately.

Okay, I still get the same error about “identifier not idents member AvatarHiearchy” mentioned above, even after I revise the HumanBase1 to be a TCastleTransform and define it beneath the TCastleScenes in the initial declaration of the ThirdPersonNavigation.

Do you know why else that the error keeps coming up, because I know for a fact that using “class.property := name of property” is the correct way to assign a property regardless of its exact type?

The evidence I have for the last statement is that I can see it at the bottom of the code that comes with the example by default, like how there’s things with “name of TCastleTransformHere := something”, so I know that detail is correct from that, and I know accessing class properties is always done with a period not only in Pascal, but in many programming languages as well.

I suggest you post the code, that way michalis or others more knowledgeable than me on CGE can verify it.

In particular, the lines that suggested that read “HitByEnemy” at the beginning each time if you are having a hard time finding it yourself.

Here is the code in full for reference; notice how near the bottom of the code there is a TCastleTransform defined and referenced in nearly the same way as what I did, the only difference is that the example at the bottom isn’t part of a class explicitly, unlike my example, but again I believe the syntax I used should be 100% perfect/correct going by how the example works.

{
  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 AssignAvatar;
  published
    ExampleLegsScene, ExampleTorsoScene: TCastleScene;
    HumanBase1: TCastleTransform;
  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.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.

It sounds a bit complicated not knowing CGE well.

AvatarHierarchy

If AvatarHierarchy is non-nil, then it should contain Avatar as a child. AvatarHierarchy can even be equal to Avatar (it is equivalent to just leaving AvatarHierarchy as Nil).

This object should be part of TCastleViewport.Items to make this navigation work, in particular when you call Init.

Avatar

Currently we require the following animations to exist: walk, idle.

When AvatarHierarchy is Nil, then Avatar is directly moved and rotated to move avatar. Otherwise, AvatarHierarchy is moved, and Avatar should be inside AvatarHierarchy.

This scene should be part of TCastleViewport.Items to make this navigation work, in particular when you call Init.

Does it seem to you that all these settings have been respected?

Does your character, forgetting the animations of individual body parts for a moment, contain walk and idle animations? If so, why don’t you forget the AvatarHierarchy property and start loading your character into the scene and moving it with keyboard keys or mouse? Do the simplest thing (which won’t even be that easy), see it work, and then you can make it better.

I made sure for the object I am calling, HumanBase1, to be in the TCastleViewport.Items section by placing it under the “items” section in the Viewport directly, like the part before you click on code to open and edit it.

If I have AvatarHiearchy referencing HumanBase1, a Transform, as my avatar, does that mean it contains the avatar as a child, and if so is there supposed to be a specific way to reference a child object vs. parent object?

And I am just using generic references before sorting through the details of animation, because you wanted me to take care of loading the scene before animations, but you’re telling me that I do need to determine some details like whether it contains “idle” or “run” animations or otherwise it won’t work? Good to know.

Yeah, unfortunately in the modern Pascal reference I found nothing at all about parent vs child objects mentioned; where is another good place to look, or is it not in the help yet?

This is what I found written in the reference API. You should have read it too.

Avatar scene, that is animated, moved and rotated when this navigation changes. This navigation component will just call Avatar.AutoAnimation := ‘xxx’ when necessary. Currently we require the following animations to exist: walk, idle.

I’m just basing it on what I read.

From what I understand you can ignore this if you just want to display the character in the scene without the separate animations (for now).

That is really weird in your second up post, that those instructions didn’t show up on my end, but maybe I was just looking at the page before those instructions were added?

And it’s a relief to know that I don’t have to deal with parent vs child not being properly documented for now, cause it would be a nightmare otherwise.

Okay, even more annoying still when I click on Scene objects in the main editor, it lets me specify animations in the panel to the right as one of its properties, but it doesn’t let me do the same thing for Transform objects.

Can I please have help figuring out how I should implement animations for Transform objects?

As in assign animations to Transform objects; I am sorry if I phrased it unclearly at first.

Unfortunately I can’t help you with this, I don’t know the editor well. But I think I recall that there is a section in the manual that refers to character animation.
I imagine that as soon as michalis can answer, however, he will try to put you on the right way.

I don’t see the section titled explicitly, like there’s a title for the “Behaviors” page for example in the manual; I don’t see any that says “Character Animation” or anything like that. Could I please have help figuring out where that section is as well, then maybe I can figure out how to add animations to Transform objects after that?

it seems to me that here, where michalis suggested you, you can find something interesting:

https://castle-engine.io/viewport_and_scenes_from_code

The first thing to do would be to forget the animations and write some code that loads your character into the scene.

One question, what extension does your character have?