Процесс разработки
Порядок, в котором строится игровой плагин, и от чего зависит каждый шаг. Идите сверху вниз; пропуск шагов даёт работу, которую придётся переделывать.
Шаг 0 — прочитайте контракт
Прочитайте страницы документации SDK целиком, в порядке из README.md. Опросник предполагает, что вы знаете, что движок уже даёт, чтобы сказать пользователю «это делает движок», а не проектировать заново.
Шаг 1 — интервью
Проведите questionnaire.md. Правила:
- Ведите его на языке пользователя, на каком бы языке ни был написан файл.
- Спрашивайте блок за блоком, а не все 14 блоков сразу.
- К каждому вопросу предлагайте вариант «как в tanks», чтобы пользователь принял его одним словом.
- Если ответ противоречит движку (собственное поле комнаты, голосование за кик, локализация в рантайме), скажите об этом сразу и предложите ближайшую рабочую замену.
- Не спрашивайте то, что пользователь уже решил неявно.
Шаг 2 — документ-проект
Напишите короткий документ-проект и получите явное подтверждение до генерации кода. В нём нужно зафиксировать:
| Решение | Куда идёт |
|---|---|
Имя пакета, id, название | package.json, manifest, оба плагина |
| URL репозитория | repository в package.json — реестр игр ссылается на него; без него ссылки нет, а правило A7 предупреждает |
| Команды и команда наблюдателей | gameConfig.teams, схема статистики, карты |
| Типы сущностей и их поля | схема снапшота, строки Rust, части |
| Действия и клавиши | playerKeys, keySetList, обработка ввода в ядре |
| Ресурсы в HUD | panel.fields (хост) + схема панели (клиент) |
| Оружие/способности | parts.weapons, логика оружия в ядре, ключи событий снапшота |
| Что предсказывается локально | состояние игрока 8 × f32, предиктор, тест паритета |
| Полотна и камера | canvasManager.canvases |
| Карты и вместимость команд | data/maps/*, respawns |
| Звуки и сигналы | конфиг звуков, soundCues |
| Прогрессия | playerState.defaultState, onCoreEvent |
Шаг 3 — скаффолд
Не пишите дерево руками — сгенерируйте его:
npm create vimp-game <directory> -- --id <game-id> --title "<Title>" \
--repository <user/repo>Передайте --repository (полный URL или сокращение user/repo) именно на этом шаге, а не правкой package.json позже: платформа берёт ссылку на проект из опубликованного пакета (repository), а добавление после публикации стоит перепубликации.
Результат — раскладка из packaging.md, уже заполненная: вся инфраструктура сборки (package.json, vite.config.js, vitest.config.js, eslint.config.js, Cargo.toml, пять скриптов сборки), пины vimp-engine и vimp-engine-core на текущие версии, dev-обвязка и минимальная, но играбельная игра — две команды, один класс актора, одно хитскан-оружие, одна процедурная карта, боты. Она проходит check:contract, core:test, test, build и sim с первого коммита, так что у каждого следующего шага есть зелёная точка отсчёта. Сгенерированный CLAUDE.md описывает границы и команды проверок.
Дальше работа — замена, а не создание: перепишите конфиги, ядро и части этой минимальной игры под документ-проект из шага 2. В assets/ лежат два авторских входа: audio-raw/ и img/ — листы тайлов и спрайты динамических тел, которые называют ваши карты. Движок не поставляет ни того, ни другого.
Писать дерево руками по-прежнему можно (packaging.md описывает каждый файл), но тогда никто не охраняет тихие инварианты, которые шаблон уже соблюдает.
Шаг 4 — конфигурация
Сначала пишите слой конфигов; это контракт, который реализует всё остальное.
src/config/snapshot.js— схема передачи.src/data/models.js,src/data/weapons.js— каталоги.src/config/game.js—HostPlugin.gameConfig(импортирует предыдущие).src/config/client.js— клиентская половина.src/config/auth.js— экран входа.src/data/maps/*.js— минимум одна карта с полнымиrespawns; каждыйspriteSheet.imgиphysicsDynamic[].img, который она называет, должен существовать вassets/img/.src/config/sounds.js— реестр звуков.
Сверяйтесь по ходу: ключи панели совпадают на обеих половинах; имена в playerKeys совпадают с именами в keySetList; у каждого ключа снапшота есть запись в gameSets; каждый класс из gameSets есть в entitiesOnCanvas.
Шаг 5 — Rust-ядро
core/src/config.rs— десериализация половиныgameиз init JSON.core/src/<actor>.rs— появление/урон/жизненные показатели актора.core/src/motion.rs— математика движения, общая с предиктором.core/src/<game>.rs—impl GameSim, включаяbuild_snapshot_blocks, выдающий строки точно в порядке схемы.core/src/client/predictor.rs— локальная предикция на основеmotion.rs.core/src/client/mod.rs—impl GameClientDef.core/src/lib.rs— две структуры плюсexport_game_core_abi!/export_client_core_abi!и написанные вручнуюnewи игровые методы клиента.
Сборка: npm run core:build.
Шаг 6 — клиентская отрисовка
src/client/parts/*— по классу на проекцию сущности.src/client/bakers/*— процедурные текстуры.src/client/<game>.css— стили ячеек панели и команд.src/client/index.js—ClientPluginс тремя хуками.src/host/index.js,createModules.js,systemMessages.js, чат-команды и менеджерscripted(ботов).
Шаг 7 — тесты
Два набора:
JavaScript (Vitest) — повторите схему tanks, два проекта:
projects: [
{
test: {
name: 'game',
environment: 'happy-dom',
include: ['tests/host/**', 'tests/client/**', 'tests/config/**'],
},
},
{ test: { name: 'integration', environment: 'node', include: ['tests/core/**'] } }, // гоняет core/pkg-node
];Покройте как минимум:
- хост-плагин выставляет все обязательные поля, а конфиг проходит проверку обязательных путей движка;
- клиентский и хостовый конфиги согласованы по ключам панели, именам клавиш, ключам снапшота,
gameSets↔entitiesOnCanvas; - чат-команды и менеджер ботов работают;
- скрипт манифеста выдаёт корректный манифест.
Rust (cargo test --workspace) — модульные тесты по каждому модулю плюс набор паритета движения: прогоните авторитетную симуляцию и предиктор с одинаковым вводом и проверьте идентичное состояние. Запускайте его после каждой правки движения.
Шаг 8 — сборка и запуск
npm run core:build # wasm-pack web + nodejs
npm run build # client → host → assets → manifest
npm run dev # standalone: матч против ботов в одной вкладкеvimp-engine/standalone запускает весь матч в одной вкладке репозитория игры — без входа и экрана лобби; сгенерированный dev/main.js уже всё подключает. startStandaloneGame({ hostPlugin, clientPlugin, wasmUrl, … }) принимает живые объекты плагинов, поэтому это самый быстрый цикл, пока плагин ещё формируется:
// dev/main.js в репозитории игры, его грузит собственный index.html
import { startStandaloneGame } from 'vimp-engine/standalone';
import hostPlugin from '../src/host/index.js';
import clientPlugin from '../src/client/index.js';
import wasmUrl from '../core/pkg/my_game_bg.wasm?url';
await startStandaloneGame({
hostPlugin,
clientPlugin,
wasmUrl,
container: document.getElementById('game'), // во весь экран, position: relative
// dev-корень ваших ресурсов: клиент читает `${assetsBase}img/` и
// `${assetsBase}sounds/`, оба кладут в build/ скрипты копирования
assetsBase: '/build/',
playerName: 'dev',
startupVotes: [['teamChange', 'team1']], // сначала уйти из наблюдателей…
startupCommands: ['/bot 4'], // …и только потом свои чат-команды
});У движка нет понятия бота: scripted-участников создаёт ваша чат-команда, и пока игрок остаётся наблюдателем, команда отклоняется — отсюда строгий порядок двух последних опций. Справка: standalone.md.
Шаг 9 — headless-симуляция (до браузера)
Две вкладки браузера — самый медленный и наименее информативный способ найти сломанный контракт. Сначала запустите headless-раннер: он замыкает весь цикл (хост → бинарный кадр → клиентское ядро → сцена) в одном процессе Node и называет каждый нарушенный контракт текстом. Полная справка: debugging.md.
npm run sim -- --scenario <scenario.json>- [ ]
entries.wasmNodeуказывает на копию Node-ядра внутриdist/(./core-node/<crate>.js) — иначе передайте--core <path>. - [ ] Есть собственные сценарии, а не только встроенный: без
--scenarioраннер гоняет однокнопочный smoke, построенный из вашегоgameConfig, и пропускает инварианты 2 и 9, которые не могут быть осмысленными для неизвестной ему игры. - [ ] По сценарию на каждую основную механику: движение, стрельба, смерть/респаун, конец раунда, смена карты, голосование.
- [ ] Все инварианты зелёные — в первую очередь
snapshotKeysUsed,fieldWidths,renderCoverage,panelContract,keyBindings: это тихие отказы изpitfalls.md, доведённые до автоматической проверки. - [ ]
--determinismзелёный, когда матч стабилен. - [ ] Расхождение предикции в пределах порога (реализуйте
GameClientDef::predicted_stateдля покомпонентного отчёта).
Исправьте всё, что сообщил раннер, прежде чем открывать браузер. Всё, что браузер поймает позже, можно записать через window.__vimpDebug и воспроизвести здесь командой npm run sim:replay.
Шаг 10 — ручная проверка
Откройте npm run dev во вкладке браузера (standalone-матч):
- [ ] Карта отрисована на каждом полотне; радар (если есть) совпадает.
- [ ] Движение отзывчиво, боты двигаются плавно.
- [ ] Выстрел даёт ровно один эффект локально (без дубля от авторитетного события).
- [ ] Панель обновляется: полоса ресурса, боезапас, активное оружие, таймер раунда.
- [ ] Таблица статистики (Tab) показывает участников с верными командами и обновляет очки/смерти.
- [ ] Чат работает; системное сообщение игры отображается со своим текстом.
- [ ] Голосование можно начать, проголосовать и довести до результата.
- [ ] Звуки играют, для выстрелов ботов — пространственно.
- [ ] Убийство завершает раунд; очки и сообщение о победителе верны.
- [ ] Голосование за смену карты ротирует карту и пересобирает всё.
- [ ]
npm run check:contractзелёный, аdist/manifest.jsonполон: платформа пропускает пакет, который не может прочитать, и игра не появится в лобби.
Матрица пересборки
| Изменение | Нужные команды |
|---|---|
src/config/*, src/data/* (JS) | dev: ничего (HMR); prod: npm run build |
src/client/**, src/host/** (JS) | dev: ничего (HMR); prod: npm run build |
Карты (src/data/maps/*) | npm run build:assets && npm run build:manifest (платформа читает dist/maps/) |
| Звуки (исходные ресурсы) | npm run audio:process && npm run build:assets && npm run build:manifest |
Картинки (assets/img/*) | npm run build:assets && npm run build:manifest; в dev predev уже кладёт build/img/ |
| Rust-ядро | npm run core:build (+ перезагрузка; для prod ещё npm run build) |
roomForm / roomDefaults | npm run build:manifest |
| Что угодно перед первым запуском | полностью npm run core:build && npm run build |
Два правила за таблицей: платформа читает dist/manifest.json и dist/maps/ из собранного пакета, а npm run build никогда не пересобирает WASM-ядро.
Критерии готовности
- [ ]
npx eslint .чисто. - [ ]
npm testзелёный (оба проекта Vitest). - [ ]
npm run core:testзелёный, включая паритет движения. - [ ]
npm run core:build && npm run buildпроходит с чистогоdist/. - [ ]
npm run sim -- --game <package>завершается с кодом0на каждом написанном вами сценарии (см.debugging.md). - [ ] Ручной чек-лист выше пройден во вкладке standalone.
- [ ] Игра опубликована и подана (
submitting.md). - [ ] Каждый пункт
pitfalls.mdпроверен.