Перейти к содержимому

Исследование движка

Прежде чем перестраивать движок, мы изучили, как выпущенные онлайн-игры ведут серверную симуляцию: как идёт время, как мир делится между потоками и машинами, как состояние доходит до клиентов и базы данных и где всё ломалось. На этой странице собрано то, что говорят публичные источники. У каждого утверждения есть ссылка на источник; заявления разработчиков и вендоров помечены как таковые. Где публичного источника нет, страница молчит.

Эталонная схема авторитетного сервера — фиксированный тик. Valve описывает цикл движка Source так: на каждом тике сервер обрабатывает пришедшие команды игроков, выполняет шаг физики, проверяет правила игры, обновляет состояние объектов и затем решает, каким клиентам нужен snapshot (Valve: Source Multiplayer Networking). Snapshot сжимается как дельта относительно последнего обновления, подтверждённого клиентом.

Частота тика различается на два порядка:

Игра Тик Источник
Old School RuneScape 600 мс OSRS Wiki: Game tick
Minecraft 50 мс (20 TPS) spark: The tick loop
Игры на Source (TF2) 15 мс (66 Гц) Valve
Valorant 128 Гц Riot Tech Blog

Важна не скорость. В шутерах тик отвечает за точность симуляции. В MMO и песочницах тик становится единицей геймдизайна, и тогда его уже нельзя свободно менять:

  • Valve отключила опцию -tickrate, «because changing tickrate causes server timing issues» (Valve).
  • Игроки RuneScape превратили работу с границами тика в отдельный навык (OSRS Wiki: Tick manipulation).
  • World of Warcraft до 2014 года обрабатывал действия между юнитами пачками раз в 400 мс, затем «usually between 1ms and 10ms later». WoW Classic намеренно вернул окно 400 мс, чтобы повторить оригинальный геймплей (wowisclassic.com), а позже тестовый сервер Classic перешёл на окно 10 мс (wowisclassic.com).

Если тик не укладывается в бюджет, следующий просто начинается позже. Minecraft откладывает следующий тик, потому что «ticks cannot execute in parallel» (spark), а загруженный мир RuneScape идёт тиками по 0,616–0,618 с вместо 0,6 с (OSRS Wiki). Обратный рефлекс — догонять время лишними шагами — ведёт к «spiral of death» (Gaffer On Games: Fix Your Timestep).

Сервер Counter-Strike 2 по-прежнему работает на 64 Гц, но клиент ставит каждому вводу метку точного момента внутри тика, и сервер оценивает действие в этот момент. Маркетинг Valve на запуске утверждал: «tick rate no longer matters for moving, shooting, or throwing» (GameWorldObserver). На практике системе понадобилась серия патчей таймингов (dust2.us), а в опросе 14 из 37 профессиональных игроков назвали sub-tick главной проблемой игры (dust2.us). Описанные симптомы, например нестабильные jump-throw (Sportskeeda), происходят от гибрида: одни действия считаются по точному времени, а состояние, от которого они зависят, всё ещё движется шагами по 64 Гц. Внутреннее устройство Valve не публиковала.

Меткам времени от клиента сервер доверять не должен. Переносится серверная часть идеи: хранить точное время каждого события внутри грубого pass. Атака, конец каста или тик эффекта хранят свой дедлайн в миллисекундах; pass выполняет все наступившие события в порядке дедлайнов, а периодическое событие перепланируется от предыдущего дедлайна, а не от момента, когда pass случайно запустился. Это тот же перенос остатка, что и в аккумуляторе из Fix Your Timestep, где оставшееся время «is not thrown away». Без него тайминги квантуются по тику: в RuneScape каждое действие попадает на границу 600 мс, а запрос посреди тика ждёт следующего (OSRS Wiki).

Стандартная структура для множества ожидающих таймеров — timer wheel (Varghese и Lauck, SOSP 1987): старт, остановка и учёт на каждом тике за константное время (the morning paper). На JVM на ней построены HashedWheelTimer в Netty, Kafka и DeadlineTimerWheel в Agrona. Сам timer wheel тоже тик с разрешением в ширину слота, и события внутри одного слота не упорядочены по дедлайну, пока их не отсортировать. Zero-Tick сравнил wheel с двоичной кучей для своих партиций и оставил кучу (см. Zero-Tick Engine).

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

Лучше всего задокументированное применение акторов в играх — Microsoft Orleans за Halo 4. Orleans обслуживал облачные сервисы: presence, статистику и пошаговую Galactic Reign. Статья приводит лабораторный тест: 1 млн акторов и около 130 000 heartbeat в секунду на 25 серверах. Combat authority оставался на консолях, «because the ground truth is always on the consoles» (Microsoft Research: Orleans).

Активация в Orleans однопоточна и выполняется «ходами» (turns), так что каждый актор — single writer. Для боя модель подходит плохо: один удар одновременно читает и пишет несколько сущностей (ману атакующего, HP цели, агро, соседей по области), и при акторе на сущность это превращается в протокол обмена сообщениями между ними. Публичных свидетельств боевой симуляции «актор на сущность» в масштабе MMO мы не нашли.

Под game loop и акторами лежит одна идея — принцип single writer из LMAX: «only one thing can ever write to a single piece of data», что полностью убирает блокировки (Oracle Java Magazine reprint).

Эмуляторы World of Warcraft. TrinityCore и AzerothCore открыты и показывают структуру целиком. Один мировой поток выполняет World::Update(diff) — последовательность подсистем, каждая из которых замеряется через METRIC_TIMER, включая обработку пакетов всех сессий. Затем он раздаёт карты пулу потоков MapUpdater и ждёт на барьере, пока не закончатся все (AzerothCore World.cpp, MapUpdater.cpp, TrinityCore MapManager.cpp). Карту — континент или инстанс — обновляет один поток, поэтому логике карты блокировки не нужны. Результаты асинхронных запросов к БД применяются в мировом потоке, а синхронный запрос в мировом или картовом потоке логируется как предупреждение (Main.cpp). Слабое место — барьер: самая медленная карта задаёт тик всем остальным. MapMgr в AzerothCore разносит континенты, поля боя и подземелья по трём шагам цикла, чтобы распределить эту нагрузку (MapMgr.cpp). Официальный WoW справляется с толпой через layering: каждый слой — полная копия мира (Wowhead).

EVE Online. Один шард, разделённый по солнечным системам. Каждая нода выполняет tasklet’ы Stackless Python, где «only one tasklet can be running at any given time», а физика Destiny обновляется раз в секунду. Когда нода перегружена, CCP замедляет её игровые часы, вместо того чтобы дать ей упасть: Time Dilation подстраивается мелкими шагами так, чтобы очередь tasklet’ов оставалась почти пустой (CCP: Introducing Time Dilation). В примере из блога с 1600 пилотами бой при 1200 вовлечённых кораблях шёл примерно на 60% скорости. В битве при Asakai TiDi упёрлась в свой нижний предел 10% и всё равно деградировала мягче, чем прежние битвы (Data Center Knowledge). Правило CCP о том, какие таймеры замедлять, стоит перенять дословно: «if you look at your watch to see what time an event is going to complete, it probably shouldn’t be dilated». Таймеры reinforcement идут по wall-clock времени, перезарядка щитов — по игровому.

Остальные привычки CCP — операционные. Jita работает на отдельной ноде (Tranquility Tech IV), под объявленные крупные бои резервируются ноды (Fleet Fight Notification), а тяжёлая непространственная работа уходит из симуляции: Brain in a Box заранее вычисляет эффекты навыков на отдельной ноде вместо пересчёта на каждом прыжке (forums archive), а Quasar выносит сериализацию и маршрутизацию за пределы GIL Python (Introducing Quasar).

Guild Wars 2. Карты работают как долгоживущие процессы-инстансы; megaserver создаёт копии карт по спросу и распределяет игроков по группе, гильдии, языку и домашнему миру (Engadget). Инстансы карт живут неделями, поэтому падение сервера карты очень болезненно (AWS for Games).

Картина везде одна: единицу мира симулирует один поток, параллелизм получают за счёт множества таких единиц, толпу поглощают копии там, где это допускает дизайн, или замедление времени там, где не допускает, а база данных и непространственные сервисы живут вне потока симуляции.

Каждая партиция выше отделена жёстким игровым швом: загрузочным экраном, гейтом, порталом или отдельной копией. Карта TrinityCore — это континент или инстанс, а не регион внутри континента; её сетка ячеек служит видимости и выгрузке, а не конкурентности (GridDefines.h). Солнечная система EVE соседствует с другими только через гейты. Через такие границы ничто не взаимодействует, поэтому не нужны ни ghost-копии, ни authority hand-off, ни распределённые транзакции.

С делением непрерывного пространства всё иначе:

  • SpatialOS. Runtime Improbable делил мир между воркерами, каждый из которых видел частичный view. Его балансировщику понадобилось отдельное исправление против «entities moving back and forth across different worker regions» (Improbable). Worlds Adrift, Mavericks и Lazarus были отменены один за другим (PCGamesInsider), хотя Bossa не винила платформу в закрытии Worlds Adrift (PCGamesInsider). В 2023 году Improbable продала свою студию мультиплеерных сервисов (CNBC).
  • New World. Блог самой Amazon описывает статическую сетку: семь процессов-хабов на мир, каждый владеет двумя несмежными ячейками, за четырьмя gateway-инстансами, которые служат единственными публичными адресами; состояние игрока переходит от хаба к хабу по мере движения (AWS for Games; блог вендора). Hand-off протокол не опубликован.
  • Star Citizen. CIG вынесла состояние мира из игровых серверов в replication layer (Star Citizen Wiki) и выпустила static server meshing в Alpha 4.0 с 500 игроками на шард (Star Citizen Wiki). В начале 2026 года CTO сообщил о 600–800 на шард; dynamic meshing всё ещё был во внутренних тестах, а среди открытых проблем была потеря предметов при замедлении бэкенда (StarShipDealers recap, Massively OP; заявления разработчика).
  • Ashes of Creation. «Dynamic gridding» показали в 2024 году и тестировали в 2025-м (Mein-MMO; заявления разработчика в пересказе прессы). В начале 2026 года студия закрылась (Wikipedia).

Ближайший прецедент на JVM — Folia, сервер Minecraft от PaperMC. Он объединяет соседние чанки в регионы, которые тикают параллельно, сливаются и делятся на ходу, и «главного потока нет» (Folia region logic). Это работает, и README честно называет цену: «I expect every single plugin that exists to require some level of modification»; глобальные scoreboard сломаны, телепорт стал асинхронным, планировщик заменён четырьмя. Folia рассчитана на серверы, где игроки разбросаны по карте, и рекомендует минимум 16 физических ядер (Folia README).

У Lineage 2 профиль нагрузки обратный: игроки собираются ровно там, где региональное деление провело бы границы, — на осадах и у рейд-боссов. На 2026 год в публичных источниках нет живой MMO, которая динамически делит непрерывное пространство и выдерживает нагрузку, а системы со статическим делением расплачивались проблемами hand-off и потерянными предметами.

Overwatch — канонический выпущенный пример строгой entity-component-system в авторитетной игре (GDC Vault). По вторичному пересказу доклада, выигрыш был структурным: сущности — это ID, компоненты — данные, системы не хранят состояние, netcode геймплея сосредоточен в трёх системах, а побочные эффекты откладываются в одно место вызова за кадр. На то, чтобы устоялись правила, по сообщениям, ушло около 1,5 лет, и главным источником багов был код, который их нарушал (Edgegap).

В движках ECS — мейнстрим: Unity DOTS с Netcode for Entities (Unity docs), Unreal Mass, которая живёт рядом с миром Actor, а не заменяет его (Epic docs), а также Flecs и EnTT в выпущенных играх (ECS FAQ).

Ограничения описывают сами авторы ECS. В FAQ, который ведёт автор Flecs, сказано, что пространственные индексы обычно живут вне ECS, иерархии через ссылки между компонентами замедляют итерацию, а правила ECS «conflict with OOP intuition» (ECS FAQ). Множество отношений или переключаемых тегов дробит сущности по таблицам (Flecs Relationships). Автор Artemis замечает, что при паре сотен сущностей выбор фреймворка «probably doesn’t matter» (Artemis wiki). Мы не нашли публичного postmortem MMO с RPG-навыками и эффектами на полной ECS и ни одного независимого бенчмарка ECS против объектов при количестве сущностей, характерном для MMO.

Hytale — предостерегающий пример. В 2024 году команда объявила переписывание на C++ с Flecs в основе (Hytale). По данным вторичных источников, в 2026 году игра вышла в ранний доступ на прежнем сервере на Java (Winternode).

Состояние на JVM. Java-библиотеки ECS Artemis и Dominion числятся неактивными (ECS FAQ). Value classes (JEP 401) интегрированы в JDK 28 только как preview (JEP 401), поэтому в 2026 году компактная раскладка данных — это примитивные массивы и примитивные коллекции. Generational ZGC — режим ZGC по умолчанию начиная с JDK 23, с целевыми паузами меньше миллисекунды (JEP 474); аллокации на hot paths всё равно стоят времени.

Современные движки выполняют репликацию отдельным pipeline после шага симуляции. Iris в Unreal ведёт dirty-битовое поле для каждого реплицируемого состояния; перед отправкой копирует и квантует dirty state, затем фильтрует по владельцу, соединению, группе или собственному пространственному правилу, затем приоритизирует. Приоритет копится между тиками, пока объект не будет отправлен, поэтому низкоприоритетные объекты рано или поздно тоже уходят, а отправка укладывается в бюджет на соединение (Components of Iris, Iris filtering, Iris prioritization).

flowchart LR
  S[шаг симуляции] --> D[dirty state]
  D --> Q[копирование и квантование]
  Q --> F[фильтр по соединению]
  F --> P[приоритет с накоплением]
  P --> B[отправка в бюджете байт]

Её предшественник, Replication Graph, создавался для Fortnite: узел двумерной сетки хранит для каждой ячейки список видимых из неё акторов, и клиент копирует список своей ячейки вместо проверки расстояния до каждого актора (Epic tech blog, Epic docs).

Отдельную фазу позже можно вынести в отдельный сервис: replication layer в Star Citizen и gateway-инстансы New World — это именно такой шаг. В массовом бою основная стоимость — сообщить всем о движениях, скиллах и эффектах всех остальных, а это задача репликации, а не симуляции.

В зрелых системах база данных не участвует в тике. Авторитетное состояние живёт в памяти и пишется асинхронно или периодически. TrinityCore сохраняет игрока раз в 90 с (PlayerSaveInterval = 90000) и при выходе, через асинхронный worker БД (worldserver.conf.dist). База EVE служит «primarily for storage» (Tranquility Tech IV). Veloren опрашивает character loader и saver раз за серверный тик (Veloren docs). Поскольку source of truth — память, ручные правки инвентаря онлайн-персонажа в БД не подхватываются или перезаписываются (TrinityCore forum).

У публично задокументированных дюпов одна причина: передача между двумя игроками сохраняется в двух разных точках.

  • RuneScape, 2020. Игрок A передаёт предметы B, B выходит из игры и сохраняется, затем сервер намеренно роняют. Игроки откатываются к сохранению «from either 15 minutes ago, or their last logout», поэтому A получает предметы обратно, а B оставляет их у себя (RuneScape Wiki).
  • New World, 2021. Персонажи со сбоем сохранения отправляли золото, выходили из игры, и сохранялась только сторона получателя (PCGamesN). Отдельный эксплойт возвращал стоимость апгрейда города, который так и не начался (Mein-MMO). На время исправления Amazon отключила все передачи ценностей (WePC).
  • Diablo IV, 2025. Начатый трейд и выход из клиента оставляли предметы у обоих игроков; Blizzard приостановила торговлю (Shacknews).
  • Order & Chaos. Многократное нажатие «extract all» в почте давало несколько копий — случай отсутствующей идемпотентности (DroidGamers).

Идентичность предметов — отдельная точка отказа. В 2010 году в EVE упал скрипт, переиспользовавший ID удалённых предметов; новые предметы не получали ID, инвентари сломались, затронуто до 114 000 ID предметов (CCP). Когда дюп всё же выходит наружу, последнее средство — откат мира, который стоит каждому игроку недавнего прогресса (OSRS Wiki: Rollback).

Лекарства следуют из инцидентов. Передача ценности — одна транзакция БД, охватывающая обе стороны, и игрок получает подтверждение только после коммита; работа McGill о персистентности MMO говорит то же: «critical events must be written synchronously» (Kemme et al., NetGames 2008). У предметов неизменяемая идентичность, которая никогда не переиспользуется. Каждая операция, которую можно повторить (получение почты, покупка), несёт idempotency key и применяется один раз. Append-only ledger движений ценностей позволяет точечно убрать продублированное вместо отката мира. Обычное состояние вроде HP, позиции или опыта может идти через write-behind, где в пределах окна записывается только последнее значение по ключу (Hazelcast MapStore); coalescing безопасен для состояния, но никогда не для передач.

Time Dilation работает только потому, что игровая логика EVE читает игровые часы, а не системные. Детерминированному replay нужно то же самое: Factorio находит расхождение, сравнивая CRC всей карты на каждом тике при replay сохранения, и разница обычно сводится к «a value of one variable» (Factorio FFF #47). Детерминизм хрупок: desync 2024 года удалось воспроизвести только симуляцией другого числа ядер процессора (FFF #415), а параллельные системы без фиксированного порядка — это ровно те «execution order ambiguities», о которых сообщает Bevy (bevy_ecs). Авторитетному серверу не нужен lockstep между клиентами, но если партиция выполняется в одном потоке и читает время и случайность только через injected-источники, для её повторного прогона достаточно её input log.

Перегрузку нужно сначала измерить, а потом обрабатывать. Spark, профайлер для Minecraft, считает простой меньше 5% времени тика признаком лага и предупреждает, что средние значения скрывают пики по 300 мс (spark); AzerothCore замеряет каждую подсистему своего мирового обновления (World.cpp).

Результат описан на странице архитектуры. Коротко:

  • Один writer на партицию, партиции за жёсткими швами. Открытый мир — одна партиция; каждый инстанс и матч олимпиады — отдельная. Непрерывный мир не делится на регионы: именно там собирается толпа в Lineage 2 и именно там ломались разделённые миры.
  • Нет глобального барьера. Партиции работают независимо, и тяжёлая не задерживает остальные.
  • Точные дедлайны вместо грубого глобального тика. Атаки, касты и эффекты хранят точное время наступления, а периодическая работа переносит остаток; таймеры уходят из общего пула потоков импортированного кода в партицию, которая владеет состоянием.
  • Injected время и случайность, с разделением по механикам на игровое и wall-clock время, чтобы замедление времени и replay оставались возможны.
  • Объекты остаются фасадом, который вызывают скрипты, и собираются из частей; data-oriented хранилища — только там, где профилирование показывает hot path. Переписывания на ECS нет.
  • Репликация — отдельная фаза после симуляции, с бюджетами на соединение.
  • База данных вне игровых потоков, write-behind для обычного состояния, одна транзакция плюс ledger на каждую передачу ценности и persistent identity предметов, которая никогда не переиспользуется.
  • Сначала измерения. Время pass по партициям и подсистемам и нагрузочные тесты ботами, включая сценарий осады.
  • Наш движок движения работает внутри той же модели партиций, что и остальная симуляция.