Thank you so much for the help! I will definitely try that. The adding the pivot point is another great point too, I definitely couldn’t think of that myself.
Unfortunately, now I get this error, about the Vector3 and single dimensional data type for the Capsule height being incompatible. I want to use only the 3rd dimension of the Vector3 because we’re relating it to a 3d dimension property, height, but how do I do that?
I followed the example tutorials, but apparently the method I tried, adding a “.Y” or “.Z” after the translation property is used, doesn’t actually treat it as only one dimension but still gives the same error about it being a Vector3 still.
The example that suggested this was the correct way of doing things was this piece of code from this page:
procedure TViewMain.Update(const SecondsPassed: Single; var HandleInput: Boolean);
procedure UpdateCarTransform(const CarTransform: TCastleTransform);
var
T: TVector3;
begin
T := CarTransform.Translation;
{ Thanks to multiplying by SecondsPassed, it is a time-based operation,
and will always move 40 units / per second along the +Z axis. }
T := T + Vector3(0, 0, 40) * Container.Fps.SecondsPassed;
{ Wrap the Z position, to move in a loop }
if T.Z > 70 then
T.Z := -50;
CarTransform.Translation := T;
end;
var
I: Integer;
begin
inherited;
{ This virtual method is executed every frame.}
LabelFps.Caption := 'FPS: ' + Container.Fps.ToString;
for I := Low(CarTransforms) to High(CarTransforms) do
UpdateCarTransform(CarTransforms[I]);
end;
My code looks like this:
{
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;
CapsuleCollider: TCastleCapsuleCollider;
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;
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;
RayCastResult: TPhysicsRayCastResult;
MoveAmount: TVector3;
RayCastDirection: TVector3;
SandyAirborne: Integer;
SandyIdling: Integer;
begin
inherited;
{ This virtual method is executed every frame (many times per second). }
SandyIdling := 0;
SandyAirborne := 0;
MoveAmount := Vector3(0, -5, 0);
RayCastDirection := Vector3(0, -1, 0);
RayCastResult := AvatarRigidBody.PhysicsRayCast(
(AvatarTransform.Translation.Z + (CapsuleCollider.Height/2)),
RayCastDirection
);
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;
// 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 message reads in particular, again because I know the more details the more easier it is to figure out what the real problem is:
Does anyone know why the compiler protests I am using a Vector3, even after I put the “.Y” and “.Z” suffixes to indicate I want only a single value?
Oops, I think after reading the error again I think I answered my own question; it actually wants a vector3 and not a single, despite the fact that the height value of the capsule is defined as a single in the API (meaning it doesn’t make sense to have a vector plus a single, cause they’re different data types).
And I was right about that last part too, because after deleting the “.Z” I get this!
So how do I get only one single value of the vector correctly, if how I was doing it was wrong? I would assume that if I followed the examples directly I should get the right results, but weird things happen all the time in the world of programming.
I know I didn’t assign a new variable and assign the car translation under the variable’s name, unlike the example, but I do believe that is actually good/the right thing to do here to keep the code simple and organized; I genuinely cannot see why I can’t just use the Translation.Z directly and it should be recognized as a single value without any errors.
Here T is a Vector3, which could be used as a source parameter for a RayCast.
You, on the other hand, are using a Single
AvatarTransform.Translation.Z + (CapsuleCollider.Height/2)
To begin with, what does the Z axis have to do with it? You have to move a point along the vertical, therefore on the Y axis.
You posted an example and then did everything differently. Why?
Then, you declare CapsuleCollider: TCastleCapsuleCollider;
but when you write (CapsuleCollider.Height/2) which CapsuleCollider are you referring to?
You cannot add a Vector3 to a Single, you can add two Vector3 together. So the solution is simple, just do as in the CarTransform example using Y instead of Z.
For point 1:
As I explained earlier in my previous post, again when you read every word of it, you will see the reason why I intentionally did it different from the example. Not to be an idiot who doesn’t listen to directions, but rather because I used my judgement and saw that there was most likely no logical need for using a new parameter and not using the car’s translation directly, given that it is well-known to be a 3D Vector/Vector3 just like the T in the example.
For point 2:
Thank you for clarifying I should use the Y axis and not the Z axis for the 3D axis; I am used to Z from so much programs, as well as school math classes and whatnot.
For point 3:
I thought I was doing it the correct way, because for example elsewhere in the code you have LabelFps defined and called in the same way, and the compiler understands that “LabelFps” is the name of an object of type TCastleLabel. This is exactly the same reasoning I was using, namely that TCastleCapsuleCollider is the type of object I want to use, and CapsuleCollider is the name I want to assign it.
For point 4:
If what I should do is add two Vector3’s together rather than two Single’s like I thought at first, that’s fine, but now a new issue comes up:
How should I turn the Capsule Height into a Vector3? I would assume the most obvious answer would be a Vector3 with two 0’s and only one component with a non zero value, but I just want to make sure instead of just relying on uneducated guesses.
Also, I would assume the other coordinates of the vector should be 0 in particular, cause I am adding a vector to a vector, so if I want to not change a component because I only want to add along one specific axis, I should keep all dimensions except the one I want to add 0.
Is that logic correct?
{
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;
CapsuleCollider: TCastleCapsuleCollider;
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;
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;
RayCastResult: TPhysicsRayCastResult;
MoveAmount: TVector3;
RayCastDirection: TVector3;
CapsuleCenter: TVector3;
SandyAirborne: Integer;
SandyIdling: Integer;
begin
inherited;
{ This virtual method is executed every frame (many times per second). }
SandyIdling := 0;
SandyAirborne := 0;
MoveAmount := Vector3(0, -5, 0);
RayCastDirection := Vector3(0, -1, 0);
CapsuleCenter := Vector3(0, (CapsuleCollider.Height/2), 0);
RayCastResult := AvatarRigidBody.PhysicsRayCast(
(AvatarTransform.Translation + CapsuleCenter),
RayCastDirection
);
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;
// 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.
Hi everyone,
I decided to just test for myself out of curiosity given I wasn’t getting replies, and it does compile and run fine;
however, it still plays the airborne pose while on the ground, even though I thought for sure I had the raycast set up properly, starting at the middle of the capsule and going down to the feet touching the ground. What the heck else is going on?
I just can’t understand why you want to complicate things by starting from the center of the character when it would be much simpler to start from the feet.
I can’t tell you if AvatarTransform.Translation + CapsuleCenter
is right, but assuming it is, and your ray starts from the center of the character, where is MaxDistance
? You really don’t think the engine decides for you that it has to stop at the character’s feet?
How does CGE handle the missing parameter? Is the ray infinite or does it end where it starts?
Another thing I don’t understand is why you don’t take a couple of hours to write the code that show the ray. How do you plan to work blindly?
I start from the center because that’s how I saw it in the examples. I chose not to use the MaxDistance because I figured in this particular application it wouldn’t be useful, but I know your point is it’s a good habit to get into just because most raycasts use a MaxDistance.
I also understand it’s a good habit to display the ray so that way you can see the work more clearly, especially for these kinds of bugs; could I please have a link to a particular project with actual code I can either copy directly or use for reference with appropriate changes, because I was stuck on how to actually implement the code to show the raycast visually?
And yeah, I understand your point now is also to start from the feet, so use the AvatarTransform.Translation as the appropriate reference point, and NOT the AvatarTransform.Height divided by 2, if I remember correctly?
Because there’s just no logical reason to start from the middle is your point and I understand that, even though that’s what I thought I should do from examples I saw.
So to recap:
1: I did the middle because I thought that’s what I should do from the examples without actually thinking critically, as you already know yourself most likely, but I know to start from the feet, trying to use AvatarTransform.Translation.
2: I know for not only this one, but any, and any, with no breaks to the rule, I should actually visibly show the raycast, because otherwise it will be impossible to actually know the actual length of the ray without guesswork.
But in addition to having reassurance that I chose the correct starting point (AvatarTransform.Translation), I need help understanding more exactly/the details of how to actually show the ray, because I couldn’t find any examples myself.
Believe me when I tell you that all the answers you are looking for are in this thread, both how to use raycast and how to display the ray.
It’s really a useless waste of time repeating the same things, I’m not saying this out of malice.
Go back to the first post in this thread and reread them all, but this time taking notes.
Okay, that makes sense that if you already said it there is no need to actually repeat yourself, because the information is already there and it would just be annoying you/wasting your time to have to repeat it - I understand it’s not to be a jerk, but because you know I’m smart and tech savvy enough to actually read the information when it’s given.
I’m pretty sure that’s exactly your point?
Okay, so I read through the example michaelis gave me way earlier in the thread, and again, like any good coder, I want to make sure I understand the material rather than just doing it randomly or haphazardly, like you and michaelis went over with me many times before.
{
Copyright 2023-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.
----------------------------------------------------------------------------
}
{ TCastleTransform descendant that renders unlit mesh using TCastleRenderUnlitMesh. }
unit GameMyMesh;
interface
uses SysUtils,
CastleRenderPrimitives, CastleBoxes, CastleTransform;
type
{ Mesh rendered using TCastleRenderUnlitMesh, not using TCastleScene.
This is not generally advised, TCastleScene has much more features
and is easier to use than TCastleRenderUnlitMesh.
We do this only to test TCastleRenderUnlitMesh with rcForceFixedFunction here. }
TMyMesh = class(TCastleTransform)
strict private
Mesh: TCastleRenderUnlitMesh;
public
procedure LocalRender(const Params: TRenderParams); override;
procedure GLContextClose; override;
function LocalBoundingBox: TBox3D; override;
end;
implementation
uses CastleVectors, CastleRenderContext, CastleColors, CastleGLUtils;
{ TMyMesh -------------------------------------------------------------------- }
function TMyMesh.LocalBoundingBox: TBox3D;
begin
Result := inherited;
Result := Result + Box3D(
Vector3(-1, -1, -1),
Vector3( 1, 1, 1)
);
end;
procedure TMyMesh.LocalRender(const Params: TRenderParams);
procedure CreateMesh;
begin
Mesh := TCastleRenderUnlitMesh.Create(true);
Mesh.SetVertexes([
Vector4(-1, -1, -1, 1),
Vector4( 1, -1, -1, 1),
Vector4( 1, 1, -1, 1),
Vector4(-1, 1, -1, 1),
Vector4(-1, -1, 1, 1),
Vector4( 1, -1, 1, 1),
Vector4( 1, 1, 1, 1),
Vector4(-1, 1, 1, 1)
], false);
Mesh.SetIndexes([
// line loop on Z = -1
0, 1,
1, 2,
2, 3,
3, 0,
// line loop on Z = 1
4, 5,
5, 6,
6, 7,
7, 4,
// connect Z = -1 with Z = 1
0, 4,
1, 5,
2, 6,
3, 7
]);
Mesh.Color := Yellow;
end;
var
SavedDepthTest: Boolean;
SavedLineWidth: Single;
begin
inherited;
SavedDepthTest := RenderContext.DepthTest;
SavedLineWidth := RenderContext.LineWidth;
RenderContext.DepthTest := true;
RenderContext.LineWidth := 5;
if Mesh = nil then
CreateMesh;
Mesh.ModelViewProjection := RenderContext.ProjectionMatrix *
Params.RenderingCamera.CurrentMatrix * WorldTransform;
Mesh.Render(pmLines);
RenderContext.DepthTest := SavedDepthTest;
RenderContext.LineWidth := SavedLineWidth;
end;
procedure TMyMesh.GLContextClose;
begin
FreeAndNil(Mesh);
inherited;
end;
end.
So I can understand in the above code snippet from michaelis’ example, “SetVertices” is supposed to set the vertices of the mesh to be rendered, but what exactly does “SetIndices” procedure do directly below that one? I assume it dictates how the lines should be drawn, but what is the actual logic/math behind it?
Similarly, why are the vertices Vector4/4 dimensions and not simply 3 dimensions per vertex, if we are making games in a 3D world?
In general:
-
See the documentation of TCastleRenderMesh, Castle Game Engine: CastleRenderPrimitives: Class TCastleRenderUnlitMesh .
-
Run the example from which the sample you cite comes,
example\research_special_rendering_methods\test_rendering_opengl_capabilities
. See what it renders. Play with code there, see how SetIndexes arguments determine which lines are drawn.
Mesh.SetIndexes
specifies indexes to the vertex array previously given to Mesh.SetVertexes
. The idea is that
-
Mesh.SetVertexes
doesn’t actually specify any lines or triangles to render, it only specifies a number of 3D points (vertexes) which can be referred to byMesh.SetIndexes
. -
Mesh.SetIndexes
specifies how the “primitives” ared connected. In this case we use laterMesh.Render(pmLines)
, so each primitive is a line. 2 consecutive numbers onMesh.SetIndexes
form a line.
These are homogeneous coordinates, as Castle Game Engine: CastleRenderPrimitives: Class TCastleRenderUnlitMesh says
Always passes vectors as 4D (in homogeneous coordinates). 4D coordinates are useful e.g. for shadow volume quads.
Search the Internet for explanation. E.g. Explaining Homogeneous Coordinates & Projective Geometry — Tom Dalling . Long story short: you will usually set the 4th component to 1.0, and then forget about this complication. E.g. if you want to specify 3D point (x,y,z) then write it as Vector4(x, y, z, 1)
in this case.
Okay, now it all makes more sense; so I am assuming for example when it says “1, 2” or “1, 3” on the list, it means to connect points 1 and 3 on the vertices specified earlier?
I assume it knows what each number is, based on the order you define it? Like the first one in the code is assigned 1, the second one is 2, etc.