212 lines
12 KiB
Markdown
212 lines
12 KiB
Markdown
|
|
# 12. Ассеты: конвейер SpriteForge
|
|||
|
|
|
|||
|
|
## Что где лежит
|
|||
|
|
|
|||
|
|
SpriteForge — внешний инструмент, отдельный репозиторий:
|
|||
|
|
`C:\Users\uuu\Documents\spriteforge`. Игра его исходники не трогает и ничего
|
|||
|
|
под конкретную игровую сущность в нём не правит.
|
|||
|
|
|
|||
|
|
Игра хранит только:
|
|||
|
|
|
|||
|
|
| Путь | Что это | В git |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `assets/manifests/*.yaml` | описания ассетов — единственный источник истины | да |
|
|||
|
|
| `assets/sources/` | исходные PNG и риги для бэкендов `import` / `blender` | да |
|
|||
|
|
| `generated/spriteforge_assets.h` | числовые ID ассетов, вывод `sf codegen` | да |
|
|||
|
|
| `build/sprites/` | собранные `.sfa`, `palettes.sfp`, `index.json` | нет |
|
|||
|
|
| `.sfcache/` | контентный кеш сборки, одноразовый | нет |
|
|||
|
|
|
|||
|
|
Собранная библиотека живёт внутри `build/`, поэтому полная очистка каталога
|
|||
|
|
сборки сносит и её. Восстановление — одна команда `sf build`; кеш `.sfcache/`
|
|||
|
|
лежит отдельно и переживает такую чистку, так что пересборка почти мгновенная.
|
|||
|
|
|
|||
|
|
## Установка
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
cd C:\Users\uuu\Documents\spriteforge
|
|||
|
|
python -m pip install -e .
|
|||
|
|
sf version
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Установка editable: правки в SpriteForge видны игре без переустановки.
|
|||
|
|
Если `sf` не находится, каталог скриптов Python не в PATH — либо добавить
|
|||
|
|
`%APPDATA%\Python\Python313\Scripts`, либо звать `python -m spriteforge.cli`.
|
|||
|
|
|
|||
|
|
## Добавить ассет
|
|||
|
|
|
|||
|
|
Порядок ровно такой, шаги не пропускать:
|
|||
|
|
|
|||
|
|
1. Поправить манифест в `assets/manifests/`.
|
|||
|
|
2. План сборки, без записи файлов:
|
|||
|
|
```powershell
|
|||
|
|
sf build assets\manifests\environment.yaml --asset stone_altar --dry-run
|
|||
|
|
```
|
|||
|
|
3. Собрать только этот ассет:
|
|||
|
|
```powershell
|
|||
|
|
sf build assets\manifests\environment.yaml --asset stone_altar `
|
|||
|
|
--output build\sprites --cache-dir .sfcache
|
|||
|
|
```
|
|||
|
|
4. Проверить текстом:
|
|||
|
|
```powershell
|
|||
|
|
sf validate assets\manifests\environment.yaml --build-dir build\sprites
|
|||
|
|
sf ascii stone_altar --build-dir build\sprites
|
|||
|
|
```
|
|||
|
|
5. Обновить числовые ID:
|
|||
|
|
```powershell
|
|||
|
|
sf codegen --build-dir build\sprites --output generated\spriteforge_assets.h
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Сборка всей библиотеки:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
sf build assets\manifests\environment.yaml --output build\sprites `
|
|||
|
|
--cache-dir .sfcache --jobs 8
|
|||
|
|
sf validate assets\manifests\environment.yaml --build-dir build\sprites
|
|||
|
|
sf codegen --build-dir build\sprites --output generated\spriteforge_assets.h
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`--rebuild-palette` пересчитывает общую палитру библиотеки и допустим только
|
|||
|
|
при полной сборке без фильтров: частичная пересборка палитры обесценила бы
|
|||
|
|
индексы в незатронутых ассетах.
|
|||
|
|
|
|||
|
|
## Контроль результата
|
|||
|
|
|
|||
|
|
Агент проверяет сборку **только текстом**: `sf list`, `sf describe`, `sf ascii`,
|
|||
|
|
`sf validate`, `sf stats`. `sf contact-sheet` делает PNG для человека — агент
|
|||
|
|
его не открывает.
|
|||
|
|
|
|||
|
|
Со стороны C++ есть своя проверка: `squad_proto.exe --sprites` печатает, что
|
|||
|
|
загрузчик реально видит в библиотеке.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
build/sprites: 3 ассет(ов)
|
|||
|
|
ID HASH FRAMES SIZE PIVOT
|
|||
|
|
floor_slab 78D3F27E 1 30x16 15,8
|
|||
|
|
floor_slab_dim 30CD3FBB 1 30x16 15,8
|
|||
|
|
wall_block 892CB17B 1 30x32 15,31
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Загрузчик
|
|||
|
|
|
|||
|
|
`engine/sfa_library.h` — единственное место игры, знающее про формат `.sfa`.
|
|||
|
|
Заголовок загрузчика `sfa.h` берётся из репозитория SpriteForge, копии в игре
|
|||
|
|
нет; путь задаётся кешируемой переменной CMake:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
cmake -S . -B build -DSPRITEFORGE_DIR=<путь к репозиторию spriteforge>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
По умолчанию `SPRITEFORGE_DIR` = `../spriteforge` рядом с игрой.
|
|||
|
|
|
|||
|
|
Слой ровно один: кадр `.sfa` (индексы палитры + RLE по строкам) превращается в
|
|||
|
|
обычный игровой `Sprite` с альфа-тестом. Дальше по коду разницы между
|
|||
|
|
процедурным спрайтом и собранным нет: тот же `Blit`, тот же свет, та же
|
|||
|
|
сортировка.
|
|||
|
|
|
|||
|
|
**Строковых ID ассетов в коде игры нет.** Обращение идёт по константам
|
|||
|
|
`SF_ASSET_*` из `generated/spriteforge_assets.h`. Строка живёт ровно в двух
|
|||
|
|
местах: в манифесте и в имени файла `<asset_id>.sfa`, по которому загрузчик и
|
|||
|
|
собирает свой индекс.
|
|||
|
|
|
|||
|
|
### Холст и точка привязки
|
|||
|
|
|
|||
|
|
SpriteForge обрезает кадр по силуэту, поэтому размер кадра плывёт вместе с
|
|||
|
|
содержимым: ромб 32x16 приходит как 30x16 с pivot (15,8). Для декора это
|
|||
|
|
безразлично, для тайлов — нет: `ShadeQuad` из `Lighting::QuadFor` считается в
|
|||
|
|
координатах спрайта и ждёт ромб на своём месте (docs/10-atmosphere.md).
|
|||
|
|
Съехавший на пиксель холст увёл бы свет пола относительно света стен.
|
|||
|
|
|
|||
|
|
Поэтому `SpriteLibrary::Read` принимает `FramePlacement`: холст нужного размера
|
|||
|
|
и точка, в которую на нём ложится pivot кадра. Тайлы просят канонический
|
|||
|
|
`TILE_W x TILE_H`, остальное берётся в натуральную величину.
|
|||
|
|
|
|||
|
|
## Тайлы из библиотеки и hot reload
|
|||
|
|
|
|||
|
|
По умолчанию весь набор спрайтов процедурный и ни от одного файла не зависит —
|
|||
|
|
`--accept` и `--headless` считают то же, что считали раньше. Пол и стены можно
|
|||
|
|
подменить собранными:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
build\bin\squad_proto.exe --sf-tiles
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Осечка на любом шаге (нет библиотеки, нет ассета, битый файл) оставляет
|
|||
|
|
процедурный набор целиком: полусобранный хуже прежнего.
|
|||
|
|
|
|||
|
|
Бойцы и клинки остаются процедурными сознательно: там цвет занят номером агента
|
|||
|
|
и раздаётся из кода, а не из манифеста.
|
|||
|
|
|
|||
|
|
## Анимированные твари
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
build\bin\squad_proto.exe --sf-enemies # только твари
|
|||
|
|
build\bin\squad_proto.exe --sf # тайлы и твари разом
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Манифест — `assets/manifests/enemies.yaml`, по одному ассету `<kind>_walk` на
|
|||
|
|
вид. **Один ассет = один клип.** Бэкенд `import` имён клипов не читает и всегда
|
|||
|
|
кладёт единственную анимацию `default`, поэтому `idle` и `attack`, когда до них
|
|||
|
|
дойдёт дело, станут отдельными ассетами `<kind>_idle`, а не второй анимацией
|
|||
|
|
внутри существующего. Правку SpriteForge ради этого заводить не нужно.
|
|||
|
|
|
|||
|
|
### Откуда берутся листы
|
|||
|
|
|
|||
|
|
`assets/sources/make_enemy_sheets.py` — детерминированный генератор:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
python assets\sources\make_enemy_sheets.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Он рисует **ту же геометрию**, что процедурный запасной вариант в
|
|||
|
|
`src/render/sprites.cpp` (объединение эллипсов, свет по верхне-левой кромке,
|
|||
|
|
обводка по силуэту), и лишь сдвигает конечности по фазе шага. Это не удобство,
|
|||
|
|
а требование: разъедься силуэты — и `--sf-enemies` менял бы не плавность
|
|||
|
|
движения, а саму тварь, которую игрок узнаёт по пятну. Цвета в скрипте — копия
|
|||
|
|
`EnemyDef` из `src/sim/target.cpp`, и разойтись им нельзя по той же причине.
|
|||
|
|
|
|||
|
|
Ни одного случайного числа: перегенерация даёт побитово тот же PNG.
|
|||
|
|
|
|||
|
|
### Кто и что хранит
|
|||
|
|
|
|||
|
|
Кадры лежат в `SpriteLibrary` в одном экземпляре на ассет. `SpriteSet` держит
|
|||
|
|
только ID (`enemyWalk[kind]`, 0 — «рисуй процедурный»), а фазу клипа — `View`:
|
|||
|
|
|
|||
|
|
- `View::enemyAnim[]` — `Animator` на каждую цель;
|
|||
|
|
- `View::enemyPrevPos[]` — позиция прошлого кадра.
|
|||
|
|
|
|||
|
|
Фаза шага живёт в `View`, а не в `Target`, ровно по правилу
|
|||
|
|
[02-architecture.md](02-architecture.md): попав в симуляцию, она поехала бы в
|
|||
|
|
цифрах приёмки. Шаг проигрывается **только когда тварь реально идёт** —
|
|||
|
|
скорости у `Target` нет (`vel` — это отдача от удара), поэтому движение
|
|||
|
|
определяется сравнением позиций. Иначе цикл крутился бы у стоящего плевка,
|
|||
|
|
который по замыслу держит дистанцию, и читался бы как ошибка.
|
|||
|
|
|
|||
|
|
`LoadEnemies` — всё или ничего: одна анимированная тварь среди четырёх
|
|||
|
|
статичных выглядит хуже пяти статичных.
|
|||
|
|
|
|||
|
|
Watch-режим в отдельном терминале:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
sf watch assets\manifests\environment.yaml --output build\sprites `
|
|||
|
|
--cache-dir .sfcache --jobs 8
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
После каждой успешной пересборки SpriteForge атомарно переписывает
|
|||
|
|
`build/sprites/reload.json`. Игра опрашивает его раз в кадр и перечитывает
|
|||
|
|
библиотеку целиком. Новая библиотека собирается рядом и подменяет старую только
|
|||
|
|
если собралась целиком — недописанная пересборка не гасит картинку.
|
|||
|
|
|
|||
|
|
Кровь при перезагрузке сбрасывается по той же причине, по которой она не
|
|||
|
|
переживает смену уровня: она запечена в спрайты пола, а пол уже другой
|
|||
|
|
(docs/10-atmosphere.md).
|
|||
|
|
|
|||
|
|
## Чего не делать
|
|||
|
|
|
|||
|
|
- не открывать `build/sprites/*.sfa`, `.sfcache/`, сгенерированные PNG и
|
|||
|
|
contact sheet — для проверки есть текстовые команды;
|
|||
|
|
- не редактировать `generated/spriteforge_assets.h` руками: это вывод
|
|||
|
|
`sf codegen`;
|
|||
|
|
- не заводить строковые ID ассетов в C++;
|
|||
|
|
- не править репозиторий SpriteForge ради одной игровой сущности: если не
|
|||
|
|
хватает генератора — это отдельная задача в его репозитории.
|