// ----------------------------------------------------------------------------- // Tile2D walk — самое маленькое приложение, которое имеет смысл. // // Ни игры, ни редактора: открыть проект, собрать этаж, походить по нему // камерой. Существует ради одного вопроса — сколько кода нужно, чтобы что-то // показать на этом движке. Ответ виден целиком на одном экране, и если он // однажды перестанет туда влезать, это будет значить, что движок оброс тем, // что должно было остаться в игре. // // Проверяет заодно то, что нельзя проверить ни игрой, ни редактором: движок // собирается и работает БЕЗ них. Игра могла бы незаметно унести к себе что-то // общее — тайлсет, проекцию, цикл, — и никто бы не заметил, пока не появился // второй потребитель. Вот он. // // tile2d_walk [--project <имя>] [--depth N] [--seed N] [--windowed] [--shot N] // WASD / стрелки — идти, R — пересобрать этаж, Esc — выход // ----------------------------------------------------------------------------- #include #include #include #include "engine/app.h" #include "engine/catalog.h" #include "engine/level_gen.h" #include "engine/project.h" #include "engine/room.h" #include "engine/tilemap.h" #include "engine/tilemap_draw.h" #include "engine/tileset.h" #include "engine/view2d.h" int main(int argc, char** argv) { const char* projectName = nullptr; int depth = 1; uint32_t seed = 1; bool windowed = false; int shotFrame = -1; // --shot N: снять кадр и выйти (картинки в документацию) for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--project") == 0 && i + 1 < argc) projectName = argv[++i]; else if (std::strcmp(argv[i], "--depth") == 0 && i + 1 < argc) depth = std::atoi(argv[++i]); else if (std::strcmp(argv[i], "--seed") == 0 && i + 1 < argc) seed = uint32_t(std::atoi(argv[++i])); else if (std::strcmp(argv[i], "--windowed") == 0) windowed = true; else if (std::strcmp(argv[i], "--shot") == 0 && i + 1 < argc) shotFrame = std::atoi(argv[++i]); } // --- проект: пути, вид отображения, кривая глубины ------------------------- Project project; std::string why; const std::string dir = FindProjectDir(projectName ? projectName : ""); if (dir.empty() || !project.Load(dir, &why)) { std::printf("проект не открыт: %s\n", dir.empty() ? "не выбран" : why.c_str()); for (const std::string& n : AvailableProjects()) std::printf(" доступен: %s\n", n.c_str()); return 1; } project.Apply(); // ставит проекцию и кривую на движок EntityCatalog catalog; RoomLibrary rooms; if (!catalog.Load(project.catalogPath, &why) || !rooms.Load(project.roomsDir, &why)) { std::printf("содержимое не читается: %s\n", why.c_str()); return 1; } // --- этаж ----------------------------------------------------------------- static Tilemap map; static GeneratedLevel level; auto build = [&]() { if (GenerateLevel(rooms, catalog, DescribeFloor(depth), seed, map, level, &why)) return true; std::printf("этаж не собрался: %s\n", why.c_str()); return false; }; if (!build()) return 1; std::printf("%s: этаж %d (%s), сид %u, комнат %d, тварей %d\n", project.name.c_str(), level.floor.depth, ThemeName(level.floor.theme), unsigned(seed), level.roomsPlaced, level.monstersPlaced); // --- окно и цикл ---------------------------------------------------------- App app; AppConfig cfg; cfg.title = "Tile2D walk"; cfg.borderless = !windowed; if (!app.Init(cfg)) return 1; TileSet tiles; tiles.Generate(); v2d::Camera cam; cam.SnapTo(level.squadStart); Vec2 pos = level.squadStart; while (app.NextFrame()) { // Ввод в ЭКРАННЫХ осях переводится в мировые проекцией — тем же // способом, что и в игре. Без этого WASD в изометрии ведёт вбок. Vec2 move{}; if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) move.x -= 1.0f; if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) move.x += 1.0f; if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) move.y -= 1.0f; if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) move.y += 1.0f; const int steps = app.Advance(false); for (int i = 0; i < steps; ++i) { if (LengthSq(move) > 0.0f) { const Vec2 step = v2d::ScreenDirToWorld(move); pos = pos + step * (6.0f * FIXED_DT); map.ResolveCircle(pos, 0.3f); // сквозь стены не ходим } } if (IsKeyPressed(KEY_R)) { ++seed; if (build()) { pos = level.squadStart; cam.SnapTo(pos); } } if (IsKeyPressed(KEY_ESCAPE)) app.quit = true; cam.Follow(pos, app.frameDt, 9.0f); int ox = 0, oy = 0; cam.ScreenOffset(ox, oy); app.fb.Clear(RGBA(8, 8, 10)); DrawTilemap(app.fb, map, tiles, ox, oy); // Где стоим, где спуск. Кружком, а не спрайтом: спрайты — дело игры, // движок про то, как выглядит отряд, не знает ничего. auto dot = [&](Vec2 w, uint32_t c) { const Vec2 s = v2d::WorldToScreen(w); app.fb.CircleFilled(int(s.x) + ox, int(s.y) + oy, 3, c); }; dot(level.exitPos, RGBA(60, 200, 90)); dot(pos, RGBA(240, 230, 200)); app.Present(); app.Finish(); if (shotFrame >= 0 && --shotFrame <= 0) { app.Screenshot("walk.png"); break; } } app.Shutdown(); return 0; }