squad-proto/engine/app.h

93 lines
4.7 KiB
C
Raw Permalink Normal View History

Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20) Десять вех одним состоянием: разделять их пофайлово нечем, потому что src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений, все проверки проходят. ДВИЖОК (engine/, цель tile2d) Каталог свой, и решение «это движок» принимается тем, куда положен файл. Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра и карты уехали в engine/config.h, потолок населения этажа стал своим (MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала центром карты вместо позиции отряда. engine/projection.* единственное место, где движок знает про изометрию; видов сразу два — абстракция с одной реализацией ничем не проверена и обычно оказывается неправильной engine/project.* манифест проекта: пути, вид, кривая глубины engine/app.* окно, кадр, fixed timestep — вместо двух копий цикла engine/tilemap_draw движок научился показывать свою карту РЕДАКТОР (editor/, цель tile2d_editor) Открывает любой проект и ни одной игры по имени не знает. --tool одиннадцать безоконных команд: комнаты, каталог, кривая, сборка этажа, ASCII-карта, проверка набора. Правку делают файлами, последствия читают командой (docs/19-agent.md) терминал снизу: те же инструменты, git, сборка и запросы к claude/codex, открытый проект уходит потомкам через TILE2D_PROJECT ПРИМЕР (samples/walk, цель tile2d_walk) Второй потребитель движка. Игра и редактор самостоятельность движка проверить не могут: редактор мира не рисует, а игра — то место, куда общий код и уезжает незаметно. ИГРА (src/) Экстракшен: вес и два порога переноски, конечный боезапас, расходники применяются сами, общая сумка отряда, трата опыта на улучшения. Каждое из правил проверяется прогоном, а не глазами, и проверки поймали: вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче); боец с клинком не тратил найденные патроны при запасном стволе в сумке; массив виджетов экрана переполнился и молча съел хвост схрона; проверка улучшений сама ломала забег, вызывая NewRun посреди спуска. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:19 +07:00
#pragma once
// -----------------------------------------------------------------------------
// Приложение Tile2D: окно, кадр и ход времени.
//
// До этого окно, безрамочный режим, фреймбуфер, блиттер и fixed timestep жили
// двумя почти одинаковыми копиями — в игре и в редакторе. Третьему приложению
// пришлось бы написать третью, и она бы уже отличалась: где-то забытый
// `SetExitKey`, где-то другой потолок шагов, где-то апскейл не целым числом.
//
// Здесь НЕ инверсия управления: цикл остаётся у приложения, а App отвечает на
// вопросы «пора ли закрываться», «сколько шагов симуляции прошло» и «куда лёг
// кадр в окне». Колбэки сделали бы порядок «шаг — рендер — интерфейс»
// невидимым, а он у каждого приложения свой и в нём вся суть.
//
// Типичный цикл целиком:
//
// App app;
// app.Init({"Моя игра"});
// while (app.NextFrame())
// {
// for (int i = 0, n = app.Advance(false); i < n; ++i) game.Step(FIXED_DT);
// Render(game, app.fb, app.Alpha());
// app.Present(); // кадр на экран
// DrawUi(app.fit); // поверх кадра, примитивами raylib
// app.Finish();
// }
// app.Shutdown();
// -----------------------------------------------------------------------------
#include "engine/config.h"
#include "engine/framebuffer.h"
struct AppConfig
{
const char* title = "Tile2D";
// Безрамочное на весь монитор — обычный режим. Окно нужно для отладки и
// для съёмки картинок: на полном экране их не сравнить между собой.
bool borderless = true;
bool vsync = true;
// Разрешение кадра. По умолчанию — размер из engine/config.h.
int frameW = INTERNAL_W;
int frameH = INTERNAL_H;
};
struct App
{
Framebuffer fb;
ScreenBlitter screen;
// --- состояние текущего кадра ---------------------------------------------
int winW = 0;
int winH = 0;
// Куда лёг кадр в окне: на безрамочном экране он центрирован и вокруг него
// чёрные поля. Интерфейс и оверлей обязаны считать координаты ЧЕРЕЗ это.
ScreenRect fit;
float frameDt = 0.0f;
// Приложение может попроситься наружу само (пункт меню «выход», Esc в
// редакторе). Отдельный флаг, а не возврат из NextFrame: решение о выходе
// принимается в середине кадра, а закрыться надо в его конце.
bool quit = false;
bool Init(const AppConfig& cfg);
void Shutdown();
// Начало кадра: закрытие окна, F11, размеры, ScreenFitRect, dt.
// false — пора выходить.
bool NextFrame();
// Сколько раз выполнить шаг симуляции в этом кадре. Шаг постоянный
// (FIXED_DT), лишнее время копится до следующего кадра, потолок
// MAX_STEPS_PER_FRAME спасает от спирали смерти.
//
// halted — пауза: время НЕ копится. Иначе после меню симуляция догоняла бы
// пропущенные секунды одним рывком.
int Advance(bool halted);
// Доля незакрытого шага, [0..1). По ней интерполируется картинка: рендер
// идёт чаще симуляции, и без неё движение дёргалось бы на 60+ Гц.
float Alpha() const;
void Present(); // BeginDrawing + кадр на экран; дальше можно рисовать UI
void Finish(); // EndDrawing
// Снять кадр в файл. Здесь, а не в приложении, потому что снимать надо
// ПОСЛЕ Finish, и об этом легко забыть.
void Screenshot(const char* file) const;
private:
double accumulator = 0.0;
};