Путаница с написанием игрового цикла

Я работаю над фреймворком для 2D-видеоигр и никогда раньше не писал игровой цикл. Большинство фреймворков, в которых я когда-либо заглядывал, похоже, реализуют как draw, так и update методы.

Для моего проекта я реализовал цикл, который вызывает эти 2 метода. Я заметил, что в других фреймворках эти методы не всегда называются чередующимися. Некоторые фреймворки будут update работать больше, чем draw. Кроме того, большинство из этих типов фреймворков будут работать со скоростью 60 кадров в секунду. Я полагаю, мне нужно здесь немного поспать.

У меня вопрос: как лучше всего реализовать этот тип цикла? Мне позвонить draw, затем update или наоборот? В моем случае я пишу оболочку для SDL2, поэтому, возможно, для этой библиотеки требуется что-то настроить в определенным образом?

Вот какой-то «псевдокод», который я придумываю для реализации.

loop do
  clear_screen
  draw
  update
  sleep(16.milliseconds)
  break if window_is_closed
end

Хотя мой проект пишется на Crystal-Lang, я больше ищу общую концепцию, которая могла бы быть применяется к любому языку.


person jeremywoertink    schedule 07.06.2017    source источник
comment
Не имеет прямого отношения, но вы можете переосмыслить полноценный clear_screen шаг. Это потенциально дорого и не всегда необходимо. Вызывающий может захотеть очистить только небольшие разделы, которые были изменены, чтобы сэкономить время процессора.   -  person Carcigenicate    schedule 07.06.2017
comment
И мне по крайней мере, если имеет смысл обновлять, то рисовать. Если вы сделаете наоборот, вы всегда будете рисовать предыдущий кадр вместо текущего.   -  person Carcigenicate    schedule 07.06.2017
comment
Хорошо, в этом есть смысл! Это то, что я ищу. Спасибо   -  person jeremywoertink    schedule 07.06.2017


Ответы (1)


Это зависит от того, чего вы хотите достичь. Некоторые игры предпочитают, чтобы игровая логика выполнялась чаще, чем частота кадров (я считаю, что исходные игры делают это), для некоторых игр вы можете захотеть, чтобы игровая логика выполнялась реже (единственный пример этого, который я могу придумать, - это серверы некоторые многопользовательские игры, в частности Overwatch).

Также важно учитывать, что это вопрос разрешения, а не скорости. Игра с логической частотой 120 и частотой кадров 60 не обязательно работает на скорости x2, любые критические по времени операции в игровой логике должны выполняться относительно часов *, а не частоты тиков, иначе ваша игра буквально перейдет в замедленное движение, если рендеринг кадров занимает слишком много времени.

Я бы порекомендовал написать такой цикл:

loop do
    time_until_update = (update_interval + time_of_last_update) - current_time
    time_until_draw = (draw_interval + time_of_last_draw) - current_time
    work_done = false

    # Update the game if it's been enough time
    if time_until_update <= 0
        update
        time_of_last_update = current_time
        work_done = true
    end

    # Draw the screen if it's been enough time
    if time_until_draw <= 0
        clear_screen
        draw
        time_of_last_draw = current_time
        work_done = true
    end

    # Nothing to do, sleep for the smallest period
    if work_done == false
        smaller = time_until_update

        if time_until_draw < smaller
            smaller = time_until_draw
        end

        sleep_for(smaller)
    end

    # Leave, maybe
    break if window_is_closed
end

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

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

person user573949    schedule 10.06.2017
comment
Вау! Спасибо за подробное объяснение. В этом есть смысл. Я подумал, что в моем примере чего-то не хватало, но вот в чем была путаница. В моем случае, все эти игры будут простыми 2D-играми, так что ничего особенного, как Overwatch. Кроме того, частота кадров не должна быть сумасшедшей, поскольку игры, созданные с использованием этого фреймворка, на самом деле не будут слишком большими. Попробую реализовать вот так, и посмотрю, как получится. Должен ли я начать с update_interval из 60 и draw_interval из 120? - person jeremywoertink; 11.06.2017
comment
Я предполагаю, что вы имеете в виду 120 итераций в секунду, а не 120 мс (что составляет всего 8,3 итераций в секунду). Я бы начал со скорости 60 / с для обоих и повысил скорость обновления, если оборудование позволяет, и это улучшает точность вашей игры (например, во многих физических системах это предотвращает постепенное прохождение объектов сквозь стены). Нет смысла повышать скорость прорисовки, так как в мире очень мало мониторов, способных обеспечивать ›60 кадров в секунду. (Хотя, возможно, включить опцию для людей, у которых есть эти мониторы?) - person user573949; 11.06.2017
comment
На самом деле я имел в виду только сырые значения. В вашем примере вы определили переменную для update_interval и draw_interval. Мне было любопытно, какие начальные значения для этих переменных вы предложите. Также, имея дело со временем вроде current_time, вы предлагаете использовать секунды? Или это должны быть миллисекунды? - person jeremywoertink; 11.06.2017
comment
Я бы всегда использовал миллисекунды, секунды огромны для большинства игр, это может испортить такие вещи, как, например, анимация регенерирующей шкалы здоровья. Для необработанных значений я бы использовал фактический размер интервала, поэтому для 60 итераций в секунду это будет 1000/60 = 16/17 миллисекунд. - person user573949; 12.06.2017