Как реализовать игровой цикл в реактивном банане?

Этот вопрос относится к реактивно-банановым симуляциям и симуляциям в реальном времени с физическим и визуальным компонентом (например, играм).

Согласно Fix Your Timestep! идеальный способ настроить игровой цикл (при условии физика, которая должна быть воспроизводимой), вам нужен фиксированный временной шаг между кадрами. После рассмотрения ряда реальных сложностей автор приходит к такому игровому циклу:

double t = 0.0;
const double dt = 0.01;

double currentTime = hires_time_in_seconds();
double accumulator = 0.0;

State previous;
State current;

while ( !quit )
{
     double newTime = time();
     double frameTime = newTime - currentTime;
     if ( frameTime > 0.25 )
          frameTime = 0.25;   // note: max frame time to avoid spiral of death
     currentTime = newTime;

     accumulator += frameTime;

     while ( accumulator >= dt )
     {
          previousState = currentState;
          integrate( currentState, t, dt );
          t += dt;
          accumulator -= dt;
     }

     const double alpha = accumulator / dt;

     State state = currentState*alpha + previousState * ( 1.0 - alpha );

     render( state );
}

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

Например, вы можете хотеть обновления с частотой 20 Гц, но визуальное обновление с частотой кадров 60 Гц. Этот цикл выполняет линейную интерполяцию физики, чтобы компенсировать разницу между обновлениями физики и обновлениями графики.

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

В этом обсуждении меня больше всего интересует такая организация, чтобы вызов физического движка (вызов integrate) всегда выполнялся с шагом dt. Позволяет ли reactive-banana пользователю написать этот цикл стилей? Если да, то как? Возможно, пример моделирования физики в реальном времени подходит (или уже существует)?


person Jason Dagit    schedule 02.10.2012    source источник


Ответы (1)


В этом обсуждении меня больше всего интересует такая организация, чтобы вызов физического движка (вызов интегрирования) всегда выполнялся с помощью dt. Позволяет ли reactive-banana пользователю писать этот цикл стиля?

Да, реактивный банан может это сделать.

Идея состоит в том, что вы пишете собственный цикл обработки событий и подключаете к нему reactive-banana. Библиотека не делает никаких предположений относительно того, откуда вы берете свои события, она «всего лишь» решает проблему аккуратного описания новых событий с точки зрения существующих. В частности, вы можете использовать функцию newAddHandler для создания двух функций обратного вызова, которые вызываются в соответствующих местах цикла обработки событий. По сути, reactive-banana — это просто умопомрачительный метод написания обычных функций обратного вызова, сохраняющих состояние. Когда и как вы вызываете эти функции, зависит от вас.

Вот общая схема:

-- set up callback functions
(renderEvent, render) <- newAddHandler
(stateUpdateEvent, stateUpdate) <- newAddHandler

-- make the callback functions do something interesting
let networkDescription = do
    eRender      <- fromAddHandler renderEvent
    eStateUpdate <- fromAddHandler stateUpdateEvent
    ...
    -- functionality here

actuate =<< compile networkDescription

-- event loop
while (! quit)
{
    ...
    while (accumulator >= dt)
    {
        stateUpdate (t,dt)      -- call one callback
        t += dt
        accumulator -= dt
    }
    ...
    render ()                   -- call another callback
}

На самом деле, я написал пример игрового цикла в этом стиле для более старой версии reactive-banana, но так и не справился до полировки и публикации на hackage. Важные вещи, которые я хотел бы видеть завершенными:

  • Выберите графический движок, который легко установить и который работает в GHCi. Концепция использует SDL, но это действительно довольно неудобно, поскольку ее нельзя использовать из GHCi. Что-то вроде OpenGL + GLFW было бы неплохо.
  • Предложите небольшую абстракцию, чтобы упростить написание фазы интерполяции. Вероятно, только две вещи: событие eTimer :: Event t (), которое представляет собой регулярные обновления физики, и поведение bSinceLastTimer :: Behavior t TimeDiff, которое измеряет время, прошедшее с момента последнего обновления физики, которое можно использовать для выполнения интерполяции. (Это поведение, а не событие, поэтому внутренние обновления «нарисуй это!» прозрачны.)

затемненный клон Андреаса Бернштейна с использованием реактивного банана может быть отличным примером реализации в этом стиле.

person Heinrich Apfelmus    schedule 02.10.2012
comment
Я не уверен насчет SDL, но с OpenGL и GLFW они оба используют локальное хранилище потока в исходном потоке процесса (это должен быть исходный поток, ограничение поставщика). По умолчанию GHCI запускает каждую команду в отдельном потоке. Это означает, что такие библиотеки, как OpenGL/GLFW (и несколько других графических библиотек), не могут правильно получить доступ к локальному хранилищу своих потоков и сходят с ума от GHCi. Решение состоит в том, чтобы добавить -fno-ghci-sandbox при запуске GHCi. Вы можете попробовать это и посмотреть, исправит ли это ваши проблемы с SDL + GHCi: haskell.org/ghc/docs/7.0.1/html/users_guide/release-7-0-1.html - person Jason Dagit; 03.10.2012
comment
Боюсь, это немного сложнее, чем -fno-ghci-sandbox. На Mac SDL необходимо скомпилировать, поскольку он переопределяет main как макрос. Версии GLFW имеют тенденцию к сбою в GHCi из-за других несовместимостей, которых я не понимаю. - person Heinrich Apfelmus; 03.10.2012
comment
GLFW-b не должен вылетать из GHCi, если вы используете -fno-ghci-sandbox. Если это так, вы столкнулись с новой ошибкой, поэтому, пожалуйста, сообщите об этом! :) - person Jason Dagit; 03.10.2012
comment
Хорошо, я проверю это снова, когда у меня будет время. Спасибо Джейсон! - person Heinrich Apfelmus; 05.10.2012
comment
@JasonDagit Да, приложение GLWF-b0demo зависает и аварийно завершает работу через пару секунд при вызове его из GHCi в OS X (с -fno-ghci-sandbox и как с трюком EnableGUI.hs, так и без него). - person Heinrich Apfelmus; 05.10.2012
comment
@HeinrichApfelmus ваш пример игрового цикла великолепен, но я совершенно не могу заставить его работать. Кроме того, может быть, это ясно дает понять, что реактивное программирование бесполезно для игрового цикла. В чем преимущество запуска события рисования по сравнению с фактическим вызовом функции рисования? - person Zhen; 17.10.2012
comment
@Zhen: я написал пример для более старой версии реактивного банана и не обновлял его, поэтому он не работает. Преимущество заключается в той части функциональности, которую я пропустил. FRP предоставляет мощные инструменты для объединения существующих событий и отслеживания внутреннего состояния. Как видите, это тоже не связано с игровым циклом. - person Heinrich Apfelmus; 18.10.2012
comment
Я действительно хочу понять ответ здесь, чтобы применить его в контексте Sodium/Typescript... но я не могу понять Haskell (пока!)... не будет ли кто-нибудь так любезен, чтобы перевести его в Javascript и requestAnimationFrame? У меня есть аналогичный вопрос на форуме натрия - и вы можете видеть, что я процитировал @HeinrichApfelmus откуда-то еще: - person davidkomer; 04.01.2018