7.0 KiB
Архитектура (ECS на miniplex)
Проект переписан на TypeScript + PixiJS с ECS-ядром на miniplex. Этот документ — описание того, как всё устроено сейчас.
Слои
ввод (InputSource) → шина событий → симуляция (мир + системы) → шина событий → рендер (Pixi) и DOM-UI
Каждый слой знает только соседей через интерфейсы: логика не знает про Pixi и DOM, рендер не содержит игровой логики, ввод не знает про игру — только команды.
Сущности и компоненты (src/ecs)
Сущность — объект с опциональными компонентами; «есть компонент» = свойство не undefined.
| Компонент | Смысл |
|---|---|
position, velocity, damping |
Движение (враги, снаряды, частицы — одна система интеграции) |
enemyKind, enemyBox |
Тип (simple/heavy/boss) и габариты рамки |
armor |
Слои слов: текущий слой отображается и уязвим только он |
projectile, player, target / targetPoint, fuse |
Снаряд: слово, кто выстрелил, цель (или точка промаха), задержка выстрела |
lifetime, particle |
Частицы — полноценные сущности, а не особый массив |
waveState |
Синглтон-сущность с состоянием волны и баннером |
Броня единая для всех типов: simple — 1 слой из 1 слова, heavy — 2 слоя по 1 слову
(порядок строгий), boss — 3 слоя по 3 слова (внутри слоя порядок свободный).
Урон снимает только текущий слой; после каждого попадания вью перерисовывается по диффу
layerIndex:killed.
Системы (порядок за кадр, src/systems)
fuseSystem— снаряд стоит у стрелка, пока не истечёт задержка ввода (150 мс), потом получает скорость.homingSystem— наведение на цель, касание → событиеprojectile:hit; цель исчезла — снаряд удаляется.missSystem— промах долетает до точки →projectile:miss.waveSystem— таймеры волны, спавн, пауза, боссы; пишет в синглтонwaveState.integrationSystem— единое движение для всех сущностей с position+velocity (и damping).cullSystem— снаряды за экраном.escapeSystem— враг дошёл до низа →enemy:escaped→ урон.lifetimeSystem— затухание частиц.
Simulation (src/game) задаёт порядок систем, принимает команды ввода (поиск цели по слову),
обрабатывает попадания/промахи/урон и держит скаляры партии (счёт, жизни, фаза, тряска)
в GameStore — по договорённости это не сущности.
Ввод (src/input) — отдельный интерфейс
Игра зависит только от порта InputSource, а не от библиотеки:
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(вместо ~40stroke()за кадр).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 и т.д.).