110 lines
7.9 KiB
Markdown
110 lines
7.9 KiB
Markdown
|
|
# 14. Твари: конвейер ассетов
|
|||
|
|
|
|||
|
|
Blender-first конвейер для пяти видов: `shambler`, `rusher`, `spitter`,
|
|||
|
|
`brute`, `lurker`. Отдельный от агентского и намеренно.
|
|||
|
|
|
|||
|
|
## Зачем отдельный конвейер
|
|||
|
|
|
|||
|
|
У агента слои бумажной куклы обязаны совпасть **пиксель в пиксель**: тело,
|
|||
|
|
броня и оружие рисуются на одном скелете, и любое расхождение видно сразу.
|
|||
|
|
Отсюда в `tools/agent_assets.py` калибровка, растровый `ingest` и проверка
|
|||
|
|
«экипировка не вылезла за силуэт тела».
|
|||
|
|
|
|||
|
|
У твари слоёв нет вовсе, склеивать нечего. Зато пять видов обязаны
|
|||
|
|
**различаться силуэтом** с одного взгляда: игрок принимает решение по пятну,
|
|||
|
|
а не по деталям. Поэтому здесь пять независимых скелетов, никакой общей
|
|||
|
|
калибровки — и проверка ровно обратного смысла: `validate` ругается, если два
|
|||
|
|
вида дали похожее пятно.
|
|||
|
|
|
|||
|
|
Общее у двух конвейеров ровно одно и намеренно: **неподвижная
|
|||
|
|
ортографическая изометрическая камера** (`elevation_deg = atan(0.5)`, тот же
|
|||
|
|
угол, под которым в игре лежит пол) и вырезка кадра постоянным
|
|||
|
|
прямоугольником. Начало координат рига проецируется в центр кадра, поэтому
|
|||
|
|
упаковщик режет все кадры одинаково — тварь не «дышит» при повороте.
|
|||
|
|
|
|||
|
|
## Файлы
|
|||
|
|
|
|||
|
|
| Файл | Что делает |
|
|||
|
|
|---|---|
|
|||
|
|
| `assets/monsters/catalog.yaml` | геометрия ячейки, камера, список видов и клипов — единственный источник чисел |
|
|||
|
|
| `tools/blender/monster_rig.py` | анатомия и анимации; запускается ВНУТРИ Blender |
|
|||
|
|
| `tools/monster_assets.py` | CLI: рендер, упаковка листов, манифест, проверки |
|
|||
|
|
| `tools/build_monster_assets.ps1` | полный прогон: init → render-all → validate → sf validate |
|
|||
|
|
| `assets/sources/monsters/*.png` | собранные листы (вход SpriteForge) |
|
|||
|
|
| `assets/manifests/monsters.generated.yaml` | манифест, пишется `sync` |
|
|||
|
|
| `generated/monster_sprites.h` | константы `MONSTER_ASSET_*` и `MonsterAsset(EnemyKind, AnimKind)` |
|
|||
|
|
|
|||
|
|
## Геометрия
|
|||
|
|
|
|||
|
|
Ячейка `48x56`, pivot `(24,47)`, восемь направлений в том же порядке, что у
|
|||
|
|
агентов: юг, юго-запад, запад, ... Лист **direction-major**: сначала все кадры
|
|||
|
|
юга, затем юго-запада.
|
|||
|
|
|
|||
|
|
`pivot_y = 47`, а не `55`: изометрический пол уходит от точки опоры **вниз** по
|
|||
|
|
экрану, и всё, что у твари позади (хвост бегуна, задние лапы затаившегося),
|
|||
|
|
проецируется ниже точки опоры. Девять строк под pivot — запас на глубину
|
|||
|
|
подставки, а не воздух.
|
|||
|
|
|
|||
|
|
Клипы: `walk` 8, `attack` 6, `hit` 4, `death` 8 кадров на направление.
|
|||
|
|
`fps_scale` в каталоге задаёт повадку: бегун частит, громила волочится при
|
|||
|
|
одинаковом числе кадров.
|
|||
|
|
|
|||
|
|
## Морфологии
|
|||
|
|
|
|||
|
|
Не капсулы. Каждый вид — своя анатомия, а не перекрашенный гуманоид:
|
|||
|
|
|
|||
|
|
* **shambler** — вскрытая наружу грудная клетка, рёбра веером, правая рука
|
|||
|
|
доросла до колена, в череп вбита пластина; хромает;
|
|||
|
|
* **rusher** — четвероногий, горизонтальный хребет, пальцеходящие задние ноги,
|
|||
|
|
косы вместо предплечий, вместо головы раскрытый цветок челюстей;
|
|||
|
|
* **spitter** — волочащийся железистый мешок больше грудной клетки, вживлённый
|
|||
|
|
в горло клапан, сопло вместо лица, атрофированные руки;
|
|||
|
|
* **brute** — к плечу приварено второе, наполовину поглощённое тело; грудь
|
|||
|
|
забрана болтами; правая рука — молот из сросшейся кости;
|
|||
|
|
* **lurker** — плоский зашитый скобами панцирь подвешен под четырьмя длинными
|
|||
|
|
выгнутыми лапами, безглазый клин с крюками.
|
|||
|
|
|
|||
|
|
Анимации описаны в **нормированном** времени (список опорных значений на 0..1)
|
|||
|
|
и сэмплируются под число кадров из каталога: `frames` в каталоге можно менять,
|
|||
|
|
не трогая `monster_rig.py`.
|
|||
|
|
|
|||
|
|
`death` — это **осадка в кучу**, а не падение во весь рост. Лежащая во весь
|
|||
|
|
рост туша не помещается в клетку `48x56` при повороте «от камеры», и в игре это
|
|||
|
|
выглядело бы как обрезанный труп.
|
|||
|
|
|
|||
|
|
## Порядок работы
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
python tools/monster_assets.py monster-init # собрать риг .blend
|
|||
|
|
python tools/monster_assets.py monster-render shambler walk # один вид, один клип
|
|||
|
|
python tools/monster_assets.py monster-render-all # всё
|
|||
|
|
python tools/monster_assets.py validate --strict
|
|||
|
|
python tools/monster_assets.py sync
|
|||
|
|
python -m unittest discover -s tools/tests
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`monster-render` и `monster-render-all` сами вызывают `sync`.
|
|||
|
|
|
|||
|
|
## Что проверяет validate
|
|||
|
|
|
|||
|
|
* холст листа равен `48 x (8 направлений x кадры)`, пустых ячеек нет;
|
|||
|
|
* силуэт **не упирается в кромку** клетки — иначе в игре это отрубленная лапа;
|
|||
|
|
* тварь стоит на общей линии пола (`death` освобождён: труп имеет право осесть);
|
|||
|
|
* «роза» из восьми направлений центрирована на `pivot_x`;
|
|||
|
|
* зеркальные пары (ЮЗ,ЮВ), (З,В), (СЗ,СВ) совпадают при отражении — ловит
|
|||
|
|
перестановку направлений;
|
|||
|
|
* **пять видов различаются пятном** (IoU силуэтов шага ниже 0.90);
|
|||
|
|
* манифест не разошёлся с каталогом по pivot и размеру кадра, и ни один ID не
|
|||
|
|
объявлен вторым манифестом.
|
|||
|
|
|
|||
|
|
## Запреты
|
|||
|
|
|
|||
|
|
* ID ассета — строго `<kind>_<animation>`: ровно это требует
|
|||
|
|
`tools/asset_coverage.py` от группы `enemy`. Переименуешь — упадёт покрытие.
|
|||
|
|
* Строковых ID в C++ нет: только `MONSTER_ASSET_*` из `generated/monster_sprites.h`.
|
|||
|
|
* PNG и contact sheet не открывать: проверка только текстом — `validate`,
|
|||
|
|
`sf list`, `sf describe`, `sf ascii`.
|
|||
|
|
* `assets/manifests/enemies.yaml` — старые односторонние листы-заглушки. Пока
|
|||
|
|
живы оба файла, ID пересекаются, и `validate` про это предупреждает: снимать
|
|||
|
|
предупреждение — это отдельное решение на стороне рантайма, не конвейера.
|