diff --git a/.gitignore b/.gitignore index ddcbd9898..df0b05856 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ conan/test_package/build CMakeSettings.json cpp.hint +# Local builds +/build/ +/build-*/ + # Bazel /bazel-* /test/bazel-* diff --git a/testbed/CMakeLists.txt b/testbed/CMakeLists.txt index f3190192d..62537a842 100644 --- a/testbed/CMakeLists.txt +++ b/testbed/CMakeLists.txt @@ -47,6 +47,7 @@ target_sources( application/application.cpp application/context.cpp meta/meta.cpp + system/gameplay_system.cpp system/hud_system.cpp system/imgui_system.cpp system/input_system.cpp diff --git a/testbed/application/application.cpp b/testbed/application/application.cpp index 8c0da7ff6..2a146435c 100644 --- a/testbed/application/application.cpp +++ b/testbed/application/application.cpp @@ -1,16 +1,22 @@ #include +#include #include #include #include #include #include +#include +#include +#include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -23,8 +29,12 @@ void application::update(entt::registry ®istry) { ImGui_ImplSDL3_NewFrame(); ImGui::NewFrame(); - // update... - static_cast(registry); + static Uint64 last_tick = SDL_GetTicks(); + const Uint64 current_tick = SDL_GetTicks(); + const float delta_time = static_cast(current_tick - last_tick) / 1000.f; + last_tick = current_tick; + + gameplay_system(registry, delta_time); } void application::draw(entt::registry ®istry, const context &context) const { @@ -44,7 +54,6 @@ void application::draw(entt::registry ®istry, const context &context) const { } void application::input(entt::registry ®istry) { - ImGuiIO &io = ImGui::GetIO(); SDL_Event event{}; while(SDL_PollEvent(&event)) { @@ -62,13 +71,23 @@ application::~application() { SDL_Quit(); } -static void static_setup_for_dev_purposes(entt::registry ®istry) { - const auto entt = registry.create(); - - registry.emplace(entt); - registry.emplace(entt, SDL_FPoint{400.f, 400.f}); - registry.emplace(entt, SDL_FRect{0.f, 0.f, 20.f, 20.f}); - registry.emplace(entt); +void application::setup(entt::registry ®istry) const { + registry.ctx().emplace(); + + const auto player = registry.create(); + registry.emplace(player); + registry.emplace(player, 420.f); + registry.emplace(player, SDL_FPoint{240.f, 240.f}); + registry.emplace(player, SDL_FRect{0.f, 0.f, 72.f, 72.f}); + registry.emplace(player, color_component{99u, 179u, 237u}); + registry.emplace(player); + + const auto collectible = registry.create(); + registry.emplace(collectible, 1); + registry.emplace(collectible, SDL_FPoint{920.f, 420.f}); + registry.emplace(collectible, SDL_FRect{0.f, 0.f, 48.f, 48.f}); + registry.emplace(collectible, color_component{255u, 211u, 92u}); + registry.emplace(collectible); } int application::run() { @@ -76,7 +95,7 @@ int application::run() { context context{}; meta_setup(); - static_setup_for_dev_purposes(registry); + setup(registry); quit = false; diff --git a/testbed/application/application.h b/testbed/application/application.h index a8b04bcad..cf06c71b4 100644 --- a/testbed/application/application.h +++ b/testbed/application/application.h @@ -22,6 +22,7 @@ class application { int run(); private: + void setup(entt::registry &) const; bool quit; }; diff --git a/testbed/component/collectible_component.h b/testbed/component/collectible_component.h new file mode 100644 index 000000000..2e8b10c39 --- /dev/null +++ b/testbed/component/collectible_component.h @@ -0,0 +1,9 @@ +#pragma once + +namespace testbed { + +struct collectible_component { + int value; +}; + +} // namespace testbed diff --git a/testbed/component/color_component.h b/testbed/component/color_component.h new file mode 100644 index 000000000..5a33f7883 --- /dev/null +++ b/testbed/component/color_component.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace testbed { + +struct color_component { + std::uint8_t red; + std::uint8_t green; + std::uint8_t blue; +}; + +} // namespace testbed diff --git a/testbed/component/game_state_component.h b/testbed/component/game_state_component.h new file mode 100644 index 000000000..3ffe9dab4 --- /dev/null +++ b/testbed/component/game_state_component.h @@ -0,0 +1,12 @@ +#pragma once + +namespace testbed { + +struct game_state_component { + int score{}; + int best_score{}; + float remaining_time{30.f}; + bool running{true}; +}; + +} // namespace testbed diff --git a/testbed/component/input_listener_component.h b/testbed/component/input_listener_component.h index 547ac1d88..096db1b2e 100644 --- a/testbed/component/input_listener_component.h +++ b/testbed/component/input_listener_component.h @@ -3,14 +3,10 @@ namespace testbed { struct input_listener_component { - enum class type { - UP, - DOWN, - LEFT, - RIGHT - }; - - type command; + bool up{}; + bool down{}; + bool left{}; + bool right{}; }; } // namespace testbed diff --git a/testbed/component/player_component.h b/testbed/component/player_component.h new file mode 100644 index 000000000..a825a362c --- /dev/null +++ b/testbed/component/player_component.h @@ -0,0 +1,9 @@ +#pragma once + +namespace testbed { + +struct player_component { + float speed; +}; + +} // namespace testbed diff --git a/testbed/meta/meta.cpp b/testbed/meta/meta.cpp index 16943e66f..9bad9b6b6 100644 --- a/testbed/meta/meta.cpp +++ b/testbed/meta/meta.cpp @@ -1,5 +1,9 @@ #include +#include +#include +#include #include +#include #include #include #include @@ -21,16 +25,12 @@ void meta_setup() { .data<&SDL_FRect::w>("w") .data<&SDL_FRect::h>("h"); - entt::meta_factory() - .type("command type") - .data("up") - .data("down") - .data("left") - .data("right"); - entt::meta_factory() .type("input listener") - .data<&input_listener_component::command>("command"); + .data<&input_listener_component::up>("up") + .data<&input_listener_component::down>("down") + .data<&input_listener_component::left>("left") + .data<&input_listener_component::right>("right"); entt::meta_factory() .type("point"); @@ -45,6 +45,27 @@ void meta_setup() { entt::meta_factory() .type("renderable"); + + entt::meta_factory() + .type("player") + .data<&player_component::speed>("speed"); + + entt::meta_factory() + .type("collectible") + .data<&collectible_component::value>("value"); + + entt::meta_factory() + .type("color") + .data<&color_component::red>("red") + .data<&color_component::green>("green") + .data<&color_component::blue>("blue"); + + entt::meta_factory() + .type("game state") + .data<&game_state_component::score>("score") + .data<&game_state_component::best_score>("best_score") + .data<&game_state_component::remaining_time>("remaining_time") + .data<&game_state_component::running>("running"); } } // namespace testbed diff --git a/testbed/system/gameplay_system.cpp b/testbed/system/gameplay_system.cpp new file mode 100644 index 000000000..5bae4746e --- /dev/null +++ b/testbed/system/gameplay_system.cpp @@ -0,0 +1,103 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace testbed { + +namespace { + +[[nodiscard]] bool intersects(const SDL_FRect &lhs, const SDL_FRect &rhs) { + return lhs.x < (rhs.x + rhs.w) && (lhs.x + lhs.w) > rhs.x && lhs.y < (rhs.y + rhs.h) && (lhs.y + lhs.h) > rhs.y; +} + +[[nodiscard]] SDL_FRect world_rect(const position_component &pos, const rect_component &rect) { + return SDL_FRect{pos.x + rect.x, pos.y + rect.y, rect.w, rect.h}; +} + +void clamp_player(position_component &position, const rect_component &rect) { + constexpr float world_width = 1920.f; + constexpr float world_height = 1080.f; + + position.x = std::clamp(position.x, 0.f, world_width - rect.w); + position.y = std::clamp(position.y, 0.f, world_height - rect.h); +} + +void respawn_collectible(position_component &position, int score) { + constexpr float min_x = 80.f; + constexpr float min_y = 120.f; + constexpr float x_span = 1680.f; + constexpr float y_span = 780.f; + + const auto seed = static_cast((score * 131) % 997); + position.x = min_x + std::fmod(seed * 73.f, x_span); + position.y = min_y + std::fmod(seed * 37.f, y_span); +} + +} // namespace + +void gameplay_system(entt::registry ®istry, float delta_time) { + auto &state = registry.ctx().get(); + + if(state.running) { + state.remaining_time = std::max(0.f, state.remaining_time - delta_time); + state.running = state.remaining_time > 0.f; + } + + auto player_view = registry.view(); + + for(auto [entity, player, input, position, rect]: player_view.each()) { + static_cast(entity); + + if(state.running) { + float x_axis = 0.f; + float y_axis = 0.f; + + if(input.left) { + x_axis -= 1.f; + } + + if(input.right) { + x_axis += 1.f; + } + + if(input.up) { + y_axis -= 1.f; + } + + if(input.down) { + y_axis += 1.f; + } + + position.x += x_axis * player.speed * delta_time; + position.y += y_axis * player.speed * delta_time; + clamp_player(position, rect); + } + + const auto player_rect = world_rect(position, rect); + auto collectible_view = registry.view(); + + for(auto [collectible_entity, collectible, collectible_position, collectible_rect]: collectible_view.each()) { + if(state.running && intersects(player_rect, world_rect(collectible_position, collectible_rect))) { + state.score += collectible.value; + state.best_score = std::max(state.best_score, state.score); + respawn_collectible(collectible_position, state.score); + + collectible.value = 1 + (state.score / 5); + break; + } + + static_cast(collectible_entity); + } + } +} + +} // namespace testbed diff --git a/testbed/system/gameplay_system.h b/testbed/system/gameplay_system.h new file mode 100644 index 000000000..1d96b15fb --- /dev/null +++ b/testbed/system/gameplay_system.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace testbed { + +void gameplay_system(entt::registry &, float); + +} // namespace testbed diff --git a/testbed/system/hud_system.cpp b/testbed/system/hud_system.cpp index dbd49ea94..7cb6b0ed0 100644 --- a/testbed/system/hud_system.cpp +++ b/testbed/system/hud_system.cpp @@ -1,13 +1,33 @@ #include +#include #include +#include #include namespace testbed { void hud_system(entt::registry ®istry, const context &ctx) { - // render... - static_cast(registry); + const auto &state = registry.ctx().get(); static_cast(ctx); + + ImGui::SetNextWindowPos(ImVec2{20.f, 20.f}, ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.8f); + + if(ImGui::Begin("Collector HUD", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) { + ImGui::Text("Arrow keys / WASD to move"); + ImGui::Text("Collect the gold square before time runs out"); + ImGui::Separator(); + ImGui::Text("Score: %d", state.score); + ImGui::Text("Best score: %d", state.best_score); + ImGui::Text("Time left: %.1f s", state.remaining_time); + + if(!state.running) { + ImGui::Separator(); + ImGui::TextUnformatted("Round complete. Restart the app to play again."); + } + } + + ImGui::End(); } } // namespace testbed diff --git a/testbed/system/imgui_system.cpp b/testbed/system/imgui_system.cpp index 62b5d0888..f772ee9b5 100644 --- a/testbed/system/imgui_system.cpp +++ b/testbed/system/imgui_system.cpp @@ -1,4 +1,8 @@ +#include +#include +#include #include +#include #include #include #include @@ -14,11 +18,23 @@ void imgui_system(const entt::registry ®istry) { ImGui::End(); ImGui::Begin("Davey - view"); - entt::davey(registry.view()); + entt::davey(registry.view()); ImGui::End(); ImGui::Begin("Davey - storage"); - entt::davey(*registry.storage()); + entt::davey(*registry.storage()); + ImGui::End(); + + ImGui::Begin("Game State"); + const auto &state = registry.ctx().get(); + ImGui::Text("Score: %d", state.score); + ImGui::Text("Best score: %d", state.best_score); + ImGui::Text("Time left: %.1f", state.remaining_time); + ImGui::Text("Running: %s", state.running ? "true" : "false"); + ImGui::End(); + + ImGui::Begin("Collectible"); + entt::davey(*registry.storage()); ImGui::End(); } diff --git a/testbed/system/input_system.cpp b/testbed/system/input_system.cpp index ad43ac9b3..584b847b0 100644 --- a/testbed/system/input_system.cpp +++ b/testbed/system/input_system.cpp @@ -7,9 +7,26 @@ namespace testbed { namespace internal { -static void update_listeners(entt::registry ®istry, input_listener_component::type command) { +static void update_listeners(entt::registry ®istry, SDL_Keycode key, bool pressed) { for([[maybe_unused]] auto [entt, elem]: registry.view().each()) { - elem.command = command; + switch(key) { + case SDLK_UP: + case SDLK_W: + elem.up = pressed; + break; + case SDLK_DOWN: + case SDLK_S: + elem.down = pressed; + break; + case SDLK_LEFT: + case SDLK_A: + elem.left = pressed; + break; + case SDLK_RIGHT: + case SDLK_D: + elem.right = pressed; + break; + } } } @@ -25,20 +42,14 @@ void input_system(entt::registry ®istry, const SDL_Event &event, bool &quit) case SDLK_ESCAPE: quit = true; break; - case SDLK_UP: - internal::update_listeners(registry, input_listener_component::type::UP); - break; - case SDLK_DOWN: - internal::update_listeners(registry, input_listener_component::type::DOWN); - break; - case SDLK_LEFT: - internal::update_listeners(registry, input_listener_component::type::LEFT); - break; - case SDLK_RIGHT: - internal::update_listeners(registry, input_listener_component::type::RIGHT); + default: + internal::update_listeners(registry, event.key.key, true); break; } break; + case SDL_EVENT_KEY_UP: + internal::update_listeners(registry, event.key.key, false); + break; } } diff --git a/testbed/system/rendering_system.cpp b/testbed/system/rendering_system.cpp index e6630c0e2..0aac71686 100644 --- a/testbed/system/rendering_system.cpp +++ b/testbed/system/rendering_system.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -16,9 +17,11 @@ void rendering_system(entt::registry ®istry, const context &ctx) { SDL_SetRenderDrawColor(ctx, 0u, 0u, 0u, SDL_ALPHA_OPAQUE); SDL_RenderClear(ctx); - for(auto [entt, pos, rect]: registry.view().each()) { + for(auto [entt, pos, rect, color]: registry.view().each()) { SDL_FRect elem{rect.x + pos.x, rect.y + pos.y, rect.w, rect.h}; - SDL_SetRenderDrawColor(ctx, 255u, 255u, 255u, SDL_ALPHA_OPAQUE); + SDL_SetRenderDrawColor(ctx, color.red, color.green, color.blue, SDL_ALPHA_OPAQUE); + SDL_RenderFillRect(ctx, &elem); + SDL_SetRenderDrawColor(ctx, 15u, 18u, 24u, SDL_ALPHA_OPAQUE); SDL_RenderRect(ctx, &elem); }