Хороший способ построить игровой цикл в OpenGL


31

В настоящее время я начинаю изучать OpenGL в школе, и на днях я начал делать простую игру (самостоятельно, а не для школы). Я использую freeglut и собираю его на C, поэтому для своего игрового цикла я действительно использовал переданную мне функцию, glutIdleFuncчтобы обновить все чертежи и физику за один проход. Это было хорошо для простых анимаций, которые меня не особо волновали по частоте кадров, но поскольку игра в основном основана на физике, я действительно хочу (нужно) связать, насколько быстро она обновляется.

Поэтому моя первая попытка состояла в том, чтобы мою функцию я передал glutIdleFunc( myIdle()), чтобы отслеживать, сколько времени прошло с момента его предыдущего вызова, и обновлять физику (и текущую графику) каждые миллисекунды. Я timeGetTime()делал это (используя <windows.h>). И это заставило меня задуматься, действительно ли использование функции простоя является хорошим способом прохождения игрового цикла?

Мой вопрос: как лучше реализовать игровой цикл в OpenGL? Следует ли мне избегать использования функции ожидания?



Я чувствую, что этот вопрос более специфичен для OpenGL и целесообразно ли использовать GLUT для цикла
Джефф

1
Здесь человек , не используйте GLUT для больших проектов . Это хорошо для меньших, все же.
Бобобобо

Ответы:


29

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

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

Лучшее объяснение и учебник, который я нашел по этому вопросу, - это « Исправление твоего временного шага» Гленна Фидлера.

В этом уроке содержится полное описание, однако, если у вас нет реального физического моделирования, вы можете пропустить истинную интеграцию, но основной цикл все еще сводится к (в подробном псевдокоде):

// The amount of time we want to simulate each step, in milliseconds
// (written as implicit frame-rate)
timeDelta = 1000/30
timeAccumulator = 0
while ( game should run )
{
  timeSimulatedThisIteration = 0
  startTime = currentTime()

  while ( timeAccumulator >= timeDelta )
  {
    stepGameState( timeDelta )
    timeAccumulator -= timeDelta
    timeSimulatedThisIteration += timeDelta
  }

  stepAnimation( timeSimulatedThisIteration )

  renderFrame() // OpenGL frame drawing code goes here

  handleUserInput()

  timeAccumulator += currentTime() - startTime 
}

Делая это таким образом, задержки в вашем коде рендеринга, обработке ввода или операционной системе не приводят к отставанию вашего игрового состояния. Этот метод также переносим и независим от графической библиотеки.

GLUT - хорошая библиотека, однако она строго основана на событиях. Вы регистрируете обратные вызовы и запускаете основной цикл. Вы всегда передаете контроль над своим основным циклом, используя GLUT. Есть обходные пути , вы также можете подделать внешний цикл, используя таймеры и тому подобное, но другая библиотека, вероятно, является лучшим (более простым) способом. Есть много альтернатив, вот несколько (с хорошей документацией и быстрыми учебниками):

  • GLFW, который дает вам возможность получать входные события inline (в вашем собственном главном цикле).
  • SDL , однако его акцент не является конкретно OpenGL.

Хорошая статья Таким образом, чтобы сделать это в OpenGL, я бы не использовал glutMainLoop, а вместо этого свой? Если бы я сделал это, смогу ли я использовать glutMouseFunc и другие функции? Я надеюсь, что это имеет смысл, я все еще довольно новичок во всем этом
Джефф

К сожалению нет. Все обратные вызовы, зарегистрированные в GLUT, запускаются из glutMainLoop по мере опустошения очереди событий и ее повторения. Я добавил несколько ссылок на альтернативы в теле ответа.
charstar

1
+1 за угробление GLUT за все остальное. Насколько я знаю, GLUT никогда не предназначался ни для чего, кроме тестовых приложений.
Яри ​​Комппа

Вы все еще должны обрабатывать системные события, нет? Насколько я понимаю, используя glutIdleFunc, он просто обрабатывает (в Windows) цикл GetMessage-TranslateMessage-DispatchMessage и вызывает вашу функцию всякий раз, когда нет сообщения, которое нужно получить. В любом случае, вы бы сделали это сами, или же страдали бы от нетерпения ОС по поводу того, чтобы пометить ваше приложение как не отвечающее, верно?
Рикет

@Ricket Технически, glutMainLoopEvent () обрабатывает входящие события, вызывая зарегистрированные обратные вызовы. glutMainLoop () эффективно {glutMainLoopEvent (); IdleCallback (); } Ключевым моментом здесь является то, что перерисовка также является обратным вызовом, обработанным из цикла событий. Вы можете заставить управляемую событиями библиотеку вести себя как управляемая временем, но Молот Маслоу и все такое ...
charstar

4

Я думаю, что glutIdleFuncэто нормально; это, по сути, то, что вы бы в конечном итоге сделали вручную, если бы не использовали его. Неважно, что у вас будет жесткий цикл, который не вызывается в фиксированном шаблоне, и вам нужно выбрать, хотите ли вы преобразовать его в цикл с фиксированным временем или оставить его в цикле с переменным временем и сделать уверен, что все ваши математические счета для интерполяции времени. Или даже смесь из них, так или иначе.

У вас, безусловно, может быть myIdleфункция, которую вы передаете glutIdleFunc, и в рамках которой вы измеряете прошедшее время, делите ее на свой фиксированный временной интервал и вызываете другую функцию много раз.

Вот что не нужно делать:

void myFunc() {
    // (calculate timeDifference here)
    int numOfTimes = timeDifference / 10;
    for(int i=0; i<numOfTimes; i++) {
        fixedTimestep();
    }
}

fixedTimestep не будет вызываться каждые 10 миллисекунд. На самом деле он будет работать медленнее, чем в режиме реального времени, и вы можете в конечном итоге выдумать цифры, чтобы компенсировать, когда на самом деле корень проблемы заключается в потере остатка при делении timeDifferenceна 10.

Вместо этого сделайте что-то вроде этого:

long queuedMilliseconds; // static or whatever, this must persist outside of your loop

void myFunc() {
    // (calculate timeDifference here)
    queuedMilliseconds += timeDifference;
    while(queuedMilliseconds >= 10) {
        fixedTimestep();
        queuedMilliseconds -= 10;
    }
}

Теперь эта структура цикла гарантирует, что ваша fixedTimestepфункция будет вызываться один раз в 10 миллисекунд, без потери каких-либо миллисекунд в качестве остатка или чего-либо подобного.

Из-за многозадачности операционных систем нельзя полагаться на функцию, вызываемую точно каждые 10 миллисекунд; но вышеприведенный цикл будет происходить достаточно быстро, и, надеюсь, он будет достаточно близок, и вы можете предположить, что каждый вызов fixedTimestepбудет увеличивать вашу симуляцию на 10 миллисекунд.

Пожалуйста, прочитайте этот ответ и особенно ссылки, приведенные в ответе.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.