Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Game-Loop in Firemonkey for Android

im currently making a 2D game in Firemonkey for my Android phone using some TImage Controls and controling their positions and angles. Simple as that. I tried to use my normal way of looping, which works well/flawless in Windows but fails on Android.

Method #1: "End-less" loop (Bad)

My main problem is that I want to avoid unwanted/unexpected behaviour by using an "end-less loop" like this where I have to call Application.ProcessMessages():

while (Game.IsRunning()) do
begin
  { Update game objects and stuff }
  World.Update();


  { Process window messages, or the window will not respond anymore obviously }
  Application.ProcessMessages(); { And thats what i want to avoid }
end;

The problem is that as im stuck in the loop and also calling the procedure to process messages, I can run into many problems, and also I just don't think that it is a good way.

Method #2: TTimer (Dont even think about it ;-) )

Since TTimer is awful in many ways and not meant to be used for such thing, the approach where i just put a TTimer with a minimum interval falls off. Also I never tried other Timers than that, but if there is one truly for Games, I will try it of course :)

Method #3: OnIdle - How I normally do it on Windows

On Windows I can use the Application.OnIdle-Event.

procedure GameLoop(Sender: TObject; var Done: Boolean);
begin
  { Update game objects and stuff }
  World.Update();

  { Set done to false }
  Done := False;
end;

...

Application.OnIdle := GameLoop;

It is being called everytime the Application is Idling over the windows messages. The performance seems to be the same or a little worse, compared to #1, and the overall architecture much more reliable which is the reason why I normally use this method. On Android however it seems that it is being called differently when combined with TForm.MouseMove. Where on Windows OnIdle keeps working perfectly, on Android it will lag/stop while TForm.MouseMove is being called from a touch input (RAD Studio compiles it into touch events automaticlly)

I can however fire OnIdle by myself in the MouseMove Event by calling Application.DoIdle and "assist" the "Loop" where i think it misses out beimg called but this works also really bad and brings again unwanted behavoir and a worse performance when working with touch input.

And this brings me back to method #1 and it seems to work the best on Android for now. Is there any other way of creating a reliable way (a constantly and fast called event like OnIdle or so) of creating such a loop or any way to avoid the problems im facing in method #3 combined with the MouseMove-Events? It seems like the Android phone isnt powerful enough to have enough "Idle-Time" next to the form-events and the world updates

Also, could I consider using a thread for the world logic and next to it the method #1 on my Main-Form/Thread to update the rendering? Is it safe to update the form controls by the endless loop (with Application.ProcessMessages()) and getting the to shown data from another thread working on the world, objects and so?

like image 874
KoalaGangsta Avatar asked Oct 17 '16 09:10

KoalaGangsta


1 Answers

I think a valid option is to implement a TAnimation and override ProcessAnimation. a TAnimation is automatically scheduled when running through TAnimator.

  TGameLoop = class(TAnimation)
  protected
    procedure ProcessAnimation; override;
  end;
  [...]
  procedure TGameLoop.ProcessAnimation;
  begin
    //call updates
  end;

and started it like this:

procedure TForm1.FormCreate(Sender: TObject);
begin
  FLoop := TGameLoop.Create(Self);
  FLoop.Loop := True;
  FLoop.Name := 'GameLoop';
  FLoop.Parent := Self;
  TAnimator.StartAnimation(Self, 'GameLoop');
end;

By default FPS is set to 60 for an animation. One thing i could not figure out right now is how to calculate the Deltatime since last call by using only properties of TAnimation. This one is essential to deal with variable frametimes. But you cold do that yourself using System.Diagnostics.TStopwatch. Stop the watch at the beginning of ProcessAnimation and get Ticks, start a new one after finishing your ProcessAnimation.

EDIT: A Basic example for Deltatime (Elapsed time since last call in Seconds). Given you placed a ViewPort3D on Form1, added TCube named Cube1, TGameLoop being declared in the same unit as Form1(i know, ugly, but Demopurpose) and TGameLoop having a Field FWatch of type TStopWatch.

procedure TGameLoop.ProcessAnimation;
var
  LDelta: Single;
begin
  FWatch.Stop;
  LDelta := FWatch.ElapsedTicks / FWatch.Frequency;
  Form1.Cube1.RotationAngle.Y := Form1.Cube1.RotationAngle.Y + 50*LDelta;
  FWatch := TStopwatch.StartNew();
end;

EDIT2: A better example which distributes rounding/measure errors over a whole game-lifetime a bit better. Instead of measuring just between calls, we measure since first frame. TGameLoop has a new Field FLastElapsedTicks(Double)

procedure TGameLoop.FirstFrame;
begin
  FWatch := TStopWatch.StartNew();
end;

procedure TGameLoop.ProcessAnimation;
var
  LDelta: Single;
  LTickDelta, LElapsed: Double;
begin
  LElapsed := FWatch.ElapsedTicks;
  LTickDelta := LElapsed - FLastElapsedTicks;
  FLastElapsedTicks := LElapsed;
  LDelta := LTickDelta / FWatch.Frequency;
  Form1.Cube1.RotationAngle.Y := Form1.Cube1.RotationAngle.Y + 50*LDelta;
end;
like image 89
Alexander B. Avatar answered Oct 15 '22 22:10

Alexander B.