Game-Loop in Firemonkey for Android
Asked Answered
A

1

6

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?

Altigraph answered 17/10, 2016 at 9:12 Comment(7)
You can use anonymous threading (with synchronize) to achieve thisGavelkind
If you want to avoid game loop lags caused by FireMonkey UI then your best choice would be to move all your game logic to secondary thread. But even that might solve your problem entirely especially if target device has only one processing core. You see FireMonkey is pretty bad/slow in processing of UI messages. As a test just create a new FireMonkey application and place about 100 panels on the form. Then compile it and observe how rapid mouse movements over your form would cause significant spike in CPU usage. ...Counterrevolution
... Such test is capable of maxing out the CPU usage on my 7 year old laptop and it is not just some weak laptop since I can play Far Cry 3 on mediums settings without any problem on it. Now I can just imagine what kind of CPU usage would FireMonkey cause on a mobile phone using this scenario. That is why I'm not considering using FireMonkey for any game development.Counterrevolution
Thank you both for your comments! Do you think i could improve the UI messaging by drawing the all game objects using the canvas in the OnPaint-Event instead of drawing like 50+ TImages? I also started a small bounty if any of you want to try :)Altigraph
@KoalaGangsta: Just curious, what did you end up doing? I also want to implement an animation loop in FM and I came across your post. ThanksJoellenjoelly
@KoalaGangsta: I found this video: youtube.com/watch?v=O4-SFLjF8OU . He used a timer.Joellenjoelly
Wondering whether docwiki.embarcadero.com/Libraries/Alexandria/en/… and/or docwiki.embarcadero.com/Libraries/Alexandria/en/… could somehow help in such scenarios. Not sure from their docs what are the scenarios they're intended for thoughCabe
A
1

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;
Acierate answered 21/10, 2016 at 12:20 Comment(3)
Thanks a lot for your answer! After some testing it seems like this does not solve the problem which makes my OnMouseMove event "lagging", but I think i have to go deeper in testing, so it might work. Do you know if there is a chance to play around with the FPS? On mobile it seems to work very slow :-/ Thanks so far!Altigraph
Did you check if your gamelogic is "burning" the CPU? You haven't specified the Phone your are working with. Otherwise putting it into an extra thread might be an option as discussed in other comments of your mainpost. Maybe you can elaborate on what you are working on in more detail? There is a lot that can influence it. To play with the FPS, use AniFrameRate-Property.Acierate
Something else you could try, is polling Keys/Mouse in your Gameloop before executing your logic, to avoid interferences with the events. Your gamelogic then needs to access some kind of interface where you have stored those values.Acierate

© 2022 - 2024 — McMap. All rights reserved.