# 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` | | свет на плите | точная билинейка по углам ромба | без косого члена (см. ниже) | ![вид сверху](view-topdown-hollow.png) Свет вплавляется в блит спрайта патчем `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`, плоть уже на четвёртом этаже вместо восьмого. ![редактор с чужим проектом](editor-hollow-topdown.png) Редактор показывает палитру 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) — открыть проект, собрать этаж, походить по нему камерой. Полторы сотни строк вместе с разбором аргументов. ![пример на движке](sample-walk.png) Он существует не для демонстрации. Игра и редактор проверить самостоятельность движка не могут: редактор мира не рисует вовсе, а игра — это ровно то место, куда общий код и уезжает незаметно. Пример же ломается сразу, как только для показа этажа понадобится что-нибудь из `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)), и с видом сверху пока не дружит.