From f5d3fe7fae243873e06b6ec944da811f31f92f5e Mon Sep 17 00:00:00 2001 From: Andrew Pogue Date: Thu, 20 Aug 2026 19:16:28 -0700 Subject: [PATCH] refactor: separate graphics and application code --- .clang-format | 14 +- .gitignore | 1 + CMakeLists.txt | 80 +++++++---- app/CMakeLists.txt | 17 +++ .../engine/input => app/include/app}/cue.hh | 5 +- .../engine => app/include/app}/event.hh | 1 + .../input => app/include/app}/keyboard.hh | 0 .../engine/app.hh => app/include/app/main.hh | 12 +- .../engine/input => app/include/app}/mouse.hh | 0 .../engine => app/include/app}/time.hh | 16 ++- app/include/app/window.hh | 38 ++++++ app/src/CMakeLists.txt | 1 + {engine/src/input => app/src}/cue.cc | 6 +- {engine => app}/src/event.cc | 2 +- {engine/src/input => app/src}/keyboard.cc | 2 +- {engine => app}/src/main.cc | 28 +--- {engine/src/input => app/src}/mouse.cc | 2 +- app/src/window.cc | 44 ++++++ content/terrain.lua | 33 +++-- core/CMakeLists.txt | 38 ++---- core/include/core/basic-catalog.hh | 4 +- core/include/core/bit-set.hh | 3 +- core/include/core/grid2.hh | 17 ++- core/include/core/grid3.hh | 20 +-- core/include/core/indexed-map.hh | 6 +- core/include/core/math.hh | 18 --- core/include/core/paged-array.hh | 8 +- core/include/core/panic.hh | 3 +- core/include/core/random.hh | 2 +- core/include/core/slot-map.hh | 22 ++- core/include/core/sparse-set.hh | 6 +- core/include/core/types.hh | 2 +- core/src/CMakeLists.txt | 2 +- core/src/panic.cc | 3 +- core/src/random.cc | 2 +- core/tests/CMakeLists.txt | 15 +- core/tests/registry.cc | 6 +- engine/CMakeLists.txt | 30 ---- engine/include/engine/render/camera.hh | 38 ------ engine/include/engine/render/draw.hh | 19 --- engine/include/engine/render/textures.hh | 14 -- engine/include/engine/render/window.hh | 11 -- engine/include/engine/ui/menu.hh | 23 ---- engine/src/CMakeLists.txt | 3 - engine/src/input/CMakeLists.txt | 1 - engine/src/render/CMakeLists.txt | 1 - engine/src/render/camera.cc | 75 ---------- engine/src/render/draw.cc | 10 -- engine/src/render/internal.cc | 87 ------------ engine/src/render/internal.hh | 18 --- engine/src/render/textures.cc | 73 ---------- game/CMakeLists.txt | 19 ++- game/src/CMakeLists.txt | 2 +- game/src/action/CMakeLists.txt | 2 +- game/src/action/move.hh | 2 +- game/src/assets.hh | 15 ++ game/src/catalog.hh | 3 +- game/src/command-buffer.cc | 6 +- game/src/command-buffer.hh | 9 +- game/src/command.cc | 8 +- game/src/command.hh | 8 +- game/src/component/CMakeLists.txt | 2 +- game/src/context.hh | 6 +- game/src/data.cc | 5 +- game/src/{app.cc => game.cc} | 16 +-- {core => game}/src/lua.cc | 6 +- {core/include/core => game/src}/lua.hh | 20 ++- game/src/pilot.hh | 4 +- game/src/simulation.hh | 2 +- game/src/sprite.cc | 50 ++++--- game/src/sprite.hh | 21 +-- game/src/types.hh | 3 + game/src/user-interface.cc | 119 ++++++++++++---- game/src/user-interface.hh | 28 ++-- game/src/world/CMakeLists.txt | 2 +- game/src/world/caves.cc | 13 +- game/src/world/caves.hh | 3 +- game/src/world/chunk.cc | 18 --- game/src/world/grassland.cc | 14 +- game/src/world/terrain.cc | 10 +- game/src/world/terrain.hh | 4 +- game/src/world/tile.hh | 1 - game/src/world/world.cc | 20 +-- game/src/world/world.hh | 5 +- gfx/CMakeLists.txt | 18 +++ gfx/include/gfx/camera2D.hh | 10 ++ gfx/include/gfx/color.hh | 13 ++ gfx/include/gfx/projection2D.hh | 50 +++++++ gfx/include/gfx/rectangle.hh | 37 +++++ gfx/include/gfx/renderer.hh | 50 +++++++ gfx/include/gfx/surface.hh | 28 ++++ gfx/include/gfx/texture.hh | 31 +++++ gfx/src/CMakeLists.txt | 1 + gfx/src/renderer.cc | 128 ++++++++++++++++++ gfx/src/surface.cc | 36 +++++ gfx/src/texture.cc | 37 +++++ 96 files changed, 997 insertions(+), 770 deletions(-) create mode 100644 app/CMakeLists.txt rename {engine/include/engine/input => app/include/app}/cue.hh (95%) rename {engine/include/engine => app/include/app}/event.hh (99%) rename {engine/include/engine/input => app/include/app}/keyboard.hh (100%) rename engine/include/engine/app.hh => app/include/app/main.hh (84%) rename {engine/include/engine/input => app/include/app}/mouse.hh (100%) rename {engine/include/engine => app/include/app}/time.hh (73%) create mode 100644 app/include/app/window.hh create mode 100644 app/src/CMakeLists.txt rename {engine/src/input => app/src}/cue.cc (91%) rename {engine => app}/src/event.cc (93%) rename {engine/src/input => app/src}/keyboard.cc (95%) rename {engine => app}/src/main.cc (68%) rename {engine/src/input => app/src}/mouse.cc (97%) create mode 100644 app/src/window.cc delete mode 100644 core/include/core/math.hh delete mode 100644 engine/CMakeLists.txt delete mode 100644 engine/include/engine/render/camera.hh delete mode 100644 engine/include/engine/render/draw.hh delete mode 100644 engine/include/engine/render/textures.hh delete mode 100644 engine/include/engine/render/window.hh delete mode 100644 engine/include/engine/ui/menu.hh delete mode 100644 engine/src/CMakeLists.txt delete mode 100644 engine/src/input/CMakeLists.txt delete mode 100644 engine/src/render/CMakeLists.txt delete mode 100644 engine/src/render/camera.cc delete mode 100644 engine/src/render/draw.cc delete mode 100644 engine/src/render/internal.cc delete mode 100644 engine/src/render/internal.hh delete mode 100644 engine/src/render/textures.cc create mode 100644 game/src/assets.hh rename game/src/{app.cc => game.cc} (82%) rename {core => game}/src/lua.cc (96%) rename {core/include/core => game/src}/lua.hh (87%) delete mode 100644 game/src/world/chunk.cc create mode 100644 gfx/CMakeLists.txt create mode 100644 gfx/include/gfx/camera2D.hh create mode 100644 gfx/include/gfx/color.hh create mode 100644 gfx/include/gfx/projection2D.hh create mode 100644 gfx/include/gfx/rectangle.hh create mode 100644 gfx/include/gfx/renderer.hh create mode 100644 gfx/include/gfx/surface.hh create mode 100644 gfx/include/gfx/texture.hh create mode 100644 gfx/src/CMakeLists.txt create mode 100644 gfx/src/renderer.cc create mode 100644 gfx/src/surface.cc create mode 100644 gfx/src/texture.cc diff --git a/.clang-format b/.clang-format index 4b3e3f2..53ea2f0 100644 --- a/.clang-format +++ b/.clang-format @@ -1,9 +1,9 @@ --- Language: Cpp BasedOnStyle: LLVM -AlignAfterOpenBracket: BlockIndent +AlignAfterOpenBracket: AlwaysBreak AlignOperands: AlignAfterOperator -AlignArrayOfStructures: Right +AlignArrayOfStructures: None AlignConsecutiveAssignments: Enabled: true AlignCompound: true @@ -49,10 +49,17 @@ BraceWrapping: AfterUnion: true BeforeCatch: false BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BracedInitializerIndentWidth: 2 BinPackParameters: OnePerLine +BinPackLongBracedList: false BitFieldColonSpacing: Both BreakBeforeBinaryOperators: All BreakConstructorInitializers: BeforeComma +# BreakAfterOpenBracketBracedList: true BreakInheritanceList: BeforeComma BreakStringLiterals: true ConstructorInitializerIndentWidth: 2 @@ -60,7 +67,7 @@ ContinuationIndentWidth: 2 ColumnLimit: 100 EmptyLineAfterAccessModifier: Leave EmptyLineBeforeAccessModifier: Always -FixNamespaceComments: true +FixNamespaceComments: false JavaScriptQuotes: Single KeepEmptyLines: AtEndOfFile: true @@ -100,5 +107,6 @@ SpaceBeforeParensOptions: AfterRequiresInExpression: true SpaceBeforeRangeBasedForLoopColon: true UseTab: Never +Cpp11BracedListStyle: false ... diff --git a/.gitignore b/.gitignore index 7b970e8..d2fbdfc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .vs/ .idea/ .cache/ +ignore/ # CMake CMakeSettings.json diff --git a/CMakeLists.txt b/CMakeLists.txt index e69ba23..accccc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,58 +2,78 @@ cmake_minimum_required (VERSION 3.25) project (game VERSION 0.0.1 LANGUAGES CXX) -set (CORE core) -set (ENGINE engine) -set (GAME ${PROJECT_NAME}) - option (BUILD_TESTS "Build tests" OFF) option (ENABLE_LINTER "Run fast static analysis (clang-tidy) during builds" OFF) option (ENABLE_DEEP_ANALYSIS "Run slow, deep static analysis (-fanalyzer or /analyze)" OFF) include (FetchContent) +FetchContent_Declare ( + SDL3 + GIT_REPOSITORY https://github.com/libsdl-org/SDL.git + GIT_TAG release-3.4.10 + GIT_SHALLOW TRUE + FIND_PACKAGE_ARGS 3.4.0 +) +set (DSDL_SHARED ON) +set (DSDL_STATIC OFF) +set (DSDL_TEST_LIBRARY OFF) +FetchContent_MakeAvailable (SDL3) + +FetchContent_Declare ( + glm + GIT_REPOSITORY https://github.com/g-truc/glm.git + GIT_TAG 1.0.3 + GIT_SHALLOW TRUE + FIND_PACKAGE_ARGS 1.0.0 +) +FetchContent_MakeAvailable (glm) + if (BUILD_TESTS) - enable_testing () - FetchContent_Declare ( - Catch2 - GIT_REPOSITORY https://github.com/catchorg/Catch2.git - GIT_TAG v3.15.0 - GIT_SHALLOW TRUE - FIND_PACKAGE_ARGS 3.0.0 - ) - FetchContent_MakeAvailable (Catch2) + enable_testing () + FetchContent_Declare ( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.15.0 + GIT_SHALLOW TRUE + FIND_PACKAGE_ARGS 3.0.0 + ) + FetchContent_MakeAvailable (Catch2) endif () add_subdirectory (core) -add_subdirectory (engine) +add_subdirectory (app) +add_subdirectory (gfx) add_subdirectory (game) add_library(project_warnings INTERFACE) if (MSVC) - target_compile_options(project_warnings INTERFACE /W4) + target_compile_options(project_warnings INTERFACE /W4) else () - target_compile_options(project_warnings INTERFACE -Wall -Wextra -Wpedantic) + target_compile_options(project_warnings INTERFACE -Wall -Wextra -Wpedantic) endif() if (ENABLE_DEEP_ANALYSIS) - if (MSVC) - target_compile_options (project_warnings INTERFACE /analyze) - elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - target_compile_options (project_warnings INTERFACE -fanalyzer) - else () - message (FATAL_ERROR "ENABLE_DEEP_ANALYSIS is ON, but ${CMAKE_CXX_COMPILER_ID} doesn't have an analyzer.") - endif () + if (MSVC) + target_compile_options (project_warnings INTERFACE /analyze) + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options (project_warnings INTERFACE -fanalyzer) + else () + message (FATAL_ERROR "ENABLE_DEEP_ANALYSIS is ON, but ${CMAKE_CXX_COMPILER_ID} doesn't have an analyzer.") + endif () endif () -target_link_libraries(${CORE} PRIVATE project_warnings) -target_link_libraries(${ENGINE} PRIVATE project_warnings) -target_link_libraries(${GAME} PRIVATE project_warnings) +target_link_libraries (core PRIVATE project_warnings) +target_link_libraries (app PRIVATE project_warnings) +target_link_libraries (gfx PRIVATE project_warnings) +target_link_libraries (game PRIVATE project_warnings) if (ENABLE_LINTER) - find_program(CLANG_TIDY_EXE NAMES clang-tidy REQUIRED) - set_target_properties (${CORE} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") - set_target_properties (${ENGINE} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") - set_target_properties (${GAME} PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") + find_program(CLANG_TIDY_EXE NAMES clang-tidy REQUIRED) + set_target_properties (core PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") + set_target_properties (app PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") + set_target_properties (gfx PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") + set_target_properties (game PROPERTIES CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") endif () diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..fec74de --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,17 @@ +add_library (app) + +target_include_directories ( + app + PUBLIC ${SDL3_INCLUDE_DIRS} + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +target_link_libraries ( + app + PUBLIC SDL3::SDL3 + PUBLIC glm::glm + PUBLIC core +) + +add_subdirectory (src) + diff --git a/engine/include/engine/input/cue.hh b/app/include/app/cue.hh similarity index 95% rename from engine/include/engine/input/cue.hh rename to app/include/app/cue.hh index 3ffe1db..1ace9fa 100644 --- a/engine/include/engine/input/cue.hh +++ b/app/include/app/cue.hh @@ -2,15 +2,17 @@ #include "core/bitmask.hh" #include "core/types.hh" #include -// It's not clear that this is necessary + struct Keyboard; struct Mouse; + // Button state enum class On : u8 { Press = 1 << 0, Release = 1 << 1, Repeat = 1 << 2, }; + // Keyboard input modifiers enum class With : u8 { None = 0, @@ -18,6 +20,7 @@ enum class With : u8 { Shift = 1 << 1, Alt = 1 << 2, }; + DEFINE_ENUM_BITWISE_OPERATORS(On) DEFINE_ENUM_BITWISE_OPERATORS(With) diff --git a/engine/include/engine/event.hh b/app/include/app/event.hh similarity index 99% rename from engine/include/engine/event.hh rename to app/include/app/event.hh index 6285703..936264a 100644 --- a/engine/include/engine/event.hh +++ b/app/include/app/event.hh @@ -1,4 +1,5 @@ #pragma once #include + bool push_event(SDL_Event event); SDL_Event make_quit_event(); diff --git a/engine/include/engine/input/keyboard.hh b/app/include/app/keyboard.hh similarity index 100% rename from engine/include/engine/input/keyboard.hh rename to app/include/app/keyboard.hh diff --git a/engine/include/engine/app.hh b/app/include/app/main.hh similarity index 84% rename from engine/include/engine/app.hh rename to app/include/app/main.hh index 43bd346..699ad71 100644 --- a/engine/include/engine/app.hh +++ b/app/include/app/main.hh @@ -2,15 +2,13 @@ // These functions need to be defined by an external binary linking against the engine. -#include "engine/render/window.hh" -#include "engine/time.hh" +#include "app/time.hh" union SDL_Event; struct AppConfig { - Hertz step_rate = 32u; - WindowConfig window; + Hertz step_rate = 32u; }; // An opaque type defined by the application; the engine only holds and passes a pointer to it. @@ -23,17 +21,17 @@ struct AppState; // Create `AppState` here. Returning null indicates application failure and `app_quit` will follow. [[nodiscard]] AppState* start(); -// Called 0 or more times per frame, once per elapsed timestep. Update simulation systems and +// Called 0 or more times per frame, once per elapsed timestep. Update simulation systems and // anything that needs determinism here. Time that can't be processed in this frame will be deferred // to the next frame, accumulating lag. void step(AppState& state); // Called once per frame. Advances by the time elapsed since the last frame, capped by 4 hertz. // Update user-interface, graphics, animations, and anything that doesn't need determinism here. -void update(AppState& state, Nanoseconds dt); +void update(AppState& state, f32 delta); // Called once per frame after `app_update` and `app_step`. -void render(AppState& state); +void render(AppState& state, f32 alpha); void handle_event(AppState& state, SDL_Event const& event); diff --git a/engine/include/engine/input/mouse.hh b/app/include/app/mouse.hh similarity index 100% rename from engine/include/engine/input/mouse.hh rename to app/include/app/mouse.hh diff --git a/engine/include/engine/time.hh b/app/include/app/time.hh similarity index 73% rename from engine/include/engine/time.hh rename to app/include/app/time.hh index ac34819..dd5a10b 100644 --- a/engine/include/engine/time.hh +++ b/app/include/app/time.hh @@ -1,22 +1,24 @@ #pragma once + +// TODO: adopt std::chrono over SDL time + #include "core/types.hh" using Hertz = u64; using Nanoseconds = u64; -using Seconds = u64; // hertz to nanoseconds [[nodiscard]] constexpr Nanoseconds from_hertz(Hertz time) noexcept { return 1'000'000'000 / time; } // nanoseconds to seconds -[[nodiscard]] constexpr Seconds to_seconds(Nanoseconds time) noexcept +[[nodiscard]] constexpr f32 to_seconds(Nanoseconds time) noexcept { - return time / 1'000'000'000; + return f32(time) / 1'000'000'000.f; } struct TimeStep { - constexpr explicit TimeStep(u64 size) noexcept : size_{size}, lag_{0u}, age_{0u} + constexpr explicit TimeStep(u64 size) noexcept : size_{ size }, lag_{ 0u }, age_{ 0u } { // allow a step size of 0? } @@ -27,7 +29,7 @@ struct TimeStep if (size_ == 0u) return 0u; lag_ += dt; u64 steps = lag_ / size_; - lag_ -= size_ * steps; + lag_ -= steps * size_; age_ += steps; return steps; } @@ -36,9 +38,11 @@ struct TimeStep [[nodiscard]] u64 age() const noexcept { return age_; } + [[nodiscard]] f32 alpha() const noexcept { return f32(lag_) / f32(size_); } + [[nodiscard]] constexpr u64 size() const noexcept { return size_; } - void reset(u64 size) + void resize(u64 size) { size_ = size; lag_ = 0.f; diff --git a/app/include/app/window.hh b/app/include/app/window.hh new file mode 100644 index 0000000..8c631e8 --- /dev/null +++ b/app/include/app/window.hh @@ -0,0 +1,38 @@ +#pragma once +#include "core/error.hh" +#include "core/types.hh" +#include + +struct Window; + +struct SDL_Window; + +[[nodiscard]] Result create_window(std::string_view title, i32 width, i32 height); + +struct Window +{ + Window() noexcept = default; + Window(Window const&) = delete; + Window& operator=(Window const&) = delete; + + explicit Window(SDL_Window*) noexcept; + Window(Window&&) noexcept; + Window& operator=(Window&&) noexcept; + ~Window() noexcept; + + std::string_view title() const; + + void set_title(std::string_view title); + + [[nodiscard]] glm::ivec2 size() const noexcept; + + void resize(i32 width, i32 height) noexcept; + + [[nodiscard]] bool valid() const noexcept { return handle_ != nullptr; } + + [[nodiscard]] SDL_Window* handle() noexcept { return handle_; } + +private: + SDL_Window* handle_ = nullptr; +}; + diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt new file mode 100644 index 0000000..3acffe6 --- /dev/null +++ b/app/src/CMakeLists.txt @@ -0,0 +1 @@ +target_sources (app PRIVATE main.cc event.cc keyboard.cc mouse.cc cue.cc window.cc) diff --git a/engine/src/input/cue.cc b/app/src/cue.cc similarity index 91% rename from engine/src/input/cue.cc rename to app/src/cue.cc index 60f8962..0303a93 100644 --- a/engine/src/input/cue.cc +++ b/app/src/cue.cc @@ -1,6 +1,6 @@ -#include "engine/input/cue.hh" -#include "engine/input/keyboard.hh" -#include "engine/input/mouse.hh" +#include "app/cue.hh" +#include "app/keyboard.hh" +#include "app/mouse.hh" static bool keyboard_matches_modifier(Keyboard const& kb, With with) { diff --git a/engine/src/event.cc b/app/src/event.cc similarity index 93% rename from engine/src/event.cc rename to app/src/event.cc index 220a3a3..4f67ce9 100644 --- a/engine/src/event.cc +++ b/app/src/event.cc @@ -1,4 +1,4 @@ -#include "engine/event.hh" +#include "app/event.hh" #include #include diff --git a/engine/src/input/keyboard.cc b/app/src/keyboard.cc similarity index 95% rename from engine/src/input/keyboard.cc rename to app/src/keyboard.cc index efd08a3..19ed71d 100644 --- a/engine/src/input/keyboard.cc +++ b/app/src/keyboard.cc @@ -1,4 +1,4 @@ -#include "engine/input/keyboard.hh" +#include "app/keyboard.hh" bool Keyboard::operator[](SDL_Scancode key) const { return state_.test(key); } diff --git a/engine/src/main.cc b/app/src/main.cc similarity index 68% rename from engine/src/main.cc rename to app/src/main.cc index 4b63183..743f7c3 100644 --- a/engine/src/main.cc +++ b/app/src/main.cc @@ -1,8 +1,7 @@ #define SDL_MAIN_USE_CALLBACKS -#include "engine/app.hh" -#include "engine/time.hh" -#include "render/internal.hh" +#include "app/main.hh" +#include "app/time.hh" #include #include #include @@ -10,12 +9,8 @@ #include #include -bool open_window(WindowConfig config); -void close_window(); -void destroy_all_textures(); - namespace { - TimeStep time_step(0u); + TimeStep timestep(0u); Nanoseconds time_prior = 0u; } // namespace @@ -28,15 +23,10 @@ SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) AppConfig const config = configure(argc, argv); - if (!open_window(config.window)) { - std::println("Failed to create window: {}", SDL_GetError()); - return SDL_APP_FAILURE; - } - *appstate = start(); if (!*appstate) return SDL_APP_FAILURE; - time_step.reset(from_hertz(config.step_rate)); + timestep.resize(from_hertz(config.step_rate)); time_prior = SDL_GetTicksNS(); return SDL_APP_CONTINUE; } @@ -47,13 +37,11 @@ SDL_AppResult SDL_AppIterate(void* appstate) Nanoseconds const now = SDL_GetTicksNS(); Nanoseconds dt = now - time_prior; - for (auto steps = time_step.advance(dt); steps > 0u; steps--) { step(*state); } + for (auto steps = timestep.advance(dt); steps > 0u; steps--) { step(*state); } - dt = dt > from_hertz(4u) ? from_hertz(4u) : dt; - update(*state, dt); + update(*state, to_seconds(dt)); - render(*state); // TODO: alpha - SDL_RenderPresent(detail::get_renderer()); + render(*state, timestep.alpha()); time_prior = now; return SDL_APP_CONTINUE; @@ -80,8 +68,6 @@ void SDL_AppQuit(void* appstate, SDL_AppResult /*result*/) { auto state = static_cast(appstate); quit(state); - destroy_all_textures(); - close_window(); SDL_Quit(); } diff --git a/engine/src/input/mouse.cc b/app/src/mouse.cc similarity index 97% rename from engine/src/input/mouse.cc rename to app/src/mouse.cc index 8aa7be7..e01bf53 100644 --- a/engine/src/input/mouse.cc +++ b/app/src/mouse.cc @@ -1,4 +1,4 @@ -#include "engine/input/mouse.hh" +#include "app/mouse.hh" bool Mouse::operator[](Uint8 btn) const { return btn < BUTTON_COUNT ? state_.test(btn) : false; } diff --git a/app/src/window.cc b/app/src/window.cc new file mode 100644 index 0000000..0432ae3 --- /dev/null +++ b/app/src/window.cc @@ -0,0 +1,44 @@ +#include "app/window.hh" +#include "core/panic.hh" +#include +#include +#include + +Window::Window(SDL_Window* handle) noexcept : handle_{ handle } { PRECONDITION(handle != nullptr); } + +Window::Window(Window&& other) noexcept : handle_{ other.handle_ } { other.handle_ = nullptr; } + +Window& Window::operator=(Window&& other) noexcept +{ + if (&other == this) return *this; + handle_ = other.handle_; + other.handle_ = nullptr; + return *this; +} + +Window::~Window() noexcept { SDL_DestroyWindow(handle_); } + +Result create_window(std::string_view title, i32 width, i32 height) +{ + ASSERT(width != 0 && height != 0); + auto window = SDL_CreateWindow(std::string(title).c_str(), width, height, 0); + if (!window) return std::unexpected(SDL_GetError()); + return Window(window); +} + +std::string_view Window::title() const { return SDL_GetWindowTitle(handle_); } + +void Window::set_title(std::string_view title) +{ + SDL_SetWindowTitle(handle_, std::string(title).c_str()); +} + +glm::ivec2 Window::size() const noexcept +{ + i32 w, h; + SDL_GetWindowSizeInPixels(handle_, &w, &h); + return { w, h }; +} + +void Window::resize(i32 width, i32 height) noexcept { SDL_SetWindowSize(handle_, width, height); } + diff --git a/content/terrain.lua b/content/terrain.lua index 4cbe98d..1b92e81 100644 --- a/content/terrain.lua +++ b/content/terrain.lua @@ -1,25 +1,34 @@ -terrain "grass-1" { - sprite = { atlas = "assets/kenney-1bitpack.png", x = 5, y = 0, color = 0x3BD872FF }, +local function make_sprite(x, y, color) + local tilesize = 16.0 + return { + atlas = 'assets/kenney-1bitpack.png', + source = { x * tilesize, y * tilesize, tilesize, tilesize}, + color = color + } +end + +terrain 'grass-1' { + sprite = make_sprite(5, 0, 0x3BD872FF) } -terrain "grass-2" { - sprite = { atlas = "assets/kenney-1bitpack.png", x = 6, y = 0, color = 0x43FF64FF }, +terrain 'grass-2' { + sprite = make_sprite(6, 0, 0x43FF64FF) } -terrain "grass-3" { - sprite = { atlas = "assets/kenney-1bitpack.png", x = 7, y = 0, color = 0x32DC96FF }, +terrain 'grass-3' { + sprite = make_sprite(7, 0, 0x32DC96FF) } -terrain "grass-tall" { - sprite = { atlas = "assets/kenney-1bitpack.png", x = 0, y = 2, color = 0x0C9B64FF }, +terrain 'grass-tall' { + sprite = make_sprite(0, 2, 0x0C9B64FF) } -terrain "dirt" { - sprite = { atlas = "assets/kenney-1bitpack.png", x = 2, y = 0, color = 0x79464bff }, +terrain 'dirt' { + sprite = make_sprite(2, 0, 0x79464bff) } -terrain "rocks" { - sprite = { atlas = "assets/kenney-1bitpack.png", x = 2, y = 0, color = 0xcec5b7ff }, +terrain 'rocks' { + sprite = make_sprite(2, 0, 0xcec5b7ff) } -- purple: 0x3B0264ff diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 4048433..14869c2 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,40 +1,18 @@ -find_package(Lua 5.4 REQUIRED) -include (FetchContent) +add_library (core) -FetchContent_Declare ( - glm - GIT_REPOSITORY https://github.com/g-truc/glm.git - GIT_TAG 1.0.3 - GIT_SHALLOW TRUE - FIND_PACKAGE_ARGS 1.0.0 -) -FetchContent_MakeAvailable (glm) +target_include_directories (core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) -add_library (${CORE}) - -target_include_directories ( - ${CORE} - PUBLIC ${LUA_INCLUDE_DIR} - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include -) - -if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - find_library(STDCXX_EXP NAMES stdc++exp) - if(STDCXX_EXP) - target_link_libraries(${CORE} PRIVATE ${STDCXX_EXP}) - target_compile_definitions(${CORE} PRIVATE HAVE_STACKTRACE) +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + find_library (STDCXX_EXP NAMES stdc++exp) + if (STDCXX_EXP) + target_link_libraries (core PRIVATE ${STDCXX_EXP}) + target_compile_definitions (core PRIVATE HAVE_STACKTRACE) endif() endif() -target_link_libraries ( - ${CORE} - PUBLIC glm::glm - PUBLIC ${LUA_LIBRARIES} -) - add_subdirectory (src) if (BUILD_TESTS) - add_subdirectory (tests) + add_subdirectory (tests) endif() diff --git a/core/include/core/basic-catalog.hh b/core/include/core/basic-catalog.hh index 949299c..da40172 100644 --- a/core/include/core/basic-catalog.hh +++ b/core/include/core/basic-catalog.hh @@ -26,7 +26,7 @@ struct BasicCatalog> template [[nodiscard]] Token find(std::string_view label) const { - return {store_of().find(label)}; + return { store_of().find(label) }; } template @@ -54,7 +54,7 @@ struct BasicCatalog> requires std::constructible_from Token emplace(std::string_view label, Args&&... args) { - return {store_of().emplace(label, std::forward(args)...)}; + return { store_of().emplace(label, std::forward(args)...) }; } template diff --git a/core/include/core/bit-set.hh b/core/include/core/bit-set.hh index 6663b0e..b96d91b 100644 --- a/core/include/core/bit-set.hh +++ b/core/include/core/bit-set.hh @@ -8,8 +8,7 @@ struct BitSet { explicit BitSet(u32 size = 0u, bool fill = false) : words_((size + 63u) / 64u, (u64)fill), size_(size) - { - } + {} BitSet(BitSet const&) = default; BitSet(BitSet&&) noexcept = default; diff --git a/core/include/core/grid2.hh b/core/include/core/grid2.hh index 47f4d29..2fc1b16 100644 --- a/core/include/core/grid2.hh +++ b/core/include/core/grid2.hh @@ -9,19 +9,19 @@ template struct Grid2 { - Grid2(u32 w, u32 h) : width_{w}, height_{h}, data_(size_t{width_} * height_) + Grid2(u32 w, u32 h) : width_{ w }, height_{ h }, data_(size_t{ width_ } * height_) { DEBUG_ASSERT(width_ != 0u && height_ != 0u, "constructed unusable grid"); } Grid2(u32 w, u32 h, Type const& value) - : width_{w}, height_{h}, data_(size_t{width_} * height_, value) + : width_{ w }, height_{ h }, data_(size_t{ width_ } * height_, value) { DEBUG_ASSERT(width_ != 0u && height_ != 0u, "constructed unusable grid"); } Grid2(Grid2&& other) noexcept - : width_{other.width_}, height_{other.height_}, data_(std::move(other.data_)) + : width_{ other.width_ }, height_{ other.height_ }, data_(std::move(other.data_)) { other.width_ = 0u; other.height_ = 0u; @@ -41,8 +41,7 @@ struct Grid2 constexpr size_t size() const { INVARIANT( - data_.size() == size_t{width_} * height_, "size of data array must match grid dimensions" - ); + data_.size() == size_t{ width_ } * height_, "size of data array must match grid dimensions"); return data_.size(); } @@ -99,7 +98,7 @@ private: constexpr size_t index(u32 x, u32 y) const { - size_t i = x + (y * size_t{width_}); + size_t i = x + (y * size_t{ width_ }); INVARIANT(i < data_.size(), "translated coordinates to invalid index"); return i; } @@ -110,7 +109,7 @@ template requires (Width > 0u && Height > 0u) struct Grid2 { - constexpr size_t size() const { return size_t{Width} * Height; } + constexpr size_t size() const { return size_t{ Width } * Height; } constexpr bool has(u32 x, u32 y) const { return x < Width && y < Height; } @@ -147,7 +146,7 @@ struct Grid2 private: - std::array data_; + std::array data_; - constexpr size_t index(u32 x, u32 y) const { return x + (y * size_t{Width}); } + constexpr size_t index(u32 x, u32 y) const { return x + (y * size_t{ Width }); } }; diff --git a/core/include/core/grid3.hh b/core/include/core/grid3.hh index 6cb7d28..7197148 100644 --- a/core/include/core/grid3.hh +++ b/core/include/core/grid3.hh @@ -10,13 +10,13 @@ template struct Grid3 { Grid3(u32 w, u32 h, u32 d) - : width_{w}, height_{h}, depth_{d}, data(size_t{width_} * height_ * depth_) + : width_{ w }, height_{ h }, depth_{ d }, data(size_t{ width_ } * height_ * depth_) { DEBUG_ASSERT(width_ != 0u && height_ != 0u && depth_ != 0u); } Grid3(u32 w, u32 h, u32 d, Type const& value) - : width_{w}, height_{h}, depth_{d}, data(size_t{width_} * height_ * depth_, value) + : width_{ w }, height_{ h }, depth_{ d }, data(size_t{ width_ } * height_ * depth_, value) { DEBUG_ASSERT(width_ != 0u && height_ != 0u && depth_ != 0u); } @@ -25,9 +25,9 @@ struct Grid3 Grid3& operator=(Grid3 const& other) = default; Grid3(Grid3&& other) noexcept - : width_{other.width_} - , height_{other.height_} - , depth_{other.depth_} + : width_{ other.width_ } + , height_{ other.height_ } + , depth_{ other.depth_ } , data(std::move(other.data)) { other.width_ = 0u; @@ -50,7 +50,7 @@ struct Grid3 constexpr size_t size() const { - INVARIANT(data.size() == size_t{width_} * height_ * depth_); + INVARIANT(data.size() == size_t{ width_ } * height_ * depth_); return data.size(); } @@ -99,7 +99,7 @@ private: constexpr size_t index(u32 x, u32 y, u32 z) const { - size_t i = x + (y * size_t{width_}) + (z * size_t{width_} * height_); + size_t i = x + (y * size_t{ width_ }) + (z * size_t{ width_ } * height_); INVARIANT(i < data.size() && "invalid index"); return i; } @@ -110,7 +110,7 @@ template requires (Width > 0u && Height > 0u && Depth > 0u) struct Grid3 { - constexpr size_t size() const { return size_t{Width} * Height * Depth; } + constexpr size_t size() const { return size_t{ Width } * Height * Depth; } constexpr bool has(u32 x, u32 y, u32 z) const { return x < Width && y < Height && z < Depth; } @@ -152,10 +152,10 @@ struct Grid3 private: - std::array data; + std::array data; constexpr size_t index(u32 x, u32 y, u32 z) const { - return x + (y * size_t{Width}) + (z * size_t{Width} * Height); + return x + (y * size_t{ Width }) + (z * size_t{ Width } * Height); } }; diff --git a/core/include/core/indexed-map.hh b/core/include/core/indexed-map.hh index bacf6b9..4ab7cdd 100644 --- a/core/include/core/indexed-map.hh +++ b/core/include/core/indexed-map.hh @@ -137,7 +137,7 @@ private: using value_type = std::pair; using reference = value_type; - Iterator() : owner_{nullptr}, idx_{nil} {} + Iterator() : owner_{ nullptr }, idx_{ nil } {} // Implicit conversion from iterator to const_iterator. operator Iterator() const noexcept requires (Access == Write) @@ -147,7 +147,7 @@ private: [[nodiscard]] reference operator*() const noexcept { - return {owner_->keys_[idx_], owner_->values_[idx_]}; + return { owner_->keys_[idx_], owner_->values_[idx_] }; } Iterator& operator++() noexcept @@ -174,7 +174,7 @@ private: friend struct IndexedMap; - Iterator(Owner* owner, u32 idx) noexcept : owner_{owner}, idx_{idx} {} + Iterator(Owner* owner, u32 idx) noexcept : owner_{ owner }, idx_{ idx } {} Owner* owner_; u32 idx_; diff --git a/core/include/core/math.hh b/core/include/core/math.hh deleted file mode 100644 index 6b03dde..0000000 --- a/core/include/core/math.hh +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include "core/types.hh" -#include - -template -using Vec2 = glm::vec<2, Type>; -using Vec2f = Vec2; -using Vec2i = Vec2; -using Vec2u = Vec2; - -template -using Vec3 = glm::vec<3, Type>; -using Vec3f = Vec3; -using Vec3i = Vec3; -using Vec3u = Vec3; - -template -using Mat4 = glm::mat<4, 4, Type>; diff --git a/core/include/core/paged-array.hh b/core/include/core/paged-array.hh index 57999a1..7ab98b6 100644 --- a/core/include/core/paged-array.hh +++ b/core/include/core/paged-array.hh @@ -78,7 +78,7 @@ public: ~PagedArray() noexcept = default; PagedArray(PagedArray const& other) requires std::is_copy_constructible_v - : pages_(), size_{other.size_} + : pages_(), size_{ other.size_ } { pages_.resize(other.pages_.size()); for (u32 i = 0; i < other.pages_.size(); ++i) { @@ -234,7 +234,7 @@ private: using value_type = std::pair; using reference = value_type; - Iterator() : owner_{nullptr}, idx_{nil} {} + Iterator() : owner_{ nullptr }, idx_{ nil } {} operator Iterator() const noexcept requires (Access == Write) { @@ -246,7 +246,7 @@ private: PRECONDITION(owner_, "dereference singular iterator"); PRECONDITION(idx_ != nil, "dereferenced end iterator"); INVARIANT(owner_->has(idx_), "dereferenced empty slot"); - return value_type{idx_, (*owner_)[idx_]}; + return value_type{ idx_, (*owner_)[idx_] }; } Iterator& operator++() noexcept @@ -273,7 +273,7 @@ private: friend struct PagedArray; - Iterator(OwnerPtr owner, u32 idx) noexcept : owner_(owner), idx_{idx} + Iterator(OwnerPtr owner, u32 idx) noexcept : owner_(owner), idx_{ idx } { INVARIANT(owner_, "constructed without parent container"); idx_ = owner_->find_occupied(idx_); diff --git a/core/include/core/panic.hh b/core/include/core/panic.hh index 82f0fc6..5bde381 100644 --- a/core/include/core/panic.hh +++ b/core/include/core/panic.hh @@ -6,8 +6,7 @@ namespace detail { char const* category, char const* condition, char const* message, - std::source_location location = std::source_location::current() - ); + std::source_location location = std::source_location::current()); } // namespace detail #define PANIC(category, cond, ...) \ diff --git a/core/include/core/random.hh b/core/include/core/random.hh index 9889fc3..9eb6a95 100644 --- a/core/include/core/random.hh +++ b/core/include/core/random.hh @@ -13,7 +13,7 @@ struct SplitMix64 static constexpr u64 max() { return UINT64_MAX; } - explicit SplitMix64(u64 seed) : x{seed} {} + explicit SplitMix64(u64 seed) : x{ seed } {} inline u64 operator()() { diff --git a/core/include/core/slot-map.hh b/core/include/core/slot-map.hh index 45b9395..d42517a 100644 --- a/core/include/core/slot-map.hh +++ b/core/include/core/slot-map.hh @@ -17,7 +17,7 @@ struct SlotMap using const_pointer = Type const*; explicit SlotMap(u32 limit = UINT32_MAX) noexcept - : slots_(), limit_{limit}, size_{0u}, first_free_{nil} + : slots_(), limit_{ limit }, size_{ 0u }, first_free_{ nil } { PRECONDITION(limit > 0u, "constructed unusable slot map"); } @@ -35,9 +35,8 @@ struct SlotMap // Assumptions: handle is not null, handle has element. template requires std::constructible_from - Handle replace( - Handle handle, Args&&... args - ) noexcept(std::is_nothrow_constructible_v) + Handle replace(Handle handle, Args&&... args) + noexcept(std::is_nothrow_constructible_v) { PRECONDITION(handle, "handle must not be null"); u32 i = locate(handle); @@ -48,7 +47,7 @@ struct SlotMap slot.value.~Type(); new (&slot.value) Type(std::forward(args)...); ++slot.generation; - return {i, slot.generation}; + return { i, slot.generation }; } // Fills a slot and returns its handle. @@ -63,7 +62,7 @@ struct SlotMap new (&slot.value) Type(std::forward(args)...); slot.live = true; ++size_; - return {i, slot.generation}; + return { i, slot.generation }; } // Empty a slot and mark it for re-use, destroying the element if it exists. @@ -101,7 +100,7 @@ struct SlotMap { // rename to get_handle? return index != nil && index < slots_.size() && slots_[index].live == true - ? Handle{index, slots_[index].generation} + ? Handle{ index, slots_[index].generation } : Handle::null(); } @@ -160,7 +159,7 @@ private: using reference = value_type; using pointer = void; // proxy; no operator-> provided - Iterator() : owner_{nullptr}, index_{0u} {} + Iterator() : owner_{ nullptr }, index_{ 0u } {} Iterator(Iterator const& other) = default; Iterator& operator=(Iterator const& other) = default; @@ -175,10 +174,7 @@ private: PRECONDITION(owner_, "dereferenced singular iterator"); PRECONDITION(index_ < owner_->slots_.size(), "dereferenced end iterator"); INVARIANT(owner_->slots_[index_].live == true, "dereferenced dead slot"); - return { - {index_, owner_->slots_[index_].generation}, - owner_->slots_[index_].value - }; + return { { index_, owner_->slots_[index_].generation }, owner_->slots_[index_].value }; } Iterator& operator++() noexcept @@ -203,7 +199,7 @@ private: friend struct SlotMap; - Iterator(Owner* map, u32 index) noexcept : owner_{map}, index_{index} + Iterator(Owner* map, u32 index) noexcept : owner_{ map }, index_{ index } { INVARIANT(owner_, "constructed without parent container"); INVARIANT(index_ <= owner_->slots_.size(), "constructed with invalid index"); diff --git a/core/include/core/sparse-set.hh b/core/include/core/sparse-set.hh index 5d9ef16..4b107e1 100644 --- a/core/include/core/sparse-set.hh +++ b/core/include/core/sparse-set.hh @@ -121,7 +121,7 @@ private: using value_type = std::pair; using reference = value_type; - Iterator() : owner_{nullptr}, idx_{nil} {} + Iterator() : owner_{ nullptr }, idx_{ nil } {} // Implicit conversion from iterator to const_iterator. operator Iterator() const noexcept requires (Access == Write) @@ -131,7 +131,7 @@ private: [[nodiscard]] reference operator*() const noexcept { - return {owner_->keys_[idx_], owner_->values_[idx_]}; + return { owner_->keys_[idx_], owner_->values_[idx_] }; } Iterator& operator++() noexcept @@ -158,7 +158,7 @@ private: friend struct SparseSet; - Iterator(Owner* owner, u32 idx) noexcept : owner_{owner}, idx_{idx} {} + Iterator(Owner* owner, u32 idx) noexcept : owner_{ owner }, idx_{ idx } {} Owner* owner_; u32 idx_; diff --git a/core/include/core/types.hh b/core/include/core/types.hh index 8ce0da2..1b56cd9 100644 --- a/core/include/core/types.hh +++ b/core/include/core/types.hh @@ -43,7 +43,7 @@ struct Handle u32 index = nil; u32 generation = nil; - [[nodiscard]] static consteval Handle null() noexcept { return {nil, nil}; } + [[nodiscard]] static consteval Handle null() noexcept { return { nil, nil }; } [[nodiscard]] constexpr std::strong_ordering operator<=>(Handle const&) const noexcept = default; diff --git a/core/src/CMakeLists.txt b/core/src/CMakeLists.txt index 0e9c2c2..8f9ecde 100644 --- a/core/src/CMakeLists.txt +++ b/core/src/CMakeLists.txt @@ -1 +1 @@ -target_sources (${CORE} PRIVATE random.cc panic.cc lua.cc) +target_sources (core PRIVATE random.cc panic.cc) diff --git a/core/src/panic.cc b/core/src/panic.cc index 40976b9..e70c503 100644 --- a/core/src/panic.cc +++ b/core/src/panic.cc @@ -7,8 +7,7 @@ #endif void detail::panic( - char const* category, char const* condition, char const* message, std::source_location location -) + char const* category, char const* condition, char const* message, std::source_location location) { #ifdef HAVE_STACKTRACE std::stacktrace trace = std::stacktrace::current(); diff --git a/core/src/random.cc b/core/src/random.cc index b9e2244..b16519d 100644 --- a/core/src/random.cc +++ b/core/src/random.cc @@ -9,7 +9,7 @@ namespace { u64 random_seed() { std::random_device rd; - return (u64{rd()} << 32) | rd(); + return (u64{ rd() } << 32) | rd(); } Xoshiro256ss::Xoshiro256ss(u64 seed) diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index ab6168c..b6bc41c 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -1,19 +1,18 @@ list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) -add_executable( - test-engine - registry.cc -) +add_executable(test-core) + +target_sources (test-core PRIVATE registry.cc) -target_include_directories(test-engine PRIVATE ${catch2_INCLUDE_DIRS}/src) +target_include_directories(test-core PRIVATE ${catch2_INCLUDE_DIRS}/src) target_link_libraries( - test-engine - PRIVATE ${CORE} + test-core + PRIVATE core PRIVATE Catch2::Catch2WithMain ) include(CTest) include(Catch) -catch_discover_tests(test-engine) +catch_discover_tests(test-core) diff --git a/core/tests/registry.cc b/core/tests/registry.cc index 0c69b40..6f66f1e 100644 --- a/core/tests/registry.cc +++ b/core/tests/registry.cc @@ -25,9 +25,9 @@ namespace { //////////////////////////////////////////////////////////////////// static inline int instances = 0; int value; - explicit Counted(int v = 0) : value{v} { ++instances; } + explicit Counted(int v = 0) : value{ v } { ++instances; } - Counted(Counted const& o) : value{o.value} { ++instances; } + Counted(Counted const& o) : value{ o.value } { ++instances; } Counted& operator=(Counted const& o) = default; @@ -73,7 +73,7 @@ TEST_CASE("Registry – entity creation and destruction", "[registry][entity]") SECTION("polling non-existent entity") { - auto h = GENERATE(Handle{}, Handle{48u, 29u}); + auto h = GENERATE(Handle{}, Handle{ 48u, 29u }); REQUIRE(r.valid(h) == false); REQUIRE(r.has(h) == false); REQUIRE(r.try_get(h) == nullptr); diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt deleted file mode 100644 index 6cbc096..0000000 --- a/engine/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ -include (FetchContent) - -FetchContent_Declare ( - SDL3 - GIT_REPOSITORY https://github.com/libsdl-org/SDL.git - GIT_TAG release-3.4.10 - GIT_SHALLOW TRUE - FIND_PACKAGE_ARGS 3.4.0 -) -set (DSDL_SHARED ON) -set (DSDL_STATIC OFF) -set (DSDL_TEST_LIBRARY OFF) -FetchContent_MakeAvailable (SDL3) - -add_library (${ENGINE}) - -target_include_directories ( - ${ENGINE} - PUBLIC ${SDL3_INCLUDE_DIRS} - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include -) - -target_link_libraries ( - ${ENGINE} - PUBLIC ${CORE} - PUBLIC SDL3::SDL3 -) - -add_subdirectory (src) - diff --git a/engine/include/engine/render/camera.hh b/engine/include/engine/render/camera.hh deleted file mode 100644 index b1eae52..0000000 --- a/engine/include/engine/render/camera.hh +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once -#include -#include - -struct Camera -{ - // `viewport`: the visible area in tiles at zoom 1 — the cull region. - // The tile iterator yields tiles within `viewport / zoom` of `position`. - glm::vec2 position, viewport; - float zoom = 1.0f; - // Maps a world tile coordinate to its pixel position in the off-screen scene - // target (the world is centred in it), and the inverse. - glm::vec2 view_coord_at(glm::vec2 world_coord, float tile_size) const; - glm::vec2 world_coord_at(glm::vec2 scene_pixel, float tile_size) const; - bool contains(glm::vec2 world_coord) const; - - struct Iterator - { - using iterator_category = std::forward_iterator_tag; - using value_type = glm::vec2; - Iterator(Iterator const& other) = default; - Iterator& operator=(Iterator const& other) = default; - glm::vec2 operator*() const; - Iterator& operator++(); - Iterator operator++(int); - bool operator==(Iterator const& other) const; - - private: - - friend struct Camera; - Iterator(glm::vec2 min, glm::vec2 max, glm::vec2 start); - float min_x, max_x; - glm::vec2 position; - }; - - Iterator begin() const; - Iterator end() const; -}; diff --git a/engine/include/engine/render/draw.hh b/engine/include/engine/render/draw.hh deleted file mode 100644 index f7ddc32..0000000 --- a/engine/include/engine/render/draw.hh +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once -#include "core/types.hh" -#include -#include -using Color = SDL_Color; -using Rectangle = SDL_FRect; - -constexpr SDL_Color hex_color(u32 rgba) -{ - return {u8(rgba >> 24), u8(rgba >> 16), u8(rgba >> 8), u8(rgba)}; -} - -void draw_rectangle(SDL_FRect rect, SDL_Color color); - -/// Begin drawing the world into the off-screen scene target at 1:1 (no zoom). -void scene_begin(); - -/// Blit the scene target to the window, scaled by the camera `zoom`. -void scene_present(float zoom); diff --git a/engine/include/engine/render/textures.hh b/engine/include/engine/render/textures.hh deleted file mode 100644 index 043609b..0000000 --- a/engine/include/engine/render/textures.hh +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once -#include "core/types.hh" -#include "engine/render/draw.hh" -#include -#include - -struct Texture; - -Handle create_texture(std::string_view path); -Handle create_texture(SDL_Surface* surface); -// TODO: Handle get_or_create_texture(std::string_view path); -// TODO: Handle get_texture(std::string_view path); -void destroy_texture(Handle handle); -void draw_texture(Handle texture, Rectangle source, Rectangle dest, Color tint); diff --git a/engine/include/engine/render/window.hh b/engine/include/engine/render/window.hh deleted file mode 100644 index 8dbb73f..0000000 --- a/engine/include/engine/render/window.hh +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once -#include "core/types.hh" - -struct WindowConfig -{ - char const* title; - u16 width, height; - // Minimum camera zoom the app will use. The off-screen scene target is - // oversized to `window / min_zoom` so zooming out never reveals gaps. - f32 min_zoom = 1.f; -}; diff --git a/engine/include/engine/ui/menu.hh b/engine/include/engine/ui/menu.hh deleted file mode 100644 index 1788ca8..0000000 --- a/engine/include/engine/ui/menu.hh +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#include "core/types.hh" -#include -#include - -struct Menu -{ - virtual ~Menu() = default; - virtual void event(SDL_Event const& event) = 0; - virtual void update(f32 dt) = 0; - virtual void render(SDL_Renderer* renderer) = 0; -}; - -struct MainMenu : Menu -{ - void event(SDL_Event const& e) override; - void update(f32 dt) override; - void render(SDL_Renderer* renderer) override; - -private: - - f32 time_ = 0.0f; -}; diff --git a/engine/src/CMakeLists.txt b/engine/src/CMakeLists.txt deleted file mode 100644 index b0c335c..0000000 --- a/engine/src/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -add_subdirectory (input) -add_subdirectory (render) -target_sources (${ENGINE} PRIVATE main.cc event.cc) diff --git a/engine/src/input/CMakeLists.txt b/engine/src/input/CMakeLists.txt deleted file mode 100644 index 07c4aef..0000000 --- a/engine/src/input/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -target_sources (${ENGINE} PRIVATE keyboard.cc mouse.cc cue.cc) diff --git a/engine/src/render/CMakeLists.txt b/engine/src/render/CMakeLists.txt deleted file mode 100644 index 610c271..0000000 --- a/engine/src/render/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -target_sources (${ENGINE} PRIVATE internal.cc textures.cc camera.cc draw.cc) diff --git a/engine/src/render/camera.cc b/engine/src/render/camera.cc deleted file mode 100644 index 88057f3..0000000 --- a/engine/src/render/camera.cc +++ /dev/null @@ -1,75 +0,0 @@ -#include "engine/render/camera.hh" -#include "core/math.hh" -#include "core/panic.hh" -#include "internal.hh" - -// Places a world tile at 1:1 in the off-screen scene target (zoom is applied -// later, once, by `scene_present`), centred on the oversized target. -Vec2f Camera::view_coord_at(Vec2f world_coord, float tile_size) const -{ - return (world_coord - position) * tile_size + detail::scene_size() * 0.5f; -} - -// Inverse of `view_coord_at`. A mouse-pick path must first map the window pixel -// to a scene pixel: `scene_xy = src.xy + mouse_px / zoom`. -Vec2f Camera::world_coord_at(Vec2f scene_pixel, float tile_size) const -{ - return (scene_pixel - detail::scene_size() * 0.5f) / tile_size + position; -} - -// The visible half-extent in tiles: `viewport` is the view at zoom 1, so the -// actual visible area shrinks as `zoom` grows. This drives culling. -bool Camera::contains(Vec2f world_coord) const -{ - Vec2f half = (viewport * 0.5f) / zoom; - Vec2f rel = world_coord - position; - return rel.x >= -half.x && rel.x <= half.x && rel.y >= -half.y && rel.y <= half.y; -} - -Camera::Iterator Camera::begin() const -{ - PRECONDITION(zoom > 0.f); - PRECONDITION(viewport.x > 0.f && viewport.y > 0.f); - Vec2f half = (viewport * 0.5f) / zoom; - Vec2f min = glm::floor(position - half); - Vec2f max = glm::ceil(position + half); - return Iterator(min, max, min); -} - -Camera::Iterator Camera::end() const -{ - PRECONDITION(zoom > 0.f); - PRECONDITION(viewport.x > 0.f && viewport.y > 0.f); - Vec2f half = (viewport * 0.5f) / zoom; - Vec2f min = glm::floor(position - half); - Vec2f max = glm::ceil(position + half); - return Iterator(min, max, Vec2f{min.x, max.y + 1.0f}); -} - -Camera::Iterator::Iterator(Vec2f min, Vec2f max, Vec2f start) - : min_x{min.x}, max_x{max.x}, position{start} -{ -} - -Vec2f Camera::Iterator::operator*() const { return position; } - -Camera::Iterator& Camera::Iterator::operator++() -{ - if (++position.x > max_x) { - ++position.y; - position.x = min_x; - } - return *this; -} - -Camera::Iterator Camera::Iterator::operator++(int) -{ - auto temp = *this; - ++(*this); - return temp; -} - -bool Camera::Iterator::operator==(Iterator const& other) const -{ - return position == other.position; -} diff --git a/engine/src/render/draw.cc b/engine/src/render/draw.cc deleted file mode 100644 index f2a2844..0000000 --- a/engine/src/render/draw.cc +++ /dev/null @@ -1,10 +0,0 @@ -#include "engine/render/draw.hh" -#include "internal.hh" -#include - -void draw_rectangle(SDL_FRect rect, SDL_Color color) -{ - auto renderer = detail::get_renderer(); - SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); - SDL_RenderFillRect(renderer, &rect); -} diff --git a/engine/src/render/internal.cc b/engine/src/render/internal.cc deleted file mode 100644 index 28a4a3d..0000000 --- a/engine/src/render/internal.cc +++ /dev/null @@ -1,87 +0,0 @@ -#include "engine/render/draw.hh" -#include "engine/render/window.hh" -#include "internal.hh" -#include -#include -#include - -namespace { - SDL_Window* window; - SDL_Renderer* renderer; - SDL_Texture* scene_target; - glm::vec2 window_px; - glm::vec2 scene_px; - - // Background fill, also shown in any letterbox region. - constexpr SDL_Color background = {73, 49, 62, SDL_ALPHA_OPAQUE}; -} // namespace - -namespace detail { - SDL_Renderer* get_renderer() { return renderer; } - - glm::vec2 scene_size() { return scene_px; } -} // namespace detail - -bool open_window(WindowConfig config) -{ - window = SDL_CreateWindow(config.title, config.width, config.height, 0); - if (!window) { - SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not create window: %s\n", SDL_GetError()); - return false; - } - renderer = SDL_CreateRenderer(window, NULL); - if (!renderer) { - SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not create renderer: %s\n", SDL_GetError()); - return false; - } - - // The scene target must be large enough to cover the most zoomed-out view - // (`window / min_zoom`) so the blit in `scene_present` never samples beyond - // its bounds. - window_px = {float(config.width), float(config.height)}; - scene_px = glm::ceil(window_px / config.min_zoom); - - scene_target = SDL_CreateTexture( - renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, int(scene_px.x), int(scene_px.y) - ); - if (!scene_target) { - SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not create scene target: %s\n", SDL_GetError()); - return false; - } - SDL_SetTextureScaleMode(scene_target, SDL_SCALEMODE_NEAREST); // crisp pixel-art - return true; -} - -void close_window() -{ - SDL_DestroyTexture(scene_target); - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); -} - -// Public render API (declared in engine/render/draw.hh). Only the game drives -// the scene, so these live in the global namespace rather than `detail`. -void scene_begin() -{ - SDL_SetRenderTarget(renderer, scene_target); - SDL_SetRenderDrawColor(renderer, background.r, background.g, background.b, background.a); - SDL_RenderClear(renderer); -} - -void scene_present(float zoom) -{ - SDL_SetRenderTarget(renderer, NULL); - SDL_SetRenderDrawColor(renderer, background.r, background.g, background.b, background.a); - SDL_RenderClear(renderer); - - // Sample a centred sub-rectangle of size `window / zoom` and stretch it to - // fill the window: that stretch is the zoom. - SDL_FRect src = { - (scene_px.x - window_px.x / zoom) * 0.5f, - (scene_px.y - window_px.y / zoom) * 0.5f, - window_px.x / zoom, - window_px.y / zoom, - }; - SDL_FRect dst = {0.f, 0.f, window_px.x, window_px.y}; - SDL_RenderTexture(renderer, scene_target, &src, &dst); -} diff --git a/engine/src/render/internal.hh b/engine/src/render/internal.hh deleted file mode 100644 index bbc059e..0000000 --- a/engine/src/render/internal.hh +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include "core/types.hh" -#include - -struct Texture; -struct SDL_Texture; -struct SDL_Renderer; - -namespace detail { - SDL_Texture* get_texture(Handle handle); - SDL_Renderer* get_renderer(); - - /// Pixel dimensions of the off-screen "scene" render target. It is oversized - /// (see `open_window`) so zooming out never reveals gaps; used to centre the - /// world in the target. The public `scene_begin`/`scene_present` (draw.hh) - /// drive rendering into and out of it. - glm::vec2 scene_size(); -} // namespace detail diff --git a/engine/src/render/textures.cc b/engine/src/render/textures.cc deleted file mode 100644 index 96fdb2b..0000000 --- a/engine/src/render/textures.cc +++ /dev/null @@ -1,73 +0,0 @@ -#include "core/panic.hh" -#include "core/slot-map.hh" -#include "core/string.hh" -#include "engine/render/textures.hh" -#include "internal.hh" -#include -#include - -struct Texture -{ - explicit Texture(SDL_Texture* t) : data{t} {} - - ~Texture() { SDL_DestroyTexture(data); } - - SDL_Texture* get() const { return data; } - -private: - - SDL_Texture* data; -}; - -namespace { ///////////////////////////////////////////////////// - SlotMap textures; - StringMap> paths; -} // namespace - -// needs happen before SDL quit -void destroy_all_textures() { textures.clear(); } - -SDL_Texture* get_texture(Handle handle) -{ - Texture* texture = textures.try_get(handle); - return texture ? texture->get() : nullptr; -} - -/// PUBLIC API ////////////////////////////////////////////////// -Handle create_texture(std::string_view path) -{ - if (auto it = paths.find(path); it != paths.end()) return it->second; - SDL_Surface* surface = SDL_LoadPNG(std::string(path).c_str()); - if (!surface) { - SDL_Log("Couldn't load png: %s", SDL_GetError()); - return Handle::null(); - } - auto handle = create_texture(surface); - SDL_DestroySurface(surface); - paths.emplace(path, handle); - return handle; -} - -Handle create_texture(SDL_Surface* surface) -{ - SDL_Texture* texture = SDL_CreateTextureFromSurface(detail::get_renderer(), surface); - if (!texture) { - SDL_Log("Couldn't create static texture: %s", SDL_GetError()); - return Handle::null(); - } - SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); - auto handle = textures.emplace(texture); - ASSERT(handle, "texture limit reached"); - return handle; -} - -void destroy_texture(Handle handle) { textures.erase(handle); } - -void draw_texture(Handle handle, Rectangle source, Rectangle dest, Color tint) -{ - auto renderer = detail::get_renderer(); - SDL_Texture* texture = get_texture(handle); - PRECONDITION(texture, "texture handle must be valid"); - SDL_SetTextureColorMod(texture, tint.r, tint.g, tint.b); - SDL_RenderTexture(renderer, texture, &source, &dest); -} diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt index 9671716..284a36e 100644 --- a/game/CMakeLists.txt +++ b/game/CMakeLists.txt @@ -1,8 +1,21 @@ -add_executable (${GAME}) +find_package(Lua 5.4 REQUIRED) -target_include_directories (${GAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +add_executable (game) -target_link_libraries (${GAME} PRIVATE ${ENGINE}) +target_include_directories ( + game + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src + PRIVATE ${LUA_INCLUDE_DIR} +) + +target_link_libraries ( + game + PRIVATE ${LUA_LIBRARIES} + PRIVATE app + PRIVATE gfx + PRIVATE core +) add_subdirectory (src) # add_subdirectory (demos) + diff --git a/game/src/CMakeLists.txt b/game/src/CMakeLists.txt index 98fe1bc..3174d32 100644 --- a/game/src/CMakeLists.txt +++ b/game/src/CMakeLists.txt @@ -1,4 +1,4 @@ add_subdirectory (action) add_subdirectory (component) add_subdirectory (world) -target_sources (${GAME} PRIVATE app.cc data.cc command.cc command-buffer.cc user-interface.cc pilot.cc sprite.cc catalog.cc registry.cc entity.cc context.cc scene.cc) +target_sources (game PRIVATE game.cc data.cc command.cc command-buffer.cc user-interface.cc pilot.cc sprite.cc catalog.cc registry.cc entity.cc context.cc scene.cc lua.cc) diff --git a/game/src/action/CMakeLists.txt b/game/src/action/CMakeLists.txt index 890514a..3bfc348 100644 --- a/game/src/action/CMakeLists.txt +++ b/game/src/action/CMakeLists.txt @@ -1 +1 @@ -target_sources (${GAME} PRIVATE move.cc) +target_sources (game PRIVATE move.cc) diff --git a/game/src/action/move.hh b/game/src/action/move.hh index 8e99589..ef152e5 100644 --- a/game/src/action/move.hh +++ b/game/src/action/move.hh @@ -5,7 +5,7 @@ struct MoveAction { - glm::vec2 direction = {0.f, 0.f}; + glm::vec2 direction = { 0.f, 0.f }; float speed = 1.f; }; diff --git a/game/src/assets.hh b/game/src/assets.hh new file mode 100644 index 0000000..3a7c682 --- /dev/null +++ b/game/src/assets.hh @@ -0,0 +1,15 @@ +#pragma once + +// TODO: lazy loading/unloading of assets as needed + +#include "core/types.hh" +#include + +struct Texture; + +struct TextureAsset +{ + std::string path; + Handle handle; +}; + diff --git a/game/src/catalog.hh b/game/src/catalog.hh index 4012595..8b6d2e3 100644 --- a/game/src/catalog.hh +++ b/game/src/catalog.hh @@ -1,9 +1,10 @@ #pragma once +#include "assets.hh" #include "core/basic-catalog.hh" #include "types.hh" #include "world/terrain.hh" -using Definitions = TypeList; +using Definitions = TypeList; using Catalog = BasicCatalog; diff --git a/game/src/command-buffer.cc b/game/src/command-buffer.cc index d5cfb0e..e66cd42 100644 --- a/game/src/command-buffer.cc +++ b/game/src/command-buffer.cc @@ -4,13 +4,13 @@ static_assert(std::is_trivially_copyable_v); static_assert(std::is_trivially_copyable_v); -CommandBuffer::CommandBuffer() : batches_(), generation_{0u} {} +CommandBuffer::CommandBuffer() : batches_(), generation_{ 0u } {} Handle CommandBuffer::post(Command cmd) { Batch& batch = CommandBuffer::get_batch(generation_); if (batch.size() >= max_command_count) return Handle::null(); - return {batch.append(cmd), generation_}; + return { batch.append(cmd), generation_ }; } std::optional CommandBuffer::poll(Handle handle) const @@ -22,7 +22,7 @@ std::optional CommandBuffer::poll(Handle handle) const PRECONDITION(handle.index < batch.size(), "handle was never issued in its generation"); CommandRequest const& request = batch[handle.index]; if (request.is_resolved()) return request.as_reply(); - return CommandReply{CommandStatus::Pending}; + return CommandReply{ CommandStatus::Pending }; } void CommandBuffer::dispatch(Context ctx) diff --git a/game/src/command-buffer.hh b/game/src/command-buffer.hh index 831fd15..07bef9d 100644 --- a/game/src/command-buffer.hh +++ b/game/src/command-buffer.hh @@ -46,7 +46,7 @@ private: enum class Kind : bool { Command, Reply }; Kind kind_ = Kind::Command; - Detail detail_ = {.command = {}}; + Detail detail_ = { .command = {} }; }; struct CommandBuffer @@ -98,9 +98,12 @@ private: return size_++; } - [[nodiscard]] std::span view() { return {requests_.data(), size_}; } + [[nodiscard]] std::span view() { return { requests_.data(), size_ }; } - [[nodiscard]] std::span view() const { return {requests_.data(), size_}; } + [[nodiscard]] std::span view() const + { + return { requests_.data(), size_ }; + } [[nodiscard]] u32 size() const { return size_; } diff --git a/game/src/command.cc b/game/src/command.cc index e260ed1..b18d232 100644 --- a/game/src/command.cc +++ b/game/src/command.cc @@ -10,11 +10,11 @@ static_assert(std::is_trivially_copy_assignable_v); static CommandReply resolve(Context ctx, Entity actor, Command::Detail::Move const& move) { auto handle = find_entity(ctx, actor); - if (!handle) return {CommandStatus::Rejected, CommandError::DeadEntity}; + if (!handle) return { CommandStatus::Rejected, CommandError::DeadEntity }; auto pose = access_registry(ctx).try_get(handle); - if (!pose) return {CommandStatus::Rejected, CommandError::MissingComponent}; + if (!pose) return { CommandStatus::Rejected, CommandError::MissingComponent }; pose->position += glm::normalize(glm::vec2(move.x, move.y)); - return {CommandStatus::Accepted}; + return { CommandStatus::Accepted }; } CommandReply submit_command(Context ctx, Command const& cmd) @@ -22,7 +22,7 @@ CommandReply submit_command(Context ctx, Command const& cmd) switch (cmd.kind()) { case Command::Kind::Nil: case Command::Kind::Custom: - return {CommandStatus::Rejected, CommandError::BadCommand}; + return { CommandStatus::Rejected, CommandError::BadCommand }; case Command::Kind::Move: return resolve(ctx, cmd.actor(), cmd.as_move()); } diff --git a/game/src/command.hh b/game/src/command.hh index 3e24a79..2171cce 100644 --- a/game/src/command.hh +++ b/game/src/command.hh @@ -41,9 +41,9 @@ struct Command } move; }; - Command() noexcept : actor_{Entity::Nil}, kind_{Kind::Nil}, body_{} {} + Command() noexcept : actor_{ Entity::Nil }, kind_{ Kind::Nil }, body_{} {} - Command(Entity e, Detail::Move detail) : Command(e, Kind::Move, Detail{detail}) {} + Command(Entity e, Detail::Move detail) : Command(e, Kind::Move, Detail{ detail }) {} Command(Command const&) noexcept = default; Command(Command&&) noexcept = default; @@ -66,7 +66,7 @@ struct Command [[nodiscard]] inline explicit operator bool() const { return !is_empty(); } private: - Command(Entity e, Kind kind, Detail detail) : actor_{e}, kind_{kind}, detail_{detail} + Command(Entity e, Kind kind, Detail detail) : actor_{ e }, kind_{ kind }, detail_{ detail } { DEBUG_ASSERT(actor_ != Entity::Nil); DEBUG_ASSERT(kind_ != Kind::Nil); @@ -84,7 +84,7 @@ private: [[nodiscard]] inline Command make_command_move(Entity e, f32 x, f32 y) { - return Command(e, Command::Detail::Move{x, y}); + return Command(e, Command::Detail::Move{ x, y }); } // Validate and translate a command into an action on its actor. diff --git a/game/src/component/CMakeLists.txt b/game/src/component/CMakeLists.txt index 75477cb..20499c6 100644 --- a/game/src/component/CMakeLists.txt +++ b/game/src/component/CMakeLists.txt @@ -1 +1 @@ -target_sources (${GAME} PRIVATE) +target_sources (game PRIVATE) diff --git a/game/src/context.hh b/game/src/context.hh index 9a6e640..4122848 100644 --- a/game/src/context.hh +++ b/game/src/context.hh @@ -35,7 +35,7 @@ private: friend bool is_valid(ConstContext, Handle); friend Tile const* tile_at(ConstContext ctx, u32 x, u32 y); - ConstContext(Simulation const& src) noexcept : src_{src} {} + ConstContext(Simulation const& src) noexcept : src_{ src } {} Simulation const* operator->() const { return &src_; } @@ -60,7 +60,7 @@ private: friend EntityWriter access_entity(Context, Handle); friend EntityWriter create_entity(Context ctx); - Context(Simulation& src) noexcept : src_{src} {} + Context(Simulation& src) noexcept : src_{ src } {} Simulation* operator->() const { return &src_; } @@ -84,7 +84,7 @@ private: friend struct Simulation; friend CatalogWriter access_catalog(LoadContext); - LoadContext(Simulation& src) noexcept : src_{src} {} + LoadContext(Simulation& src) noexcept : src_{ src } {} Simulation* operator->() const { return &src_; } diff --git a/game/src/data.cc b/game/src/data.cc index 6c0d453..f505812 100644 --- a/game/src/data.cc +++ b/game/src/data.cc @@ -1,7 +1,7 @@ #include "data.hh" #include "catalog.hh" #include "context.hh" -#include "core/lua.hh" +#include "lua.hh" #include "world/terrain.hh" #include #include @@ -16,8 +16,7 @@ bool load_content(LoadContext ctx, std::string_view path) if (!result) { SDL_LogError( SDL_LOG_CATEGORY_ERROR, "Error in %s:\n\t %s", path.data(), // NOLINT - result.error().msg.data() - ); + result.error().msg.data()); return false; } INVARIANT(lua_gettop(L) == 0 && "unprocessed items still on the lua stack"); diff --git a/game/src/app.cc b/game/src/game.cc similarity index 82% rename from game/src/app.cc rename to game/src/game.cc index b03cb0a..8a4dc20 100644 --- a/game/src/app.cc +++ b/game/src/game.cc @@ -1,10 +1,9 @@ +#include "app/main.hh" #include "command-buffer.hh" #include "component/pose.hh" #include "context.hh" -#include "core/math.hh" #include "core/random.hh" #include "data.hh" -#include "engine/app.hh" #include "entity.hh" #include "simulation.hh" #include "types.hh" @@ -26,14 +25,14 @@ void load_chunk(LoadContext ctx, Chunk& chunk) { static u64 const seed = random_seed(); static SplitMix64 rng(seed); - std::uniform_int_distribution dist{0u, chunk_size - 1u}; + std::uniform_int_distribution dist{ 0u, chunk_size - 1u }; GrasslandGenerator(ctx, seed).generate(dist(rng), dist(rng), chunk); } Entity load_player(Context ctx) { auto player = create_entity(ctx); - player.emplace(Vec2f{chunk_size * 0.5f, chunk_size * 0.5f}); + player.emplace(glm::vec2{ chunk_size * 0.5f, chunk_size * 0.5f }); INVARIANT(find_entity(ctx, player.id()) != Handle::null()); return player.id(); } @@ -42,7 +41,6 @@ AppConfig configure(int /*argc*/, char*[] /*argv*/) { return { .step_rate = 32, - .window = {.title = "My Game", .width = 800, .height = 600, .min_zoom = 0.7f}, }; } @@ -78,16 +76,16 @@ void step(AppState& app) */ } -void update(AppState& app, Nanoseconds dt) +void update(AppState& app, f32 delta) { auto ctx = app.sim.context(); - app.ui.update(ctx, dt); + app.ui.update(ctx, delta); } -void render(AppState& app) +void render(AppState& app, f32 alpha) { auto ctx = app.sim.context(); - app.ui.render(ctx); + app.ui.render(ctx, alpha); } void handle_event(AppState& app, SDL_Event const& event) { app.ui.handle_event(event); } diff --git a/core/src/lua.cc b/game/src/lua.cc similarity index 96% rename from core/src/lua.cc rename to game/src/lua.cc index cff6bae..014ac6f 100644 --- a/core/src/lua.cc +++ b/game/src/lua.cc @@ -1,9 +1,10 @@ +#include "lua.hh" #include "core/defer.hh" -#include "core/lua.hh" #include "core/panic.hh" #include #include #include + using namespace lua; static int handle_error(lua_State* L) @@ -14,7 +15,8 @@ static int handle_error(lua_State* L) Error lua::make_content_error(lua_State* L, int found, int expected) { - return {std::format("expected {}, found {}", lua_typename(L, expected), lua_typename(L, found))}; + return { std::format( + "expected {}, found {}", lua_typename(L, expected), lua_typename(L, found)) }; } void lua::push_string(lua_State* L, std::string_view value) diff --git a/core/include/core/lua.hh b/game/src/lua.hh similarity index 87% rename from core/include/core/lua.hh rename to game/src/lua.hh index cbf46e5..a422356 100644 --- a/core/include/core/lua.hh +++ b/game/src/lua.hh @@ -39,6 +39,7 @@ namespace lua { // type wasn't found. Assumes: [L] is not null, [idx] is non-zero, value at // [idx] is a table [[nodiscard]] Result try_push_field(lua_State* L, int type, int idx, std::string_view field); + [[nodiscard]] Result try_get_boolean(lua_State* L, int idx, unsigned field); [[nodiscard]] Result try_get_boolean(lua_State* L, int idx, std::string_view field); [[nodiscard]] Result try_get_string(lua_State* L, int idx, unsigned field); @@ -64,6 +65,22 @@ namespace lua { else static_assert(false, "unsupported field type"); } + template + Result try_get(lua_State* L, int idx, int field) + { + if constexpr (std::same_as) return try_get_boolean(L, idx, field); + else if constexpr (std::integral) + return try_get_integer(L, idx, field).transform([](auto value) { + return static_cast(value); + }); + else if constexpr (std::floating_point) + return try_get_number(L, idx, field).transform([](auto value) { + return static_cast(value); + }); + else if constexpr (std::same_as) return try_get_string(L, idx, field); + else static_assert(false, "unsupported field type"); + } + template struct Field { @@ -108,8 +125,7 @@ namespace lua { out.*member = std::move(*value); return {}; }, - field.member - ); + field.member); if (!result) return std::unexpected(result.error()); } return out; diff --git a/game/src/pilot.hh b/game/src/pilot.hh index f1ae704..895f87f 100644 --- a/game/src/pilot.hh +++ b/game/src/pilot.hh @@ -3,8 +3,8 @@ // TODO: polling of action results // TODO: HerdPilot -#include "engine/input/keyboard.hh" -#include "engine/input/mouse.hh" +#include "app/keyboard.hh" +#include "app/mouse.hh" #include "types.hh" union SDL_Event; diff --git a/game/src/simulation.hh b/game/src/simulation.hh index 9c2881d..51db978 100644 --- a/game/src/simulation.hh +++ b/game/src/simulation.hh @@ -35,7 +35,7 @@ struct Simulation Handle create() { - auto id = Entity{entity_count_++}; + auto id = Entity{ entity_count_++ }; auto handle = registry_.create(id); auto [_, success] = lookup_.emplace(id, handle); INVARIANT(success); diff --git a/game/src/sprite.cc b/game/src/sprite.cc index 9c42309..68abdc7 100644 --- a/game/src/sprite.cc +++ b/game/src/sprite.cc @@ -1,34 +1,48 @@ #include "sprite.hh" +#include "assets.hh" +#include "catalog.hh" #include "core/defer.hh" -#include "core/lua.hh" -#include "world/tile.hh" +#include "gfx/rectangle.hh" +#include "lua.hh" +#include "types.hh" -Sprite make_sprite_1x1(Handle atlas, f32 x, f32 y, Color color) +static Result try_get_rectangle(lua_State* L, int idx, std::string_view field) { - return { - atlas, {x * tile_size, y * tile_size, tile_size, tile_size}, - color - }; -} - -void Sprite::draw(float x, float y, float scale) const -{ - draw_texture(atlas, source, {x, y, source.w * scale, source.h * scale}, color); + auto rectangle = lua::try_push_field(L, LUA_TTABLE, idx, field); + if (!rectangle) return std::unexpected(rectangle.error()); + DEFER(lua_pop(L, 1)); + auto x = lua::try_get(L, *rectangle, 1); + if (!x) return std::unexpected(x.error()); + auto y = lua::try_get(L, *rectangle, 2); + if (!y) return std::unexpected(y.error()); + auto w = lua::try_get(L, *rectangle, 3); + if (!w) return std::unexpected(y.error()); + auto h = lua::try_get(L, *rectangle, 4); + if (!h) return std::unexpected(y.error()); + return Rectangle({ *x, *y }, { *w, *h }); } // sprite = { atlas = "path", x = N, y = N, color = N } -Result lua::try_get_sprite(lua_State* L, int idx, std::string_view field) +Result +lua::try_get_sprite(CatalogWriter catalog, lua_State* L, int idx, std::string_view field) { auto sprite = try_push_field(L, LUA_TTABLE, idx, field); if (!sprite) return std::unexpected(sprite.error()); DEFER(lua_pop(L, 1)); auto atlas = try_get_string(L, *sprite, "atlas"); if (!atlas) return std::unexpected(atlas.error()); - auto x = try_get(L, *sprite, "x"); - if (!x) return std::unexpected(x.error()); - auto y = try_get(L, *sprite, "y"); - if (!y) return std::unexpected(y.error()); + auto source = try_get_rectangle(L, *sprite, "source"); + if (!source) return std::unexpected(source.error()); auto color = try_get(L, *sprite, "color"); if (!color) return std::unexpected(color.error()); - return make_sprite_1x1(create_texture(*atlas), *x, *y, hex_color(*color)); + auto texture = catalog.find(*atlas); + if (!texture) + texture + = catalog.emplace(*atlas, TextureAsset{ *atlas, Handle::null() }); + return Sprite{ + .atlas = texture, + .source = *source, + .tint = make_color_hex(*color), + }; } + diff --git a/game/src/sprite.hh b/game/src/sprite.hh index 888b6f4..a4b4763 100644 --- a/game/src/sprite.hh +++ b/game/src/sprite.hh @@ -1,17 +1,22 @@ #pragma once #include "core/error.hh" -#include "engine/render/textures.hh" +#include "gfx/color.hh" +#include "gfx/rectangle.hh" +#include "types.hh" + +struct TextureAsset; +struct lua_State; struct Sprite { - Handle atlas; - Rectangle source; - Color color; + Token atlas; + Rectangle source; + Color tint; // assumes the sprite is valid void draw(float x, float y, float scale = 1.f) const; }; -Sprite make_sprite_1x1(Handle atlas, f32 x, f32 y, Color color); -struct lua_State; - -namespace lua { Result try_get_sprite(lua_State* L, int idx, std::string_view field); } +namespace lua { + Result + try_get_sprite(CatalogWriter catalog, lua_State* L, int idx, std::string_view field); +} diff --git a/game/src/types.hh b/game/src/types.hh index d3a8460..c411087 100644 --- a/game/src/types.hh +++ b/game/src/types.hh @@ -26,3 +26,6 @@ struct CatalogView; using CatalogReader = CatalogView; using CatalogWriter = CatalogView; +// TODO: make this a user-interface implementation detail +constexpr f32 pixels_per_unit = 16.f; + diff --git a/game/src/user-interface.cc b/game/src/user-interface.cc index fbadef6..2e6eb44 100644 --- a/game/src/user-interface.cc +++ b/game/src/user-interface.cc @@ -1,28 +1,75 @@ +#include "user-interface.hh" +#include "app/event.hh" +#include "assets.hh" #include "catalog.hh" #include "command-buffer.hh" #include "component/pose.hh" #include "context.hh" #include "core/panic.hh" -#include "engine/event.hh" -#include "engine/render/draw.hh" #include "entity.hh" +#include "gfx/projection2D.hh" +#include "gfx/rectangle.hh" +#include "gfx/renderer.hh" +#include "gfx/surface.hh" #include "pilot.hh" #include "scene.hh" -#include "user-interface.hh" +#include "types.hh" +#include "world/chunk.hh" #include "world/tile.hh" +#include #include #include -Result UserInterface::load(ConstContext, Entity player) +// void scene_present(float zoom) +// { +// SDL_SetRenderTarget(renderer, NULL); +// SDL_SetRenderDrawColor(renderer, background.r, background.g, background.b, background.a); +// SDL_RenderClear(renderer); + +// // Sample a centred sub-rectangle of size `window / zoom` and stretch it to +// // fill the window: that stretch is the zoom. +// SDL_FRect src = { +// (scene_px.x - window_px.x / zoom) * 0.5f, +// (scene_px.y - window_px.y / zoom) * 0.5f, +// window_px.x / zoom, +// window_px.y / zoom, +// }; +// SDL_FRect dst = {0.f, 0.f, window_px.x, window_px.y}; +// SDL_RenderTexture(renderer, scene_target, &src, &dst); +// } +// scene_px = glm::ceil(window_px / config.min_zoom); +// constexpr SDL_Color background = {73, 49, 62, SDL_ALPHA_OPAQUE}; + +Result UserInterface::load(LoadContext ctx, Entity player) { PRECONDITION(player != Entity::Nil); - player_ = player; - pilot_ = std::make_unique(); - camera_ = { - .position = { 0.f, 0.f}, - .viewport = {800.f / tile_size, 600.f / tile_size}, - .zoom = 1.3f, + player_ = { + .entity = player, + .pilot = std::make_unique(), + .camera = { + .position = { 0.f, 0.f }, + .zoom = 1.3f, + }, }; + { + auto result = create_window("Game", 800, 600); + if (!result) return std::unexpected(result.error()); + window_ = std::move(*result); + } + { + auto result = create_renderer(window_); + if (!result) return std::unexpected(result.error()); + renderer_ = std::move(*result); + } + auto catalog = access_catalog(ctx); + for (auto& asset : catalog.each()) { + // TODO: reuse the same surface, don't recreate a surface every time + auto surface = load_image(asset.path); + if (!surface) return std::unexpected(surface.error()); + auto texture = renderer_.create_texture(*surface); + if (!texture) return std::unexpected(texture.error()); + asset.handle = textures_.emplace(std::move(*texture)); + } return {}; } @@ -40,39 +87,59 @@ void UserInterface::handle_event(SDL_Event const& event) } break; case SDL_EVENT_MOUSE_WHEEL: - camera_.zoom = glm::clamp(camera_.zoom + 0.5f * event.wheel.y, 0.7f, 1.9f); + player_.camera.zoom = glm::clamp(player_.camera.zoom + 0.5f * event.wheel.y, 0.7f, 1.9f); return; default: break; } - if (pilot_) pilot_->handle_event(event); + if (player_.pilot) player_.pilot->handle_event(event); } void UserInterface::step(CommandBuffer& cmds, ConstContext ctx) { - if (pilot_) pilot_->steer(cmds, ctx, player_); - auto h = find_entity(ctx, player_); + if (player_.pilot) player_.pilot->steer(cmds, ctx, player_.entity); + auto h = find_entity(ctx, player_.entity); if (!h) return; auto e = access_entity(ctx, h); auto pose = e.try_get(); - if (pose) camera_.position = pose->position; + if (pose) player_.camera.position = pose->position; } -void UserInterface::update(ConstContext, Nanoseconds) {} +void UserInterface::update(ConstContext, f32) {} -void UserInterface::render(ConstContext ctx) const +void UserInterface::render(ConstContext ctx, f32) { - scene_begin(); + constexpr Color background = { 73, 49, 62, SDL_ALPHA_OPAQUE }; + Projection2D const project{ + .extent = window_.size(), + .pixels_per_unit = pixels_per_unit, + }; + Rectangle const bounds = project.clip(player_.camera); + + renderer_.clear(background); + auto catalog = access_catalog(ctx); - for (auto coord : camera_) { - auto x = u32(coord.x); - auto y = u32(coord.y); - auto tile = tile_at(ctx, x, y); - if (!tile) continue; - auto pixel = camera_.view_coord_at({x, y}, tile_size); - catalog[tile->terrain].sprite.draw(pixel.x, pixel.y, 1.0f); + + glm::ivec2 lo = glm::ivec2(glm::floor(bounds.min())); + glm::ivec2 hi = glm::ivec2(glm::ceil(bounds.max())); + + lo = glm::max(lo, glm::ivec2{ 0 }); + hi = glm::min(hi, glm::ivec2{ chunk_size }); + + for (i32 x = lo.x; x < hi.x; ++x) { + for (i32 y = lo.y; y < hi.y; ++y) { + Tile const* tile = tile_at(ctx, u32(x), u32(y)); + if (!tile) continue; + auto pixel = project.to_screen_space(player_.camera, { x, y }); + auto& sprite = catalog[tile->terrain].sprite; + Texture* texture = textures_.try_get(catalog[sprite.atlas].handle); + if (!texture) continue; + auto scale = player_.camera.zoom * pixels_per_unit; + Rectangle dst{ pixel, { scale, scale } }; + renderer_.draw_texture(*texture, &sprite.source, &dst, &sprite.tint); + } } - scene_present(camera_.zoom); + renderer_.present(); } diff --git a/game/src/user-interface.hh b/game/src/user-interface.hh index 5680013..c546bfb 100644 --- a/game/src/user-interface.hh +++ b/game/src/user-interface.hh @@ -1,7 +1,10 @@ #pragma once +#include "app/window.hh" #include "core/error.hh" -#include "engine/render/camera.hh" -#include "engine/time.hh" +#include "core/slot-map.hh" +#include "gfx/camera2D.hh" +#include "gfx/renderer.hh" +#include "gfx/texture.hh" #include "pilot.hh" #include @@ -17,16 +20,23 @@ struct UserInterface UserInterface& operator=(UserInterface&&) noexcept = default; UserInterface& operator=(UserInterface const&) = delete; - Result load(ConstContext ctx, Entity player); + Result load(LoadContext ctx, Entity player); void handle_event(SDL_Event const& event); void step(CommandBuffer& cmds, ConstContext ctx); - void update(ConstContext ctx, Nanoseconds dt); - void render(ConstContext ctx) const; + void update(ConstContext ctx, f32 dt); + void render(ConstContext ctx, f32 a); private: - Entity player_; - std::unique_ptr pilot_; - Handle gui_, scene_; - Camera camera_; + struct Player + { + Entity entity; + std::unique_ptr pilot; + Camera2D camera; + }; + + Player player_; + Window window_; + Renderer renderer_; + SlotMap textures_; }; diff --git a/game/src/world/CMakeLists.txt b/game/src/world/CMakeLists.txt index d0687dd..1060e1c 100644 --- a/game/src/world/CMakeLists.txt +++ b/game/src/world/CMakeLists.txt @@ -1 +1 @@ -target_sources (${GAME} PRIVATE world.cc chunk.cc terrain.cc grassland.cc) +target_sources (game PRIVATE terrain.cc grassland.cc) diff --git a/game/src/world/caves.cc b/game/src/world/caves.cc index d7c06b3..908fabc 100644 --- a/game/src/world/caves.cc +++ b/game/src/world/caves.cc @@ -7,8 +7,7 @@ #include void generate_cave( - Grid2& out, u32 wall, u32 floor, u32 birth, u32 survival, u32 range, u32 iterations -) + Grid2& out, u32 wall, u32 floor, u32 birth, u32 survival, u32 range, u32 iterations) { DEBUG_ASSERT(out.size() > 0u); DEBUG_ASSERT(iterations > 0u); @@ -48,7 +47,9 @@ void generate_cave( } CaveGenerator::CaveGenerator(Context const ctx, u64 seed) - : seed_{seed}, wall_{ctx.codex.terrain.find("stone")}, floor_{ctx.codex.terrain.find("dirt")} + : seed_{ seed } + , wall_{ ctx.codex.terrain.find("stone") } + , floor_{ ctx.codex.terrain.find("dirt") } { PRECONDITION(wall_ != nil); PRECONDITION(floor_ != nil); @@ -56,11 +57,11 @@ CaveGenerator::CaveGenerator(Context const ctx, u64 seed) void CaveGenerator::generate(u32 x, u32 y, Chunk& chunk) { - auto const hash = split_mix(seed_ ^ split_mix((u64{x} << 32) | y)); + auto const hash = split_mix(seed_ ^ split_mix((u64{ x } << 32) | y)); Grid2 cave(chunk_size, chunk_size); // uniformly random fill - Xoshiro256ss rng{hash}; - std::bernoulli_distribution coin{0.45f}; + Xoshiro256ss rng{ hash }; + std::bernoulli_distribution coin{ 0.45f }; for (auto& tile : cave) tile = coin(rng) ? wall_ : floor_; generate_cave(cave, wall_, floor_, 5, 6, 1, 1); generate_cave(cave, wall_, floor_, 3, 4, 2, 2); diff --git a/game/src/world/caves.hh b/game/src/world/caves.hh index 9ea9d8f..e669d11 100644 --- a/game/src/world/caves.hh +++ b/game/src/world/caves.hh @@ -5,8 +5,7 @@ // cellular automata rule used for cave generation void generate_cave( - Grid2& out, u32 wall, u32 floor, u32 birth, u32 survival, u32 range, u32 iterations -); + Grid2& out, u32 wall, u32 floor, u32 birth, u32 survival, u32 range, u32 iterations); struct CaveGenerator : ChunkGenerator { diff --git a/game/src/world/chunk.cc b/game/src/world/chunk.cc deleted file mode 100644 index 198bf98..0000000 --- a/game/src/world/chunk.cc +++ /dev/null @@ -1,18 +0,0 @@ -#include "world/chunk.hh" -#include "catalog.hh" -#include "context.hh" -#include "engine/render/camera.hh" -#include "world/terrain.hh" - -void Chunk::render(ConstContext ctx, Camera const& camera, float tile_size) const -{ - auto catalog = access_catalog(ctx); - for (auto coord : camera) { - auto x = u32(coord.x); - auto y = u32(coord.y); - auto tile = get(x, y); - if (!tile) continue; - auto pixel = camera.view_coord_at({x, y}, tile_size); - catalog[tile->terrain].sprite.draw(pixel.x, pixel.y, 1.0f); - } -} diff --git a/game/src/world/grassland.cc b/game/src/world/grassland.cc index 5ce26bf..b4f4a5b 100644 --- a/game/src/world/grassland.cc +++ b/game/src/world/grassland.cc @@ -10,20 +10,20 @@ Token find_terrain(Context ctx, std::string_view label) } GrasslandGenerator::GrasslandGenerator(Context const ctx, u64 seed) - : seed_{seed} + : seed_{ seed } , terrain_{ - find_terrain(ctx, "grass-1"), find_terrain(ctx, "grass-2"), find_terrain(ctx, "grass-3"), - find_terrain(ctx, "grass-tall"), find_terrain(ctx, "dirt"), find_terrain(ctx, "rocks"), - } + find_terrain(ctx, "grass-1"), find_terrain(ctx, "grass-2"), find_terrain(ctx, "grass-3"), + find_terrain(ctx, "grass-tall"), find_terrain(ctx, "dirt"), find_terrain(ctx, "rocks"), + } { for (auto token : terrain_) { PRECONDITION(token, "terrain undefined"); } } void GrasslandGenerator::generate(u32 x, u32 y, Chunk& chunk) { - auto const hash = split_mix(seed_ ^ split_mix((u64{x} << 32) | y)); - Xoshiro256ss rng{hash}; - std::uniform_int_distribution distribution{0u, (u32)std::size(terrain_) - 1u}; + auto const hash = split_mix(seed_ ^ split_mix((u64{ x } << 32) | y)); + Xoshiro256ss rng{ hash }; + std::uniform_int_distribution distribution{ 0u, (u32)std::size(terrain_) - 1u }; for (auto& tile : chunk) { tile.terrain = terrain_[distribution(rng)]; } } diff --git a/game/src/world/terrain.cc b/game/src/world/terrain.cc index fc8b640..f8b837a 100644 --- a/game/src/world/terrain.cc +++ b/game/src/world/terrain.cc @@ -1,7 +1,7 @@ #include "world/terrain.hh" #include "catalog.hh" #include "core/defer.hh" -#include "core/lua.hh" +#include "lua.hh" #include "sprite.hh" #include "types.hh" #include @@ -17,13 +17,12 @@ static int parse_terrain_table(lua_State* L) // arg 1: definition table if (!lua_istable(L, 1)) { lua::push_fstring( - L, "terrain '{}': expected table, found {}", name, lua_typename(L, lua_type(L, 1)) - ); + L, "terrain '{}': expected table, found {}", name, lua_typename(L, lua_type(L, 1))); break; } int terrain = lua_gettop(L); - auto sprite = lua::try_get_sprite(L, terrain, "sprite"); + auto sprite = lua::try_get_sprite(*catalog, L, terrain, "sprite"); if (!sprite) { lua::push_string(L, sprite.error().msg); break; @@ -43,8 +42,7 @@ static int build_terrain(lua_State* L) // arg 1: name string if (!lua_isstring(L, 1)) { lua_pushstring( - L, std::format("terrain: expected string, found {}", lua_typename(L, lua_type(L, 1))).data() - ); + L, std::format("terrain: expected string, found {}", lua_typename(L, lua_type(L, 1))).data()); return lua_error(L); } lua_pushvalue(L, lua_upvalueindex(1)); diff --git a/game/src/world/terrain.hh b/game/src/world/terrain.hh index 92bc815..571ff11 100644 --- a/game/src/world/terrain.hh +++ b/game/src/world/terrain.hh @@ -2,11 +2,11 @@ #include "sprite.hh" #include "types.hh" +struct lua_State; + struct Terrain { Sprite sprite; }; -struct lua_State; - namespace lua { void add_terrain_builder(lua_State* L, CatalogWriter& catalog); } diff --git a/game/src/world/tile.hh b/game/src/world/tile.hh index 609491e..12ae302 100644 --- a/game/src/world/tile.hh +++ b/game/src/world/tile.hh @@ -1,6 +1,5 @@ #pragma once #include "core/types.hh" -constexpr f32 tile_size = 16.f; struct Terrain; diff --git a/game/src/world/world.cc b/game/src/world/world.cc index b9a639e..1f9f60d 100644 --- a/game/src/world/world.cc +++ b/game/src/world/world.cc @@ -9,10 +9,9 @@ World::World( u32 width, u32 height, std::unique_ptr generator, - std::unique_ptr loader -) - : width_{width} - , height_{height} + std::unique_ptr loader) + : width_{ width } + , height_{ height } , generator_(generator.release()) , loader_(loader.release()) , chunks_() @@ -61,16 +60,3 @@ Chunk& World::get_chunk(u64 key) return chunk; } -void World::render(ConstContext ctx, Camera const& camera, float tile_size) const -{ - auto catalog = access_catalog(ctx); - for (auto coord : camera) { - auto x = u32(coord.x); - auto y = u32(coord.y); - wrap_around(x, y); - auto tile = find(x, y); - INVARIANT(tile, "tile not found despite wrap around"); - auto pixel = camera.view_coord_at({x, y}, tile_size); - catalog[tile->terrain].sprite.draw(pixel.x, pixel.y, 1.0f); - } -} diff --git a/game/src/world/world.hh b/game/src/world/world.hh index 8e90fca..09e6845 100644 --- a/game/src/world/world.hh +++ b/game/src/world/world.hh @@ -13,14 +13,13 @@ struct World u32 width, u32 height, std::unique_ptr generator, - std::unique_ptr loader - ); + std::unique_ptr loader); World(World const& other) = delete; World& operator=(World const& other) = delete; World(World&& other) = default; World& operator=(World&& other) = default; - constexpr u64 size() const { return u64{chunk_size} * width_ * height_; } + constexpr u64 size() const { return u64{ chunk_size } * width_ * height_; } constexpr bool has(u32 x, u32 y) const { return x < width_ && y < height_; } diff --git a/gfx/CMakeLists.txt b/gfx/CMakeLists.txt new file mode 100644 index 0000000..12ce0c1 --- /dev/null +++ b/gfx/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library (gfx) + +target_include_directories ( + gfx + PRIVATE ${SDL3_INCLUDE_DIRS} + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +target_link_libraries ( + gfx + PRIVATE SDL3::SDL3 + PUBLIC glm::glm + PUBLIC core + PUBLIC app +) + +add_subdirectory (src) + diff --git a/gfx/include/gfx/camera2D.hh b/gfx/include/gfx/camera2D.hh new file mode 100644 index 0000000..18387c0 --- /dev/null +++ b/gfx/include/gfx/camera2D.hh @@ -0,0 +1,10 @@ +#pragma once +#include "core/types.hh" +#include + +struct Camera2D +{ + glm::vec2 position; // the world position the camera is centered on + f32 zoom; +}; + diff --git a/gfx/include/gfx/color.hh b/gfx/include/gfx/color.hh new file mode 100644 index 0000000..a727303 --- /dev/null +++ b/gfx/include/gfx/color.hh @@ -0,0 +1,13 @@ +#pragma once +#include "core/types.hh" + +struct Color +{ + u8 r, g, b, a; +}; + +constexpr Color make_color_hex(u32 rgba) +{ + return { u8(rgba >> 24), u8(rgba >> 16), u8(rgba >> 8), u8(rgba) }; +} + diff --git a/gfx/include/gfx/projection2D.hh b/gfx/include/gfx/projection2D.hh new file mode 100644 index 0000000..32f9a74 --- /dev/null +++ b/gfx/include/gfx/projection2D.hh @@ -0,0 +1,50 @@ +#pragma once +#include "gfx/camera2D.hh" +#include "gfx/rectangle.hh" +#include + +struct Projection2D +{ + glm::vec2 extent; // dimensions in pixels + f32 pixels_per_unit; // needed to translate pixel coordinates to world coordinates + + [[nodiscard]] glm::vec2 to_world_space(Camera2D const& camera, glm::vec2 pixel) const noexcept + { + return view_to_world(camera, screen_to_view(pixel)); + } + + [[nodiscard]] glm::vec2 to_screen_space(Camera2D const& camera, glm::vec2 unit) const noexcept + { + return view_to_screen(world_to_view(camera, unit)); + } + + [[nodiscard]] Rectangle clip(Camera2D const& camera) const noexcept + { + glm::vec2 const lo = to_world_space(camera, { 0.f, 0.f }); + glm::vec2 const hi = to_world_space(camera, extent); + return { lo, hi - lo }; + } + +private: + + [[nodiscard]] glm::vec2 view_to_screen(glm::vec2 unit) const noexcept + { + return unit * pixels_per_unit + extent * 0.5f; + } + + [[nodiscard]] glm::vec2 screen_to_view(glm::vec2 pixel) const noexcept + { + return (pixel - extent * 0.5f) / pixels_per_unit; + } + + [[nodiscard]] glm::vec2 world_to_view(Camera2D const& camera, glm::vec2 unit) const noexcept + { + return (unit - camera.position) * camera.zoom; + } + + [[nodiscard]] glm::vec2 view_to_world(Camera2D const& camera, glm::vec2 unit) const noexcept + { + return unit / camera.zoom + camera.position; + } +}; + diff --git a/gfx/include/gfx/rectangle.hh b/gfx/include/gfx/rectangle.hh new file mode 100644 index 0000000..8bb1b23 --- /dev/null +++ b/gfx/include/gfx/rectangle.hh @@ -0,0 +1,37 @@ +#pragma once +#include "core/types.hh" +#include +#include + +struct Rectangle +{ + glm::vec2 origin, extent; + + constexpr bool contains(glm::vec2 coord) const + { + return coord.x >= origin.x && coord.y >= origin.y && coord.y < origin.y + extent.y + && coord.x < origin.x + extent.x; + } + + [[nodiscard]] constexpr glm::vec2 min() const noexcept { return origin; } + + [[nodiscard]] constexpr glm::vec2 max() const noexcept { return origin + extent; } + + [[nodiscard]] constexpr f32 x() const noexcept { return origin.x; } + + [[nodiscard]] constexpr f32 y() const noexcept { return origin.y; } + + [[nodiscard]] constexpr f32 width() const noexcept { return extent.x; } + + [[nodiscard]] constexpr f32 height() const noexcept { return extent.y; } + + [[nodiscard]] constexpr f32 area() const noexcept { return extent.x * extent.y; } + + [[nodiscard]] Rectangle clip(Rectangle bounds) const + { + auto const lo = glm::clamp(bounds.min(), min(), max()); + auto const hi = glm::clamp(bounds.max(), lo, max()); + return Rectangle(lo, hi - lo); + } +}; + diff --git a/gfx/include/gfx/renderer.hh b/gfx/include/gfx/renderer.hh new file mode 100644 index 0000000..32762b6 --- /dev/null +++ b/gfx/include/gfx/renderer.hh @@ -0,0 +1,50 @@ +#pragma once + +// TODO: migration to SDL3 GPU for shader support +// TODO: headless renderer + +#include "core/error.hh" +#include "gfx/color.hh" +#include "gfx/rectangle.hh" + +struct Renderer; +struct Surface; +struct Texture; +struct Window; + +struct SDL_Renderer; + +[[nodiscard]] Result create_renderer(Window& window); + +struct Renderer +{ + Renderer() noexcept = default; + Renderer(Renderer const&) = delete; + Renderer& operator=(Renderer const&) = delete; + + explicit Renderer(SDL_Renderer*) noexcept; + Renderer(Renderer&&) noexcept; + Renderer& operator=(Renderer&&) noexcept; + ~Renderer() noexcept; + + void draw_texture( + Texture& texture, Rectangle const* src, Rectangle const* dst, Color const* tint) noexcept; + + [[nodiscard]] Result create_texture(int width, int height); + [[nodiscard]] Result create_texture(Surface& surface); + [[nodiscard]] Result create_target(int width, int height); + + void set_target(Texture& texture) noexcept; + void reset_target() noexcept; + + void clear(Color color) noexcept; + void present() noexcept; + + [[nodiscard]] bool valid() const noexcept { return handle_ != nullptr; } + + [[nodiscard]] SDL_Renderer* handle() noexcept { return handle_; } + +private: + SDL_Renderer* handle_ = nullptr; +}; + diff --git a/gfx/include/gfx/surface.hh b/gfx/include/gfx/surface.hh new file mode 100644 index 0000000..83e0432 --- /dev/null +++ b/gfx/include/gfx/surface.hh @@ -0,0 +1,28 @@ +#pragma once +#include "core/error.hh" +#include + +struct Surface; + +struct SDL_Surface; + +[[nodiscard]] Result load_image(std::string_view path); + +struct Surface +{ + Surface() noexcept = default; + Surface(Surface const&) = delete; + Surface& operator=(Surface const&) = delete; + + explicit Surface(SDL_Surface* data) noexcept; + Surface(Surface&& other) noexcept; + Surface& operator=(Surface&& other) noexcept; + ~Surface() noexcept; + + [[nodiscard]] bool valid() const noexcept { return handle_ != nullptr; } + + [[nodiscard]] SDL_Surface* handle() noexcept { return handle_; } + +private: + SDL_Surface* handle_ = nullptr; +}; diff --git a/gfx/include/gfx/texture.hh b/gfx/include/gfx/texture.hh new file mode 100644 index 0000000..e2ed0df --- /dev/null +++ b/gfx/include/gfx/texture.hh @@ -0,0 +1,31 @@ +#pragma once + +#include + +struct SDL_Renderer; +struct SDL_Texture; + +struct Texture +{ + Texture() noexcept = default; + Texture(Texture const&) = delete; + Texture& operator=(Texture const&) = delete; + + Texture(SDL_Renderer*, SDL_Texture*) noexcept; + Texture(Texture&&) noexcept; + Texture& operator=(Texture&&) noexcept; + ~Texture() noexcept; + + [[nodiscard]] bool valid() const noexcept { return handle_ != nullptr; } + + [[nodiscard]] SDL_Texture* handle() noexcept { return handle_; } + + [[nodiscard]] SDL_Renderer* owner() noexcept { return owner_; } + + glm::vec2 size() noexcept; + +private: + SDL_Renderer* owner_ = nullptr; + SDL_Texture* handle_ = nullptr; +}; + diff --git a/gfx/src/CMakeLists.txt b/gfx/src/CMakeLists.txt new file mode 100644 index 0000000..0078a7a --- /dev/null +++ b/gfx/src/CMakeLists.txt @@ -0,0 +1 @@ +target_sources (gfx PRIVATE renderer.cc texture.cc surface.cc) diff --git a/gfx/src/renderer.cc b/gfx/src/renderer.cc new file mode 100644 index 0000000..1bf066c --- /dev/null +++ b/gfx/src/renderer.cc @@ -0,0 +1,128 @@ +#include "gfx/renderer.hh" +#include "app/window.hh" +#include "core/panic.hh" +#include "gfx/surface.hh" +#include "gfx/texture.hh" +#include +#include +#include +#include +#include +#include +#include + +static void to_sdl_rect(SDL_FRect& to, Rectangle const& from) noexcept +{ + to.x = from.x(); + to.y = from.y(); + to.w = from.width(); + to.h = from.height(); +} + +Renderer::Renderer(SDL_Renderer* handle) noexcept : handle_{ handle } +{ + PRECONDITION(handle != nullptr); +} + +Renderer::Renderer(Renderer&& other) noexcept : handle_{ other.handle_ } +{ + other.handle_ = nullptr; +} + +Renderer& Renderer::operator=(Renderer&& other) noexcept +{ + if (&other == this) return *this; + handle_ = other.handle_; + other.handle_ = nullptr; + return *this; +} + +Renderer::~Renderer() noexcept { SDL_DestroyRenderer(handle_); } + +Result create_renderer(Window& window) +{ + SDL_Renderer* handle = SDL_CreateRenderer(window.handle(), nullptr); + if (!handle) return std::unexpected(SDL_GetError()); + if (!SDL_SetRenderVSync(handle, 1)) + std::println("[Warning] Could not enable vsync: %s", SDL_GetError()); + return Renderer(handle); +} + +void Renderer::draw_texture( + Texture& texture, Rectangle const* src, Rectangle const* dst, Color const* tint) noexcept +{ + DEBUG_ASSERT(texture.owner() == handle_); + + SDL_FRect s, d; + SDL_FRect* from = NULL; + SDL_FRect* to = NULL; + + if (src) { + to_sdl_rect(s, *src); + from = &s; + } + + if (dst) { + to_sdl_rect(d, *dst); + to = &d; + } + + if (tint) { + SDL_SetTextureColorMod(texture.handle(), tint->r, tint->g, tint->b); + SDL_SetTextureAlphaMod(texture.handle(), tint->a); + } + + SDL_RenderTexture(handle_, texture.handle(), from, to); +} + +Result Renderer::create_texture(int w, int h) +{ + SDL_Texture* texture + = SDL_CreateTexture(handle_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STATIC, w, h); + if (!texture) return std::unexpected(SDL_GetError()); + if (!SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST)) + std::println("[Warning] Could not set scale mode: %s", SDL_GetError()); + return Texture(handle_, texture); +} + +Result Renderer::create_texture(Surface& surface) +{ + SDL_Texture* texture = SDL_CreateTextureFromSurface(handle_, surface.handle()); + if (!texture) return std::unexpected(SDL_GetError()); + if (!SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST)) + std::println("[Warning] Could not set scale mode: %s", SDL_GetError()); + return Texture(handle_, texture); +} + +Result Renderer::create_target(int width, int height) +{ + auto target + = SDL_CreateTexture(handle_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, width, height); + if (!target) return std::unexpected(SDL_GetError()); + if (!SDL_SetTextureScaleMode(target, SDL_SCALEMODE_NEAREST)) + std::println("[Warning] Could not set scale mode: %s", SDL_GetError()); + return Texture(handle_, target); +} + +void Renderer::set_target(Texture& texture) noexcept +{ + SDL_SetRenderTarget(handle_, texture.handle()); +} + +void Renderer::reset_target() noexcept { SDL_SetRenderTarget(handle_, NULL); } + +void Renderer::clear(Color color) noexcept +{ + SDL_SetRenderDrawColor(handle_, color.r, color.g, color.b, color.a); + SDL_RenderClear(handle_); +} + +void Renderer::present() noexcept { SDL_RenderPresent(handle_); } + +// void renderer::draw_rectangle(rectangle& rect, color color) +// { +// sdl_frect r; +// to_sdl_rect(r, rect); +// sdl_setrenderdrawcolor(handle_, color.r, color.g, color.b, color.a); +// sdl_renderfillrect(handle_, &r); +// } diff --git a/gfx/src/surface.cc b/gfx/src/surface.cc new file mode 100644 index 0000000..b18ec59 --- /dev/null +++ b/gfx/src/surface.cc @@ -0,0 +1,36 @@ +#include "gfx/surface.hh" +#include "core/panic.hh" +#include +#include +#include + +Surface::Surface(SDL_Surface* handle) noexcept : handle_{ handle } +{ + PRECONDITION(handle != nullptr); +} + +Surface::Surface(Surface&& other) noexcept : handle_{ other.handle_ } { other.handle_ = nullptr; } + +Surface& Surface::operator=(Surface&& other) noexcept +{ + if (this == &other) return *this; + handle_ = other.handle_; + other.handle_ = nullptr; + return *this; +} + +Surface::~Surface() noexcept { SDL_DestroySurface(handle_); } + +Result load_image(std::string_view path) +{ + SDL_Surface* surface = nullptr; + if (path.ends_with(".png")) surface = SDL_LoadPNG(std::string(path).c_str()); + else if (path.ends_with(".bmp")) surface = SDL_LoadBMP(std::string(path).c_str()); + else { + return std::unexpected( + std::format("Unsupported file type (expected BMP or PNG): {}", path)); + } + if (!surface) { return std::unexpected(SDL_GetError()); } + return Surface(surface); +} + diff --git a/gfx/src/texture.cc b/gfx/src/texture.cc new file mode 100644 index 0000000..3b3636a --- /dev/null +++ b/gfx/src/texture.cc @@ -0,0 +1,37 @@ +#include "gfx/texture.hh" +#include "core/panic.hh" +#include "core/types.hh" +#include + +Texture::Texture(SDL_Renderer* renderer, SDL_Texture* texture) noexcept + : owner_{ renderer }, handle_{ texture } +{ + PRECONDITION(renderer != nullptr); + PRECONDITION(texture != nullptr); +} + +Texture::Texture(Texture&& other) noexcept : owner_{ other.owner_ }, handle_{ other.handle_ } +{ + other.handle_ = nullptr; + other.owner_ = nullptr; +} + +Texture& Texture::operator=(Texture&& other) noexcept +{ + if (this == &other) return *this; + handle_ = other.handle_; + owner_ = other.owner_; + other.handle_ = nullptr; + other.owner_ = nullptr; + return *this; +} + +Texture::~Texture() noexcept { SDL_DestroyTexture(handle_); } + +glm::vec2 Texture::size() noexcept +{ + f32 w, h; + SDL_GetTextureSize(handle_, &w, &h); + return { w, h }; +} +