Хороший спосіб побудувати ігровий цикл у OpenGL


31

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

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

Моє запитання, який кращий спосіб реалізувати ігровий цикл у OpenGL? Чи слід уникати використання функції простою?



Я вважаю, що це питання є більш специфічним для OpenGL, і чи доцільно використовувати GLUT для циклу
Jeff

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, який дає вам змогу отримувати події вводу вбудований (у власному головному циклі).
  • SDL , однак його акцент не є спеціально OpenGL.

Гарна стаття. Отже, щоб зробити це у OpenGL, я б не використовував glutMainLoop, а замість свого? Якби я це зробив, чи зможу я використовувати glutMouseFunc та інші функції? Я сподіваюся, що це має сенс, я все ще досить новий у всьому цьому
Джефф

На жаль ні. Усі зворотні виклики, зареєстровані в GLUT, вимикаються зсередини glutMainLoop, коли черга подій вичерпується, і вона повторюється. Я додав кілька посилань на альтернативи в тілі відповідей.
charstar

1
+1 для викидання GLUT на все інше. Наскільки мені відомо, GLUT ніколи не був призначений ні для чого, крім тестових програм.
Jari Komppa

Ви все ще повинні обробляти системні події, ні? Я розумів, що, використовуючи glutIdleFunc, він просто обробляє (в Windows) цикл GetMessage-TranslateMessage-DispatchMessage і викликає вашу функцію, коли не буде отримано повідомлення. Ви все одно зробите це самі, інакше страждаєте від нетерпіння ОС у визначенні вашої програми як невідповідної, правда?
Ricket

@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();
    }
}

fixTimestep не буде викликатися кожні 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.