Need Help Understanding How to Assign an Avatar

And yes, I did use “AvatarTransform.Translation” for the RayOrigin, and yet it registers it as a duplicate when I declare a new, unrelated function called “AvatarTransform” below, which I am saying doesn’t make sense given that I already used “AvatarTransform.Move” and it didn’t tell me then that the new function contradicted the old AvatarTransform.

If Vector3(0, -1, 0) is a direction, how did you get it?

At that particular part, I just followed the directions verbatim I was given, assuming the person posting it initially knew that it was the correct direction.

However, it’s still not related to the actual issue at hand, namely about AvatarTransform being registered as a duplicate identifier even though I had the same two things named “AvatarTransform” compiling fine in the code before that error came up; if it was a problem now it should have been a problem then, because it doesn’t make sense for code in general not to work the same each and every time you compile it (although obviously random number functions and the like are a different story, but you know what I mean; something like “var x = 5” should always assign the same variable and the same value each and every time logically speaking).

And I can understand that duplicate identifers are not a thing you should do period, cause I have experience with that from other languages as well, but what I am asking is what should I rename the line “function AvatarTransform: TCastleTransform” further down the code so that it doesn’t contradict the existing AvatarTransform declared in the list of objects in the scene?

Vector3(0, -1, 0) corresponds to Vector3.Down, which may be correct in your case.
If I understand correctly, you are shooting a ray from your character towards the ground, perpendicularly.

Do you get an error on this line: RayOrigin := AvatarTransform.Translation; ?

That is exactly what I was pointing out just in the post before yours as well as my earlier posts, but the error isn’t exactly at that line, it’s further down where I am told the new function declared AvatarTransform is a duplicate identifier of the existing AvatarTransform referencing an object from the scene with the same name.

What should I rename the function so that everything still works properly without the error?

And yes, I do want to shoot the ray straight down, to test if the character is standing on the ground or not.

I don’t really understand what you’re doing. You declare a local function AvatarTransform(), but then don’t call it anywhere in TViewPlay.Press().
Maybe you meant to assign it to HitByAvatar ?

Also you should not call it AvatarTransform as you have already defined an identifier with the same name.

I was just doing that part based off how the example already was, they did that before I added anything to it.

And I understood that part from the first step/very start because that’s what the compiler was explaining very clearly with the error message it gave me, but I was wondering how I should fix that cause I didn’t know myself.

Perhaps I can just omit it entirely though is your point? Makes sense to me as well.

You need to know if you need this piece of code. In any case it’s a local function, you can call it “foo” and do HitByAvatar := foo;

Okay, here is the fixed code, where I omit all the unnecessary parts that were in the old code:

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

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

var
  ViewPlay: TViewPlay;
  MyThirdPersonNavigation: TMyThirdPersonNavigation;
  GroundRayCast: TPhysicsRayCastResult;
  RayOrigin: TVector3;
  MoveAmount: TVector3;
  MaxDistanceToGround: Single;
  StandOnGround: Boolean;

implementation

uses SysUtils, Math, StrUtils,
  CastleSoundEngine, CastleLog, CastleStringUtils, CastleFilesUtils, CastleUtils,
  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;

begin
  inherited;
  { This virtual method is executed every frame (many times per second). }
  RayOrigin := AvatarTransform.Translation;
  MoveAmount := Vector3(0, -5, 0);
  MaxDistanceToGround := 2.36;
  GroundRayCast := AvatarRigidBody.PhysicsRayCast(
    RayOrigin,
    Vector3(0, -1, 0),
    MaxDistanceToGround
  );
  StandOnGround := GroundRayCast.Hit and (GroundRayCast.Distance <= 1.23 + 0.01);
  if StandOnGround then
  begin
  LabelFps.Caption := 'FPS: PLACEHOLDER';
  SandyLegs.PlayAnimation('LEGS_IDLE', true);
  SandyTorso.PlayAnimation('TORSO_IDLE', true);
  end
  else
  begin
  SandyTorso.PlayAnimation('TORSO_AIRBORNE', true);
  SandyLegs.PlayAnimation('LEGS_AIRBORNE', true);
  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;
  SandyInRunning: Boolean;
begin
  SandyInRunning := false;
  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;

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.

It compiles without any errors now, but the Touching Ground condition is still never met, and the FPS label and animation for Sandy never changes to the specified directions. What gives?

And no, I didn’t need the “HitByAvatar” piece for now so I omitted/deleted it as you can see above, but again it was just like that in the original code and I just assumed everything was there for a reason.

However, I can tell making judgements for myself that the script for hitting an enemy isn’t practical/useful at all if you don’t actually have enimies, so it’s omitted completely for now.

Because this condition never becomes true.
Why are you using 1.23 + 0.01 ?
This might work for one character and maybe not for yours.

Agian, I was just using the conditional given to me verbatim. Do you know what the correct measurement for the distance should be?

I can not know that. You have to wonder why GroundRayCast.Hit depends on (GroundRayCast.Distance <= 1.23 + 0.01) to continue being true.
First try removing the “and” condition to check if StandOnGround is really true regardless of the next condition.

If it is, the parameters after “and” need to be adjusted.

Sounds good, Will try that

Unfortunately, I narrowed it down to that detail in particular, namely that it doesn’t register as hitting at all without the distance limitation,

I believe that the fix should be to change the end of the vector where I first configure its details/paramaters (in particular, the parameter MaxDistancetoGround), so that it should hit the ground cause I think that’s what’s wrong (too small/too short endpoint)?

Does anyone know what the correct number should be if that is the culprit, because otherwise I would just have to use trial and error/random guessing and checking until something works?

I’m not familiar with the CGE API, but I don’t think it’s as you say.
The code above should still work without the condition after the “and”.
In my opinion something is wrong with RayCast.

This code is cited as an example by the API:

//So for example query like this works naturally: (by Michalis)
RBody := MyTransform.FindBehavior(TCastleRigidBody) as TCastleRigidBody;
Hit := RBody.RayCast(MyTransform.Translation, MyTransform.Direction, MaxDistance);

As you can see Hit does not require additional conditions, but it will be true or false.

Yeah, and that’s what I was testing myself.

The good news is that I could get the hit to actually register correctly once I changed the MaxDistancetoGround through trial and error, but the bad news is that no matter what numbers I pick, it will either be in the air standing correctly or on the ground in the airborne pose, with no states in between or nuances like that.

Does anyone know the correct number I should use to make it so that it is in the idle animation and standing on the ground?

Again, LabelFps.Caption becomes PLACEHOLDER?