diff --git a/CMakeLists.txt b/CMakeLists.txt index 594ce07..676cc4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,9 @@ include(FetchContent) FetchContent_Declare( bgf GIT_REPOSITORY https://github.com/karnkaul/bgf - GIT_TAG v0.1.4 + + # GIT_TAG v0.1.5 + GIT_TAG adc217f SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/bgf" ) diff --git a/assets/images/beam_round.png b/assets/images/beam_round.png new file mode 100644 index 0000000..5019785 Binary files /dev/null and b/assets/images/beam_round.png differ diff --git a/assets/images/star_blue.png b/assets/images/star_blue.png new file mode 100644 index 0000000..3e249e8 Binary files /dev/null and b/assets/images/star_blue.png differ diff --git a/assets/images/star_red.png b/assets/images/star_red.png new file mode 100644 index 0000000..15d7ee5 Binary files /dev/null and b/assets/images/star_red.png differ diff --git a/assets/images/star_yellow.png b/assets/images/star_yellow.png new file mode 100644 index 0000000..f030ae4 Binary files /dev/null and b/assets/images/star_yellow.png differ diff --git a/assets/music/game.mp3 b/assets/music/game.mp3 new file mode 100644 index 0000000..dfaf8c1 Binary files /dev/null and b/assets/music/game.mp3 differ diff --git a/assets/particles/explode.json b/assets/particles/explode.json index 736b2b0..cfefbff 100644 --- a/assets/particles/explode.json +++ b/assets/particles/explode.json @@ -27,8 +27,8 @@ } }, "angular": { - "lo": -90.000000, - "hi": 90.000000 + "lo": -360.000000, + "hi": 360.000000 } }, "lerp": { @@ -52,8 +52,8 @@ "hi": 0.800000 }, "quad_size": [ - 100.000000, - 100.000000 + 150.000000, + 150.000000 ], "count": 40, "respawn": true diff --git a/assets/styles.json b/assets/styles.json index 15883fc..a59d300 100644 --- a/assets/styles.json +++ b/assets/styles.json @@ -6,7 +6,9 @@ "milk": "#e5cdaeff", "ice": "#d6dbe1e1", "orange": "#f75c03ff", - "gun_beam": "#bc96e6ff" + "gun_beam": "#bc96e6ff", + "bg_top": "#10020eff", + "bg_bottom": "#040003ff" }, "buttons": { "default": { diff --git a/src/spaced/spaced/game/arsenal.cpp b/src/spaced/spaced/game/arsenal.cpp index 8b7e9d0..e928d0d 100644 --- a/src/spaced/spaced/game/arsenal.cpp +++ b/src/spaced/spaced/game/arsenal.cpp @@ -1,4 +1,5 @@ #include +#include #include namespace spaced { @@ -6,7 +7,10 @@ using bave::Seconds; using bave::Services; using bave::Shader; -Arsenal::Arsenal(Services const& services) : m_primary(services), m_stats(&services.get()) {} +Arsenal::Arsenal(Services const& services) + : m_stats(&services.get()), m_weapon_changed_signal(&services.get().weapon_changed), m_primary(services) { + m_weapon_changed_signal->dispatch(get_weapon()); +} auto Arsenal::get_weapon() const -> Weapon const& { if (m_special) { return *m_special; } @@ -34,13 +38,19 @@ void Arsenal::tick_weapons(Seconds const dt) { if (m_special) { m_special->tick(dt); // if the special weapon has no more rounds and is idle, reset it. - if (m_special->get_rounds_remaining() == 0 && m_special->is_idle()) { m_special.reset(); } + if (m_special->get_rounds_remaining() == 0 && m_special->is_idle()) { + m_special.reset(); + m_weapon_changed_signal->dispatch(get_weapon()); + } } } void Arsenal::check_switch_weapon() { // if there is a next weapon on standby and the current weapon is idle, switch to the next weapon. - if (m_next && get_weapon().is_idle()) { m_special = std::move(m_next); } + if (m_next && get_weapon().is_idle()) { + m_special = std::move(m_next); + m_weapon_changed_signal->dispatch(get_weapon()); + } } void Arsenal::fire_weapon(glm::vec2 const muzzle_position) { diff --git a/src/spaced/spaced/game/arsenal.hpp b/src/spaced/spaced/game/arsenal.hpp index 38827f4..ae96bee 100644 --- a/src/spaced/spaced/game/arsenal.hpp +++ b/src/spaced/spaced/game/arsenal.hpp @@ -3,6 +3,7 @@ namespace spaced { struct Stats; +struct SigWeaponChanged; // Arsenal models a main/primary weapon, and an possible special weapon. // Weapons only switch when they are idle. @@ -26,8 +27,10 @@ class Arsenal { void fire_weapon(glm::vec2 muzzle_position); void tick_rounds(IWeaponRound::State const& round_state, bave::Seconds dt); - GunKinetic m_primary; // main weapon bave::NotNull m_stats; + bave::NotNull m_weapon_changed_signal; + + GunKinetic m_primary; // main weapon std::unique_ptr m_special{}; // special weapon std::unique_ptr m_next{}; // next special weapon (on standby until current weapon is idle) std::vector> m_rounds{}; diff --git a/src/spaced/spaced/game/controllers/player_controller.cpp b/src/spaced/spaced/game/controllers/player_controller.cpp index 18d1e1a..f5ea264 100644 --- a/src/spaced/spaced/game/controllers/player_controller.cpp +++ b/src/spaced/spaced/game/controllers/player_controller.cpp @@ -9,20 +9,19 @@ using bave::Action; using bave::EnumArray; using bave::GamepadAxis; using bave::GamepadButton; -using bave::IDisplay; using bave::im_text; using bave::PointerMove; using bave::PointerTap; using bave::Seconds; using bave::Services; -PlayerController::PlayerController(Services const& services) : m_display(&services.get()), m_gamepad_provider(&services.get()) { - max_y = 0.5f * m_display->get_world_space().y; +PlayerController::PlayerController(Services const& services) : m_layout(&services.get()), m_gamepad_provider(&services.get()) { + max_y = 0.5f * m_layout->world_space.y; min_y = -max_y; // NOLINT(cppcoreguidelines-prefer-member-initializer) } void PlayerController::on_move(PointerMove const& pointer_move) { - auto const world_pos = m_display->project_to_world(pointer_move.pointer.position); + auto const world_pos = m_layout->project(pointer_move.pointer.position); if (m_type == Type::eTouch) { if (!is_in_move_area(world_pos)) { @@ -41,7 +40,7 @@ void PlayerController::on_move(PointerMove const& pointer_move) { } void PlayerController::on_tap(PointerTap const& pointer_tap) { - auto const world_pos = m_display->project_to_world(pointer_tap.pointer.position); + auto const world_pos = m_layout->project(pointer_tap.pointer.position); if (m_type == Type::eTouch && is_in_move_area(world_pos)) { // pointer tap in move area is ingored return; @@ -68,7 +67,7 @@ void PlayerController::stop_firing() { } auto PlayerController::is_in_move_area(glm::vec2 const position) const -> bool { - auto const width = m_display->get_world_space().x; + auto const width = m_layout->world_space.x; auto const n_pos = (position.x + 0.5f * width) / width; return n_pos <= n_move_area; } diff --git a/src/spaced/spaced/game/controllers/player_controller.hpp b/src/spaced/spaced/game/controllers/player_controller.hpp index ec4726a..200816d 100644 --- a/src/spaced/spaced/game/controllers/player_controller.hpp +++ b/src/spaced/spaced/game/controllers/player_controller.hpp @@ -1,11 +1,11 @@ #pragma once #include #include -#include #include #include #include #include +#include namespace spaced { class PlayerController : public FollowController { @@ -45,8 +45,8 @@ class PlayerController : public FollowController { auto tick_y(bave::Seconds dt) -> float final; void do_inspect() final; - bave::Ptr m_display{}; - bave::Ptr m_gamepad_provider{}; + bave::NotNull m_layout; + bave::NotNull m_gamepad_provider; Type m_type{}; float m_y{}; diff --git a/src/spaced/spaced/game/hud.cpp b/src/spaced/spaced/game/hud.cpp index 119df44..5f7e013 100644 --- a/src/spaced/spaced/game/hud.cpp +++ b/src/spaced/spaced/game/hud.cpp @@ -3,56 +3,54 @@ #include #include #include +#include #include namespace spaced { -using bave::IDisplay; +using bave::Display; using bave::Resources; using bave::Seconds; using bave::Services; -using bave::Shader; using bave::Styles; +using bave::Text; using bave::TextHeight; using bave::Texture; namespace ui = bave::ui; -Hud::Hud(Services const& services) - : ui::View(services), m_display(&services.get()), m_layout(&services.get()), m_styles(&services.get()) { +Hud::Hud(Services const& services) : ui::View(services), m_layout(&services.get()), m_styles(&services.get()) { create_background(); create_score(services); - create_lives_icon(services); + create_lives(services); + create_weapon(services); block_input_events = false; - render_view = m_display->get_world_view(); -} - -void Hud::set_lives(int const lives) { - if (lives <= 0) { - m_lives_icon.instances.clear(); - return; - } + render_view = services.get().world.render_view; - m_lives_icon.instances.resize(static_cast(lives)); - auto x_offset = 0.0f; - for (auto& instance : m_lives_icon.instances) { - instance.transform.position.x += x_offset; - x_offset += 2.0f * m_lives_icon.get_shape().size.x; - } + m_on_weapon_changed = services.get().weapon_changed.connect([this](Weapon const& weapon) { set_weapon(weapon.get_icon()); }); } -void Hud::on_death() { - if (m_lives_icon.instances.empty()) { return; } - m_lives_icon.instances.pop_back(); +void Hud::set_lives(int lives) { + lives = std::clamp(lives, 0, 99); // TODO: max_lives + m_lives_count->text.set_string(fmt::format("x{}", lives)); } void Hud::set_score(std::int64_t const score) { m_score->text.set_string(fmt::format("{}", score)); } void Hud::set_hi_score(std::int64_t const score) { m_hi_score->text.set_string(fmt::format("HI {}", score)); } -void Hud::render(Shader& shader) const { - View::render(shader); - m_lives_icon.draw(shader); +void Hud::set_weapon(std::shared_ptr texture) { + if (texture) { m_weapon_icon->sprite.set_size(texture->get_size()); } + m_weapon_icon->sprite.set_texture(std::move(texture)); +} + +auto Hud::make_text(Services const& services) const -> std::unique_ptr { + auto const& rgbas = m_styles->rgbas; + auto text = std::make_unique(services); + text->text.set_height(TextHeight{60}); + text->text.transform.position = m_layout->hud_area.centre(); + text->text.tint = rgbas["grey"]; + return text; } void Hud::create_background() { @@ -66,45 +64,54 @@ void Hud::create_background() { } void Hud::create_score(Services const& services) { - auto const& rgbas = m_styles->rgbas; - - auto make_text = [&] { - auto text = std::make_unique(services); - text->text.set_height(TextHeight{60}); - text->text.transform.position = m_layout->hud_area.centre(); - text->text.tint = rgbas["grey"]; - return text; - }; - - auto text = make_text(); + auto text = make_text(services); m_score = text.get(); text->text.set_string("9999999999"); m_text_bounds_size = text->text.get_bounds().size(); text->text.transform.position.y -= 0.5f * m_text_bounds_size.y; set_score(0); - push(std::move(text)); - text = make_text(); + text = make_text(services); m_hi_score = text.get(); text->text.transform.position.x = m_layout->hud_area.rb.x - 50.0f; text->text.transform.position.y -= 0.5f * m_text_bounds_size.y; - text->text.set_align(bave::Text::Align::eLeft); + text->text.set_align(Text::Align::eLeft); set_hi_score(0); - push(std::move(text)); } -void Hud::create_lives_icon(Services const& services) { - auto quad = m_lives_icon.get_shape(); - quad.size = glm::vec2{20.0f}; +void Hud::create_lives(Services const& services) { + auto sprite = std::make_unique(); + m_lives_icon = sprite.get(); + m_lives_icon->sprite.set_size(glm::vec2{20.0f}); auto const& resources = services.get(); if (auto const texture = resources.get("images/player_ship_icon.png")) { - quad.size = texture->get_size(); - m_lives_icon.set_texture(texture); + m_lives_icon->sprite.set_texture(texture); + m_lives_icon->sprite.set_size(texture->get_size()); } - m_lives_icon.set_shape(quad); - m_lives_icon.transform.position = m_layout->hud_area.centre(); - m_lives_icon.transform.position.x = m_layout->hud_area.lt.x + 100.0f; + auto position = m_layout->hud_area.centre(); + position.x = m_layout->hud_area.lt.x + 100.0f; + m_lives_icon->set_position(position); + push(std::move(sprite)); + + auto text = make_text(services); + text->text.transform.position.y -= 0.5f * m_text_bounds_size.y; + text->text.transform.position.x = m_lives_icon->get_position().x + m_lives_icon->get_size().x; + text->text.set_align(Text::Align::eRight); + text->text.set_string("0"); + m_lives_count = text.get(); + push(std::move(text)); +} + +void Hud::create_weapon(Services const& /*services*/) { + auto sprite = std::make_unique(); + m_weapon_icon = sprite.get(); + sprite->sprite.set_size(glm::vec2{50.0f}); + auto position = m_lives_icon->get_position(); + position.y -= 5.0f; + position.x = m_lives_count->get_position().x + 200.0f; + sprite->set_position(position); + push(std::move(sprite)); } } // namespace spaced diff --git a/src/spaced/spaced/game/hud.hpp b/src/spaced/spaced/game/hud.hpp index 638b8f9..ec6cbb5 100644 --- a/src/spaced/spaced/game/hud.hpp +++ b/src/spaced/spaced/game/hud.hpp @@ -1,9 +1,10 @@ #pragma once -#include #include #include +#include #include #include +#include namespace spaced { struct Layout; @@ -13,26 +14,31 @@ class Hud : public bave::ui::View { explicit Hud(bave::Services const& services); void set_lives(int lives); - void on_death(); void set_score(std::int64_t score); void set_hi_score(std::int64_t score); + void set_weapon(std::shared_ptr texture); private: - void render(bave::Shader& shader) const final; + [[nodiscard]] auto make_text(bave::Services const& services) const -> std::unique_ptr; void create_background(); void create_score(bave::Services const& services); - void create_lives_icon(bave::Services const& services); + void create_lives(bave::Services const& services); + void create_weapon(bave::Services const& services); - bave::NotNull m_display; bave::NotNull m_layout; bave::NotNull m_styles; glm::vec2 m_text_bounds_size{}; + SignalHandle m_on_weapon_changed{}; + bave::Ptr m_background{}; bave::Ptr m_score{}; bave::Ptr m_hi_score{}; - bave::Instanced m_lives_icon{}; + bave::Ptr m_lives_icon{}; + bave::Ptr m_lives_count{}; + + bave::Ptr m_weapon_icon{}; }; } // namespace spaced diff --git a/src/spaced/spaced/game/player.cpp b/src/spaced/spaced/game/player.cpp index dcf018c..6ed3857 100644 --- a/src/spaced/spaced/game/player.cpp +++ b/src/spaced/spaced/game/player.cpp @@ -22,7 +22,7 @@ using bave::Shader; using bave::Texture; Player::Player(Services const& services, std::unique_ptr controller) - : m_services(&services), m_stats(&services.get()), m_controller(std::move(controller)), m_shield(services) { + : m_services(&services), m_stats(&services.get()), m_controller(std::move(controller)), m_shield(services), m_arsenal(services) { auto const& layout = services.get(); ship.transform.position.x = layout.player_x; diff --git a/src/spaced/spaced/game/player.hpp b/src/spaced/spaced/game/player.hpp index 7c37479..c5b4bfa 100644 --- a/src/spaced/spaced/game/player.hpp +++ b/src/spaced/spaced/game/player.hpp @@ -37,6 +37,7 @@ class Player : public bave::IDrawable { [[nodiscard]] auto get_controller() const -> IController const& { return *m_controller; } void set_special_weapon(std::unique_ptr weapon) { m_arsenal.set_special(std::move(weapon)); } + [[nodiscard]] auto get_weapon() const -> Weapon const& { return m_arsenal.get_weapon(); } void set_shield(bave::Seconds ttl); @@ -67,7 +68,7 @@ class Player : public bave::IDrawable { bave::ParticleEmitter m_death_source{}; std::optional m_death{}; - Arsenal m_arsenal{*m_services}; + Arsenal m_arsenal; Health m_health{}; }; } // namespace spaced diff --git a/src/spaced/spaced/game/powerups/pu_base.cpp b/src/spaced/spaced/game/powerups/pu_base.cpp index f30d6b5..6e11e90 100644 --- a/src/spaced/spaced/game/powerups/pu_base.cpp +++ b/src/spaced/spaced/game/powerups/pu_base.cpp @@ -2,18 +2,17 @@ #include namespace spaced { +using bave::Circle; using bave::ParticleEmitter; using bave::Resources; -using bave::RoundedQuad; using bave::Seconds; using bave::Services; using bave::Shader; 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; - shape.set_shape(quad); + auto circle = Circle{}; + circle.diameter = 40.0f; + shape.set_shape(circle); auto const& resources = services.get(); if (auto const pu_emitter = resources.get("particles/powerup.json")) { emitter = *pu_emitter; } @@ -23,7 +22,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->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().diameter) { 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 4c200c4..6d8fcc0 100644 --- a/src/spaced/spaced/game/powerups/pu_base.hpp +++ b/src/spaced/spaced/game/powerups/pu_base.hpp @@ -20,7 +20,7 @@ class PUBase : public IPowerup { [[nodiscard]] auto is_destroyed() const -> bool final { return m_destroyed; } float speed{300.0f}; - bave::RoundedQuadShape shape{}; + bave::CircleShape shape{}; bave::ParticleEmitter emitter{}; protected: diff --git a/src/spaced/spaced/game/star_field.cpp b/src/spaced/spaced/game/star_field.cpp new file mode 100644 index 0000000..9156b6d --- /dev/null +++ b/src/spaced/spaced/game/star_field.cpp @@ -0,0 +1,95 @@ +#include +#include +#include +#include +#include + +namespace spaced { +using bave::random_in_range; +using bave::Seconds; +using bave::Services; +using bave::Shader; +using bave::Texture; + +StarField::StarField(Services const& services) : m_layout(&services.get()) {} + +void StarField::add_field(std::shared_ptr texture, Config const& config) { + auto& field = m_fields.emplace_back(); + field.sprite.set_size(glm::vec2{20.0f}); + field.sprite.set_texture(std::move(texture)); + field.play_area = m_layout->play_area; + field.config = config; + field.pre_warm(); +} + +void StarField::tick(Seconds const dt) { + for (auto& field : m_fields) { field.tick(dt); } +} + +void StarField::draw(Shader& shader) const { + for (auto const& field : m_fields) { field.sprite.draw(shader); } +} + +auto StarField::Field::spawn() const -> Star { + auto ret = Star{}; + + auto const sprite_size = sprite.get_size(); + ret.position.x = play_area.rb.x + 0.5f * sprite_size.x; + auto const y_min = play_area.rb.y + 0.5f * sprite_size.y; + auto const y_max = play_area.lt.y - 0.5f * sprite_size.y; + ret.position.y = random_in_range(y_min, y_max); + + auto const alpha = random_in_range(0.0f, 1.0f); + ret.scale = std::lerp(config.scale.lo, config.scale.hi, alpha); + ret.speed = std::lerp(config.speed.lo, config.speed.hi, alpha); + + return ret; +} + +void StarField::Field::pre_warm() { + auto const dt = 0.5f * config.spawn_rate; + while (true) { + if (tick_stars(dt)) { return; } + tick_spawn(dt); + } +} + +void StarField::Field::tick(Seconds const dt) { + tick_stars(dt); + tick_spawn(dt); + sync(); + + assert(sprite.instances.size() < 10000); +} + +auto StarField::Field::tick_stars(Seconds const dt) -> bool { + for (auto& star : stars) { star.position.x -= star.speed * dt.count(); } + + auto const play_exit_x = play_area.lt.x; + auto const sprite_size_x = sprite.get_size().x; + auto const func = [sprite_size_x, play_exit_x](Star const& s) { + auto const exit_x = play_exit_x - (0.5f * sprite_size_x * s.scale); + return s.position.x < exit_x; + }; + return std::erase_if(stars, func) > 0; +} + +void StarField::Field::tick_spawn(Seconds const dt) { + next_spawn -= dt; + if (next_spawn <= 0s) { + next_spawn = config.spawn_rate; + stars.push_back(spawn()); + } +} + +void StarField::Field::sync() { + sprite.instances.clear(); + sprite.instances.reserve(stars.size()); + for (auto const& star : stars) { + auto& render_instance = sprite.instances.emplace_back(); + render_instance.transform.position = star.position; + render_instance.transform.scale = glm::vec2{star.scale}; + render_instance.tint = star.tint; + } +} +} // namespace spaced diff --git a/src/spaced/spaced/game/star_field.hpp b/src/spaced/spaced/game/star_field.hpp new file mode 100644 index 0000000..dcbb466 --- /dev/null +++ b/src/spaced/spaced/game/star_field.hpp @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace spaced { +class StarField : public bave::IDrawable { + public: + struct Config { + bave::Seconds spawn_rate{3s}; + bave::InclusiveRange speed{10.0f, 50.0f}; + bave::InclusiveRange scale{0.1f, 1.0f}; + }; + + explicit StarField(bave::Services const& services); + + void add_field(std::shared_ptr texture, Config const& config); + + void tick(bave::Seconds dt); + void draw(bave::Shader& shader) const final; + + Config config{}; + + private: + struct Star { + glm::vec2 position{}; + float scale{1.0f}; + float speed{}; + bave::Rgba tint{bave::white_v}; + }; + + struct Field { + Config config{}; + bave::Rect<> play_area{}; + bave::Instanced sprite{}; + + std::vector stars{}; + bave::Seconds next_spawn{}; + + [[nodiscard]] auto spawn() const -> Star; + + void pre_warm(); + + void tick(bave::Seconds dt); + auto tick_stars(bave::Seconds dt) -> bool; + void tick_spawn(bave::Seconds dt); + void sync(); + }; + + bave::NotNull m_layout; + + std::vector m_fields{}; + bave::Seconds m_next_spawn{}; +}; +} // namespace spaced diff --git a/src/spaced/spaced/game/weapon.cpp b/src/spaced/spaced/game/weapon.cpp index fffa8a1..ca9395c 100644 --- a/src/spaced/spaced/game/weapon.cpp +++ b/src/spaced/spaced/game/weapon.cpp @@ -3,11 +3,10 @@ namespace spaced { using bave::IAudio; -using bave::IDisplay; using bave::im_text; using bave::Services; -Weapon::Weapon(Services const& services, std::string name) : m_log{std::move(name)}, m_display(&services.get()), m_audio(&services.get()) {} +Weapon::Weapon(Services const& services, std::string name) : m_log{std::move(name)}, m_layout(&services.get()), m_audio(&services.get()) {} auto Weapon::fire(glm::vec2 const muzzle_position) -> std::unique_ptr { return do_fire(muzzle_position); } diff --git a/src/spaced/spaced/game/weapon.hpp b/src/spaced/spaced/game/weapon.hpp index 05b08f0..14fe378 100644 --- a/src/spaced/spaced/game/weapon.hpp +++ b/src/spaced/spaced/game/weapon.hpp @@ -2,9 +2,9 @@ #include #include #include -#include #include #include +#include namespace spaced { class Weapon : public bave::Polymorphic { @@ -13,7 +13,8 @@ class Weapon : public bave::Polymorphic { explicit Weapon(bave::Services const& services, std::string name); - [[nodiscard]] auto get_rounds_remaining() const -> int { return rounds < 0 ? 1 : rounds; } + [[nodiscard]] auto get_rounds_remaining() const -> int { return rounds; } + [[nodiscard]] virtual auto get_icon() const -> std::shared_ptr { return {}; } auto fire(glm::vec2 muzzle_position) -> std::unique_ptr; [[nodiscard]] virtual auto is_idle() const -> bool = 0; @@ -27,7 +28,7 @@ class Weapon : public bave::Polymorphic { int rounds{-1}; protected: - [[nodiscard]] auto get_display() const -> bave::IDisplay const& { return *m_display; } + [[nodiscard]] auto get_display() const -> Layout const& { return *m_layout; } [[nodiscard]] auto get_audio() const -> bave::IAudio& { return *m_audio; } virtual auto do_fire(glm::vec2 muzzle_position) -> std::unique_ptr = 0; @@ -36,7 +37,7 @@ class Weapon : public bave::Polymorphic { bave::Logger m_log{}; private: - bave::NotNull m_display; + bave::NotNull m_layout; bave::NotNull m_audio; }; } // namespace spaced diff --git a/src/spaced/spaced/game/weapons/gun_beam.cpp b/src/spaced/spaced/game/weapons/gun_beam.cpp index 4ec0577..da595f4 100644 --- a/src/spaced/spaced/game/weapons/gun_beam.cpp +++ b/src/spaced/spaced/game/weapons/gun_beam.cpp @@ -1,29 +1,31 @@ #include #include +#include #include #include +#include namespace spaced { -using bave::IDisplay; using bave::im_text; using bave::NotNull; using bave::Ptr; using bave::Rect; -using bave::Rgba; +using bave::Resources; using bave::Seconds; using bave::Services; using bave::Shader; using bave::Sprite; -using bave::Styles; +using bave::Texture; namespace { class LaserCharge : public IWeaponRound { public: using Config = GunBeam::Config; - explicit LaserCharge(NotNull display, Config config, glm::vec2 const muzzle_position) - : m_display(display), m_config(config), m_fire_remain(config.fire_duration) { + explicit LaserCharge(NotNull layout, NotNull config, glm::vec2 const muzzle_position) + : m_layout(layout), m_config(config), m_fire_remain(config->fire_duration) { m_ray.transform.position.y = muzzle_position.y; + m_ray.set_texture(m_config->beam_texture); } [[nodiscard]] auto is_destroyed() const -> bool final { return m_destroyed; } @@ -38,23 +40,22 @@ class LaserCharge : public IWeaponRound { sort_entries(state.targets, state.muzzle_position); - auto const world_space = m_display->get_world_space(); + auto const world_space = m_layout->world_space; auto const left_x = state.muzzle_position.x; auto right_x = 0.5f * world_space.x; for (auto const& entry : m_entries) { - if (entry.target->take_damage(m_config.dps * dt.count())) { + if (entry.target->take_damage(m_config->dps * dt.count())) { right_x = entry.target->get_bounds().top_left().x; break; } } - auto const left_y = 0.5f * m_config.beam_height; + auto const left_y = 0.5f * m_config->beam_height; auto const right_y = -left_y; auto const rect = Rect<>{.lt = {left_x, left_y}, .rb = {right_x, right_y}}; m_ray.transform.position.x = rect.centre().x; m_ray.transform.position.y = state.muzzle_position.y; m_ray.set_size(rect.size()); - m_ray.tint = m_config.beam_tint; update_scale(); } @@ -64,13 +65,13 @@ class LaserCharge : public IWeaponRound { void update_scale() { static constexpr auto delta_v{0.25f}; - auto const fire_elapsed = m_config.fire_duration - m_fire_remain; - if (auto const expand_until = delta_v * m_config.fire_duration; fire_elapsed < expand_until) { + auto const fire_elapsed = m_config->fire_duration - m_fire_remain; + if (auto const expand_until = delta_v * m_config->fire_duration; fire_elapsed < expand_until) { m_ray.transform.scale.y = std::lerp(0.0f, 1.0f, fire_elapsed / expand_until); return; } - if (auto const shrink_from = delta_v * m_config.fire_duration; m_fire_remain < shrink_from) { + if (auto const shrink_from = delta_v * m_config->fire_duration; m_fire_remain < shrink_from) { m_ray.transform.scale.y = std::lerp(0.0f, 1.0f, m_fire_remain / shrink_from); return; } @@ -96,8 +97,8 @@ class LaserCharge : public IWeaponRound { float distance{}; }; - NotNull m_display; - Config m_config; + NotNull m_layout; + NotNull m_config; Sprite m_ray{}; Seconds m_fire_remain{}; @@ -107,8 +108,7 @@ class LaserCharge : public IWeaponRound { } // namespace GunBeam::GunBeam(Services const& services) : Weapon(services, "GunBeam") { - auto const& rgbas = services.get().rgbas; - config.beam_tint = rgbas.get_or("gun_beam", rgbas["grey"]); + config.beam_texture = services.get().get("images/beam_round.png"); } void GunBeam::tick(Seconds const dt) { @@ -129,7 +129,7 @@ auto GunBeam::do_fire(glm::vec2 const muzzle_position) -> std::unique_ptr m_fire_remain = config.fire_duration; m_reload_remain = 0s; get_audio().play_sfx("sfx/beam_fire.wav"); - return std::make_unique(&get_display(), config, muzzle_position); + return std::make_unique(&get_display(), &config, muzzle_position); } void GunBeam::do_inspect() { @@ -141,8 +141,6 @@ void GunBeam::do_inspect() { if (ImGui::DragFloat("fire duration (s)", &fire_duration, 0.25f, 0.25f, 10.0f)) { config.fire_duration = Seconds{fire_duration}; } auto reload_delay = config.reload_delay.count(); if (ImGui::DragFloat("reload delay (s)", &reload_delay, 0.25f, 0.25f, 10.0f)) { config.reload_delay = Seconds{reload_delay}; } - auto beam_tint = config.beam_tint.to_vec4(); - if (ImGui::ColorEdit4("beam tint", &beam_tint.x)) { config.beam_tint = Rgba::from(beam_tint); } ImGui::DragFloat("dps", &config.dps, 0.25f, 0.25f, 10.0f); } } diff --git a/src/spaced/spaced/game/weapons/gun_beam.hpp b/src/spaced/spaced/game/weapons/gun_beam.hpp index 038288c..1b7c1af 100644 --- a/src/spaced/spaced/game/weapons/gun_beam.hpp +++ b/src/spaced/spaced/game/weapons/gun_beam.hpp @@ -8,13 +8,15 @@ class GunBeam final : public Weapon { float beam_height{5.0f}; bave::Seconds fire_duration{2s}; bave::Seconds reload_delay{1s}; - bave::Rgba beam_tint{bave::red_v}; + std::shared_ptr beam_texture{}; float dps{10.0f}; }; explicit GunBeam(bave::Services const& services); - [[nodiscard]] auto is_idle() const -> bool final { return m_fire_remain <= 0s; } + [[nodiscard]] auto is_idle() const -> bool final { return m_fire_remain <= 0s && m_reload_remain <= 0s; } + [[nodiscard]] auto get_icon() const -> std::shared_ptr final { return config.beam_texture; } + void tick(bave::Seconds dt) final; Config config{}; diff --git a/src/spaced/spaced/game/weapons/gun_kinetic.hpp b/src/spaced/spaced/game/weapons/gun_kinetic.hpp index c321966..02efd32 100644 --- a/src/spaced/spaced/game/weapons/gun_kinetic.hpp +++ b/src/spaced/spaced/game/weapons/gun_kinetic.hpp @@ -9,6 +9,7 @@ class GunKinetic final : public Weapon { explicit GunKinetic(bave::Services const& services); [[nodiscard]] auto is_idle() const -> bool final { return m_reload_remain <= 0s; } + [[nodiscard]] auto get_icon() const -> std::shared_ptr final { return projectile_config.texture; } void tick(bave::Seconds dt) final; diff --git a/src/spaced/spaced/game/weapons/projectile.cpp b/src/spaced/spaced/game/weapons/projectile.cpp index 67a3940..d30c760 100644 --- a/src/spaced/spaced/game/weapons/projectile.cpp +++ b/src/spaced/spaced/game/weapons/projectile.cpp @@ -1,7 +1,6 @@ #include namespace spaced { -using bave::IDisplay; using bave::NotNull; using bave::Rect; using bave::Seconds; @@ -15,7 +14,7 @@ namespace { } } // namespace -Projectile::Projectile(NotNull display, Config config, glm::vec2 const muzzle_position) : m_display(display), m_config(std::move(config)) { +Projectile::Projectile(NotNull layout, Config config, glm::vec2 const muzzle_position) : m_layout(layout), m_config(std::move(config)) { m_sprite.set_texture(std::move(m_config.texture)); m_sprite.set_auto_size(m_config.size); m_sprite.tint = m_config.tint; @@ -38,7 +37,7 @@ void Projectile::tick(State const& state, Seconds const dt) { } } - if (m_sprite.transform.position.x > 0.5f * (m_display->get_world_space().x + m_config.size.x)) { m_destroyed = true; } + if (m_sprite.transform.position.x > 0.5f * (m_layout->world_space.x + m_config.size.x)) { m_destroyed = true; } } void Projectile::draw(Shader& shader) const { m_sprite.draw(shader); } diff --git a/src/spaced/spaced/game/weapons/projectile.hpp b/src/spaced/spaced/game/weapons/projectile.hpp index f47d32b..a57ae89 100644 --- a/src/spaced/spaced/game/weapons/projectile.hpp +++ b/src/spaced/spaced/game/weapons/projectile.hpp @@ -1,8 +1,8 @@ #pragma once #include -#include #include #include +#include namespace spaced { class Projectile : public IWeaponRound { @@ -17,14 +17,14 @@ class Projectile : public IWeaponRound { Instigator instigator{Instigator::ePlayer}; }; - explicit Projectile(bave::NotNull display, Config config, glm::vec2 muzzle_position); + explicit Projectile(bave::NotNull layout, Config config, glm::vec2 muzzle_position); void tick(State const& state, bave::Seconds dt) override; [[nodiscard]] auto is_destroyed() const -> bool override { return m_destroyed; } void draw(bave::Shader& shader) const override; protected: - bave::NotNull m_display; + bave::NotNull m_layout; Config m_config{}; bave::Sprite m_sprite{}; diff --git a/src/spaced/spaced/game/world.cpp b/src/spaced/spaced/game/world.cpp index 1f3c8d0..21a549c 100644 --- a/src/spaced/spaced/game/world.cpp +++ b/src/spaced/spaced/game/world.cpp @@ -1,7 +1,9 @@ #include #include +#include #include #include +#include #include #include @@ -17,21 +19,34 @@ using bave::Resources; using bave::Seconds; using bave::Services; using bave::Shader; +using bave::Styles; using bave::Texture; World::World(bave::NotNull services, bave::NotNull scorer) : m_services(services), m_resources(&services->get()), m_audio(&services->get()), m_stats(&services->get()), m_scorer(scorer), - m_background(*services) { + m_star_field(*services) { m_enemy_factories["CreepFactory"] = std::make_unique(services); - m_background.set_texture(services->get().get("images/background.png")); - m_background.set_tile_size(glm::vec2{300.0f}); - m_background.x_speed = 50.0f; + auto const& resources = services->get(); + + auto const play_area = services->get().play_area; + auto const& rgbas = services->get().rgbas; + auto quad = bave::Quad{.size = play_area.size()}; + auto geometry = quad.to_geometry(); + geometry.vertex_array.vertices[0].rgba = geometry.vertex_array.vertices[1].rgba = rgbas["bg_bottom"].to_vec4(); + geometry.vertex_array.vertices[2].rgba = geometry.vertex_array.vertices[3].rgba = rgbas["bg_top"].to_vec4(); + m_background.set_geometry(std::move(geometry)); + m_background.transform.position = play_area.centre(); + + auto const config = StarField::Config{.spawn_rate = 0.2s}; + m_star_field.add_field(resources.get("images/star_blue.png"), config); + m_star_field.add_field(resources.get("images/star_red.png"), config); + m_star_field.add_field(resources.get("images/star_yellow.png"), config); } void World::tick(Seconds const dt, bool const in_play) { if (in_play) { - m_background.tick(dt); + m_star_field.tick(dt); for (auto& [_, factory] : m_enemy_factories) { if (auto enemy = factory->tick(dt)) { m_active_enemies.push_back(std::move(enemy)); } } @@ -58,6 +73,7 @@ void World::tick(Seconds const dt, bool const in_play) { void World::draw(Shader& shader) const { m_background.draw(shader); + m_star_field.draw(shader); for (auto const& enemy : m_active_enemies) { enemy->draw(shader); } for (auto const& emitter : m_enemy_death_emitters) { emitter.draw(shader); } for (auto const& powerup : m_active_powerups) { powerup->draw(shader); } diff --git a/src/spaced/spaced/game/world.hpp b/src/spaced/spaced/game/world.hpp index 8d0385c..0a63275 100644 --- a/src/spaced/spaced/game/world.hpp +++ b/src/spaced/spaced/game/world.hpp @@ -3,8 +3,8 @@ #include #include #include +#include #include -#include namespace bave { struct Resources; @@ -41,7 +41,8 @@ class World : public ITargetProvider { bave::NotNull m_stats; bave::NotNull m_scorer; - TiledBg m_background; + bave::CustomShape m_background{}; + StarField m_star_field; std::unordered_map> m_enemy_factories{}; diff --git a/src/spaced/spaced/scenes/game.cpp b/src/spaced/spaced/scenes/game.cpp index 47d5c64..4d92c58 100644 --- a/src/spaced/spaced/scenes/game.cpp +++ b/src/spaced/spaced/scenes/game.cpp @@ -15,9 +15,7 @@ namespace spaced { using bave::Action; using bave::App; -using bave::AssetList; using bave::AssetManifest; -using bave::AsyncExec; using bave::FocusChange; using bave::im_text; using bave::Key; @@ -34,7 +32,25 @@ using bave::Styles; namespace ui = bave::ui; namespace { -auto get_manifest() -> AssetManifest { +[[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.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().player_x); +} +} // namespace + +GameScene::GameScene(App& app, Services const& services) : Scene(app, services, "Game"), m_save(&app) { clear_colour = services.get().rgbas["mocha"]; } + +auto GameScene::get_asset_manifest() -> AssetManifest { return AssetManifest{ .textures = { @@ -43,7 +59,11 @@ auto get_manifest() -> AssetManifest { "images/shield.png", "images/creep_ship.png", "images/background.png", + "images/star_blue.png", + "images/star_red.png", + "images/star_yellow.png", "images/kinetic_projectile.png", + "images/beam_round.png", }, .audio_clips = { @@ -61,47 +81,33 @@ auto get_manifest() -> AssetManifest { }; } -[[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.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; -} +void GameScene::on_loaded() { + auto const& services = get_services(); -[[nodiscard]] auto make_auto_controller(ITargetProvider const& target_provider, Services const& services) { - return std::make_unique(&target_provider, services.get().player_x); -} -} // namespace + auto hud = std::make_unique(services); + m_hud = hud.get(); + push_view(std::move(hud)); -GameScene::GameScene(App& app, Services const& services) : Scene(app, services, "Game"), m_save(&app) { clear_colour = services.get().rgbas["mocha"]; } + start_play(); -auto GameScene::build_load_stages() -> std::vector { - auto ret = std::vector{}; - auto asset_list = AssetList{make_loader(), get_services()}; - asset_list.add_manifest(get_manifest()); - ret.push_back(asset_list.build_load_stage()); - return ret; + switch_track("music/game.mp3"); } -void GameScene::on_loaded() { +void GameScene::start_play() { auto const& services = get_services(); - m_world.emplace(&services, this); + m_world.emplace(&services, this); m_player.emplace(services, make_player_controller(services)); - auto hud = std::make_unique(services); - m_hud = hud.get(); + m_score = 0; + m_spare_lives = 2; + m_hud->set_score(m_score); m_hud->set_hi_score(m_save.get_hi_score()); m_hud->set_lives(m_spare_lives); - push_view(std::move(hud)); ++services.get().game.play_count; - switch_track("music/game.mp3"); + m_game_over_dialog_pushed = false; } void GameScene::on_focus(FocusChange const& focus_change) { m_player->on_focus(focus_change); } @@ -123,7 +129,7 @@ void GameScene::tick(Seconds const dt) { auto const player_state = Player::State{.targets = m_world->get_targets(), .powerups = m_world->get_powerups()}; auto const player_died = m_player->tick(player_state, dt); - if (player_died) { m_hud->on_death(); } + if (player_died) { m_hud->set_lives(m_spare_lives - 1); } if (m_player->is_idle()) { on_player_death(); } @@ -163,7 +169,7 @@ void GameScene::on_game_over() { auto dci = ui::DialogCreateInfo{ .size = {600.0f, 200.0f}, .content_text = "GAME OVER", - .main_button = {.text = "RESTART", .callback = [this] { get_switcher().switch_to(); }}, + .main_button = {.text = "RESTART", .callback = [this] { start_play(); }}, .second_button = {.text = "QUIT", .callback = [this] { get_app().shutdown(); }}, }; @@ -207,6 +213,8 @@ void GameScene::inspect(Seconds const dt, Seconds const frame_time) { ImGui::Checkbox("fps lock", &m_debug.fps.lock); ImGui::Separator(); + if (ImGui::Button("restart")) { start_play(); } + ImGui::SameLine(); if (ImGui::Button("reload scene")) { get_switcher().switch_to(); } } ImGui::End(); diff --git a/src/spaced/spaced/scenes/game.hpp b/src/spaced/spaced/scenes/game.hpp index 2b654f6..6dcc219 100644 --- a/src/spaced/spaced/scenes/game.hpp +++ b/src/spaced/spaced/scenes/game.hpp @@ -13,7 +13,7 @@ class GameScene : public bave::Scene, public IScorer { GameScene(bave::App& app, bave::Services const& services); private: - auto build_load_stages() -> std::vector final; + auto get_asset_manifest() -> bave::AssetManifest final; void on_loaded() final; void on_focus(bave::FocusChange const& focus_change) final; @@ -27,6 +27,8 @@ class GameScene : public bave::Scene, public IScorer { [[nodiscard]] auto get_score() const -> std::int64_t final { return m_score; } void add_score(std::int64_t score) final; + void start_play(); + void on_player_death(); void respawn_player(); void on_game_over(); diff --git a/src/spaced/spaced/scenes/menu.cpp b/src/spaced/spaced/scenes/menu.cpp index 41a4b36..442c405 100644 --- a/src/spaced/spaced/scenes/menu.cpp +++ b/src/spaced/spaced/scenes/menu.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -12,8 +11,7 @@ namespace spaced { using bave::App; -using bave::AssetList; -using bave::AsyncExec; +using bave::AssetManifest; using bave::Seconds; using bave::Services; using bave::TextHeight; @@ -22,12 +20,10 @@ namespace ui = bave::ui; MenuScene::MenuScene(App& app, Services const& services) : Scene(app, services, "Home") {} -auto MenuScene::build_load_stages() -> std::vector { - auto ret = std::vector{}; - auto asset_list = AssetList{make_loader(), get_services()}; - asset_list.add_audio_clip("music/menu.mp3"); - ret.push_back(asset_list.build_load_stage()); - return ret; +auto MenuScene::get_asset_manifest() -> AssetManifest { + return AssetManifest{ + .audio_clips = {"music/menu.mp3"}, + }; } void MenuScene::on_loaded() { diff --git a/src/spaced/spaced/scenes/menu.hpp b/src/spaced/spaced/scenes/menu.hpp index 5d5ea94..7894590 100644 --- a/src/spaced/spaced/scenes/menu.hpp +++ b/src/spaced/spaced/scenes/menu.hpp @@ -7,7 +7,7 @@ class MenuScene : public bave::Scene { explicit MenuScene(bave::App& app, bave::Services const& services); private: - auto build_load_stages() -> std::vector final; + auto get_asset_manifest() -> bave::AssetManifest final; void on_loaded() final; void create_ui(); diff --git a/src/spaced/spaced/services/game_signals.hpp b/src/spaced/spaced/services/game_signals.hpp new file mode 100644 index 0000000..18a3a67 --- /dev/null +++ b/src/spaced/spaced/services/game_signals.hpp @@ -0,0 +1,13 @@ +#pragma once +#include +#include +#include + +namespace spaced { +struct SigWeaponChanged : Signal {}; + +class GameSignals : public bave::IService { + public: + SigWeaponChanged weapon_changed{}; +}; +} // namespace spaced diff --git a/src/spaced/spaced/services/layout.hpp b/src/spaced/spaced/services/layout.hpp index 61d8b69..f60121d 100644 --- a/src/spaced/spaced/services/layout.hpp +++ b/src/spaced/spaced/services/layout.hpp @@ -1,14 +1,21 @@ #pragma once #include +#include #include namespace spaced { struct Layout : bave::IService { + bave::NotNull render_device; + glm::vec2 world_space{}; bave::Rect<> play_area{}; bave::Rect<> hud_area{}; float player_x{-700.0f}; glm::vec2 player_size{100.0f}; + + explicit Layout(bave::NotNull render_device) : render_device(render_device) {} + + [[nodiscard]] auto project(glm::vec2 fb_point) const -> glm::vec2 { return render_device->project_to(world_space, fb_point); } }; } // namespace spaced diff --git a/src/spaced/spaced/signal.hpp b/src/spaced/spaced/signal.hpp new file mode 100644 index 0000000..a483ee2 --- /dev/null +++ b/src/spaced/spaced/signal.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include + +namespace spaced { +template +class Signal; + +using SignalHandle = std::shared_ptr; + +template +class Signal { + public: + using Callback = std::function; + + [[nodiscard]] auto connect(Callback callback) -> SignalHandle { + auto handle = std::make_shared(); + m_listeners.push_back(Listener{.callback = std::move(callback), .handle = handle}); + return handle; + } + + void dispatch(Args const&... args) const { + std::erase_if(m_listeners, [](Listener const& l) { return l.handle.expired(); }); + auto copy = m_listeners; + for (auto const& listener : copy) { listener.callback(args...); } + } + + private: + struct Listener { + Callback callback{}; + std::weak_ptr handle{}; + }; + + mutable std::vector m_listeners{}; +}; +} // namespace spaced diff --git a/src/spaced/spaced/spaced.cpp b/src/spaced/spaced/spaced.cpp index a0fde84..5011b37 100644 --- a/src/spaced/spaced/spaced.cpp +++ b/src/spaced/spaced/spaced.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -17,10 +18,10 @@ namespace spaced { namespace { using bave::App; +using bave::Display; using bave::GameDriver; using bave::Gamepad; using bave::IAudio; -using bave::IDisplay; using bave::NotNull; using bave::Persistor; using bave::Rect; @@ -108,17 +109,19 @@ void Spaced::create_services() { auto stats = std::make_unique(&get_app()); ++stats->run_count; m_services.bind(std::move(stats)); + + m_services.bind(std::make_unique()); } void Spaced::set_layout() { static constexpr auto world_space_v = glm::vec2{1920.0f, 1080.0f}; - auto layout = std::make_unique(); + auto layout = std::make_unique(&get_app().get_render_device()); m_layout = layout.get(); - auto& display = m_services.get(); - display.set_world_space(display.get_viewport_scaler().match_width(world_space_v)); + auto& world_space = m_services.get().world; + world_space.render_view.viewport = world_space.get_viewport_scaler().match_width(world_space_v); - layout->world_space = display.get_world_space(); + layout->world_space = world_space.get_size(); layout->player_x = -0.5f * layout->world_space.x + 0.2f * layout->world_space.x; auto const hud_size = glm::vec2{layout->world_space.x, 100.0f}; auto const hud_origin = glm::vec2{0.0f, 0.5f * (layout->world_space.y - hud_size.y)};