движки миров_ Оглавление
02
Блок I · Инженерия миров · Глава 2 из 10

Архитектура симуляционных движков

Как течёт время в симуляции, зачем отделять расчётное ядро от картинки, что дают ECS и детерминизм — и почему у игровой индустрии уже готов несущий каркас для движка управления компанией.

~5 000 слов~28 мин чтения8 схем24 источника

Эта глава — инженерное ядро всего учебника. Если предыдущая глава отвечала на вопрос «что такое движок мира», то здесь мы разбираем, как он устроен внутри: как течёт время в симуляции, в каком порядке обновляются системы, как отделить расчётное ядро от картинки, как хранить и воспроизводить состояние мира и как всё это масштабировать и тестировать. Для проектировщика движка управления компанией эта глава даёт готовый несущий каркас: компания в такой системе — это симулируемый мир, где сотрудники и контрагенты — агенты, бизнес-процессы — системы обновления, финансы — ресурсные потоки, а управленческое решение — команда, поданная в детерминированную симуляцию. Почти каждая архитектурная проблема, с которой столкнётся такой движок — воспроизводимость сценариев «что если», перемотка истории компании назад, ускоренный прогон квартала за секунды, симуляция тысяч сущностей без деградации, — уже была решена индустрией игр, причём решена под куда более жёсткими требованиями к производительности и надёжности. Мы будем разбирать эти решения на первоисточниках: от канонической статьи «Fix Your Timestep!» Гленна Фидлера и классики сетевого RTS-кода «1500 Archers on a 28.8» до девблогов Factorio и докладов GDC о data-oriented архитектурах.

60 UPS
фиксированный тикрейт Factorio — мир обновляется ровно 60 раз в секунду
16 мс
«командный кадр» симуляции Overwatch — фиксированный шаг AAA-шутера
6 часов
игрового времени в одном тике Victoria 3 — свой масштаб у каждой подсистемы
28.8 кбит/с
модем, по которому lockstep синхронизировал 1500 лучников в Age of Empires
7 лет
жил тредовый баг детерминизма в Factorio — нашли его тесты, а не удача
tick — квант модельного времени, один шаг симуляции ECS — entity-component-system: данные отдельно, поведение отдельно lockstep — синхронизация обменом командами, а не состоянием desync — расхождение миров из-за потери детерминизма snapshot — лёгкий слепок состояния мира на тике N seed — зерно RNG: воспроизводимая случайность headless — ядро, работающее без графики simulation LOD — уровень детализации самой симуляции

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 секунд) и ставить жёсткий потолок числа шагов за кадр — пусть лучше симуляция видимо замедлится при пиковой нагрузке, чем умрёт в спирали.

Симуляция отстаёт X с мира стоят Y > X с CPU Долг времени растёт аккумулятор копит отставание Больше шагов за кадр навёрстываем несколько dt Кадр ещё длиннее CPU не успевает сильнее Спираль смерти положительная обратная связь Рецепты Фидлера Запас производительности обновление X секунд мира должно стоить ≪ X секунд CPU Потолок шагов за кадр лучше видимое замедление при пиковой нагрузке, чем смерть в спирали
Рис. 2.1. «Спираль смерти» полуфиксированного шага: чем сильнее симуляция отстаёт, тем больше шагов ей нужно на кадр — и тем сильнее она отстаёт. Разрывают петлю запас производительности и жёсткий потолок числа шагов.

Свободная физика с аккумулятором — итоговый паттерн. Если нужна точная воспроизводимость от запуска к запуску (а для детерминированного lockstep-мультиплеера и для реплеев — нужна), шаг обязан быть строго постоянным: даже «остаточный» маленький шаг полуфиксированной схемы ломает побитовую повторяемость из-за конечной точности плавающей запятой. Фидлер предлагает сменить точку зрения: рендерер производит время, а симуляция потребляет его дискретными порциями dt. Прошедшее реальное время складывается в аккумулятор; пока в аккумуляторе больше dt, симуляция делает целые шаги; остаток меньше dt переносится на следующий кадр, а не выбрасывается. Остаётся визуальный артефакт — картинка показывает состояние, чуть отстающее от реального времени, с дрожанием — и он лечится интерполяцией: рендерер хранит предыдущее и текущее состояния симуляции и смешивает их с коэффициентом alpha = accumulator / dt.

следующий кадр ↺ Кадр рендера измеряем frame_time прошлого кадра Аккумулятор acc += frame_time пока acc ≥ dt — целые шаги Тик симуляции шаг строго dt · acc −= dt Интерполяция и рендер mix(prev, curr, alpha) · alpha = acc / dt Два времени, одна симуляция Симуляция: тики строго каждые dt N N+1 alpha ≈ 0.8 Представление: кадры как успевает машина кадр показывает смесь состояний тиков N и N+1: state = mix(prev, curr, alpha) — плавно и без дрожания, при этом сама симуляция остаётся побитово повторяемой
Рис. 2.2. Итоговый паттерн «Fix Your Timestep!»: рендерер производит время, аккумулятор выдаёт его симуляции целыми порциями dt, остаток задаёт коэффициент интерполяции alpha. Слева — цикл кадра, справа — две несовпадающие оси времени.
«Рендерер производит время, а симуляция потребляет его дискретными порциями dt».
Гленн Фидлер, «Fix Your Timestep!», Gaffer On Games, 2004

Эта схема — фиксированный детерминированный шаг симуляции плюс интерполируемое представление на произвольной частоте — стала де-факто стандартом. Она же по построению разделяет симуляцию и рендеринг, о чём подробно в разделе 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, возведённый в архитектурный паттерн: компоненты одного типа лежат в памяти плотными массивами, и система пробегает их линейно, что идеально для префетчера и кэш-линий.

ООП: объекты разбросаны по куче Юнит 1 Юнит 2 Юнит 3 Юнит 4 указатели, виртуальные вызовы, данные врозь — каждый объект — промах кэша DOD: компоненты плотными массивами Позиция e1e2e3e4e5e6 Скорость Здоровье система читает линейно — префетчер и кэш-линии работают на полную Архетип: сущности — строки, компоненты — колонки сущность Позиция Скорость Здоровье №1(x, y)(dx, dy)100 №2(x, y)(dx, dy)72 №3(x, y)(dx, dy)100 №4(x, y)(dx, dy)15 Система «Движение»: запрос (Позиция, Скорость) — проход по колонкам таблицы архетип: одинаковый набор компонентов → одна таблица (быстрая итерация запросов) sparse set: свой плотный массив на каждый компонент (дешёвая смена состава)
Рис. 2.3. Память решает: разбросанные по куче объекты против плотных массивов компонентов (вверху) и архетипная таблица «сущности — строки, компоненты — колонки», по которой системы ходят запросами (внизу).
«Единственное назначение любой программы — преобразование данных. Где один — там много».
Майк Эктон, «Data-Oriented Design and C++», CppCon 2014

Витриной боевого применения 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 постоянно сверял контрольные суммы состояния между машинами, чтобы ловить рассинхронизацию как можно раньше.

Lockstep — Age of Empires Игрок A полная симуляция мир целиком Игрок B полная симуляция мир целиком команды тик N+2 • по сети — только команды игроков • каждый исполняет их на одном и том же тике • трафик не зависит от размера мира • цена: расхождение в один бит = desync • защита: постоянная сверка CRC состояния State-sync — сервер-авторитет Сервер авторитетная симуляция Клиент 1 предсказание + откат Клиент 2 предсказание + откат состояние ↓ ↑ ввод • сервер — единственная истина, читы сложнее • трафик растёт с размером мира • клиент тонкий, детерминизм не обязателен
Рис. 2.4. Две школы сетевой синхронизации: lockstep гоняет по сети только команды и требует абсолютного детерминизма всех копий мира; state-sync держит истину на сервере и рассылает состояние — дороже по трафику, но терпимее к недетерминизму.

Враги детерминизма

Главный враг — плавающая запятая. Гленн Фидлер в «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» — свободной перемоткой матча в реальном времени — построен на хранении истории значений как кривых во времени (непроверено в деталях реализации). Для движка симуляции важен сам принцип: машина времени — это не мистика, а снапшоты плюс детерминизм.

сценарий Б: изменили команду на тике 1400 факт (сценарий А) тики → журнал команд с метками тиков S₀ · тик 0 S₁ · тик 1000 S₂ · тик 2000 доигрыш из S₁до тика 1400 машина времени: откат к ближайшему снапшоту + доигрыш журнала
Рис. 2.5. Event sourcing долгоживущей симуляции: периодические снапшоты плюс журнал команд между ними. Перемотка к тику 1400 — это откат к S₁ и детерминированный доигрыш; изменённая команда в той же точке порождает альтернативную ветку сценария.
💡 Что взять для движка управления компанией

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 тысяч активных машин) — остальное население существует статистически. Общий приём: полная агентная симуляция — только там, где на неё смотрят или где она влияет на игрока; остальной мир живёт агрегатами и «просыпается» по требованию. Важная ловушка, о которой предупреждают дизайнеры: если события за пределами взгляда влияют на исход (глобальная стратегия, экономика), упрощённая модель должна быть согласована с полной, иначе игрок научится эксплуатировать разницу.

В фокусе — там, где смотрит игрок полная поагентная симуляция, каждый тик Рядом — на границе внимания упрощённые модели, редкие тики Далеко — вне поля зрения агрегаты и статистика, «просыпается» по требованию Cities: Skylines — жёсткий потолок: 65 тыс. агентов, остальное — статистика Тикеры RimWorld: не всем нужен каждый тик Normal — каждый тик Rare — каждые 250 тиков Long — каждые 2000 тиков
Рис. 2.6. Simulation LOD как кольца внимания: полная симуляция только в фокусе, упрощённые модели рядом, агрегаты вдали. Внизу — стратификация тикеров RimWorld: одна и та же сущность может обновлять разные аспекты с разной частотой.

Кэширование и «сон» сущностей: школа 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 неприкосновенен.

Фаза чтения — все потоки параллельно Фаза записи Состояние мира тик N Сенсоры лент поток 1 · только читает Комбинаторы поток 2 · только читает Roboports / логистика поток 3 · только читает барьер синхронизации Мутации состояния 1 · поезда 2 · электросеть 3 · конвейеры один поток, фиксированный порядок → детерминизм цел
Рис. 2.7. Паттерн Factorio FFF-421 «параллелим читателей»: read-only проходы масштабируются на все ядра бесплатно, а мутации состояния выполняются одним потоком в фиксированном порядке — параллелизм без потери детерминизма.

Антипример и пределы: 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 публично описывает тот же контур: телеметрия винрейтов агентов как диагностика, указывающая, где баланс требует вмешательства.

Телеметрия Боты-игроки Фаззинг команд Инварианты домена Детерминизм: CRC-регрессия Slay the Spire · Valorant баланс по данным, а не по ощущениям The Division · Battlefield V AI-игроки находят непроходимости BiFuzz · фаззинг GDC случайные команды + проверка инвариантов «деньги ≥ 0», «агент не застрял» проверяются на каждом тике Factorio heavy mode save/load каждый тик + сверка CRC фундамент: без детерминизма верхние уровни не работают
Рис. 2.8. Пирамида тестирования симуляции: от CRC-регрессии детерминизма в основании через инварианты и фаззинг к ботам и телеметрии. Каждый уровень опирается на нижние — и все требуют headless-ядра.
💡 Что взять для движка управления компанией

Тестовый конвейер движка компании собирается из тех же трёх контуров. (1) Детерминированные сценарные тесты: библиотека эталонных мини-компаний («стартап 10 человек», «завод с сезонным спросом»), для которых прогон N тиков даёт зафиксированный CRC состояния, — любое изменение логики, сдвинувшее результат, видно немедленно; свой «heavy mode» (save/load каждый модельный день со сверкой) охраняет сериализацию. (2) Фаззинг решений: случайные последовательности управленческих команд с проверкой инвариантов домена — баланс сходится, касса не уходит в минус без кредита, ни один процесс не зависает; это же — стресс-тест устойчивости модели к абсурдным решениям, который обязателен, если движком будут пользоваться LLM-агенты. (3) Телеметрия калибровки: реальные данные компании — это «телеметрия боевых партий»; расхождение прогнозов движка с фактом должно измеряться непрерывно, как винрейты в Valorant, и питать пересмотр параметров модели. Заметьте: все три контура требуют headless-ядра и детерминизма — тестируемость не добавляется к движку, а следует из его архитектуры.

2.8. Итог: архитектурный инвариант симуляционного движка

Сквозь все семь тем главы проходит один инвариант, который стоит сформулировать явно. Зрелый симуляционный движок — это детерминированное headless-ядро с фиксированным тиком, данные которого организованы для массовой обработки (ECS/DOD), состояние — воспроизводимо из снапшотов и журнала команд, вычислительный бюджет — управляется через simulation LOD, а корректность — охраняется автоматическим конвейером детерминированных тестов, фаззинга и телеметрии. Каждый элемент этой формулы поддерживает остальные: детерминизм делает возможными реплеи и дешёвые тесты; разделение симуляции и представления — headless-прогоны, ботов и произвольную скорость времени; ECS — сериализацию, параллелизм и комбинаторную гибкость модели; LOD — масштаб; тесты — эволюцию всего перечисленного без страха. Именно эта взаимная поддержка, а не любой элемент в отдельности, отличает движок мира от набора скриптов — и именно её мы будем требовать от движка управления компанией в синтезирующей главе.

Factorio

Wube Software · 2016 · фабричный сим

Детерминизм как религия: 60 UPS, CRC-тесты 24/7, heavy mode, инварианты домена вместо грубой силы. Семилетний баг нашла тестовая машина, а не удача.

60 UPSCRC-регрессия

Overwatch

Blizzard · 2016 · AAA-шутер

Строгий ECS в продакшене: данные — в компонентах, поведение — в системах, фиксированный порядок. Полтора года на дисциплину — и она окупилась.

ECS16 мс кадр

Dwarf Fortress

Bay 12 Games · 2006 · колония-сим

Глубочайшая агентная симуляция — и предостережение: без LOD и архитектуры данных глубина упирается в одноядерную стену и FPS death.

FPS deathантипример

RimWorld

Ludeon Studios · 2018 · колония-сим

Стратификация тиков 1/250/2000 — простая и рабочая модель simulation LOD: не каждой сущности нужен каждый тик.

тикерыsim LOD

Источники и куда копать глубже

книга
Robert Nystrom. Game Programming Patterns

Канонические главы Game Loop и Update Method; вся книга бесплатна онлайн.

книга
Jason Gregory. Game Engine Architecture (3rd ed.)

Энциклопедия устройства движков; главы о game loop и обновлении игровых объектов (выдержка).

книга
Ben Sunshine-Hill. Phenomenal AI Level-of-Detail Control with the LOD Trader (Game AI Pro, гл. 14)

LOD симуляции как задача оптимизации под бюджет.

видео
Mike Acton. «Data-Oriented Design and C++», CppCon 2014

Манифест data-oriented design, идейный фундамент ECS.

видео
Timothy Ford. «Overwatch Gameplay Architecture and Netcode», GDC 2017

Строгий ECS в AAA-продакшене и цена дисциплины.

видео
Tynan Sylvester. «RimWorld: Contrarian, Ridiculous, and Impossible Game Design Methods», GDC 2017

Философия «симуляция как генератор историй».

видео
Anthony Giovannetti. «Slay the Spire: Metrics Driven Design and Balance», GDC 2019

Балансировка по телеметрии.

видео
«Automated Testing: Using AI Controlled Players to Test The Division», GDC

Боты как QA-инфраструктура симуляции.

статья
Glenn Fiedler. «Fix Your Timestep!» (2004)

Каноническое решение проблемы шага времени: аккумулятор + интерполяция.

статья
Glenn Fiedler. «Floating Point Determinism»

Почему float недетерминирован между платформами и что с этим делать.

статья
Paul Bettner, Mark Terrano. «1500 Archers on a 28.8» (GDC 2001)

Классика детерминированного lockstep в Age of Empires.

статья
Martin Fowler. «Event Sourcing»

Состояние как производная журнала событий; мост между гейм- и энтерпрайз-архитектурой.

статья
Stephen Chenney. «Simulation Level-Of-Detail» (GDC 2001)

Основополагающая работа о симуляции вне поля зрения.

статья
SnapNet. «Netcode Architectures Part 1: Lockstep»

Современный разбор lockstep, fixed point и борьбы с десинками.

статья
Kato et al. «BiFuzz: A Two-Stage Fuzzing Tool for Open-World Video Games» (2025)

Свежий академический фаззинг геймплея.

девблог
Factorio Friday Facts #176 «Belts optimization»

Инварианты домена и кэширование как метод оптимизации симуляции.

девблог
Factorio Friday Facts #215 «Multithreading issues»

Честный разбор провала наивного параллелизма: cache invalidation и организация памяти.

девблог
Factorio Friday Facts #421 «Optimizations 2.0»

Зрелый паттерн «параллелим читателей»; итоги оптимизаций 2.0 (2024).

девблог
Factorio Friday Facts #62 «The automation of Factorio»

Тестовый конвейер, CRC-регрессия детерминизма, heavy mode.

девблог
Sander Mertens. «Building an ECS: Storage in Pictures»

Наглядное сравнение archetype и sparse set хранилищ от автора flecs.

девблог
Bevy 0.16 release notes (ECS Relationships)

Реифицированные связи сущностей в открытом Rust-ECS (2025).

девблог
Victoria 3 Dev Diary #76 «Performance»

Устройство тиков и потоков в гранд-стратегии Paradox.

сообщество
Dwarf Fortress Wiki. «Maximizing framerate»

Коллективный опыт сообщества по борьбе с FPS death; профиль стоимости глубокой симуляции.

девблог
Dom Williams. «Devlog #2: game engine architecture»

Практика simulation/presentation split и push-модели в инди-симуляции (Handmade-сообщество).

← Глава 1
Таксономия и ландшафт