267 lines
18 KiB
Markdown
267 lines
18 KiB
Markdown
|
|
# 15 — Движок и редактор комнат
|
|||
|
|
|
|||
|
|
## Зачем это отдельно
|
|||
|
|
|
|||
|
|
До этой вехи проект был одним исполняемым файлом. Теперь их три, и граница
|
|||
|
|
между ними проведена **линковкой**, а не договорённостью:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
tile2d (STATIC) engine/ — не знает про правила боя и ни про одну игру по имени
|
|||
|
|
├── squad_proto.exe игра Descent: src/ + projects/descent/
|
|||
|
|
└── tile2d_editor.exe редактор комнат: editor/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`tile2d_editor` линкуется **только** с движком. Поэтому «редактор полез в правила
|
|||
|
|
игры» — это ошибка сборки, а не то, что надо заметить на ревью. Это единственная
|
|||
|
|
причина, по которой редактор сделан отдельным приложением, а не режимом игры:
|
|||
|
|
режим внутри игры видит `EnemyKind`, `Agent` и `g_tune` целиком, и удержать его
|
|||
|
|
от этого нечем.
|
|||
|
|
|
|||
|
|
### Что лежит в движке
|
|||
|
|
|
|||
|
|
Всё, что лежит в `engine/`. Раньше состав задавался списком в
|
|||
|
|
[CMakeLists.txt](../CMakeLists.txt), потому что каталоги `src/render` и
|
|||
|
|
`src/world` делили обе цели; теперь у движка свой каталог, и решение «это
|
|||
|
|
движок» принимается тем, куда положен файл ([18-engine.md](18-engine.md)).
|
|||
|
|
|
|||
|
|
| Файл | О чём |
|
|||
|
|
|---|---|
|
|||
|
|
| `engine/config.h` | размеры кадра, тайла и карты: то, что нельзя менять без пересборки |
|
|||
|
|
| `engine/framebuffer` | софтверный буфер RGBA8, световая рампа, блит на экран |
|
|||
|
|
| `engine/sfa_library` | чтение библиотеки SpriteForge |
|
|||
|
|
| `engine/projection` | ЕДИНСТВЕННОЕ место, где движок знает про изометрию |
|
|||
|
|
| `engine/view2d.h` | камера, список отрисовки, сортировка по глубине |
|
|||
|
|
| `engine/project` | манифест проекта: пути, вид отображения, кривая глубины |
|
|||
|
|
| `engine/tilemap` | карта, DDA-рейкаст, circle-vs-AABB |
|
|||
|
|
| `engine/pixel` | ромб, обводка силуэта, `ShadeColor` |
|
|||
|
|
| `engine/tileset` | пол и стены по темам отделки |
|
|||
|
|
| `engine/theme` | сколько тем и как они называются |
|
|||
|
|
| `engine/catalog` | каталог размещаемых сущностей |
|
|||
|
|
| `engine/room` | формат комнаты-шаблона и набор комнат |
|
|||
|
|
| `engine/level_gen` | сборка этажа из комнат по сиду |
|
|||
|
|
| `engine/progression` | кривая глубины: что означает этаж N ([16-descent.md](16-descent.md)) |
|
|||
|
|
| `engine/microfont` | растровый шрифт 3×5 для софтверного буфера |
|
|||
|
|
| `engine/text_parse.h` | разбор строчных текстовых форматов |
|
|||
|
|
|
|||
|
|
Чего в движке НЕТ и быть не может: путей к содержимому. Каталог комнат и
|
|||
|
|
каталог сущностей называет проект, поэтому один и тот же `tile2d_editor`
|
|||
|
|
открывает и `projects/descent`, и `projects/hollow`.
|
|||
|
|
|
|||
|
|
## Каталог сущностей — мост между движком и игрой
|
|||
|
|
|
|||
|
|
[projects/descent/catalog/entities.txt](../projects/descent/catalog/entities.txt):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
entity shambler class=monster depth=1 danger=1.0 color=116,52,52 sprite=shambler_walk name=SHAMBLER
|
|||
|
|
entity crate class=prop depth=1 color=104,76,46 sprite=prop_crate name=CRATE
|
|||
|
|
entity plate class=loot tier=3 depth=9 color=140,146,156 sprite=item_icon_plate name=PLATE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Редактор знает про сущность ровно это: строковый id, класс, цвет, радиус, имя
|
|||
|
|
ассета SpriteForge и на какой глубине она встречается. Что такое `SHAMBLER` и
|
|||
|
|
чем он бьёт — знание игры, и живёт оно в `sim/target.cpp`. Связывает одно с
|
|||
|
|
другим единственная таблица —
|
|||
|
|
[src/sim/spawn_catalog.cpp](../src/sim/spawn_catalog.cpp).
|
|||
|
|
|
|||
|
|
**Добавить тварь** = строка в каталоге + запись в `EnemyDefOf` + строка в
|
|||
|
|
таблице перевода. Формат комнат и редактор при этом не меняются вообще.
|
|||
|
|
|
|||
|
|
Формат `ключ=значение`, а не позиционный: свойства сущности прирастают (глубина,
|
|||
|
|
цена, тир), и каждое новое ломало бы все уже написанные строки. Смысл ключей —
|
|||
|
|
в [16-descent.md](16-descent.md).
|
|||
|
|
|
|||
|
|
Цвет и радиус в каталоге не для красоты: ими редактор рисует сущность, пока для
|
|||
|
|
неё не собран спрайт. Пустая палитра не должна мешать планировать уровень.
|
|||
|
|
|
|||
|
|
## Комната — это шаблон, а не кусок карты
|
|||
|
|
|
|||
|
|
[projects/descent/rooms/arena.room](../projects/descent/rooms/arena.room):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
name arena
|
|||
|
|
size 14 12
|
|||
|
|
weight 3 вес при выборе генератором: «как часто встречается»
|
|||
|
|
theme 0 набор тайлов отделки
|
|||
|
|
start 0 годится ли как стартовая комната отряда
|
|||
|
|
depth 1 0 с какого этажа и по какой (0 — до бесконечности)
|
|||
|
|
tiles
|
|||
|
|
####++####++## '.' пол '#' стена '+' дверь
|
|||
|
|
#............#
|
|||
|
|
...
|
|||
|
|
spawn 4 4 0.90 shambler:4 rusher:3 spitter:2
|
|||
|
|
spawn 6 1 0.40 crate:2 barrel:1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Ключевое в формате — **точка спавна не хранит, кто в ней стоит**. Она хранит
|
|||
|
|
шанс срабатывания и таблицу весов по id каталога. Одна и та же комната,
|
|||
|
|
поставленная дважды, населена по-разному; уровень из восьми комнат читается как
|
|||
|
|
уровень, а не как одна кишка, повторённая девять раз.
|
|||
|
|
|
|||
|
|
Ограничения формата и почему они такие:
|
|||
|
|
|
|||
|
|
- размер от 5×5 до 14×14 — генератор ставит комнаты в слоты по 16 тайлов, и
|
|||
|
|
комната размером в слот не оставила бы коридорам места;
|
|||
|
|
- двери только на рамке и не в углах — коридор тянется НАРУЖУ, а из угла
|
|||
|
|
непонятно, в какую сторону;
|
|||
|
|
- точка спавна только на полу; точка с пустой таблицей в файл не пишется.
|
|||
|
|
|
|||
|
|
Текстовый построчный формат выбран потому, что комнаты лежат в git и правка
|
|||
|
|
планировки обязана читаться в diff.
|
|||
|
|
|
|||
|
|
## Сборка уровня
|
|||
|
|
|
|||
|
|
[engine/level_gen.h](../engine/level_gen.h). Карта 48×48 делится на
|
|||
|
|
сетку 3×3 слотов по 16 тайлов.
|
|||
|
|
|
|||
|
|
1. в каждый слот выбирается комната по весу;
|
|||
|
|
2. если ни одной стартовой не выпало — одна ставится принудительно;
|
|||
|
|
3. вся карта заливается камнем, комнаты отпечатываются по центрам слотов;
|
|||
|
|
4. по слотам строится **остовное дерево** (обход в глубину со случайным
|
|||
|
|
порядком соседей) — оно гарантирует, что из стартовой достижима каждая;
|
|||
|
|
5. поверх дерева добавляются лишние связи с шансом 1/3: без них уровень —
|
|||
|
|
дерево-кишка, с шансом 1 — решётка без тупиков;
|
|||
|
|
6. связи режутся коридорами шириной 2 от двери к двери, тремя отрезками по
|
|||
|
|
осям (нет двери на нужной стороне — стена пробивается посередине);
|
|||
|
|
7. разыгрываются точки спавна;
|
|||
|
|
8. заливкой от точки старта считается достижимость, недостижимые точки
|
|||
|
|
снимаются и **попадают в отчёт**.
|
|||
|
|
|
|||
|
|
Шаг 8 — не перестраховка: он поймал замурованную внутреннюю камеру у `storage`
|
|||
|
|
в тот же день, когда её нарисовали. Тварь в отрезанном кармане не дойдёт до
|
|||
|
|
отряда, отряд не дойдёт до неё, а счётчик зачистки будет вечно показывать
|
|||
|
|
недобитого противника.
|
|||
|
|
|
|||
|
|
### Про детерминизм
|
|||
|
|
|
|||
|
|
RNG крутится **только здесь**, один раз, при сборке уровня. К моменту первого
|
|||
|
|
`Game::Step` всё уже разложено, и правило «в симуляции ни одного вызова RNG»
|
|||
|
|
([02-architecture.md](02-architecture.md)) не нарушено. Один и тот же набор
|
|||
|
|
комнат плюс один и тот же сид дают побайтово ту же карту и то же население.
|
|||
|
|
|
|||
|
|
Броски по каждой точке спавна делаются **всегда**, даже когда точка не
|
|||
|
|
сработает: иначе последовательность уводило бы вбок и сид перестал бы значить
|
|||
|
|
одно и то же.
|
|||
|
|
|
|||
|
|
`--accept` и `--headless` набор комнат не подключают никогда — критерии
|
|||
|
|
меряются на прежней захардкоженной карте, иначе цифры не с чем сравнивать.
|
|||
|
|
|
|||
|
|
## Темы отделки
|
|||
|
|
|
|||
|
|
Слой `Tilemap::theme[]` — отдельный от `tiles[]` намеренно: проходимость и
|
|||
|
|
внешний вид это два разных вопроса, и смешав их, мы получили бы отдельный тип
|
|||
|
|
тайла на каждое сочетание. Симуляция этот слой не читает вообще.
|
|||
|
|
|
|||
|
|
Тем четыре: `STEEL`, `RUST`, `FLESH`, `FROST`. Пока они получаются перекраской
|
|||
|
|
базового набора по каналам — это честная заглушка, а не временное решение: она
|
|||
|
|
даёт различимые материалы, не требуя ни одного собранного ассета. Когда для темы
|
|||
|
|
соберут свой тайлсет, поменяется `engine/tileset.cpp`, а формат комнат и уже
|
|||
|
|
нарисованные комнаты — нет.
|
|||
|
|
|
|||
|
|
## Редактор
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
build/bin/tile2d_editor.exe
|
|||
|
|
```
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
Раскладка кадра 480×270: верхняя панель — операции над комнатами, холст —
|
|||
|
|
изометрия комнаты **тем же кодом, которым рисует игра**, правая панель — кисти,
|
|||
|
|
свойства и палитра из каталога, нижняя полоса — таблица выбранной точки спавна.
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
| Действие | Мышь | Клавиши |
|
|||
|
|
|---|---|---|
|
|||
|
|
| рисовать / стирать | ЛКМ / ПКМ | — |
|
|||
|
|
| кисть | панель справа | `1`–`5` |
|
|||
|
|
| сущность палитры | вкладки `MOB`/`PROP`/`LOOT`, колесо | `Q` / `E` |
|
|||
|
|
| тема отделки | кнопка темы | `T` |
|
|||
|
|
| диапазон глубин комнаты | счётчики `D` | — |
|
|||
|
|
| соседняя комната набора | `<` `>` | `[` `]` |
|
|||
|
|
| новая комната | `NEW` | `N` |
|
|||
|
|
| сохранить / перечитать | `SAVE` / `RELOAD` | `Ctrl+S` / `Ctrl+R` |
|
|||
|
|
| сборка этажа / новый сид | `LEVEL` / `SEED` | `F5` / `F6` |
|
|||
|
|
| этаж предпросмотра | `D-` / `D+` | `PgUp` / `PgDn` |
|
|||
|
|
| проверить в бою | `PLAY` | — |
|
|||
|
|
| сдвиг холста | — | стрелки, `Home` |
|
|||
|
|
|
|||
|
|
Точка спавна ставится одним нажатием, а не мазком: кисть, тянущаяся по сетке,
|
|||
|
|
засеяла бы полкомнаты.
|
|||
|
|
|
|||
|
|
`PLAY` запускает `squad_proto --rooms --seed N` **отдельным процессом**. Не
|
|||
|
|
встроенным режимом: иначе редактору пришлось бы знать правила игры, и вся
|
|||
|
|
граница между целями сборки была бы декоративной.
|
|||
|
|
|
|||
|
|
### Предпросмотр сборки
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
`F5` показывает этаж целиком видом **сверху**: 48×48 тайлов в изометрии — это
|
|||
|
|
полторы тысячи пикселей по ширине, и на 480×270 они не помещаются даже близко.
|
|||
|
|
Предпросмотр отвечает на вопрос «какая вышла планировка», а не «как это
|
|||
|
|
выглядит», и для этого вид сверху честнее.
|
|||
|
|
|
|||
|
|
В строке над картой — сид, число комнат, коридоров и сущностей. Всё, из-за чего
|
|||
|
|
уровень собрался не так, как задумано (срезано потолком, недостижимо от старта),
|
|||
|
|
печатается отдельной строкой и только когда оно не ноль: постоянная строка
|
|||
|
|
«0 ошибок» перестаёт читаться через минуту работы.
|
|||
|
|
|
|||
|
|
### Самопроверка набора
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
tile2d_editor.exe --check
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Аналог `squad_proto --accept`, только для содержимого: набор комнат — такой же
|
|||
|
|
исходник, как код, и ломаться он обязан на проверке.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
ROOM SIZE DOORS SPAWNS THEME STATE
|
|||
|
|
arena 14x12 12 6 STEEL OK
|
|||
|
|
...
|
|||
|
|
[PASS] сид 42 комнат 9, коридоров 9, сущностей 31, достижимо 859, ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Проверяются: разбор каталога, разбор каждой комнаты, `RoomProblem` (двери, пол,
|
|||
|
|
точки), **круговой прогон через запись** и сборка уровня на пяти сидах.
|
|||
|
|
|
|||
|
|
Круговой прогон здесь не для полноты: записывающий путь — единственный, которым
|
|||
|
|
редактор может уничтожить работу. Читающий проверяется сам собой при каждом
|
|||
|
|
запуске, пишущий — только тут.
|
|||
|
|
|
|||
|
|
## Запуск игры на собранном уровне
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
squad_proto.exe --rooms [каталог] собрать уровень из комнат
|
|||
|
|
squad_proto.exe --seed N сид сборки; без него 1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Без `--rooms` игра работает ровно как раньше, на захардкоженной карте. Набор не
|
|||
|
|
нашёлся или не собрался — откат на неё же: прототип обязан запускаться на голом
|
|||
|
|
репозитории.
|
|||
|
|
|
|||
|
|
Собранный уровень населяет себя сам: захардкоженная расстановка мишеней и
|
|||
|
|
тварей относится к другой карте, и мешать их — верный способ получить тварь в
|
|||
|
|
стене на пустом месте.
|
|||
|
|
|
|||
|
|
Кто и когда меняет сид:
|
|||
|
|
|
|||
|
|
| Действие | Сид | Почему |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `NEW RAID` в меню | следующий | новая вылазка — новый уровень |
|
|||
|
|
| `TRY AGAIN` после поражения | тот же | это переигрывание, а не новый заход |
|
|||
|
|
| `R` в бою | тот же | на карте, которая меняется под руками, нечего отлаживать |
|
|||
|
|
|
|||
|
|
«Следующий» считается тем же линейным конгруэнтным шагом, что и кнопка `SEED`
|
|||
|
|
в редакторе: сид, подобранный в редакторе, обязан повторяться в игре.
|
|||
|
|
|
|||
|
|
## Что сюда ещё не входит
|
|||
|
|
|
|||
|
|
- **Предметы на полу** появились отдельной вехой и раскладываются генератором
|
|||
|
|
по тиру этажа, а не ставятся в комнате руками ([16-descent.md](16-descent.md)).
|
|||
|
|
- **Поворот комнат при сборке.** Комната ставится как нарисована. Повороты
|
|||
|
|
удвоили бы разнообразие, но автор комнаты перестал бы понимать, что увидит.
|
|||
|
|
- **Свои тайлсеты на тему.** Пока перекраска базового набора; место для сборки
|
|||
|
|
из SpriteForge уже есть — `TileSet::LoadFromForge`.
|