# Movement scaling outside the Update procedure

Greetings again!
I want to go deeper inside time management of Castle.
And my first problem - move scaling to a fixed distance after key single tap. After key pressing map scrolling nice, but, of course, faster then neurons signals in my brain.
First and simple way out for me was set Delay(); between frames but it works wrong: delay from cycle summed, game awaits one second and, after that, scroll map immediately again to all. Really it may be problem of my and Pascal relations again.
Below is my procedure. I use Vector3 in 2D game because plan big net of dungeons underground and it makes traveling a little bit 3D
(I put player coordinates in beauty TVector3 soon too, but so far itâ€™s primitive 3 z, x and y Integers ).
And LookTo - its player look direction in 8 numbers to 8 sides of the world. Sorry, if code lookâ€¦ specific, but at least you can see my problem with frames delay.

procedure HeroMove(var Map: TMinimap; var Player: THero);
var
WayDirection: TVector3;
begin
case Player.LookTo of
0: begin WayDirection:= Vector3(0, -1, 0);
Player.GlobalY -= 1;
end;
1: begin WayDirection:= Vector3(0, -1, -1);
Player.GlobalY -= 1;
Player.GlobalX -= 1;
end;
2: begin WayDirection:= Vector3(0, 0, -1);
Player.GlobalX -= 1;
end;
3: begin WayDirection:= Vector3(0, 1, -1);
Player.GlobalY += 1;
Player.GlobalX -= 1;
end;
4: begin WayDirection:= Vector3(0, 1, 0);
Player.GlobalY += 1;
end;
5: begin WayDirection:= Vector3(0, 1, 1);
Player.GlobalY += 1;
Player.GlobalX += 1;
end;
6: begin WayDirection:= Vector3(0, 0, 1);
Player.GlobalX += 1;
end;
7: begin WayDirection:= Vector3(0, -1, 1);
Player.GlobalY -= 1;
Player.GlobalX += 1;
end;
end;
for i := 0 to (MapTileH - 1) do
begin
MapPosition := MapCanvas.Translation;
MapPosition := MapPosition + WayDirection;
MapCanvas.Translation := MapPosition;
Delay(60);
end;
end;

Hi!

Usually you donâ€™t need to use things like Sleep or Delay in â€śmodernâ€ť games. Yes, it takes some time to get used to â€śasynchronous inputâ€ť paradigm, but it gives a lot of fruit, ability to animate objects and environment first of all.

So, the logic of â€ścontemporaryâ€ť games is - there are multiple objects, each of which has some sort of Update method, which gets called every frame. Inside of this Update method objects receive information on how much time has passed since the last frame, itâ€™s called SecondsPassed in Castle Game Engine. Then every frame Playerâ€™s input is processed and based on it, the current status of objects change (e.g. if the Player holds key â€śleftâ€ť then player character object moves left, according to its speed and time passed from the previous frame = distance moved this frame).

This way you have something like:

procedure TStateGame.Update(const SecondsPassed: Single; var HandleInput: Boolean);
begin
inherited;
if Container.Pressed[keyLeft] then
Player.GlobalX -= Player.Speed * SecondsPassed;
if Container.Pressed[keyRight] then
Player.GlobalX += Player.Speed * SecondsPassed;
if Container.Pressed[keyUp] then
Player.GlobalY -= Player.Speed * SecondsPassed;
if Container.Pressed[keyDown] then
Player.GlobalY += Player.Speed * SecondsPassed;
end;

This is example of how itâ€™s done in a real-time game. Like sidescroller or twin-stick shooter.

In turn-based games the situation is extremely similar, but you may want to react only to user â€śpressingâ€ť the key, not only holding (note that on some OSes if player holds the key the OS will start sending repetitive events and you also may need to watch for â€śreleaseâ€ť event). And make sure to make discrete steps on every keypress. Something like:

function TStateGame.Press(const Event: TInputPressRelease): Boolean;
begin
Result := inherited;
if Event.IsKey(keyLeft) then
Player.GlobalX -= 1;
if Event.IsKey(keyRight) then
Player.GlobalX += 1;
if Event.IsKey(keyUp) then
Player.GlobalY -= 1;
if Event.IsKey(keyDown) then
Player.GlobalY += 1;
end;
2 Likes

Also see manual Designing user interface and handling events (press, update) within the state | Manual | Castle Game Engine where I documented Update and Press and SecondsPassed usage

1 Like

Ok, thanks. One clarifying question. With help of SecondsPassed, i set move speed in pixels per frame.
So in example with plane:

procedure TStateMain.Update(const SecondsPassed: Single; var HandleInput: Boolean);
var
PlayerPosition: TVector2;
begin
inherited;
LabelFps.Caption := 'FPS: ' + Container.Fps.ToString;

{ update player position to fall down }
PlayerPosition := ImagePlayer.Translation;
PlayerPosition.Y := Max(PlayerPosition.Y - SecondsPassed * 400, 0);
ImagePlayer.Translation := PlayerPosition;
end;

We move plane to Max ( 1 / 60 * 400 ) = 7 pixels every frame and to 420 pixels every second in case of 60 FPS?
So, if i need fixed move to 64 pixels, i should not be guided by the press time, but by the distance. That is, act in reverse order. Something like this:

var SecondsSumm: Single;

...

While SecondsSumm < 60 do:
SecondsSumm += SecondsPassed;

And after that, when 60 milliseconds are gone, picture will step one pixel.

Unfortunately Iâ€™m not 100% sure what exactly you are trying to achieve here.

In the first case SecondsPassed * 400 means youâ€™re moving with speed of 400 pixels per second (not per frame). This speed will be the same regardless of whether your game runs at 15 FPS or 120 FPS. And thatâ€™s a big advantage of this approach - the movement speed doesnâ€™t depend on Playerâ€™s hardware.

If you need a fixed move to 64 pixels per a pressed key, this might get a bit trickier. Literally just a tiny bit, the logic remains the same, but the code is rewritten in a more â€śmanageableâ€ť way. So, what you want to do here is â€śgive player character commandâ€ť and then â€śexecute this command for some timeâ€ť. Like this (pseudocode, just to illustrate the idea):

const
TileSize = 64; // this is a global constant for "tile size". All moving and static objects should use it to render themselves properly

type
{ This is our "Player" class which handles everything related to the Player
In a "real" case it should "inherit" from a more abstract "TObjectOnMap" class
So that other things, like monsters, can reuse its methods, like Update }
TPlayerCharacter = class(TObject)
private
{ Destination, in integer tiles }
DestinationX, DestinationY: Integer;
{ Old position, before the movement started }
OldX, OldY: Integer;
{ How long is the Player into movement }
Phase: Single;
{ If the player moving now? }
IsMoving: Boolean;
public
{ Calculated coordinates for the Player, they can be non-integer if Player is moving }
X, Y: Single;
{ Used to "tell the Player" to go right/left/up/down }
procedure MoveInDirection(const DX, DY: Integer);
{ Update this object, for now only movement, but in future can do shooting,
interacting with map and literally any other imaginable action }
procedure Update(const SeconsPassed: Single);
{ Call this  to "render" the player on the screen. Super basic, but just for example }
procedure Render;
end;

procedure TPlayerCharacter.MoveInDirection(const DX, DY: Integer);
begin
if (DX = 0) and (DY = 0) then
Exit;
// Ignore user's input when player character is already moving
if IsMoving then
Exit;
// Set movement destination
DestinationX := OldX + DX;
DestinationY := OldY + DY;
// and let Update know we're in progress of movement
IsMoving := true;
Phase := 0;
end;

procedure TPlayerCharacter.Update(const SeconsPassed: Single);
begin
if IsMoving then
begin
// this will "move 1 tile in 1 seconds", otherwise will need a bit of tinkering with constants below
Phase += SecondsPassed;
if Phase < 1.0 then
begin
// Destination not reached, Update current values of X and Y depending on "movement phase" or "percent of tile moved this far"
X := OldX + Sign(DestinationX - OldX) * Phase;
Y := OldY + Sign(DestinationY - OldY) * Phase;
end else
begin
// Destination reached. Set exact coordinate values
X := DestinationX;
Y := DestinationY;
// And stop movement
OldX := DestinationX;
OldY := DestinationY;
IsMoving := false;
end;
end;
end;

procedure TPlayerCharacter.Render;
begin
// Just draw some TDrawableImage on screen at Player's position
PlayerImage.Draw(Player.X * TileSize, Player.Y * TileSize);
end;
...

procedure TMyUiState.Update(const SecondsPassed: Single);
begin
inherited;
// Let Player class handle the update
Player.Update(SecondsPassed);
end;

procedure TMyUiState.Render;
begin
inherited;
// Ask Player class to render itself
Player.Render;
end;

function TMyUiState.Press(const Event: TPressReleaseEvent)
begin
Result := inherited;
// Handle user input
case Event.Key of
keyUp: Player.MoveInDirection(0, -1);
keyDown: Player.MoveInDirection(0, 1);
keyLeft: Player.MoveInDirection(-1, 0);
keyRight: Player.MoveInDirection(1, 0);
end;
end;
1 Like

I get the idea, thanks for such detailed explanation!

I think I understand your use-case, and yes it makes sense.

Let me repeat what I understand:

• instead of linear movement, with e.g. speed of 64 units per seconds, where the object moves from X = 0 to X = 64 in 1 second, and in between the object can be at X = 0 (time = 0), X = 10, X = 20, X = 30, X = 32 (time= 0.5 sec)â€¦ X = 64 (time = 1 sec).

• â€¦ you want a rapid (â€śstepâ€ť) movement. Which means that in long-term the object moves still with speed of 64 units per second, but now you want the object to stay at X = 0 for 1 second, then rapidly move to X = 64 when time = 1 and so on.

Yes, your pseudo-code is a solution. You can basically count seconds, and only act once you have â€śaccumulatedâ€ť more than 1 second. I would

1. declare a variable like AccumulatedTime: TFloatTime in your state

2. in the state TMyState.Update, do

const
TimeToMove = 1.0; // in seconds
begin
AccumulatedTime += SecondsPassed;
while SecondsPassed >= TimeToMove then
begin
MyObject.Translation := MyObject.Translation + Vector3(64, 0, 0);
AccumulatedTime -= TimeToMove;
end;
end;

Indeed this is more or less what you propose too. Note that I use TFloatTime for AccumulatedTime, this is just Double â€“ compared to Single it will have a better precision, and so is better to use for something that will be incremented many times.

Note: TCastleTimer class also allows to do that. See example in examples/user_interface/timer_test/ .