Автор рассказывает, как превратить уже почти готовую игру Ridiculous Space Battles (космический автобаттлер от создателя Gratuitous Space Battles) в полностью детерминированную систему симуляции.
Зачем нужна детерминированность
В игре результат боя должен быть повторяемым при одинаковых условиях: не только победа/поражение, но и точный счёт. Метаигра завязана на минимальный размер флота: игрок получает больше очков, если выигрывает меньшим числом эскадрилий. Разница в 1% (99 отрядов вместо 100) должна надёжно проверяться повторными прогоном боя. Любое микросмещение снаряда на 0.0001 единицы может изменить порядок событий (кто первым выстрелил, что сбила ПВО и т.п.) и полностью поменять исход битвы. Поэтому нужна не «почти», а 100% детерминированность.
Базовые принципы
Автор опирается на классические подходы:
- Постоянный шаг симуляции, отделённый от кадровой частоты.
- Отказ от настоящего рандома в симуляции — только псевдослучайные числа от фиксированного сида.
- Жёсткое разделение симуляции и UI: рандом и вариативность допустимы только в интерфейсе, но не в логике боя.
Переход на фиксированный шаг симуляции
Изначально игра работала с переменным фреймрейтом (до 60 FPS). Автор ввёл двухконтурную систему: симуляция идёт с постоянным шагом 4 мс (SIMTIME), а отрисовка — с независимой кадровой частотой (FRAMETIME). В игре есть 5 скоростей (от 0.25x до 4x), поэтому меняется только соотношение тиков симуляции к кадрам. «Сглаживание» кадров для слабых GPU частично потеряно, но компенсируется настройками графики.
Важно, что симуляция дискретная: ракета двигается и проверяет столкновения каждый тик. Нельзя просто «объединить» 4 тика в один при ускорении в 4 раза, иначе события (попадание, регенерация щитов и т.п.) сдвинутся и нарушат детерминированность.
Поиск дрейфа и отладка
Основная сложность — найти, где именно значения начинают расходиться между двумя идентичными прогонами. Автор написал отдельный класс «Determinism», который собирает данные симуляции за первый прогон и сравнивает их со вторым.
Ошибки в отладочном подходе:
- Изначально он хранил полные данные для обоих прогонов, хотя достаточно сохранять только первый и сравнивать «на лету» со вторым.
- Вместо прямого индексного доступа по номеру тика он искал «совпадающий тик» в массивах, что было избыточно.
Из-за 32-битного лимита памяти (2–4 ГБ) нельзя «просто сохранить всё» для длинных и крупных боёв с 240 тиками в секунду. Приходится балансировать между полнотой снимков состояния и объёмом данных.
Критические ошибки архитектуры
Хотя код формально разделён на SIM_ и GUI_-классы, в двух местах UI влиял на симуляцию и ломал детерминизм.
1. Побег кораблей через гиперпространство
Корабли «улетают» с разной скоростью для эффекта гиперпрыжка. Скорость выбиралась через настоящий рандом в GUI-коде, а момент завершения анимации сообщался в симуляцию как время фактического выхода корабля из боя. Иногда дополнительное ~50 мс присутствия корабля в бою давало ему шанс сделать ещё один выстрел и меняло исход.
2. Ударные волны от взрывов
Графические шоквейвы от взрывов сдвигали обломки, капсулы и ракеты. Логика была реализована в GUI: по радиусу волны находились объекты и на них применялась физическая сила. Поскольку соотношение тиков/кадров меняется, момент воздействия волны на ракету мог сдвигаться на один тик между прогонами, что тоже ломало детерминизм. Временное решение — отключить реакцию ракет на шоквейвы, оставив влияние только на чисто визуальные элементы.
Текущий результат
После правок автор получил 100% совпадение результатов в шести последовательных тестах на двух небольших уровнях. Процесс занял недели и потребовал детективной работы по отслеживанию самого первого расхождения значений между прогонами.
Выводы
- Для соревновательных и мета-зависимых автобаттлеров нужна строгая 100% детерминированность, а не «почти».
- Фиксированный шаг симуляции и отделение его от рендера — базовое условие предсказуемости.
- Настоящий рандом и UI-логика не должны влиять на симуляцию; любое пересечение ломает повторяемость.
- Детальная система логирования и сравнения прогонов обязательна для поиска дрейфа в сложной симуляции.
- Закладывать детерминизм нужно с самого начала проекта — переделка готового кода обходится очень дорого.