Эта глава — инженерное ядро всего учебника. Если предыдущая глава отвечала на вопрос «что такое движок мира», то здесь мы разбираем, как он устроен внутри: как течёт время в симуляции, в каком порядке обновляются системы, как отделить расчётное ядро от картинки, как хранить и воспроизводить состояние мира и как всё это масштабировать и тестировать. Для проектировщика движка управления компанией эта глава даёт готовый несущий каркас: компания в такой системе — это симулируемый мир, где сотрудники и контрагенты — агенты, бизнес-процессы — системы обновления, финансы — ресурсные потоки, а управленческое решение — команда, поданная в детерминированную симуляцию. Почти каждая архитектурная проблема, с которой столкнётся такой движок — воспроизводимость сценариев «что если», перемотка истории компании назад, ускоренный прогон квартала за секунды, симуляция тысяч сущностей без деградации, — уже была решена индустрией игр, причём решена под куда более жёсткими требованиями к производительности и надёжности. Мы будем разбирать эти решения на первоисточниках: от канонической статьи «Fix Your Timestep!» Гленна Фидлера и классики сетевого RTS-кода «1500 Archers on a 28.8» до девблогов Factorio и докладов GDC о data-oriented архитектурах.
2.1. Игровой цикл: сердце любой симуляции
Анатомия цикла
В основе любого движка — реального времени или пошагового — лежит игровой цикл (game loop). Роберт Нистром в книге «Game Programming Patterns» формулирует его назначение так: цикл обрабатывает пользовательский ввод, не блокируясь в его ожидании, обновляет состояние мира и отрисовывает результат, при этом контролируя скорость хода времени. Это трёхтактная машина: processInput() → update() → render(), крутящаяся десятки раз в секунду. Внутри update() живёт второй классический паттерн Нистрома — метод обновления (Update Method): каждый объект мира получает метод, который симулирует один кадр его поведения, и цикл вызывает эти методы для всех живых объектов каждую итерацию.
Джейсон Грегори в «Game Engine Architecture» уточняет важную деталь, которую часто упускают новички: в зрелых движках цикл обновляет не объекты по одному, а подсистемы — рендеринг, анимацию, физику, коллизии, звук, игровую логику. Каждая подсистема обслуживается главным циклом напрямую, а игровой объект, которому нужны её услуги, лишь регистрирует в ней своё состояние. Обновление объектов — это процесс вычисления состояния каждого объекта в текущий момент времени по его состоянию в предыдущий момент; когда все объекты обновлены, «текущее время» становится «предыдущим», и цикл повторяется. Такая перестановка — от «объект обновляет себя целиком» к «система обновляет свой аспект всех объектов» — кажется мелочью, но именно она откроет нам дорогу к ECS в разделе 2.3.
Fixed vs variable timestep: почему «просто измерить дельту» не работает
Центральный вопрос цикла — каким шагом двигать время. Каноническим текстом здесь остаётся статья Гленна Фидлера «Fix Your Timestep!» (Gaffer On Games, 2004) — вероятно, самый цитируемый текст об архитектуре игрового времени за двадцать с лишним лет. Фидлер последовательно разбирает четыре стратегии.
Фиксированная дельта (fixed delta time). Просто шагаем по 1/60 секунды. Идеально, пока частота обновления совпадает с частотой дисплея и машина успевает. На медленной машине симуляция начнёт идти медленнее реального времени, на быстрой с выключенным VSYNC — быстрее.
Переменная дельта (variable delta time). Измеряем длительность прошлого кадра и подаём её как шаг следующего. Кажется очевидным решением, но Фидлер называет его огромной ошибкой: поведение симуляции начинает зависеть от переданной дельты. Эффект варьируется от неуловимого («игра по-разному ощущается на разных частотах кадров») до катастрофического: пружинная система при большом шаге взрывается в бесконечность, быстрые объекты туннелируют сквозь стены. Нереалистично ожидать, что симуляция корректно обработает любую дельту — у любой численной схемы есть предел устойчивости.
Полуфиксированный шаг (semi-fixed timestep). Признаём, что симуляция устойчива только при шаге ≤ dt, и нарезаем фактическое время кадра на куски не больше dt. Появляется верхняя граница шага, но и новая опасность — «спираль смерти» (spiral of death): если симуляция X секунд мира занимает Y > X секунд реального времени, движок отстаёт, для навёрстывания ему нужно больше шагов, отчего он отстаёт ещё сильнее. Рецепты Фидлера: оставлять запас производительности (обновление X секунд мира должно занимать значительно меньше X секунд) и ставить жёсткий потолок числа шагов за кадр — пусть лучше симуляция видимо замедлится при пиковой нагрузке, чем умрёт в спирали.
Свободная физика с аккумулятором — итоговый паттерн. Если нужна точная воспроизводимость от запуска к запуску (а для детерминированного lockstep-мультиплеера и для реплеев — нужна), шаг обязан быть строго постоянным: даже «остаточный» маленький шаг полуфиксированной схемы ломает побитовую повторяемость из-за конечной точности плавающей запятой. Фидлер предлагает сменить точку зрения: рендерер производит время, а симуляция потребляет его дискретными порциями dt. Прошедшее реальное время складывается в аккумулятор; пока в аккумуляторе больше dt, симуляция делает целые шаги; остаток меньше dt переносится на следующий кадр, а не выбрасывается. Остаётся визуальный артефакт — картинка показывает состояние, чуть отстающее от реального времени, с дрожанием — и он лечится интерполяцией: рендерер хранит предыдущее и текущее состояния симуляции и смешивает их с коэффициентом alpha = accumulator / dt.
Эта схема — фиксированный детерминированный шаг симуляции плюс интерполируемое представление на произвольной частоте — стала де-факто стандартом. Она же по построению разделяет симуляцию и рендеринг, о чём подробно в разделе 2.2.
Tick rate и порядок обновления
Частоту фиксированного шага называют тикрейтом (tick rate). Factorio обновляет мир ровно 60 раз в секунду (сообщество измеряет производительность в UPS — updates per second), симуляция Overwatch работает на фиксированных «командных кадрах» по 16 мс, а гранд-стратегии Paradox живут в совсем ином масштабе: в Victoria 3 один тик равен шести игровым часам, и поверх обычных тиков надстроены дневные, недельные, месячные и годовые — разные подсистемы экономики обновляются с разной частотой. Выбор тикрейта — это компромисс между отзывчивостью, точностью численных схем и бюджетом CPU, и он не обязан быть одинаковым для всех систем мира.
Не менее важен порядок обновления систем внутри тика. Он должен быть, во-первых, осмысленным (ввод — до логики, логика — до физики, физика — до рендера; иначе появляются лаги в один кадр и трудноуловимые баги «порядка обновления»), во-вторых — строго детерминированным. Factorio, например, внутри тика обновляет поезда, затем электрическую сеть, затем конвейеры — всегда в одном и том же порядке, потому что любая недетерминированность порядка (обход хеш-таблицы, зависящий от адресов памяти; гонка потоков; итерация по объектам в порядке создания, различающемся между машинами) немедленно ломает воспроизводимость. Это же требование — детерминированный порядок систем — Blizzard в докладе об Overwatch называет одним из «жёстких правил» их ECS: поведение (behavior) живёт только в системах, и системы выполняются в фиксированной последовательности.
Движку компании нужен явный тик — квант модельного времени (час, день, неделя — по гранулярности данных), и аккумуляторная схема Фидлера, если симуляция должна идти «в реальном времени» рядом с живыми данными. Переменный шаг («обновим, когда придут данные, на сколько получится») — прямой путь к невоспроизводимым прогнозам: результат симуляции квартала начнёт зависеть от того, с какой частотой её дёргали. Порядок систем внутри тика (сначала спрос, потом производство, потом финансы, потом HR — или иной, но зафиксированный) — это, по сути, регламент закрытия периода в бухгалтерии: он должен быть детерминированным и версионируемым. И заложите защиту от «спирали смерти»: если пересчёт модельного дня стал занимать больше реального дня, движок должен деградировать управляемо (укрупнять шаг, снижать детализацию), а не копить отставание.
2.2. Симуляция и представление: два разных мира
Интерполяция из статьи Фидлера — частный случай общего принципа: состояние симуляции и его визуальное представление — разные вещи, живущие в разном времени и не обязанные знать друг о друге. Симуляция — это чистая функция «состояние + входы → новое состояние», работающая на фиксированном тике. Представление (presentation) — рендеринг, интерфейс, анимации, звук — читает состояние симуляции и показывает его пользователю на любой удобной частоте, ничего в нём не меняя.
Практическая проверка чистоты этого разделения — способность движка работать «безголовым» (headless): собираться и запускаться вообще без графики. В мультиплеерных играх это не абстрактная добродетель, а производственная необходимость: из одной кодовой базы собирают клиент (с рендерингом, UI, вводом) и headless-сервер, который никогда не рисует ни одного кадра — только симулирует мир, принимает ввод игроков и рассылает обновления состояния. Выгоды каскадные: серверу не нужен GPU, поэтому на одной машине крутятся десятки инстансов мира; симуляцию можно гонять на CI-серверах без графического контекста; поверх «безголового» ядра позже строятся боты, реплеи, инструменты модерации и аналитики. Разработчики инди-симуляций формулируют и обратное правило потока данных: симуляция проталкивает (push) данные о себе в слой представления, а не представление лезет внутрь симуляции за данными — так граница остаётся честной.
Тот же принцип объясняет, почему в Factorio, RimWorld и играх Paradox пауза и ускорение времени работают «бесплатно»: пауза — это просто остановка тиков симуляции при живом рендере и UI; ускорение ×3 — три тика симуляции на один кадр представления. Если бы логика была вплетена в рендер (как в наивных движках, где скорость мира привязана к FPS), ни то ни другое не было бы возможно без костылей.
Есть и менее очевидная выгода: слой представления можно заменять. Один и тот же симулируемый мир может показываться как 3D-сцена, как 2D-карта, как набор дашбордов или вовсе как текстовый лог — Dwarf Fortress двадцать лет жил с псевдографикой ASCII, а в 2022 году получил тайловую графику в Steam-версии практически без изменения симуляционного ядра, потому что ядро никогда о графике не знало.
Это, возможно, самое прямое переносимое правило всей главы. Ядро движка компании обязано быть headless-библиотекой: «состояние компании + поток событий/решений → новое состояние», ни строчки UI внутри. Тогда одно и то же ядро обслуживает интерактивный «пульт» руководителя, ночные пакетные прогоны тысяч сценариев Monte-Carlo, CI-тесты бизнес-логики и API для внешних агентов (включая LLM-агентов, играющих роли менеджеров). Дашборды, отчёты и визуализации — сменные «шкуры» поверх push-потока данных из ядра. Проверочный вопрос на каждом ревью архитектуры: «можем ли мы прогнать год работы компании на сервере без единого пикселя?» Если нет — граница протекла.
2.3. Entity-Component-System: данные вместо иерархий
Зачем ECS возник
Классическое ООП-моделирование игрового мира — глубокие иерархии наследования («Юнит → ДвижущийсяЮнит → ВооружённыйДвижущийсяЮнит…») — ломается дважды. Первый раз — комбинаторно: когда дизайнеру нужен объект «и то и другое, но без третьего», иерархия не даёт места для него без ромбовидного наследования или дублирования. Второй раз — на железе: объекты с виртуальными вызовами и разбросанными по куче данными убивают кэш процессора.
Ответ на первую проблему — композиция: сущность (entity) — это просто идентификатор, к которому прицеплен набор компонентов (components) — чистых данных без поведения; поведение живёт в системах (systems), каждая из которых обходит все сущности с нужным набором компонентов. Ответ на вторую проблему дала философия data-oriented design (DOD), у которой есть каноническая точка отсчёта — keynote Майка Эктона (тогда — engine director Insomniac Games) «Data-Oriented Design and C++» на CppCon 2014. Тезисы Эктона стали мантрами индустрии: единственное назначение любой программы — преобразование данных; «где один — там много» (обрабатывай сущности пачками, а не по одной); решай задачу, которая у тебя есть, а не воображаемую общую; понимание данных и железа первично, а «красивая» объектная модель, антитетичная этим фактам, — источник десятикратных потерь производительности на промахах кэша. ECS — это DOD, возведённый в архитектурный паттерн: компоненты одного типа лежат в памяти плотными массивами, и система пробегает их линейно, что идеально для префетчера и кэш-линий.
Витриной боевого применения ECS стал доклад Тимоти Форда «Overwatch Gameplay Architecture and Netcode» (GDC 2017): весь геймплей Overwatch, включая предсказание на клиенте и откаты, построен на строгом ECS. Форд честно описывает и цену дисциплины: команде понадобилось около полутора лет, чтобы устаканить правила (компоненты — только данные, поведение — только в системах, системы не вызывают друг друга напрямую), и именно код, написанный до этих правил или в их нарушение, оставался главным источником багов и стоимости сопровождения.
Ландшафт реализаций
К середине 2020-х сложились четыре референсные реализации, на которых стоит калибровать своё понимание.
Unity DOTS — Data-Oriented Technology Stack: ECS (пакет Entities), C# Job System для безопасного распараллеливания и Burst-компилятор, транслирующий байткод .NET в оптимизированный нативный код через LLVM. После долгих экспериментальных лет Entities 1.x к 2024–2026 годам считается production-ready; связка Burst + jobs позволяет обновлять десятки тысяч сущностей там, где GameObject-подход упирается в потолок. Показательно, что сама Unity рекомендует DOTS для массовых симуляций (RTS-толпы, физика, орды агентов) и не рекомендует для простых 2D- и UI-центричных игр — маркер того, что ECS не универсален.
EnTT — header-only библиотека на C++, эталон хранения sparse set: компоненты каждого типа лежат в собственном плотном массиве с разреженным индексом. Дёшево добавлять и удалять компоненты, итерация по одному типу — линейная. EnTT используется в Minecraft (Bedrock Edition) — вероятно, самое массовое развёртывание ECS в истории.
flecs Сандера Мертенса — ECS для C/C++ с самой богатой моделью данных: сущности-отношения (relationships), иерархии, запросы в стиле языка правил. Классически flecs — архетипный (archetype): сущности с одинаковым набором компонентов группируются в таблицы, итерация по запросу из многих компонентов сверхбыстрая, зато смена состава компонентов дороже (перенос между таблицами). В версии 4.1 (2025) flecs стал первым открытым ECS, поддерживающим оба способа хранения — архетипы и sparse set — на выбор для каждого компонента.
Bevy ECS — ядро Rust-движка Bevy, тоже архетипное; система в Bevy — обычная функция, параметры которой описывают запрос к данным, а планировщик автоматически распараллеливает системы с непересекающимися запросами. В версии 0.16 (2025) Bevy получил встроенные ECS-отношения (relationships) — двунаправленные связи «сущность-сущность», на которые переведена и иерархия parent-child; в 0.17 API отношений и событий дополнительно вычищен.
Дихотомия «sparse set против архетипов» — главный внутренний trade-off ECS-хранилищ, подтверждённый и академическими сравнениями 2025 года: sparse set дешевле при частой смене состава компонентов, архетипы быстрее при массовой итерации сложных запросов. Для симуляции, где состав сущностей стабилен, а обновления массовы, архетипы обычно выигрывают.
Когда ECS оправдан, а когда нет
ECS оправдан, когда выполняются два условия: сущностей много (тысячи и более) и их состав комбинаторен (дизайнеры постоянно собирают новые типы объектов из старых кусков). Тогда окупаются и кэш-дружелюбная раскладка, и дисциплина «данные отдельно, поведение отдельно», и автоматический параллелизм систем. ECS не оправдан, когда сущностей мало и они разнородны (типичная бизнес-CRUD-система, головоломка на 50 объектов, UI): вы заплатите налог на непривычную архитектуру, косвенность и слабую поддержку инструментов, не получив ни одного из выигрышей. Опыт Overwatch добавляет третье условие: ECS работает только при жёсткой дисциплине правил — «наполовину ECS» с поведением в компонентах и перекрёстными вызовами систем собирает худшее из обоих миров. Наконец, даже в подходящих проектах ECS — это ядро симуляции, а не вся программа: UI, сеть, ассеты вокруг него могут и должны жить в других парадигмах.
Сущностная модель компании ложится на ECS удивительно естественно: сотрудник — entity; компоненты — «Роль», «Компетенции», «Загрузка», «Зарплата», «Мотивация»; проект — entity с компонентами «Бюджет», «Срок», «Команда»; системы — «НачислениеЗарплат», «РасчётЗагрузки», «ОттокПерсонала», прогоняемые каждый тик по всем сущностям с нужными компонентами. Главный подарок ECS здесь даже не производительность (тысяч сотрудников мало по игровым меркам), а комбинаторная гибкость: новый тип сущности («подрядчик» = сотрудник без «Зарплаты», но с «Контрактом») собирается из готовых компонентов без переписывания модели, а новая аналитика — это просто новая система-запрос. Отдельно присмотритесь к flecs и Bevy relationships: реифицированные связи «сущность-отношение-сущность» (кто кому подчиняется, кто на каком проекте) — это готовая онтологическая машина. Но помните анти-урок: если ваша модель компании — 30 агрегатов с уникальной логикой, ECS вам не нужен, достаточно честного разделения «данные/системы/представление».
2.4. Детерминизм: симуляция как воспроизводимый эксперимент
Определение и ставки
Симуляция детерминирована, если из одинакового начального состояния при одинаковой последовательности входов она приходит к побитово одинаковому результату — на любой машине, при любом запуске. Детерминизм превращает симуляцию из «примерно правдоподобной анимации» в воспроизводимый эксперимент: любой баг можно воспроизвести, любой прогон — повторить и разобрать по шагам.
Классический текст о том, что детерминизм даёт на практике, — статья Пола Беттнера и Марка Террано «1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond» (GDC 2001). Задача Ensemble Studios звучала безнадёжно для модемов конца 90-х: эпические битвы, до 8 игроков, полторы тысячи лучников на поле — по модему 28.8 кбит/с. Синхронизировать состояние такого мира по сети невозможно физически. Решение — детерминированный lockstep: по сети передаются не юниты, а только команды игроков («атаковать туда», «строить это»), и каждая машина независимо прогоняет одну и ту же детерминированную симуляцию, исполняя одинаковые команды на одинаковых тиках. Команды исполняются с задержкой в два «коммуникационных хода» (длина хода ~200 мс подстраивается под пинг и скорость машин), чтобы успеть разойтись по всем участникам. Трафик перестаёт зависеть от размера мира — хоть полторы тысячи лучников, хоть пятнадцать: по проводам идут только клики игроков. Цена — абсолютная нетерпимость к недетерминизму: единственное расхождение в младшем бите на одной машине, и миры игроков молча расходятся (desync), поэтому Age of Empires постоянно сверял контрольные суммы состояния между машинами, чтобы ловить рассинхронизацию как можно раньше.
Враги детерминизма
Главный враг — плавающая запятая. Гленн Фидлер в «Floating Point Determinism» и современные разборы lockstep-архитектур (например, серия SnapNet «Netcode Architectures») сходятся в диагнозе: IEEE 754 детерминирован на одной машине с одним бинарником, но между платформами и компиляторами результаты расходятся — разные наборы инструкций (x87 против SSE против NEON), переупорядочивание операций оптимизатором, автовекторизация, разные реализации трансцендентных функций (sin, cos, tan) в разных math-библиотеках. Радикальное лекарство, которое выбирают многие RTS, — вообще изгнать float из симуляции и считать всё в целочисленной арифметике с фиксированной запятой (fixed point; типично 64-битное целое с 16 битами дробной части); представление при этом может рисовать во float сколько угодно — оно на состояние не влияет.
Второй враг — скрытая недетерминированность порядка: обход неупорядоченных контейнеров, зависящий от адресов памяти; несинхронизированный параллелизм; системное время и внешние источники внутри логики. Третий — случайность: она обязана быть seed-управляемой. Генератор псевдослучайных чисел с явным зерном (seed) — часть состояния мира, сохраняется и восстанавливается вместе с ним; «случайность» становится воспроизводимой, а разные сценарии — это просто разные зёрна. Важная дисциплина: у симуляции и у представления должны быть разные генераторы, иначе случайная визуальная искорка, дёрнувшая общий RNG, изменит исход боя.
Насколько хрупок детерминизм, показывает признание Wube Software в Friday Facts #415: баг тредовой недетерминированности жил в Factorio с 22 июля 2017 года и потребовал совпадения четырёх независимых условий, чтобы проявиться, — нашли и починили его только к версии 2.0, то есть спустя семь лет непрерывного автоматического тестирования детерминизма (о котором в разделе 2.7).
Реплеи как последовательность команд
Детерминизм даёт почти бесплатный реплей: вместо записи видео или покадровых состояний достаточно сохранить начальный seed и лог команд с номерами тиков — прогон этого лога через ту же симуляцию побитово воспроизводит всю партию. Файл многочасовой сессии весит килобайты. Тот же механизм — золотой инструмент отладки: разработчики Factorio ловят баги детерминизма, периодически сохраняя игру во время записи реплея, затем прогоняя реплей и сравнивая сохранения — точка расхождения указывает на виновную систему. Обратная сторона: реплей инвалидируется любым изменением логики (реплеи старой версии на новой не играются) — потому серьёзные проекты версионируют симуляцию и хранят соответствие «реплей ↔ версия ядра».
Детерминизм — это то, что отличает движок управления от «калькулятора с настроением», и для бизнес-применения он важнее, чем для игр: решению CEO нужен аудиторский след. Правила переносятся дословно. (1) Все управленческие решения — это команды с меткой модельного времени; журнал команд + начальное состояние = полная воспроизводимость истории. (2) Вся стохастика (спрос, отток клиентов, сроки найма) — только через seed-управляемые RNG; сценарный анализ — это N прогонов одного журнала команд с разными зёрнами, и разница исходов гарантированно объясняется только случайностью, а не гонками в коде. (3) Финансовую арифметику считайте в fixed point / decimal — бухгалтерия и так не терпит float, и это же решает проблему кросс-платформенного детерминизма. (4) Сверка контрольных сумм состояния — готовый паттерн для сверки инстансов движка (локального и облачного). Семилетний баг Factorio — предупреждение: детерминизм не «делается один раз», а непрерывно охраняется тестами.
2.5. Состояние мира: сохранение, снапшоты, машина времени
Сериализация и снапшоты
Состояние мира — это полный набор данных, необходимый и достаточный для продолжения симуляции: все сущности с компонентами, состояние всех систем, счётчик тиков и состояние RNG. Сериализация (save/load) — умение записать это состояние на диск и восстановить побитово. Требование «побитово» здесь не педантизм: если после save/load симуляция ведёт себя хоть чуть иначе, чем без него, — детерминизм сломан, и Factorio, как мы увидим, проверяет именно это самым жестоким из своих тестов. Практические уроки индустрии: сериализацию нужно проектировать с первого дня (доклеить её к движку, полному указателей и скрытого состояния, мучительно); формат должен версионироваться и мигрировать (сейв старой версии обязан открываться в новой); ECS здесь снова помогает — состояние и так лежит плотными таблицами данных без поведения, сериализация почти сводится к дампу массивов.
Снапшот (snapshot) — это то же сохранение, но лёгкое и частое, обычно в память: слепок мира на тике N, к которому можно мгновенно откатиться.
Event sourcing: состояние как следствие событий
Альтернативный и дополняющий взгляд пришёл из корпоративной разработки — паттерн event sourcing, канонизированный Мартином Фаулером: истиной считается не текущее состояние, а журнал событий; состояние — производная величина, восстанавливаемая проигрыванием журнала с нуля (или от ближайшего снапшота — стандартная оптимизация, чтобы не проигрывать миллионы событий). Журнал даёт временные запросы («каким было состояние на любой момент прошлого»), полный аудит и возможность перестроить новые проекции состояния задним числом. Родство с игровыми реплеями очевидно и глубоко: детерминированный реплей — это event sourcing, где журнал хранит только команды (входы), а «проигрыватель» — сама симуляция; шахматная партия, записанная нотацией, — древнейший event store. Гибрид «периодические снапшоты + журнал команд между ними» — рабочая формула управления состоянием долгоживущей симуляции: снапшоты дают быстрый произвольный доступ к истории, журнал — точность до тика и компактность.
Управление скоростью времени и перемотка
Пауза и ускорение, как показано в 2.2, — прямое следствие разделения симуляции и представления: скорость мира — это число тиков на кадр. Гранд-стратегии Paradox дают пять скоростей именно так; Factorio и RimWorld — аналогично; при этом «ускорение» упирается в CPU: на поздней стадии партии Victoria 3 или города в 100 тысяч жителей в Cities: Skylines машина перестаёт успевать прогонять тики быстрее реального времени, и скорость ×5 становится номинальной — предел ускорения задаёт стоимость тика, и это главный аргумент за оптимизацию симуляции даже в «медленных» жанрах.
Перемотка назад (rewind) честным обращением симуляции невозможна (системы необратимы), поэтому реализуется двумя способами: плотными снапшотами (Braid хранит состояние каждого кадра, что дорого по памяти) или гибридом «снапшот + детерминированный доигрыш»: откатиться к ближайшему снапшоту до тика N и прогнать журнал команд до N. Planetary Annihilation с его «Chrono Cam» — свободной перемоткой матча в реальном времени — построен на хранении истории значений как кривых во времени (непроверено в деталях реализации). Для движка симуляции важен сам принцип: машина времени — это не мистика, а снапшоты плюс детерминизм.
Event sourcing — родная стихия бизнес-домена: транзакции, сделки, приказы, кадровые события и есть события; движок компании должен хранить журнал событий и решений как первичную истину, а «состояние компании на дату» — как производную (снапшот + доигрыш). Это даёт сразу четыре продуктовые способности: аудит («почему модель показала кассовый разрыв в мае?» — проигрываем журнал), машину времени («вернись на 1 марта и покажи, что было бы, прими мы другое решение» — снапшот + ветка с изменённой командой), сценарные ветки (несколько альтернативных «будущих» от одного снапшота — это же основа A/B-анализа решений) и дешёвые бэкапы. Скорость времени — ключевой UX движка: «прожить» год за минуту в ускоренном прогоне, ставить на паузу для разбора, замедляться в кризисных эпизодах. Заложите бюджет стоимости тика с самого начала — предел ускорения сценарных прогонов определит, сколько вариантов будущего руководитель успеет посмотреть за встречу.
2.6. Масштабируемость: как симулировать больше, чем влезает в кадр
Simulation LOD: не всё заслуживает полной симуляции
Графика давно живёт с уровнями детализации (LOD): далёкие объекты рисуются грубее. Тот же принцип применим к самой симуляции — simulation level of detail: сущности вне «поля зрения» (вне экрана, вне зоны интереса игрока) обновляются реже, грубее или агрегированно. Основополагающая работа — «Simulation Level-Of-Detail» Стивена Ченни (GDC 2001): вне видимости мир можно симулировать упрощёнными моделями, восстанавливая правдоподобные детали при возвращении взгляда. Развитие идеи — «LOD Trader» Бена Саншайн-Хилла (Game AI Pro): выбор уровня детализации для каждого аспекта каждого агента рассматривается как задача оптимизации «максимум правдоподобия при заданном бюджете CPU», решаемая каждый кадр. Академические эксперименты с LOD-иерархиями поведения виртуальных людей показывают сокращение затрат AI на порядки (до <1% исходного времени) без заметной потери правдоподобия.
Практика жанра колоний-симов делает это проще и грубее. RimWorld стратифицирует обновления через типы тикеров: часть сущностей обновляется каждый тик, часть — каждые 250 тиков (rare), часть — каждые 2000 (long); документация моддинг-сообщества прямо предписывает выносить тяжёлую логику в редкие тики. Cities: Skylines жёстко ограничивает число полноценных агентов (65 тысяч симулируемых горожан, 16 тысяч активных машин) — остальное население существует статистически. Общий приём: полная агентная симуляция — только там, где на неё смотрят или где она влияет на игрока; остальной мир живёт агрегатами и «просыпается» по требованию. Важная ловушка, о которой предупреждают дизайнеры: если события за пределами взгляда влияют на исход (глобальная стратегия, экономика), упрощённая модель должна быть согласована с полной, иначе игрок научится эксплуатировать разницу.
Кэширование и «сон» сущностей: школа Factorio
Девблог Factorio (Friday Facts) — лучший в индустрии открытый учебник оптимизации симуляции. Два примера из него стоит знать наизусть.
FFF-176 «Belts optimization for 0.15»: конвейеры — сердце Factorio, и их обновление съедало огромную долю тика. Ключевые ходы: смежные ленты объединяются в «транспортные линии» и обновляются как один объект (стоимость платится только на стыках); для заблокированных лент используется инвариант «однажды сжавшиеся предметы никогда не расжимаются», что позволяет кэшировать индекс последнего зазора и обновлять его инкрементально вместо сканирования всей линии. Итог — фабрики с 25 UPS поднялись до 35–40 UPS. Урок общего вида: найди инвариант домена → превращай O(n)-сканирование в O(1)-инкремент → объединяй однородные цепочки в агрегаты.
FFF-215 «Multithreading issues»: коварекс (основатель Wube) распараллелил обновление поездов, электросети и конвейеров — почти независимых подсистем — и получил замедление. Причина — false sharing: данные логически независимых объектов лежали в одних кэш-линиях, и потоки, пишущие в «свои» объекты, непрерывно инвалидировали кэши друг друга (при этом чисто читающая параллельная фаза подготовки рендера прекрасно масштабировалась на 8 потоков — чтение общих страниц кэша бесплатно, запись — нет). Вывод Wube: параллелизм по-настоящему окупается только после реорганизации памяти — данные каждой задачи должны лежать компактно и раздельно (свои аллокаторы на чанк/подсистему). Это тот же тезис Эктона с другой стороны: не «добавь потоков», а «сначала приведи данные в порядок».
К версии 2.0 (FFF-421 «Optimizations 2.0», 2024) Wube довёл подход до зрелости: параллелятся прежде всего читающие проходы (сенсоры лент, комбинаторы, roboports — +9,5% на тестовой карте), а обновление лент ускорено с ~4 мс до ~1,6 мс, что даёт 20–40% выигрыша на мегабазах. Паттерн «параллель — читателям, последовательность — писателям» сохраняет детерминизм, который для Factorio неприкосновенен.
Антипример и пределы: Dwarf Fortress и одноядерная стена
Dwarf Fortress — самая глубокая агентная симуляция в истории игр (Тарн Адамс оценивает прогресс к «симуляции существования» в проценты от списка из ~2600 задач) и одновременно самый известный пример «смерти от FPS» (FPS death): любая долгоживущая крепость постепенно замедляется до неиграбельности. Вопреки фольклору, профилирование показывает, что дело не в пресловутом патфайндинге: по данным обсуждений с участием разработчиков, более 60% времени уходит на «ходы» юнитов, из которых собственно поиск пути — менее 10%, а около 20% CPU съедают проверки видимости/близости юнитами друг друга (непроверено в точных цифрах — оценки из community-профилирования). Симуляция десятилетиями была однопоточной; вики сообщества ведёт целый раздел «Maximizing framerate» с советами вроде «меньше мир, короче история, меньше кошек». Урок двоякий: (1) глубина симуляции без архитектуры данных и LOD упирается в одноядерную стену — и это же подтверждают Victoria 3 (главный поток сессии тянет симуляцию, task-потоки лишь помогают) и Cities: Skylines II, «брутально» зависимая от однопоточной производительности; (2) квадратичные взаимодействия «каждый с каждым» (проверки близости) масштабируются хуже всего — их надо душить пространственными индексами и редкими тиками в первую очередь.
Simulation LOD — это готовая стратегия работы с горизонтом и фокусом внимания: подразделение, на которое смотрит руководитель (или по которому принимается решение), симулируется поагентно на мелком тике; остальная компания — агрегатами (department-level модели) на крупном; рынок и внешняя среда — статистическими процессами. Иерархия тикеров RimWorld (1/250/2000) — прямой шаблон для «операционного дня / месячного цикла / годовой стратегии». Уроки Factorio переносятся как принципы: ищите инварианты домена для инкрементальных пересчётов (P&L не пересчитывают с нуля при каждой транзакции — это и есть кэш с инкрементальным обновлением); спящие сущности не должны стоить ничего; параллельте read-only аналитику поверх состояния, но мутации ведите детерминированной последовательностью. А Dwarf Fortress — предостережение о соблазне «просимулировать всё»: без явной модели LOD глубина сожрёт производительность, и предел придёт раньше, чем вы думаете, — от квадратичных взаимодействий агентов (в компании это коммуникации «каждый с каждым»).
2.7. Тестирование симуляций
Детерминированные тесты: конвейер Factorio
Симуляция — худший объект для ручного QA (комбинаторное пространство состояний, баги проявляются на сотом часу партии) и лучший — для автоматического, если она детерминирована. Эталон снова Factorio (FFF-60 «Tests all around», FFF-62 «The automation of Factorio»). Их конвейер: тысячи детерминированных интеграционных тестов, где тест — это сценарий в реальном движке («построй, подай предметы, прогони N тиков, проверь состояние»), исполняемый headless без графики; билд-сервер гоняет полный набор 24/7 на всех платформах и конфигурациях и рассылает письма при поломке. Специфические для симуляций техники: регрессия детерминизма через CRC-контрольные суммы состояния, сверяемые с эталонными; «heavy mode», который каждый тик сохраняет и загружает мир, сравнивая CRC до и после, — чудовищно медленно, но вылавливает любое состояние, не переживающее сериализацию (то есть одновременно тестирует и save/load, и детерминизм); реплеи как регрессионные тесты — записанная партия прогоняется на новой сборке до точки расхождения. Именно эта машина позволила Wube найти упомянутый семилетний тредовый баг.
Fuzzing и армии ботов
Fuzzing — систематическая подача мутируемых случайных входов — переносится на игры двумя путями. Классический (доклад «How to Protect Your Game's Code with Fuzzing», GDC) — фаззинг парсеров и протоколов (сейвы, сетевые пакеты, моды) ради безопасности и устойчивости. Современный — фаззинг поведения: детерминированной симуляции скармливаются длинные случайные последовательности команд с проверкой инвариантов («деньги не отрицательные», «агент не застрял»); свежая работа BiFuzz (2025) демонстрирует двухступенчатый фаззер, находящий «застревания» игрока в open-world играх. Между фаззингом и QA-автоматизацией — армии ботов: Ubisoft для The Division построила Client Bots — AI-игроков, имитирующих человеческий ввод, проходящих миссии и репортящих непроходимости и просадки производительности (GDC 2019); DICE для Battlefield V — AutoPlayers, от 64-ботовых soak-тестов стабильности до скриптованных сценариев. Всё это возможно ровно потому, что симуляция отделена от представления и управляется командами — бот подключается к тому же интерфейсу, что и игрок.
Телеметрия и балансировка
Третий контур тестирования — не «работает ли», а «сходится ли баланс». Metrics-driven подход канонизирован докладом Mega Crit «Slay the Spire: Metrics Driven Design and Balance» (GDC 2019): с раннего доступа собирается телеметрия всех партий (винрейты, выборы карт), и баланс правится по данным, а не по ощущениям, — при сохранении за дизайнером права на «game feel». На промышленном полюсе — Square Enix (GDC, «Balancing Nightmares»): генетический алгоритм ищет ломающие баланс комбинации экипировки в пространстве из ~10^100 вариантов, то есть симуляция тестирует симуляцию. Riot для Valorant публично описывает тот же контур: телеметрия винрейтов агентов как диагностика, указывающая, где баланс требует вмешательства.
Тестовый конвейер движка компании собирается из тех же трёх контуров. (1) Детерминированные сценарные тесты: библиотека эталонных мини-компаний («стартап 10 человек», «завод с сезонным спросом»), для которых прогон N тиков даёт зафиксированный CRC состояния, — любое изменение логики, сдвинувшее результат, видно немедленно; свой «heavy mode» (save/load каждый модельный день со сверкой) охраняет сериализацию. (2) Фаззинг решений: случайные последовательности управленческих команд с проверкой инвариантов домена — баланс сходится, касса не уходит в минус без кредита, ни один процесс не зависает; это же — стресс-тест устойчивости модели к абсурдным решениям, который обязателен, если движком будут пользоваться LLM-агенты. (3) Телеметрия калибровки: реальные данные компании — это «телеметрия боевых партий»; расхождение прогнозов движка с фактом должно измеряться непрерывно, как винрейты в Valorant, и питать пересмотр параметров модели. Заметьте: все три контура требуют headless-ядра и детерминизма — тестируемость не добавляется к движку, а следует из его архитектуры.
2.8. Итог: архитектурный инвариант симуляционного движка
Сквозь все семь тем главы проходит один инвариант, который стоит сформулировать явно. Зрелый симуляционный движок — это детерминированное headless-ядро с фиксированным тиком, данные которого организованы для массовой обработки (ECS/DOD), состояние — воспроизводимо из снапшотов и журнала команд, вычислительный бюджет — управляется через simulation LOD, а корректность — охраняется автоматическим конвейером детерминированных тестов, фаззинга и телеметрии. Каждый элемент этой формулы поддерживает остальные: детерминизм делает возможными реплеи и дешёвые тесты; разделение симуляции и представления — headless-прогоны, ботов и произвольную скорость времени; ECS — сериализацию, параллелизм и комбинаторную гибкость модели; LOD — масштаб; тесты — эволюцию всего перечисленного без страха. Именно эта взаимная поддержка, а не любой элемент в отдельности, отличает движок мира от набора скриптов — и именно её мы будем требовать от движка управления компанией в синтезирующей главе.
Factorio
Детерминизм как религия: 60 UPS, CRC-тесты 24/7, heavy mode, инварианты домена вместо грубой силы. Семилетний баг нашла тестовая машина, а не удача.
60 UPSCRC-регрессияOverwatch
Строгий ECS в продакшене: данные — в компонентах, поведение — в системах, фиксированный порядок. Полтора года на дисциплину — и она окупилась.
ECS16 мс кадрDwarf Fortress
Глубочайшая агентная симуляция — и предостережение: без LOD и архитектуры данных глубина упирается в одноядерную стену и FPS death.
FPS deathантипримерRimWorld
Стратификация тиков 1/250/2000 — простая и рабочая модель simulation LOD: не каждой сущности нужен каждый тик.
тикерыsim LODИсточники и куда копать глубже
Канонические главы Game Loop и Update Method; вся книга бесплатна онлайн.
Энциклопедия устройства движков; главы о game loop и обновлении игровых объектов (выдержка).
LOD симуляции как задача оптимизации под бюджет.
Манифест data-oriented design, идейный фундамент ECS.
Строгий ECS в AAA-продакшене и цена дисциплины.
Философия «симуляция как генератор историй».
Балансировка по телеметрии.
Боты как QA-инфраструктура симуляции.
Каноническое решение проблемы шага времени: аккумулятор + интерполяция.
Почему float недетерминирован между платформами и что с этим делать.
Классика детерминированного lockstep в Age of Empires.
Состояние как производная журнала событий; мост между гейм- и энтерпрайз-архитектурой.
Основополагающая работа о симуляции вне поля зрения.
Современный разбор lockstep, fixed point и борьбы с десинками.
Свежий академический фаззинг геймплея.
Инварианты домена и кэширование как метод оптимизации симуляции.
Честный разбор провала наивного параллелизма: cache invalidation и организация памяти.
Зрелый паттерн «параллелим читателей»; итоги оптимизаций 2.0 (2024).
Тестовый конвейер, CRC-регрессия детерминизма, heavy mode.
Наглядное сравнение archetype и sparse set хранилищ от автора flecs.
Реифицированные связи сущностей в открытом Rust-ECS (2025).
Коллективный опыт сообщества по борьбе с FPS death; профиль стоимости глубокой симуляции.
Практика simulation/presentation split и push-модели в инди-симуляции (Handmade-сообщество).