# Helix Empire — 10 000 игроков в одном реальном мире

Браузерная космическая стратегия в реальном времени, ядро которой — генетика: игроки выводят касты существ, редактируя ДНК. Сложное здесь — движок: 10 000 игроков в одном общем мире на одном сервере, вживую. Я спроектировал архитектуру вокруг настоящего бутылочного горлышка (исходящий трафик, а не CPU) и доказал измеренным нагрузочным тестом: 237.6 Мбит/с на 10k, 24% бюджета. Сделано в одиночку, сейчас в альфе.

- **Author:** Фёдор Чищин
- **Role:** Архитектор-разработчик (соло)
- **Period:** 2026 — present
- **Stack:** Rust · WebAssembly · WebTransport / QUIC · Event sourcing · Area of Interest · Hetzner bare-metal
- **Source:** https://fedorthinks.com/ru/work/helix-empire

---

## Задача

**Helix Empire** — космическая стратегия реального времени в браузере. Её ядро — не строительство,
а **генетика**: игрок выводит касты существ, редактируя их ДНК (отсюда «Helix» — двойная спираль).
Гены задают признаки, признаки решают, как существа работают в разных средах, это двигает экономику
и армию, что двигает науку, а та открывает новые правки генома. Замкнутая петля:
**гены → признаки → экономика → наука → лаборатория → снова гены.** Сила империи =
гены × планеты × технологии.

Технически это значит **тысячи игроков в одном общем мире**, живущем в реальном времени — экономика
тикает, существа размножаются, флоты летают, ресурсы меняются каждую секунду — и **всё это должно
доходить до браузера каждого игрока без задержек.**

Большинство онлайн-игр прячут масштаб: делят игроков на комнаты по 20–50 или на «шарды». Я поставил
цель сложнее намеренно — **10 000 одновременных игроков в одном мире на одном сервере** — потому что
ровно в этом ограничении и живёт интересная архитектура.

## Четыре стены

Произнесите «10 000 в одном мире» вслух — и появляются четыре стены, о которые разбивается наивное
решение.

1. **Рассылка «всем про всех» — это O(N²).** Если каждый должен знать про каждого, один тик —
   `10 000 × 10 000 = 100 000 000` пар, и так несколько раз в секунду. Физически нереализуемо.
   Удвоили игроков — учетверили работу.
2. **Главная статья расходов — трафик, а не CPU.** Неинтуитивный факт хостинга: **исходящий трафик
   (egress) — самое дорогое**, на гиперскейлерах в десятки раз дороже, чем на bare-metal. Если каждый
   тик шлёт каждому игроку много байт — счёт за трафик убивает проект раньше, чем нагрузка на
   процессор. Значит, архитектуру надо строить вокруг минимизации трафика.
3. **Один мир хочет жить на одном сервере.** Размазать мир по машинам — это распределённый консенсус
   (чья версия мира правильная?), медленно и сложно. Я выбрал **single-writer**: ровно один процесс
   имеет право менять мир. Никаких гонок и консенсуса. (У него есть коварная ловушка — о ней ниже.)
4. **Тонкий клиент не вытянет.** Если сервер считает всё, а браузер только рисует, сервер становится
   узким местом на 10 000 человек сразу. Поэтому клиент должен быть **толстым** — уметь достроить
   картинку из минимума данных.

## Стек, выбранный против стен, а не под моду

Смысл архитектуры в том, что **технологии подчинены ограничениям, а не моде.** Каждый выбор отвечает
на конкретную стену.

## Что я построил, шаг за шагом — измеряя на каждом шаге

Я не строил всё сразу. Шёл слоями, **измеряя на каждом** — нельзя оптимизировать то, что не измерил.

**Шаг 0 — базовая линия.** Прежде чем что-то чинить, я измерил, как плохо: чтения состояния со
скачками до **4–10 секунд**, пуш клиенту примерно **1 кадр в 20 секунд**. Появилась точка отсчёта.

**Шаги 1–3 — убрать контеншн single-writer.** Вот та ловушка. Раз мир меняет один процесс, то пока он
применяет тик (а тик пишет в хранилище — медленно), **все читатели ждут тот же замок**. Игрок
открывает экран обороны и висит 10 секунд, потому что сервер в этот момент сохраняет тик. Решение —
**lock-free read-model-кэши в RAM**, обновляемые инкрементально и читаемые **без** замка записи.

Архитектурный ход: **отделить чтение от записи.** Писатель по-прежнему владеет консистентностью,
читатели читают RAM-проекции и перестают ждать. Контеншн исчез, все эндпоинты стали стабильно
sub-second.

**Шаги 4–5 — слать разницу, а не всё состояние.** Даже с быстрыми чтениями каждый тик слал _полное_
состояние — заново пересылая горы того, что не менялось. Решение — **дельты**. Математически
проверенный тип: одна функция строит разницу двух состояний, другая её применяет, а тест
конвергенции доказывает, что цепочка дельт, сложенная клиентом, _в точности_ воспроизводит снапшот
сервера. Один и тот же Rust-код на сервере и клиенте — они не могут разойтись. Потерян кадр? Клиент
это замечает (`ResyncRequired`) и просит полный снапшот.

**Шаг 6 — Area of Interest рубит O(N²) до линейного.** Игроку не нужны все 10 000 — только соседи по
пространству и дипломатии. `AreaOfInterest` + `filter_frame` режут дельту по области интереса:
каждый подписчик получает релевантное ему, а общеколониальный контекст (чат, сделки) сохраняется.
Квадратичная рассылка становится линейной: трафик растёт как N × (размер области интереса), а не N².

**Шаг 7 — честная последняя миля.** Самая важная часть истории. Нагрузочный тест показал, что красивая
цифра «320 Мбит/с» держалась на **зашитой константе** «32 байта на дельту игрока». Я подставил
реальный бинарный кодек и измерил: настоящая дельта — **104 байта**, потому что я слал _весь_
профиль игрока (11 ресурсных полей), даже если менялось одно. 104 против 32 — это **в 3.25 раза
больше**, проекция вылетала в ~1.04 Гбит/с на 10k. Честный ответ на тот момент:
**«нет, на реальном формате 10k я не держу».**

Решение — **пофилдовая дельта** `WirePlayerDelta`:

```
player_id (4 байта) + битовая маска изменённых полей (2 байта) + ТОЛЬКО изменённые значения
```

Если изменились population, food и science — на провод уходят ровно эти три плюс маска:
`4 + 2 + 4 + 8 + 8 = 26 байт` вместо 104. Маска — 16 бит, по одному на поле; стоит бит — значение
следует в payload, не стоит — поле не изменилось и не стоит ничего.

## Как это доказано — и почему цифре можно верить

Архитектура без доказательства — обещание. Я доказал числами: round-trip- и конвергенс-тесты,
контрактные тесты на каждый порт и **42 e2e-теста в настоящем Chromium** поверх WebTransport (включая
«получает серверные тики по realtime-сокету без поллинга» и «два игрока в одном мире синхронизируются
в реальном времени»).

Главное доказательство — тест на 10 000 ботов, который меряет egress **настоящим бинарным кодеком** —
никаких зашитых констант. Создаёт 10 000 ботов-клиентов в одном мире, прогоняет настоящий авторитетный
тик, применяет видимые каждому боту дельты через реальную AoI-фильтрацию и суммирует измеренные байты.

Цифра защищена гейтом `check:load`: отчёт проверяется на каждом прогоне, а проекция выводится **из
измеренного значения, а не из допущения**. Раздует кто-то формат дельты — гейт упадёт.

## Честная граница

Зрелость архитектора — не только хорошая цифра, но и честность про её границы.

**Доказано:** по вычислениям и латентности fan-out на 10 000 ботов укладывается в 200 мс с большим
запасом; по egress — 237.6 Мбит/с (24% бюджета 1 Гбит/с), причём это измерение настоящим кодеком, а
не допущение.

**Пока не доказано:** это проекция из одного измеренного тика на 10k (боты в одном процессе), а не
живой кластер с 10 000 реальных QUIC-сокетов под нагрузкой часами. Поведение CPU/RAM под устойчивым
многоминутным потоком и индивидуальный per-subscriber AoI-канал для толстого WASM-клиента — следующий
слой работы. Но **главный риск — egress, который я сам честно пометил как «за бюджетом на реальном
формате», — закрыт и измерен.**

## Что здесь от архитектора, а не только от разработчика

1. **Правильно назвал бутылочное горлышко:** egress, а не CPU. Весь стек подчинён этому выводу.
2. **Отделил чтение от записи:** single-writer для консистентности + lock-free RAM-проекции для чтений.
3. **Свёл O(N²) к линейному** через Area of Interest — без этого 10k невозможны в принципе.
4. **Дельты вместо снапшотов**, с одним кодом на сервере и клиенте — конвергенция гарантирована
   архитектурно.
5. **Не поверил красивой цифре, пока не измерил.** Нашёл, что «32 байта» — допущение, измерил 104,
   придумал пофилдовую дельту, довёл до 26 и **доказал замером**.
6. **Закрепил результат гейтом**, чтобы регресс не прошёл незамеченным.

Движок, который **измеренно** обслуживает 10 000 игроков в одном мире на 24% бюджета трафика, — это
не везение и не один трюк. Это последовательность архитектурных решений, каждое отвечает на конкретную
стену и подтверждено числом.

## Снимок стека

| Слой           | Выбор                                         | Какую стену решает                             |
| -------------- | --------------------------------------------- | ---------------------------------------------- |
| Ядро симуляции | Rust, детерминированное, в нативку + WASM     | толстый клиент, больше игроков/сервер (№2, №4) |
| Транспорт      | WebTransport / QUIC, фолбэк WebSocket         | бинарный поток с низкой задержкой (№2)         |
| Модель мира    | Single-writer + event sourcing                | без консенсуса, восстановимо (№3)              |
| Чтения         | Lock-free RAM read-model-проекции             | sub-second чтения без контеншна                |
| Обновления     | Пофилдовые бинарные дельты + Area of Interest | O(N²) → линейно, минимум egress (№1, №2)       |
| Хостинг        | Hetzner bare-metal                            | egress в 20–40× дешевле гиперскейлеров (№2)    |

Helix Empire в альфе, сделан в одиночку — продуктовый дизайн, Rust-ядро симуляции, WASM-клиент,
транспорт, разделение чтения/записи, конвейер дельт + AoI, нагрузочный харнесс и измеренное
доказательство. Скоро будет доступен на **helixempire.com**.
