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 :slight_smile:
(I put player coordinates in beauty TVector3 soon too, but so far it’s primitive 3 z, x and y Integers :slight_smile: ).
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 :slight_smile:

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! :slightly_smiling_face:

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/ .