Firemonkey在Android中的游戏循环

6
我正在使用一些TImage控件并控制它们的位置和角度,在Firemonkey中为我的Android手机制作2D游戏,就这么简单。我尝试使用在Windows上运行良好/无缺陷的常规循环方式,但在Android上失败了。
方法#1:“无限”循环(不好)
我的主要问题是想避免使用像这样的“无限循环”导致的不必要或意外的行为,在此循环中必须调用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;

问题在于当我陷入循环并调用处理消息的过程时,我可能会遇到许多问题。而且,我认为这不是一个好方法。
第二种方法:TTimer(甚至别想了;-))
由于TTimer在很多方面都很糟糕,而且并不适合用于这种情况,所以我放一个最小间隔的TTimer的方法不行。虽然我从未尝试过其他计时器,但如果有一种真正适用于游戏的计时器,我当然会尝试它 :)
第三种方法:OnIdle-我在Windows上通常都是这么做的
在Windows上,我可以使用Application.OnIdle事件。
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;

每当应用程序在Windows消息上空闲时,它就被称为“OnIdle”。相比于方法#1,性能似乎相同或略差,但总体架构更可靠,这也是我通常使用此方法的原因。然而,在Android上,当与TForm.MouseMove结合使用时,它似乎被以不同的方式调用。在Windows上,OnIdle仍然工作得很完美,但在Android上,当TForm.MouseMove从触摸输入中调用时,它会出现滞后/停止(RAD Studio将其自动编译为触摸事件)。
然而,我可以通过在MouseMove事件中调用“Application.DoIdle”来手动触发OnIdle,并协助“Loop”,其中我认为它被遗漏了,但这也效果很差,并且在处理触摸输入时会带来不良行为和性能下降。
这使我回到了方法#1,并且现在看起来在Android上工作得最好。是否有任何其他可靠的方式(例如像OnIdle这样不断快速调用的事件),可以创建这样的循环或任何避免我在方法#3中遇到的问题与MouseMove事件相结合?似乎Android手机没有足够的“空闲时间”来处理表单事件和世界更新之间的切换。
此外,我是否可以考虑在我的主线程/线程上使用方法#1来更新渲染,并在旁边使用一个线程处理世界逻辑?使用无限循环(带有Application.ProcessMessages())更新表单控件并从另一个工作在世界、对象等的线程中获取显示数据是否安全?

你可以使用匿名线程(带有同步)来实现这个。 - Freddie Bell
如果你想避免FireMonkey UI引起的游戏循环延迟,那么最好的选择是将所有游戏逻辑移动到辅助线程中。但即使如此,如果目标设备只有一个处理核心,这也可能无法完全解决问题。你会发现,FireMonkey在处理UI消息方面非常糟糕/缓慢。作为一个测试,只需创建一个新的FireMonkey应用程序,并在表单上放置大约100个面板。然后编译它并观察如何快速地在表单上移动鼠标会导致CPU使用率显著增加。... - SilverWarior
@KoalaGangsta:只是好奇,你最终做了什么?我也想在FM中实现一个动画循环,然后看到了你的帖子。谢谢。 - boggy
@KoalaGangsta:我找到了这个视频:https://www.youtube.com/watch?v=O4-SFLjF8OU。他使用了一个计时器。 - boggy
不确定 https://docwiki.embarcadero.com/Libraries/Alexandria/en/FMX.Forms.TApplication.ActionUpdateDelay 和/或 https://docwiki.embarcadero.com/Libraries/Alexandria/en/FMX.Forms.ActionUpdateDelayNever 能否在这种情况下有所帮助。但从它们的文档中不确定它们的预期应用场景是什么。 - George Birbilis
显示剩余2条评论
1个回答

1

我认为一个有效的选择是实现一个TAnimation并重写ProcessAnimation方法。当通过TAnimator运行时,TAnimation会自动调度。

  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;

默认情况下,动画的FPS设置为60。目前我无法弄清楚如何仅使用TAnimation的属性计算自上次调用以来的Delta时间。这对于处理可变帧时间至关重要。但是,您可以使用System.Diagnostics.TStopwatch自行完成。在ProcessAnimation开始时停止观察并获取Ticks,在完成ProcessAnimation后启动一个新观察。

编辑: Deltatime(自上次调用以来经过的时间,以秒为单位)的基本示例。假设您在Form1上放置了ViewPort3D,添加了名为Cube1的TCube,声明了与Form1相同单元中的TGameLoop(我知道,很丑,但是演示目的),并且TGameLoop具有类型为TStopWatch的FWatch字段。

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;

编辑2:更好的示例,可以更好地分配舍入/测量误差到整个游戏寿命中。我们不仅在调用之间进行测量,而是从第一帧开始进行测量。TGameLoop有一个新字段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;

非常感谢您的回答!经过一些测试,似乎这并不能解决我的OnMouseMove事件“滞后”的问题,但我认为我需要更深入地测试,这可能会有所作用。您知道是否有机会调整FPS吗?在移动设备上,它似乎运行得非常缓慢 :-/ 到目前为止还是非常感谢! - KoalaGangsta
你有检查过游戏逻辑是否在“烧”CPU吗?你没有指定你正在使用的手机。否则,将其放入额外的线程中可能是一个选项,就像在主贴的其他评论中讨论的那样。也许你可以更详细地阐述你正在做什么?有很多因素会影响它。要玩FPS,请使用AniFrameRate属性。 - Alexander B.
你可以尝试的另一种方法是,在执行逻辑之前,在游戏循环中轮询键盘/鼠标,以避免事件干扰。然后,你的游戏逻辑需要访问某种接口,其中存储了这些值。 - Alexander B.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接