适用于Android的Firemonkey游戏循环

Koa*_*sta 6 delphi android game-engine game-loop firemonkey

我目前正在Firemonkey中为我的Android手机制作2D游戏,使用一些TImage控件并控制它们的位置和角度.就那么简单.我尝试使用我正常的循环方式,在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;
Run Code Online (Sandbox Code Playgroud)

问题是,当我陷入循环并调用过程来处理消息时,我可能遇到很多问题,而且我也不认为这是一个好方法.

方法#2:TTimer(甚至不考虑它;-))

由于TTimer在很多方面都很糟糕而且并不意味着用于这样的事情,我只是将TTimer与最小间隔放在一起的方法就会失效.此外,我从未尝试过其他计时器,但如果有一个真正的游戏,我会尝试它当然:)

方法#3:OnIdle - 我在Windows上通常如何操作

在Windows上,我可以使用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;
Run Code Online (Sandbox Code Playgroud)

每次应用程序在Windows消息上空闲时都会调用它.与#1相比,性能似乎相同或稍差,整体架构更可靠,这也是我通常使用此方法的原因.然而,在Android上,当与TForm.MouseMove结合使用时,它似乎被称为不同.在Windows OnIdle保持完美工作的地方,在Android上,它将滞后/停止,而TForm.MouseMove是从触摸输入调用的(RAD Studio自动将其编译为触摸事件)

然而,我可以在MouseMove事件中通过调用Application.DoIdle和"辅助""循环" 来激活OnIdle ,我认为它错过了beimg被调用,但这也很糟糕,并且在使用触摸输入时再次带来不必要的行为和更差的性能.

这让我回到方法#1,它现在似乎在Android上运行最好.是否有任何其他方法可以创建一个可靠的方法(像OnIdle这样的持续快速调用的事件)创建这样的循环或任何方法来避免我在方法#3中与MouseMove-Events结合使用的问题?看起来Android手机不够强大,足以在表格事件和世界更新旁边留下足够的"空闲时间"

另外,我可以考虑使用线程作为世界逻辑,并在其旁边使用我的Main-Form/Thread上的方法#1来更新渲染吗?通过无限循环(使用Application.ProcessMessages())更新表单控件并从另一个线程处理世界,对象等所显示的数据是否安全?

Ale*_* B. 1

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

  TGameLoop = class(TAnimation)
  protected
    procedure ProcessAnimation; override;
  end;
  [...]
  procedure TGameLoop.ProcessAnimation;
  begin
    //call updates
  end;
Run Code Online (Sandbox Code Playgroud)

并像这样开始:

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

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

编辑:Deltatime 的基本示例(自上次调用以来经过的时间,以秒为单位)。假设您在 Form1 上放置了一个 ViewPort3D,添加了名为 Cube1 的 TCube,TGameLoop 在与 Form1 相同的单元中声明(我知道,丑陋,但演示目的),并且 TGameLoop 具有 TStopWatch 类型的 Field 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;
Run Code Online (Sandbox Code Playgroud)

EDIT2:一个更好的例子,可以更好地在整个游戏生命周期内分配舍入/测量误差。我们不是仅在调用之间进行测量,而是从第一帧开始进行测量。TGameLoop 有一个新的 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;
Run Code Online (Sandbox Code Playgroud)