227 lines
16 KiB
Markdown
227 lines
16 KiB
Markdown
|
|
# 18 — Tile2D: движок и проекты
|
|||
|
|
|
|||
|
|
## Что изменилось
|
|||
|
|
|
|||
|
|
До этой вехи «движком» называлась библиотека `squad_engine`, которая лежала
|
|||
|
|
внутри `src/`, включала `src/tuning.h` и сама искала `assets/rooms`. То есть она
|
|||
|
|
не собиралась без игры и знала имя ровно одной игры. Такой движок работает с
|
|||
|
|
одним проектом и притворяется общим.
|
|||
|
|
|
|||
|
|
Теперь их трое, и границы проведены не договорённостью, а сборкой:
|
|||
|
|
|
|||
|
|
| Что | Где | Что знает |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **Tile2D** | `engine/` | окно и цикл, кадр, проекции 2D, тайлы, комнаты, генератор этажей, формат проекта |
|
|||
|
|
| **редактор** | `editor/` | Tile2D и **любой** проект |
|
|||
|
|
| **Descent** | `src/` + `projects/descent/` | правила боя, отряд, снаряжение — то есть игра |
|
|||
|
|
| **пример** | `samples/walk/` | ничего: он и есть проверка, что движком можно пользоваться со стороны |
|
|||
|
|
|
|||
|
|
Игра стала **проектом внутри движка**. Проверяется это не обещанием, а вторым
|
|||
|
|
проектом: [projects/hollow](../projects/hollow) собирается тем же движком, тем
|
|||
|
|
же редактором и тем же исполняемым файлом, а отличается видом отображения,
|
|||
|
|
кривой сложности, каталогом и комнатами.
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
squad_proto.exe --rooms # DESCENT, изометрия
|
|||
|
|
squad_proto.exe --project hollow # HOLLOW, вид сверху
|
|||
|
|
tile2d_editor.exe --check --project hollow # проверить чужой набор ЕГО кривой
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Что именно вынуто из игры
|
|||
|
|
|
|||
|
|
Три зависимости, которые делали движок несамостоятельным. Каждая нашлась не
|
|||
|
|
рассуждением, а компилятором — после переезда в свой каталог движок просто
|
|||
|
|
перестал собираться:
|
|||
|
|
|
|||
|
|
- **`src/tuning.h`.** Размеры кадра, тайла и карты уехали в
|
|||
|
|
[engine/config.h](../engine/config.h); `tuning.h` теперь его включает и
|
|||
|
|
оставляет прежние имена, поэтому код игры не изменился ни строкой.
|
|||
|
|
- **`MAX_TARGETS`.** Генератор уровней кэпил население игровой константой —
|
|||
|
|
шириной маски удара клинком. У движка теперь свой потолок
|
|||
|
|
`MAX_LEVEL_SPAWNS`, а `src/game.h` проверяет `static_assert`-ом, что одно
|
|||
|
|
влезает в другое. Раньше рассогласование дало бы молча обкусанный этаж.
|
|||
|
|
- **`SQUAD_START_X/Y`.** Запасная точка старта этажа была позицией отряда из
|
|||
|
|
игры. Стала центром карты: движок не знает, кто к нему приедет.
|
|||
|
|
|
|||
|
|
Ещё две — `FindRoomsDir()` и `FindCatalogPath()` — просто удалены. Путь к
|
|||
|
|
содержимому называет проект.
|
|||
|
|
|
|||
|
|
## Проекция: шов под виды 2D
|
|||
|
|
|
|||
|
|
[engine/projection.h](../engine/projection.h) — единственное место, где движок
|
|||
|
|
знает про изометрию. Проекция отвечает на четыре вопроса:
|
|||
|
|
|
|||
|
|
1. куда мировая точка попадает на экран и обратно;
|
|||
|
|
2. чем сортируется список отрисовки;
|
|||
|
|
3. какой формы плита пола;
|
|||
|
|
4. насколько крыша стены поднята над своей плитой.
|
|||
|
|
|
|||
|
|
Всё остальное — тайлсет, свет, кровь, эффекты, курсор редактора — считается
|
|||
|
|
**через** эти четыре ответа и потому переживает смену вида.
|
|||
|
|
|
|||
|
|
Режима сразу два, и это не запас на будущее: абстракция с одной реализацией
|
|||
|
|
ничем не проверена и обычно оказывается неправильной. Вид сверху нашёл ровно
|
|||
|
|
такие места — кольцо эвакуации рисовало свой ромб мимо проекции и висело поверх
|
|||
|
|
квадратной плиты в другом ракурсе.
|
|||
|
|
|
|||
|
|
| | Изометрия | Вид сверху |
|
|||
|
|
|---|---|---|
|
|||
|
|
| плита | ромб 32×16, наклон 2:1 | квадрат 32×32 |
|
|||
|
|
| стена | крыша + две вертикальные грани | только крыша |
|
|||
|
|
| сортировка | `x + y` | `y` |
|
|||
|
|
| свет на плите | точная билинейка по углам ромба | без косого члена (см. ниже) |
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
Свет вплавляется в блит спрайта патчем `a + b·px + c·py + e·px² + f·py²`
|
|||
|
|
(`ShadeQuad`). В изометрии билинейка по четырём углам ромба раскладывается по
|
|||
|
|
этим членам **точно**, поэтому на общем ребре соседних тайлов значений не
|
|||
|
|
расходится и швов нет. У квадрата честным слагаемым был бы `u·v`, которого в
|
|||
|
|
этой форме нет, — он отбрасывается. На тайле в 32 пикселя это доли ступени
|
|||
|
|
рампы, и лишний член в горячем блите того не стоит.
|
|||
|
|
|
|||
|
|
Вид выбирается **один раз** при старте: тайлсет рисуется под форму плиты, и
|
|||
|
|
смена вида на ходу означала бы перерисовку всех спрайтов посреди кадра.
|
|||
|
|
|
|||
|
|
Чего вид сверху пока не умеет: тайлы из SpriteForge. Библиотека нарисована в
|
|||
|
|
изометрии, и положить её в другой ракурс нельзя — это не «неточно», это другой
|
|||
|
|
ракурс. `LoadFromForge` в этом режиме честно отказывается, и набор остаётся
|
|||
|
|
процедурным целиком.
|
|||
|
|
|
|||
|
|
## Проект
|
|||
|
|
|
|||
|
|
[engine/project.h](../engine/project.h). Построчный `ключ значение`, как у
|
|||
|
|
комнат и каталога: файл лежит в git, правится руками и обязан читаться в diff.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
name DESCENT
|
|||
|
|
view iso # iso | topdown
|
|||
|
|
rooms rooms
|
|||
|
|
catalog catalog/entities.txt
|
|||
|
|
startdepth 1
|
|||
|
|
danger 3.0 2.2 0.10 # база, линейный рост, квадратичный хвост
|
|||
|
|
density 0.05 1.5 # прирост за этаж, упор
|
|||
|
|
loot 2 4 3 # база, этажей на +1 предмет, этажей на тир
|
|||
|
|
sight 0.035 0.60 # сжатие за этаж, нижний упор
|
|||
|
|
xp 0.15
|
|||
|
|
themes 1 4 8 13 # с какого этажа начинается каждая тема
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Неизвестный ключ **валит загрузку**, а не пропускается: опечатка в текстовом
|
|||
|
|
формате — самая дорогая ошибка, строка просто не действует, и это выглядит как
|
|||
|
|
«движок не слушается».
|
|||
|
|
|
|||
|
|
Кривая глубины ([16-descent.md](16-descent.md)) переехала из кода в манифест:
|
|||
|
|
её правят на ощупь и часто, и «сделать десятый этаж чуть злее» не должно
|
|||
|
|
означать пересборку движка. **Форма** кривой при этом осталась в коде —
|
|||
|
|
`FloorCurve` гнёт её, но не может подменить линейный рост экспонентой. Проект,
|
|||
|
|
которому нужна другая форма, — это повод завести ещё одно поле, а не язык
|
|||
|
|
выражений в текстовом файле.
|
|||
|
|
|
|||
|
|
### Как проект находится
|
|||
|
|
|
|||
|
|
`--project` принимает **имя** (каталог в `projects/`) или путь. Без ключа берётся
|
|||
|
|
единственный найденный проект. Поиск идёт от текущего каталога вверх — запуск из
|
|||
|
|
корня репозитория и из `build/bin` одинаково рабочие.
|
|||
|
|
|
|||
|
|
Несколько проектов и ни одного названного — отказ со списком доступных. Молча
|
|||
|
|
взятый «первый попавшийся» читался бы как «движок открыл не то».
|
|||
|
|
|
|||
|
|
## Второй проект
|
|||
|
|
|
|||
|
|
[projects/hollow](../projects/hollow) — вид сверху, четыре комнаты, свой
|
|||
|
|
каталог, короткая и злая кривая: `danger 5.0 3.4 0.25` против `3.0 2.2 0.10`,
|
|||
|
|
плоть уже на четвёртом этаже вместо восьмого.
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
Редактор показывает палитру HOLLOW (без мишени приёмки, которой в этом наборе
|
|||
|
|
нет) и считает предпросмотр **его** кривой: `danger 4.3 / 5.0` на первом этаже,
|
|||
|
|
где у DESCENT было бы `3.0`.
|
|||
|
|
|
|||
|
|
Общее у двух проектов ровно одно — **id сущностей**. Это граница ИГРЫ, а не
|
|||
|
|
движка: `squad_proto` умеет превращать в тварей только те id, что перечислены в
|
|||
|
|
[src/sim/spawn_catalog.cpp](../src/sim/spawn_catalog.cpp). Движку всё равно, как
|
|||
|
|
они называются; проект с чужими id соберётся, но эта игра его не населит.
|
|||
|
|
|
|||
|
|
## Что осталось компилтаймовым
|
|||
|
|
|
|||
|
|
В [engine/config.h](../engine/config.h), и это честно записано там же:
|
|||
|
|
|
|||
|
|
- **размер кадра** 480×270 — по нему считаются раскладки HUD, меню и оверлея;
|
|||
|
|
- **размер карты** 48×48 — тайлмап плоский массив;
|
|||
|
|
- **размер тайла** — проекция берёт свой, но `TILE_MAX_*` задаёт верхнюю оценку
|
|||
|
|
для отсечения по краю кадра.
|
|||
|
|
|
|||
|
|
Проект пока не может поменять ни одно из трёх. Сделать их данными — отдельная
|
|||
|
|
веха: это означает динамические буферы там, где сейчас массивы, и трогает
|
|||
|
|
раскладку всего интерфейса.
|
|||
|
|
|
|||
|
|
## Приложение: окно, кадр, ход времени
|
|||
|
|
|
|||
|
|
[engine/app.h](../engine/app.h). Раньше окно, безрамочный режим, фреймбуфер,
|
|||
|
|
блиттер и fixed timestep жили двумя почти одинаковыми копиями — в игре и в
|
|||
|
|
редакторе. Третьему приложению пришлось бы написать третью, и она бы уже
|
|||
|
|
отличалась: где-то забытый `SetExitKey`, где-то другой потолок шагов.
|
|||
|
|
|
|||
|
|
Инверсии управления здесь нет: цикл остаётся у приложения, а `App` отвечает на
|
|||
|
|
три вопроса — пора ли закрываться, сколько шагов симуляции прошло, куда лёг кадр
|
|||
|
|
в окне. Колбэки спрятали бы порядок «шаг — рендер — интерфейс», а он у каждого
|
|||
|
|
приложения свой, и в нём вся суть.
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
while (app.NextFrame())
|
|||
|
|
{
|
|||
|
|
for (int i = 0, n = app.Advance(halted); i < n; ++i) game.Step(FIXED_DT);
|
|||
|
|
RenderGame(game, view, app.fb, app.Alpha(), app.frameDt);
|
|||
|
|
app.Present(); // кадр на экран
|
|||
|
|
ui.Draw(game, app.fit); // поверх кадра
|
|||
|
|
app.Finish();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Третье приложение
|
|||
|
|
|
|||
|
|
[samples/walk](../samples/walk/main.cpp) — открыть проект, собрать этаж,
|
|||
|
|
походить по нему камерой. Полторы сотни строк вместе с разбором аргументов.
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
Он существует не для демонстрации. Игра и редактор проверить самостоятельность
|
|||
|
|
движка не могут: редактор мира не рисует вовсе, а игра — это ровно то место,
|
|||
|
|
куда общий код и уезжает незаметно. Пример же ломается сразу, как только для
|
|||
|
|
показа этажа понадобится что-нибудь из `src/`.
|
|||
|
|
|
|||
|
|
Первое, что он потребовал, — `DrawTilemap` ([engine/tilemap_draw.h](../engine/tilemap_draw.h)):
|
|||
|
|
до него движок не умел нарисовать собственную карту. Игра свой проход по полу
|
|||
|
|
оставила себе, потому что вплавляет в каждый блит свет; две реализации здесь не
|
|||
|
|
дублирование, а разные задачи — «показать карту» и «нарисовать сцену со светом».
|
|||
|
|
Общий у них порядок сортировки, и он один на всех, потому что живёт в проекции.
|
|||
|
|
|
|||
|
|
## Проверки
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
squad_proto.exe --accept приёмка игры (спек 11)
|
|||
|
|
tile2d_editor.exe --check --project descent DESCENT: набор и покрытие глубин
|
|||
|
|
tile2d_editor.exe --check --project hollow HOLLOW: то же, его кривой
|
|||
|
|
squad_proto.exe --descend 6 забег DESCENT
|
|||
|
|
squad_proto.exe --project hollow --descend 4 забег HOLLOW
|
|||
|
|
tile2d_walk.exe --project hollow --depth 3 пример: движок без игры
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`--check` на втором проекте сразу нашёл в нём то же, что когда-то в `storage`:
|
|||
|
|
глухую камеру посередине комнаты `maw`. На сетке она выглядит нормально, но
|
|||
|
|
точка спавна внутри недостижима — тварь не дойдёт до отряда и не будет убита, и
|
|||
|
|
этаж тихо станет легче.
|
|||
|
|
|
|||
|
|
## Чего ещё нет
|
|||
|
|
|
|||
|
|
- **Пространства имён.** Типы движка (`Sprite`, `Tilemap`, `Room`) лежат в
|
|||
|
|
глобальном пространстве. Пока потребитель один репозиторий — терпимо; как
|
|||
|
|
библиотека для чужого кода — нет.
|
|||
|
|
- **Своей палитры видов.** Вид сверху есть, гексов и косой проекции нет.
|
|||
|
|
Добавлять их теперь дёшево: это одна запись в `MODES` и две ветки в
|
|||
|
|
`Projection`.
|
|||
|
|
- **Генерации спрайтов внутри движка.** Она снаружи, в SpriteForge
|
|||
|
|
([12-assets.md](12-assets.md)), и с видом сверху пока не дружит.
|