Я хотел бы добиться детерминизма в своем игровом движке, чтобы иметь возможность сохранять и воспроизводить входные последовательности и облегчать работу в сети.
Мой движок в настоящее время использует переменный временной шаг: каждый кадр я вычисляю, сколько времени потребовалось, чтобы обновить / нарисовать последний и передать его методу обновления моих сущностей. Это делает игры со скоростью 1000 кадров в секунду такими же быстрыми, как игры со скоростью 30 кадров в секунду, но в то же время вводит недетерминированное поведение.
Решением может быть исправление игры до 60FPS, но это сделает ввод более задержанным и не получит преимущества более высокой частоты кадров.
Поэтому я попытался использовать поток (который постоянно вызывает update (1), а затем спит в течение 16 мс) и рисовать как можно быстрее в игровом цикле. Это вроде работает, но часто вылетает, и мои игры становятся неиграбельными.
Есть ли способ реализовать многопоточность в моем игровом цикле для достижения детерминизма без необходимости переписывать все игры, которые зависят от движка?
Вы должны отделить игровые фреймы от графических фреймов. Графические рамки должны отображать только графику, и ничего больше. Для воспроизведения не имеет значения, сколько графических кадров мог выполнить ваш компьютер, будь то 30 в секунду или 1000 в секунду, компьютер-проигрыватель, скорее всего, воспроизведет его с другой графической частотой кадров.
Но вы действительно должны исправить игровые рамки. Например. до 100 игровых кадров в секунду. В игровом фрейме игровая логика выполнена: материал, который важен для вашей игры (и воспроизведения).
Ваш игровой цикл должен выполнять графические фреймы всякий раз, когда нет необходимости в игровом фрейме, поэтому, если вы исправите свою игру до 100 игровых фреймов в секунду, это будет 0,01 секунды на игровой фрейм. Если вашему компьютеру требуется только 0,001 для выполнения этой логики в игровом фрейме, остальные 0,009 секунды остаются для повторения графических фреймов.
Это небольшой, но неполный и не на 100% точный пример:
uint16_t const GAME_FRAMERATE = 100;
uint16_t const SKIP_TICKS = 1000 / GAME_FRAMERATE;
uint16_t next_game_tick;
Timer sinceLoopStarted = Timer(); // Millisecond timer starting at 0
unsigned long next_game_tick = sinceLoopStarted.getMilliseconds();
while (gameIsRunning)
{
//! Game Frames
while (sinceLoopStarted.getMilliseconds() > next_game_tick)
{
executeGamelogic();
next_game_tick += SKIP_TICKS;
}
//! Graphical Frames
render();
}
Следующая ссылка содержит очень хорошую и полную информацию о создании точного игрового цикла:
Быть детерминированным через сеть, вам нужна единая точка правды, обычно называемая «сервером». В игровом сообществе есть поговорка: «Клиент в руках врага». Это правда. Вы не можете доверять ничему, что рассчитано на клиента для честной игры.
Если, например, ваша игра станет проще, если по каким-то причинам ваш поток обновляется только 59 раз в секунду вместо 60, люди узнают об этом. Возможно, в начале они даже не будут злыми. В то время они просто загружали свои машины, и ваш процесс не доходил до 60 раз в секунду.
Если у вас есть сервер (может быть, даже внутрипроцессный, как поток в одиночной игре), который не заботится о графике или циклах обновления и работает на своей собственной скорости, он становится достаточно детерминированным, чтобы хотя бы получить те же результаты для всех игроков. Это может все еще не быть на 100% детерминированным на основании того факта, что компьютер не работает в режиме реального времени. Даже если вы скажете ему обновлять каждую частоту $, это может не произойти из-за того, что другие процессы на компьютере слишком загружены.
Сервер и клиенты должны обмениваться данными, поэтому серверу необходимо отправить копию своего состояния (для производительности может быть дельта из последней копии) каждому клиенту. Клиент может нарисовать эту копию с наилучшей доступной скоростью.
Если ваша игра рушится с потоком, возможно, это вариант фактически вывести «сервер» из процесса и обмениваться данными по сети, таким образом вы довольно быстро узнаете, какие переменные потребовали бы блокировок, потому что если вы просто переместите их в другой проект, ваш клиент больше не будет компилироваться.
Разделите игровую логику и графику на разные темы. Поток игровой логики должен работать с постоянной скоростью (скажем, он обновляется 60 раз в секунду или даже выше, если ваша логика не слишком сложна, чтобы добиться более плавного игрового процесса). Затем ваш графический поток должен всегда извлекать последнюю информацию, предоставленную логическим потоком, как можно быстрее, чтобы достичь высокой частоты кадров.
Чтобы предотвратить отрисовку частичных данных, вам, вероятно, следует использовать какую-то двойную буферизацию, когда логический поток записывает в один буфер, а графический поток читает из другого. Затем переключайте буферы каждый раз, когда логический поток выполняет одно обновление.
Это должно гарантировать, что вы всегда используете графическое оборудование компьютера в полной мере. Конечно, это означает, что вы накладываете ограничения на минимальную скорость процессора.
Я не знаю, поможет ли это, но, если я правильно помню, Doom сохранил ваши входные последовательности и использовал их для генерации поведения ИИ и некоторых других вещей. Демоверсия в Doom — это набор чисел, представляющих не состояние игры, а ваш вклад. Исходя из этого, игра сможет восстановить то, что произошло, и, таким образом, достичь некоторого детерминизма … Хотя я помню, что это иногда не синхронизировалось.