Игровой цикл в Firemonkey для Android

#android #delphi #game-engine #firemonkey #game-loop

#Android #delphi #игровой движок #firemonkey #игровой цикл

Вопрос:

в настоящее время я создаю 2D-игру в Firemonkey для своего телефона Android, используя некоторые элементы управления временем и контролируя их положения и углы. Все просто. Я попытался использовать свой обычный способ зацикливания, который хорошо / безупречно работает в 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;
 

Проблема в том, что, поскольку я застрял в цикле, а также вызываю процедуру для обработки сообщений, я могу столкнуться со многими проблемами, а также я просто не думаю, что это хороший способ.

Способ №2: TTimer (даже не думайте об этом 😉 )

Поскольку TTimer ужасен во многих отношениях и не предназначен для использования для таких целей, подход, при котором я просто ставлю TTimer с минимальным интервалом, отпадает. Кроме того, я никогда не пробовал другие таймеры, кроме этого, но если есть действительно подходящий для игр, я, конечно, попробую его 🙂

Метод № 3: OnIdle — Как я обычно делаю это в Windows

В Windows я могу использовать это приложение.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. Производительность кажется такой же или немного хуже по сравнению с # 1, а общая архитектура намного надежнее, поэтому я обычно использую этот метод. Однако на Android кажется, что он вызывается по-другому в сочетании с TForm.MouseMove. Там, где в Windows OnIdle продолжает работать отлично, на Android он будет отставать / останавливаться, пока TForm.MouseMove вызывается из сенсорного ввода (RAD Studio автоматически компилирует его в события касания)

Однако я могу запустить OnIdle самостоятельно в событии MouseMove, вызвав Application.DoIdle и «assist» «Цикл», где, я думаю, он пропускает вызов beimg, но это работает также очень плохо и снова приводит к нежелательному поведению и ухудшению производительности при работе с сенсорным вводом.

И это возвращает меня к методу № 1, и, похоже, на данный момент он работает лучше всего на Android. Есть ли какой-либо другой способ создания надежного способа (постоянно и быстро вызываемого события, такого как OnIdle или около того) создания такого цикла или какой-либо способ избежать проблем, с которыми я сталкиваюсь в методе # 3 в сочетании с MouseMove-Events? Похоже, что телефон Android недостаточно мощный, чтобы иметь достаточно «времени простоя» рядом с формой-события и обновления мира

Кроме того, могу ли я рассмотреть возможность использования потока для мировой логики и рядом с ним метода # 1 в моей основной форме / потоке для обновления рендеринга? Безопасно ли обновлять элементы управления формой с помощью бесконечного цикла (с помощью Application.ProcessMessages ()) и получение отображаемых данных из другого потока, работающего с миром, объектами и так далее?

Комментарии:

1. Вы можете использовать анонимную потоковую передачу (с синхронизацией) для достижения этой цели

2. Если вы хотите избежать задержек игрового цикла, вызванных пользовательским интерфейсом FireMonkey, то лучшим выбором будет переместить всю вашу игровую логику во вторичный поток. Но даже это может полностью решить вашу проблему, особенно если целевое устройство имеет только одно ядро обработки. Вы видите, что FireMonkey довольно плохо / медленно обрабатывает сообщения пользовательского интерфейса. В качестве теста просто создайте новое приложение FireMonkey и разместите около 100 панелей в форме. Затем скомпилируйте его и понаблюдайте, как быстрые движения мыши над вашей формой приведут к значительному скачку загрузки процессора. …

3. … Такой тест способен максимально увеличить загрузку процессора на моем 7-летнем ноутбуке, и это не просто какой-то слабый ноутбук, так как я могу играть в Far Cry 3 на средних настройках без каких-либо проблем. Теперь я могу только представить, какую нагрузку на процессор вызовет FireMonkey на мобильном телефоне при использовании этого сценария. Вот почему я не рассматриваю возможность использования FireMonkey для какой-либо разработки игр.

4. Спасибо вам обоим за ваши комментарии! Как вы думаете, я мог бы улучшить обмен сообщениями в пользовательском интерфейсе, нарисовав все игровые объекты с помощью canvas в OnPaint-Event вместо рисования, как 50 TImages? Я также начал небольшую награду, если кто-нибудь из вас хочет попробовать 🙂

5. @KoalaGangsta: Просто любопытно, что ты в итоге сделал? Я также хочу реализовать цикл анимации в FM, и я наткнулся на ваш пост. Спасибо

Ответ №1:

Я думаю, что допустимым вариантом является реализация TAnimation и переопределение ProcessAnimation. танимация автоматически запланирована при запуске через TAnimator.

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

и начал все вот так:

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

По умолчанию для анимации установлено значение 60 кадров в секунду. Одна вещь, которую я не мог понять прямо сейчас, — это как вычислить время задержки с момента последнего вызова, используя только свойства TAnimation . Это важно для работы с переменным временем цикла. Но вы можете сделать это сами, используя System.Диагностика.TStopwatch. Остановите часы в начале ProcessAnimation и получите тики, запустите новый после завершения ProcessAnimation.

РЕДАКТИРОВАТЬ: базовый пример для Deltatime (прошедшее время с момента последнего вызова в секундах). Учитывая, что вы разместили Viewport3D в Form1, добавили TCube с именем Cube1, TGameLoop объявляется в том же блоке, что и Form1 (я знаю, уродливый, но демонстрационный), а TGameLoop имеет поле FWatch типа 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;
 

РЕДАКТИРОВАНИЕ 2: лучший пример, который распределяет ошибки округления / измерения по всей игре — время жизни немного лучше. Вместо того, чтобы измерять только между вызовами, мы измеряем с первого кадра. В TGameLoop появилось новое поле FLastElapsedTicks (двойное)

 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;
 

Комментарии:

1. Большое спасибо за ваш ответ! После некоторого тестирования кажется, что это не решает проблему, из-за которой мое событие OnMouseMove «запаздывает», но я думаю, что мне нужно углубиться в тестирование, чтобы это могло сработать. Знаете ли вы, есть ли возможность поиграть с FPS? На мобильных устройствах это, кажется, работает очень медленно: -/ Пока спасибо!

2. Вы проверяли, не «сжигает» ли ваша игровая логика процессор? Вы не указали телефон, с которым работаете. В противном случае можно было бы поместить его в дополнительный поток, как обсуждалось в других комментариях вашего основного сообщения. Может быть, вы могли бы подробнее рассказать о том, над чем вы работаете? Есть многое, что может повлиять на это. Чтобы играть с FPS, используйте свойство AniFrameRate.

3. Что-то еще, что вы могли бы попробовать, это опрос клавиш / мыши в вашем игровом цикле перед выполнением вашей логики, чтобы избежать вмешательства в события. Затем вашей игровой логике необходимо получить доступ к какому-либо интерфейсу, в котором вы сохранили эти значения.