feat: move to typescript, miniplex ecs
This commit is contained in:
+88
-23
@@ -1,33 +1,98 @@
|
||||
# ECS рефакторинг — план
|
||||
# Архитектура (ECS на miniplex)
|
||||
|
||||
## Суть
|
||||
Проект переписан на TypeScript + PixiJS с ECS-ядром на [miniplex](https://github.com/hmans/miniplex).
|
||||
Этот документ — описание того, как всё устроено сейчас.
|
||||
|
||||
Вместо классов врагов/снарядов с методами update() и draw() — единый контейнер сущностей, где каждая сущность это набор компонентов (позиция, скорость, тип, слово и т.д.). Логика вынесена в отдельные функции-системы, которые каждую итерацию проходят по нужным подмножествам сущностей и обновляют их.
|
||||
## Слои
|
||||
|
||||
## Компоненты
|
||||
```txt
|
||||
ввод (InputSource) → шина событий → симуляция (мир + системы) → шина событий → рендер (Pixi) и DOM-UI
|
||||
```
|
||||
|
||||
Позиция и скорость — общие для всех движущихся сущностей. Тип врага и данные слова — только для врагов. Ссылка на цель — только для снарядов. Броня и босс-данные — для усиленных и босс-врагов. Пустой маркер — чтобы система знала, что сущность является снарядом.
|
||||
Каждый слой знает только соседей через интерфейсы: логика не знает про Pixi и DOM,
|
||||
рендер не содержит игровой логики, ввод не знает про игру — только команды.
|
||||
|
||||
## Системы (по порядку за кадр)
|
||||
## Сущности и компоненты (src/ecs)
|
||||
|
||||
1. Система волн — считает таймеры, решает когда спавнить нового врага и какого типа, создаёт сущность в контейнере.
|
||||
2. Система ввода — получает слово, ищет лучшего врага (точное совпадение > более позднее слово > частичное), создаёт снаряд с ссылкой на цель.
|
||||
3. Система движения — для всех сущностей с позицией и скоростью: позиция += скорость.
|
||||
4. Система наведения — для снарядов с целью: вычисляет расстояние, если рядом — помечает попадание, если цель исчезла — удаляет снаряд, иначе двигает к цели.
|
||||
5. Система урона — для снарядов с пометкой попадания: проверяет броню (если есть и не ноль — снимает), проверяет босс-данные (считает убитые слова в слое, если слой пройден — переключает), если враг умер — удаляет и вызывает callback.
|
||||
6. Система рендера — берёт все враги, сортирует по высоте, рисует форму/слово/планку брони. Отдельно рисует снаряды. Отрисовка HUD отдельно от ECS.
|
||||
7. Система очистки — удаляет из контейнера все сущности с пометкой "мёртвый".
|
||||
Сущность — объект с опциональными компонентами; «есть компонент» = свойство не `undefined`.
|
||||
|
||||
## Что остаётся за пределами ECS
|
||||
| Компонент | Смысл |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `position`, `velocity`, `damping` | Движение (враги, снаряды, частицы — одна система интеграции) |
|
||||
| `enemyKind`, `enemyBox` | Тип (simple/heavy/boss) и габариты рамки |
|
||||
| `armor` | Слои слов: текущий слой отображается и уязвим только он |
|
||||
| `projectile`, `player`, `target` / `targetPoint`, `fuse` | Снаряд: слово, кто выстрелил, цель (или точка промаха), задержка выстрела |
|
||||
| `lifetime`, `particle` | Частицы — полноценные сущности, а не особый массив |
|
||||
| `waveState` | Синглтон-сущность с состоянием волны и баннером |
|
||||
|
||||
Волны, ввод, рендер HUD, очистка — это системы, но не entity. Жизни/счёт/номер волны — простые переменные, не сущности. Частицы — не сущности, рисуются как визуальный эффект по времени от взрыва.
|
||||
Броня единая для всех типов: simple — 1 слой из 1 слова, heavy — 2 слоя по 1 слову
|
||||
(порядок строгий), boss — 3 слоя по 3 слова (внутри слоя порядок свободный).
|
||||
Урон снимает только текущий слой; после каждого попадания вью перерисовывается по диффу
|
||||
`layerIndex:killed`.
|
||||
|
||||
## Порядок миграции
|
||||
## Системы (порядок за кадр, src/systems)
|
||||
|
||||
1. Контейнер сущностей — замена глобальных массивов.
|
||||
2. Система движения — самая простая, проверка что всё работает.
|
||||
3. Система наведения и урона — core gameplay.
|
||||
4. Система волн — спавн.
|
||||
5. Система ввода — поиск цели + создание снаряда.
|
||||
6. Система рендера — вся отрисовка.
|
||||
7. Остальное — очистка, game loop, удаление старых классов.
|
||||
1. `fuseSystem` — снаряд стоит у стрелка, пока не истечёт задержка ввода (150 мс), потом получает скорость.
|
||||
2. `homingSystem` — наведение на цель, касание → событие `projectile:hit`; цель исчезла — снаряд удаляется.
|
||||
3. `missSystem` — промах долетает до точки → `projectile:miss`.
|
||||
4. `waveSystem` — таймеры волны, спавн, пауза, боссы; пишет в синглтон `waveState`.
|
||||
5. `integrationSystem` — единое движение для всех сущностей с position+velocity (и damping).
|
||||
6. `cullSystem` — снаряды за экраном.
|
||||
7. `escapeSystem` — враг дошёл до низа → `enemy:escaped` → урон.
|
||||
8. `lifetimeSystem` — затухание частиц.
|
||||
|
||||
`Simulation` (src/game) задаёт порядок систем, принимает команды ввода (поиск цели по слову),
|
||||
обрабатывает попадания/промахи/урон и держит скаляры партии (счёт, жизни, фаза, тряска)
|
||||
в `GameStore` — по договорённости это не сущности.
|
||||
|
||||
## Ввод (src/input) — отдельный интерфейс
|
||||
|
||||
Игра зависит только от порта `InputSource`, а не от библиотеки:
|
||||
|
||||
```ts
|
||||
interface InputSource {
|
||||
readonly origin: "local" | "chat";
|
||||
start(): void;
|
||||
stop(): void;
|
||||
onCommand(handler: (command: InputCommand) => void): () => void;
|
||||
}
|
||||
|
||||
type InputCommand =
|
||||
| { kind: "start" } // !play, награда за баллы канала и т.п.
|
||||
| { kind: "clear" }
|
||||
| { kind: "word"; word: string; player: Player };
|
||||
```
|
||||
|
||||
Сейчас реализованы `TwitchChatInputSource` (tmi.js, парсит `!play` и первое слово сообщения)
|
||||
и `LocalKeyboardInputSource` (тест без Twitch). Будущий переход на Twurple и запуск через
|
||||
баллы канала = новый источник, который эмитит `{ kind: 'start' }` на событие награды;
|
||||
`InputRouter` просто регистрирует его рядом с остальными.
|
||||
|
||||
## Рендер (src/render)
|
||||
|
||||
- `GridLayer` — сетка одним `TilingSprite` (вместо ~40 `stroke()` за кадр).
|
||||
- `ParticleLayer` — все частицы в одном `ParticleContainer` (один draw call, пул объектов).
|
||||
- Свечение — заранее запечённая радиальная текстура с аддитивным блендингом
|
||||
вместо попиксельного `shadowBlur`.
|
||||
- Вью врагов/снарядов создаются и уничтожаются по сигналам мира
|
||||
(`world.onEntityAdded/onEntityRemoved`), каждый кадр только синхронизируют позицию/альфу.
|
||||
- Тряска экрана — смещение корневого контейнера, состояние в `GameStore`.
|
||||
- Цикл: внешний `GameClock` (rAF, delta в 60-кадрах), pixi-ticker остановлен; в простое
|
||||
(интро закончилось, партия не идёт) рендер вообще не тикает — canvas хранит последний кадр.
|
||||
Сбой одного кадра не убивает цикл.
|
||||
|
||||
## События (src/game/events.ts)
|
||||
|
||||
Типизированная шина `EventBus`: ввод, бой (`projectile:hit/miss`, `enemy:*`) и состояние
|
||||
партии (`game:started/over`, `score/wave/lives`, `effect:glitch`). Рендер и DOM-UI — только
|
||||
подписчики.
|
||||
|
||||
## DOM-UI (src/ui)
|
||||
|
||||
HUD, экраны старта/поражения, таблица зрителей (имена вставляются через `textContent` —
|
||||
ник из чата не должен стать разметкой), фидбек ввода, масштабирование оверлея под окно.
|
||||
|
||||
## Отладка
|
||||
|
||||
В консоли браузера доступен хук `window.__sw` — мир, стор, шина, метрики и рендерер
|
||||
(`__sw.world.with('armor').entities`, `__sw.store.score` и т.д.).
|
||||
|
||||
Reference in New Issue
Block a user