From 65f0acc23900a6d8796211e7c8ea21a3863935eb Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Thu, 13 Jun 2024 15:15:11 +0530 Subject: [PATCH 1/7] Upgrade `Scene`. --- .gitignore | 1 + src/spaced/spaced/scene.cpp | 63 ++++++++++++++++++++++-- src/spaced/spaced/scene.hpp | 25 ++++++++-- src/spaced/spaced/scenes/load_assets.cpp | 17 ++----- src/spaced/spaced/scenes/load_assets.hpp | 11 +---- src/spaced/spaced/spaced.cpp | 3 +- src/spaced/spaced/ui/view.hpp | 1 + 7 files changed, 91 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 7a70de4..ca09e17 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ compile_commands.json imgui.ini bave*.log /spaced +/notes.txt diff --git a/src/spaced/spaced/scene.cpp b/src/spaced/spaced/scene.cpp index a339c55..dd73b7c 100644 --- a/src/spaced/spaced/scene.cpp +++ b/src/spaced/spaced/scene.cpp @@ -4,28 +4,49 @@ namespace spaced { using bave::App; +using bave::FocusChange; using bave::KeyInput; using bave::PointerMove; using bave::PointerTap; using bave::Seconds; +using bave::Shader; Scene::Scene(App& app, Services const& services, std::string name) : m_log{std::move(name)}, m_app(app), m_services(services) { m_log.info("constructed"); } -void Scene::on_key_event(KeyInput const& key_input) { on_key(key_input); } +void Scene::start_loading() { + auto stages = build_load_stages(); + if (stages.empty()) { + on_loaded(); + return; + } + + m_load.emplace(std::move(stages)); + m_loading_screen.emplace(m_services); +} + +void Scene::on_key_event(KeyInput const& key_input) { + if (is_loading()) { return; } + on_key(key_input); +} -void Scene::on_focus_event(bave::FocusChange const& focus_change) { on_focus(focus_change); } +void Scene::on_focus_event(FocusChange const& focus_change) { on_focus(focus_change); } void Scene::on_move_event(PointerMove const& pointer_move) { + if (is_loading()) { return; } if (on_ui_event([pointer_move](ui::View& view) { view.on_move(pointer_move); })) { return; } on_move(pointer_move); } void Scene::on_tap_event(PointerTap const& pointer_tap) { + if (is_loading()) { return; } if (on_ui_event([pointer_tap](ui::View& view) { view.on_tap(pointer_tap); })) { return; } on_tap(pointer_tap); } -void Scene::on_scroll_event(bave::MouseScroll const& mouse_scroll) { on_scroll(mouse_scroll); } +void Scene::on_scroll_event(bave::MouseScroll const& mouse_scroll) { + if (is_loading()) { return; } + on_scroll(mouse_scroll); +} auto Scene::is_ui_blocking_input() const -> bool { return std::any_of(m_views.begin(), m_views.end(), [](auto const& view) { return view->block_input_events; }); @@ -36,7 +57,16 @@ void Scene::push_view(std::unique_ptr view) { m_views.push_back(std::move(view)); } +auto Scene::pop_view() -> std::unique_ptr { + if (m_views.empty()) { return {}; } + auto ret = std::move(m_views.back()); + m_views.pop_back(); + return ret; +} + void Scene::tick_frame(Seconds const dt) { + update_loading(dt); + if (is_loading()) { return; } tick(dt); for (auto const& view : cache_views()) { view->tick(dt); } std::erase_if(m_views, [](auto const& view) { return view->is_destroyed(); }); @@ -45,6 +75,7 @@ void Scene::tick_frame(Seconds const dt) { void Scene::render_frame() const { auto shader = get_app().load_shader("shaders/default.vert", "shaders/default.frag"); if (!shader) { return; } + if (render_loading(*shader)) { return; } render(*shader); for (auto const& view : m_views) { view->render(*shader); } } @@ -54,6 +85,7 @@ auto Scene::on_ui_event(F per_view) -> bool { auto const cached_views = cache_views(); for (auto it = cached_views.rbegin(); it != cached_views.rend(); ++it) { auto const& view = *it; + if (!view->active) { continue; } per_view(*view); if (view->block_input_events) { return true; } } @@ -66,4 +98,29 @@ auto Scene::cache_views() -> std::span const> { for (auto const& view : m_views) { m_cached_views.push_back(view.get()); } return m_cached_views; } + +void Scene::update_loading(Seconds const dt) { + if (!m_loading_screen) { return; } + + if (!m_load) { + m_loading_screen->update(dt, 1.0f); + return; + } + + auto const load_status = m_load->update(); + auto const progress = load_status.progress(); + m_loading_screen->update(dt, progress); + + if (load_status.is_complete()) { + on_loaded(); + m_loading_screen.reset(); + m_load.reset(); + } +} + +auto Scene::render_loading(Shader& shader) const -> bool { + if (!m_loading_screen) { return false; } + m_loading_screen->draw(shader); + return true; +} } // namespace spaced diff --git a/src/spaced/spaced/scene.hpp b/src/spaced/spaced/scene.hpp index 77acfcd..a098abe 100644 --- a/src/spaced/spaced/scene.hpp +++ b/src/spaced/spaced/scene.hpp @@ -2,13 +2,15 @@ #include #include #include +#include #include +#include #include namespace spaced { class Scene : public bave::PolyPinned { public: - explicit Scene(bave::App& app, Services const& services, std::string name = "Scene"); + void start_loading(); void on_focus_event(bave::FocusChange const& focus_change); void on_key_event(bave::KeyInput const& key_input); @@ -22,16 +24,22 @@ class Scene : public bave::PolyPinned { [[nodiscard]] auto get_app() const -> bave::App& { return m_app; } [[nodiscard]] auto get_services() const -> Services const& { return m_services; } + [[nodiscard]] auto make_loader() const -> bave::Loader { return bave::Loader{&m_app.get_data_store(), &m_app.get_render_device()}; } + [[nodiscard]] auto is_loading() const -> bool { return m_loading_screen.has_value(); } [[nodiscard]] auto is_ui_blocking_input() const -> bool; - [[nodiscard]] auto make_loader() const -> bave::Loader { return bave::Loader{&m_app.get_data_store(), &m_app.get_render_device()}; } - void push_view(std::unique_ptr view); + auto pop_view() -> std::unique_ptr; bave::Rgba clear_colour{bave::black_v}; protected: + explicit Scene(bave::App& app, Services const& services, std::string name); + + virtual auto build_load_stages() -> std::vector { return {}; } + virtual void on_loaded() {} + virtual void on_focus(bave::FocusChange const& /*focus_change*/) {} virtual void on_resize(bave::WindowResize const& /*window_resize*/) {} virtual void on_resize(bave::FramebufferResize const& /*framebuffer_resize*/) {} @@ -53,9 +61,20 @@ class Scene : public bave::PolyPinned { auto cache_views() -> std::span const>; + void update_loading(bave::Seconds dt); + auto render_loading(bave::Shader& shader) const -> bool; + bave::App& m_app; Services const& m_services; std::vector> m_views{}; std::vector> m_cached_views{}; + + std::optional m_load{}; + std::optional m_loading_screen{}; +}; + +class EmptyScene : public Scene { + public: + explicit EmptyScene(bave::App& app, Services const& services) : Scene(app, services, "EmptyScene") {} }; } // namespace spaced diff --git a/src/spaced/spaced/scenes/load_assets.cpp b/src/spaced/spaced/scenes/load_assets.cpp index ec6e481..276bd8c 100644 --- a/src/spaced/spaced/scenes/load_assets.cpp +++ b/src/spaced/spaced/scenes/load_assets.cpp @@ -9,8 +9,6 @@ namespace spaced { using bave::App; using bave::Loader; -using bave::Seconds; -using bave::Shader; namespace { auto make_load_stages(Loader loader, Services const& services) -> std::vector { @@ -24,18 +22,9 @@ auto make_load_stages(Loader loader, Services const& services) -> std::vector().switch_to(); } - -void LoadAssets::tick(Seconds const dt) { - auto const load_status = m_load.update(); - auto const progress = load_status.progress(); - m_loading_screen.update(dt, progress); +auto LoadAssets::build_load_stages() -> std::vector { return make_load_stages(make_loader(), get_services()); } - if (load_status.is_complete()) { on_loaded(); } -} - -void LoadAssets::render(Shader& shader) const { m_loading_screen.draw(shader); } +void LoadAssets::on_loaded() { get_services().get().switch_to(); } } // namespace spaced diff --git a/src/spaced/spaced/scenes/load_assets.hpp b/src/spaced/spaced/scenes/load_assets.hpp index 95cc0c3..1b10cc6 100644 --- a/src/spaced/spaced/scenes/load_assets.hpp +++ b/src/spaced/spaced/scenes/load_assets.hpp @@ -1,7 +1,5 @@ #pragma once -#include #include -#include namespace spaced { class LoadAssets : public Scene { @@ -9,12 +7,7 @@ class LoadAssets : public Scene { explicit LoadAssets(bave::App& app, Services const& services); private: - void on_loaded(); - - void tick(bave::Seconds dt) final; - void render(bave::Shader& shader) const final; - - ui::LoadingScreen m_loading_screen; - AsyncExec m_load; + auto build_load_stages() -> std::vector final; + void on_loaded() final; }; } // namespace spaced diff --git a/src/spaced/spaced/spaced.cpp b/src/spaced/spaced/spaced.cpp index 21ddd17..90a011a 100644 --- a/src/spaced/spaced/spaced.cpp +++ b/src/spaced/spaced/spaced.cpp @@ -139,7 +139,7 @@ struct SceneSwitcher : ISceneSwitcher { void Spaced::set_bindings([[maybe_unused]] Serializer& serializer) {} -Spaced::Spaced(App& app) : Driver(app), m_scene(std::make_unique(app, m_services)) { +Spaced::Spaced(App& app) : Driver(app), m_scene(std::make_unique(app, m_services)) { m_log.info("using MSAA: {}x", static_cast(app.get_render_device().get_sample_count())); load_resources(); set_layout(); @@ -165,6 +165,7 @@ void Spaced::tick() { if (m_scene_switcher->next_scene) { switch_track(m_scene->get_music_uri(), m_scene_switcher->next_scene->get_music_uri()); m_scene = std::move(m_scene_switcher->next_scene); + m_scene->start_loading(); } m_scene->tick_frame(dt); diff --git a/src/spaced/spaced/ui/view.hpp b/src/spaced/spaced/ui/view.hpp index d5bf621..c91ac1e 100644 --- a/src/spaced/spaced/ui/view.hpp +++ b/src/spaced/spaced/ui/view.hpp @@ -21,6 +21,7 @@ class View : public bave::Polymorphic { virtual void render(bave::Shader& shader) const; bool block_input_events{true}; + bool active{true}; protected: std::vector> m_elements{}; From a6bf3c7845028f65f885bcd57d96589f2b2e0fd6 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Thu, 13 Jun 2024 16:06:34 +0530 Subject: [PATCH 2/7] Upgrade bave to v0.5.5, use `EventSink`. --- CMakeLists.txt | 2 +- src/spaced/spaced/scene.hpp | 13 ++----------- src/spaced/spaced/spaced.cpp | 2 +- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3753ed5..a7e215e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ include(FetchContent) FetchContent_Declare( bave GIT_REPOSITORY https://github.com/karnkaul/bave - GIT_TAG v0.5.4 + GIT_TAG v0.5.5 SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/bave" ) diff --git a/src/spaced/spaced/scene.hpp b/src/spaced/spaced/scene.hpp index a098abe..7b27376 100644 --- a/src/spaced/spaced/scene.hpp +++ b/src/spaced/spaced/scene.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include #include @@ -8,7 +9,7 @@ #include namespace spaced { -class Scene : public bave::PolyPinned { +class Scene : public bave::EventSink { public: void start_loading(); @@ -40,16 +41,6 @@ class Scene : public bave::PolyPinned { virtual auto build_load_stages() -> std::vector { return {}; } virtual void on_loaded() {} - virtual void on_focus(bave::FocusChange const& /*focus_change*/) {} - virtual void on_resize(bave::WindowResize const& /*window_resize*/) {} - virtual void on_resize(bave::FramebufferResize const& /*framebuffer_resize*/) {} - virtual void on_key(bave::KeyInput const& /*key_input*/) {} - virtual void on_char(bave::CharInput const& /*char_input*/) {} - virtual void on_tap(bave::PointerTap const& /*pointer_tap*/) {} - virtual void on_move(bave::PointerMove const& /*pointer_move*/) {} - virtual void on_scroll(bave::MouseScroll const& /*mouse_scroll*/) {} - virtual void on_drop(std::span /*paths*/) {} - virtual void tick(bave::Seconds /*dt*/) {} virtual void render(bave::Shader& /*shader*/) const {} diff --git a/src/spaced/spaced/spaced.cpp b/src/spaced/spaced/spaced.cpp index 90a011a..81a29ec 100644 --- a/src/spaced/spaced/spaced.cpp +++ b/src/spaced/spaced/spaced.cpp @@ -178,7 +178,7 @@ void Spaced::render() const { m_scene->render_frame(); } void Spaced::load_resources() { auto const loader = Loader{&get_app().get_data_store(), &get_app().get_render_device()}; auto resources = std::make_unique(); - resources->main_font = loader.load_font("fonts/hesitation.regular.ttf"); + resources->main_font = loader.load_font("fonts/CuteDino.otf"); resources->spinner = loader.load_texture("images/spinner.png", true); m_resources = resources.get(); m_services.bind(std::move(resources)); From 516baa9b7a8ab04ee812dbb6ca2fbfd1a5d831e3 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Thu, 13 Jun 2024 21:39:00 +0530 Subject: [PATCH 3/7] Add `GameDriver`, separate screen layout from game layout. --- src/spaced/CMakeLists.txt | 2 + src/spaced/spaced/game/enemies/creep.cpp | 2 +- src/spaced/spaced/game/enemy.cpp | 6 +- src/spaced/spaced/game/enemy.hpp | 4 +- src/spaced/spaced/game/hud.cpp | 3 +- src/spaced/spaced/game/player.cpp | 6 +- src/spaced/spaced/game/powerups/pu_base.cpp | 4 +- src/spaced/spaced/game/powerups/pu_base.hpp | 2 +- src/spaced/spaced/game/world.cpp | 8 +- src/spaced/spaced/game_driver.cpp | 146 ++++++++++++++++++++ src/spaced/spaced/game_driver.hpp | 43 ++++++ src/spaced/spaced/scene.cpp | 3 + src/spaced/spaced/scene.hpp | 4 + src/spaced/spaced/scenes/game.cpp | 8 +- src/spaced/spaced/scenes/load_assets.cpp | 2 +- src/spaced/spaced/scenes/menu.cpp | 2 +- src/spaced/spaced/services/layout.hpp | 13 +- src/spaced/spaced/services/styles.cpp | 104 +++++++++++--- src/spaced/spaced/spaced.cpp | 138 +++--------------- src/spaced/spaced/spaced.hpp | 23 +-- 20 files changed, 342 insertions(+), 181 deletions(-) create mode 100644 src/spaced/spaced/game_driver.cpp create mode 100644 src/spaced/spaced/game_driver.hpp diff --git a/src/spaced/CMakeLists.txt b/src/spaced/CMakeLists.txt index 3be00f3..fbdde26 100644 --- a/src/spaced/CMakeLists.txt +++ b/src/spaced/CMakeLists.txt @@ -19,3 +19,5 @@ target_include_directories(${PROJECT_NAME} PUBLIC file(GLOB_RECURSE sources LIST_DIRECTORIES false CONFIGURE_DEPENDS "spaced/*.?pp") target_sources(${PROJECT_NAME} PRIVATE ${sources}) + +target_precompile_headers(${PROJECT_NAME} REUSE_FROM bave) diff --git a/src/spaced/spaced/game/enemies/creep.cpp b/src/spaced/spaced/game/enemies/creep.cpp index 903ec8c..db37e37 100644 --- a/src/spaced/spaced/game/enemies/creep.cpp +++ b/src/spaced/spaced/game/enemies/creep.cpp @@ -8,6 +8,6 @@ void Creep::tick(Seconds const dt, bool const in_play) { if (!in_play) { return; } shape.transform.position.x -= x_speed * dt.count(); - if (shape.transform.position.x < -0.5f * (get_layout().get_world_space().x + shape.get_shape().size.x)) { set_destroyed(); } + if (shape.transform.position.x < -0.5f * (get_layout().world_space.x + shape.get_shape().size.x)) { set_destroyed(); } } } // namespace spaced diff --git a/src/spaced/spaced/game/enemy.cpp b/src/spaced/spaced/game/enemy.cpp index e6e6a39..3bdd7af 100644 --- a/src/spaced/spaced/game/enemy.cpp +++ b/src/spaced/spaced/game/enemy.cpp @@ -11,9 +11,9 @@ using bave::RoundedQuad; using bave::Seconds; using bave::Shader; -Enemy::Enemy(Services const& services, std::string_view const type) : m_layout(&services.get()), m_health_bar(services), m_type(type) { +Enemy::Enemy(Services const& services, std::string_view const type) : m_layout(&services.get()), m_health_bar(services), m_type(type) { static constexpr auto init_size_v = glm::vec2{100.0f}; - auto const play_area = m_layout->get_play_area(); + auto const play_area = m_layout->play_area; auto const y_min = play_area.rb.y + 0.5f * init_size_v.y; auto const y_max = play_area.lt.y - 0.5f * init_size_v.y - 50.0f; setup(init_size_v, random_in_range(y_min, y_max)); @@ -49,7 +49,7 @@ void Enemy::setup(glm::vec2 max_size, float y_position) { rounded_quad.size = max_size; rounded_quad.corner_radius = 0.2f * max_size.x; shape.set_shape(rounded_quad); - shape.transform.position.x = 0.5f * (get_layout().get_world_space().x + rounded_quad.size.x); + shape.transform.position.x = 0.5f * (get_layout().world_space.x + rounded_quad.size.x); shape.transform.position.y = y_position; } diff --git a/src/spaced/spaced/game/enemy.hpp b/src/spaced/spaced/game/enemy.hpp index 04ffe58..94764ae 100644 --- a/src/spaced/spaced/game/enemy.hpp +++ b/src/spaced/spaced/game/enemy.hpp @@ -27,7 +27,7 @@ class Enemy : public IDamageable, public bave::IDrawable { void setup(glm::vec2 max_size, float y_position); - [[nodiscard]] auto get_layout() const -> ILayout const& { return *m_layout; } + [[nodiscard]] auto get_layout() const -> GameLayout const& { return *m_layout; } void inspect() { if constexpr (bave::debug_v) { do_inspect(); } @@ -43,7 +43,7 @@ class Enemy : public IDamageable, public bave::IDrawable { private: virtual void do_inspect(); - bave::NotNull m_layout; + bave::NotNull m_layout; ui::ProgressBar m_health_bar; diff --git a/src/spaced/spaced/game/hud.cpp b/src/spaced/spaced/game/hud.cpp index 9799892..3c73229 100644 --- a/src/spaced/spaced/game/hud.cpp +++ b/src/spaced/spaced/game/hud.cpp @@ -1,11 +1,12 @@ #include #include +#include #include namespace spaced { using bave::TextHeight; -Hud::Hud(Services const& services) : ui::View(services), m_styles(&services.get()), m_area(m_layout->get_hud_area()) { +Hud::Hud(Services const& services) : ui::View(services), m_styles(&services.get()), m_area(services.get().hud_area) { create_background(); create_score(services); diff --git a/src/spaced/spaced/game/player.cpp b/src/spaced/spaced/game/player.cpp index c123d4b..7e1997e 100644 --- a/src/spaced/spaced/game/player.cpp +++ b/src/spaced/spaced/game/player.cpp @@ -19,10 +19,10 @@ using bave::Shader; Player::Player(Services const& services, std::unique_ptr controller) : m_services(&services), m_stats(&services.get()), m_controller(std::move(controller)) { - auto const& layout = services.get(); - ship.transform.position.x = layout.get_player_x(); + auto const& layout = services.get(); + ship.transform.position.x = layout.player_x; auto rounded_quad = RoundedQuad{}; - rounded_quad.size = layout.get_player_size(); + rounded_quad.size = layout.player_size; rounded_quad.corner_radius = 20.0f; ship.set_shape(rounded_quad); diff --git a/src/spaced/spaced/game/powerups/pu_base.cpp b/src/spaced/spaced/game/powerups/pu_base.cpp index 22cd99f..2cc908b 100644 --- a/src/spaced/spaced/game/powerups/pu_base.cpp +++ b/src/spaced/spaced/game/powerups/pu_base.cpp @@ -7,7 +7,7 @@ using bave::RoundedQuad; using bave::Seconds; using bave::Shader; -PUBase::PUBase(Services const& services, std::string_view const name) : m_services(&services), m_layout(&services.get()), m_name(name) { +PUBase::PUBase(Services const& services, std::string_view const name) : m_services(&services), m_layout(&services.get()), m_name(name) { auto quad = RoundedQuad{}; quad.size = glm::vec2{40.0f}; quad.corner_radius = 12.5f; @@ -21,7 +21,7 @@ PUBase::PUBase(Services const& services, std::string_view const name) : m_servic void PUBase::tick(Seconds const dt) { shape.transform.position.x -= speed * dt.count(); - if (shape.transform.position.x < m_layout->get_play_area().lt.x - 0.5f * shape.get_shape().size.x) { m_destroyed = true; } + if (shape.transform.position.x < m_layout->play_area.lt.x - 0.5f * shape.get_shape().size.x) { m_destroyed = true; } emitter.set_position(shape.transform.position); if (!m_emitter_ticked) { diff --git a/src/spaced/spaced/game/powerups/pu_base.hpp b/src/spaced/spaced/game/powerups/pu_base.hpp index baf64e7..6a6ebd4 100644 --- a/src/spaced/spaced/game/powerups/pu_base.hpp +++ b/src/spaced/spaced/game/powerups/pu_base.hpp @@ -27,7 +27,7 @@ class PUBase : public IPowerup { virtual void do_activate(Player& player) = 0; bave::NotNull m_services; - bave::NotNull m_layout; + bave::NotNull m_layout; std::string_view m_name{}; bool m_emitter_ticked{}; bool m_destroyed{}; diff --git a/src/spaced/spaced/game/world.cpp b/src/spaced/spaced/game/world.cpp index 9e55db6..d86721e 100644 --- a/src/spaced/spaced/game/world.cpp +++ b/src/spaced/spaced/game/world.cpp @@ -21,16 +21,16 @@ namespace { [[nodiscard]] auto make_player_controller(Services const& services) { auto ret = std::make_unique(services); if constexpr (bave::platform_v == bave::Platform::eAndroid) { ret->set_type(PlayerController::Type::eTouch); } - auto const& layout = services.get(); - auto const half_size = 0.5f * layout.get_player_size(); - auto const play_area = layout.get_play_area(); + auto const& layout = services.get(); + auto const half_size = 0.5f * layout.player_size; + auto const play_area = layout.play_area; ret->max_y = play_area.lt.y - half_size.y; ret->min_y = play_area.rb.y + half_size.y; return ret; } [[nodiscard]] auto make_auto_controller(ITargetProvider const& target_provider, Services const& services) { - return std::make_unique(&target_provider, services.get().get_player_x()); + return std::make_unique(&target_provider, services.get().player_x); } } // namespace diff --git a/src/spaced/spaced/game_driver.cpp b/src/spaced/spaced/game_driver.cpp new file mode 100644 index 0000000..fa6d898 --- /dev/null +++ b/src/spaced/spaced/game_driver.cpp @@ -0,0 +1,146 @@ +#include +#include +#include +#include + +namespace spaced { +using bave::App; +using bave::AudioClip; +using bave::AudioDevice; +using bave::AudioStreamer; +using bave::FocusChange; +using bave::KeyInput; +using bave::MouseScroll; +using bave::NotNull; +using bave::PointerMove; +using bave::PointerTap; +using bave::Rect; +using bave::RenderDevice; +using bave::RenderView; +using bave::Seconds; + +namespace { +struct Audio : IAudio { + NotNull audio_device; + NotNull audio_streamer; + NotNull resources; + + explicit Audio(NotNull audio_device, NotNull audio_streamer, NotNull resources) + : audio_device(audio_device), audio_streamer(audio_streamer), resources(resources) {} + + [[nodiscard]] auto get_sfx_gain() const -> float final { return audio_device->sfx_gain; } + void set_sfx_gain(float const gain) final { audio_device->sfx_gain = gain; } + + [[nodiscard]] auto get_music_gain() const -> float final { return audio_streamer->gain; } + void set_music_gain(float const gain) final { audio_streamer->gain = gain; } + + void play_sfx(std::string_view const uri) final { + auto const clip = resources->get(uri); + if (!clip) { return; } + audio_device->play_once(*clip); + } + + void play_music(std::string_view const uri, Seconds const crossfade) final { + auto const clip = resources->get(uri); + if (!clip) { return; } + audio_streamer->play(clip, crossfade); + } + + void stop_music() final { audio_streamer->stop(); } +}; +} // namespace + +struct GameDriver::SceneSwitcher : ISceneSwitcher { + App& app; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + Services const& services; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) + std::unique_ptr next_scene{}; + + explicit SceneSwitcher(App& app, Services const& services) : app(app), services(services) {} + + void switch_to_scene(std::unique_ptr new_scene) final { next_scene = std::move(new_scene); } + + [[nodiscard]] auto get_app() const -> App& final { return app; } + [[nodiscard]] auto get_services() const -> Services const& final { return services; } +}; + +struct GameDriver::Layout : ILayout { + NotNull render_device; + RenderView main_view{}; + glm::vec2 framebuffer_size{}; + Rect<> play_area{}; + Rect<> hud_area{}; + + explicit Layout(NotNull render_device) : render_device(render_device) {} + + [[nodiscard]] auto get_main_view() const -> RenderView const& final { return main_view; } + [[nodiscard]] auto get_framebuffer_size() const -> glm::vec2 final { return framebuffer_size; } + [[nodiscard]] auto get_world_space() const -> glm::vec2 final { return main_view.viewport; } + + [[nodiscard]] auto project_to_world(glm::vec2 fb_point) const -> glm::vec2 final { return render_device->project_to(get_world_space(), fb_point); } + + [[nodiscard]] auto unproject(glm::vec2 const pointer) const -> glm::vec2 final { + if (!bave::is_positive(framebuffer_size)) { return pointer; } + return main_view.unproject(pointer / framebuffer_size); + } +}; + +GameDriver::GameDriver(App& app) : Driver(app), m_scene(std::make_unique(app, m_services)) { + auto layout = std::make_unique(&app.get_render_device()); + m_layout = layout.get(); + m_layout->framebuffer_size = app.get_framebuffer_size(); + m_layout->main_view = app.get_render_device().get_default_view(); + m_services.bind(std::move(layout)); + + auto switcher = std::make_unique(app, m_services); + m_switcher = switcher.get(); + m_services.bind(std::move(switcher)); + + m_services.bind(std::make_unique()); + + auto audio = std::make_unique