Skip to content

Commit

Permalink
Multiple lives (#49)
Browse files Browse the repository at this point in the history
* Move `Player` to `GameScene`. Trail out exhaust on death.

* Add `Shield`, fixup sprite sizes.

* Add lives icon on HUD.

* [Android] Hide status bar.

* Use blue player ship and exhaust.
  • Loading branch information
karnkaul authored Jun 19, 2024
1 parent 40801e4 commit 3cf1c29
Show file tree
Hide file tree
Showing 17 changed files with 286 additions and 98 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ include(FetchContent)
FetchContent_Declare(
bgf
GIT_REPOSITORY https://github.com/karnkaul/bgf
GIT_TAG v0.1.3
GIT_TAG v0.1.4
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/bgf"
)

Expand Down
Binary file modified assets/images/player_ship.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/player_ship_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/shield.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions assets/particles/exhaust.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"lerp": {
"tint": {
"lo": "#f48018ff",
"lo": "#36bbf5ff",
"hi": "#00000000"
},
"scale": {
Expand All @@ -58,4 +58,4 @@
"count": 200,
"respawn": true
}
}
}
4 changes: 3 additions & 1 deletion src/android/app/src/main/res/values-night/themes.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Spaced" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.Spaced" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
Expand All @@ -12,5 +12,7 @@
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
</resources>
4 changes: 3 additions & 1 deletion src/android/app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Spaced" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.Spaced" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
Expand All @@ -12,5 +12,7 @@
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
</resources>
42 changes: 42 additions & 0 deletions src/spaced/spaced/game/hud.cpp
Original file line number Diff line number Diff line change
@@ -1,31 +1,60 @@
#include <fmt/format.h>
#include <bave/services/resources.hpp>
#include <bave/services/styles.hpp>
#include <bave/ui/button.hpp>
#include <spaced/game/hud.hpp>
#include <spaced/services/layout.hpp>

namespace spaced {
using bave::IDisplay;
using bave::Resources;
using bave::Seconds;
using bave::Services;
using bave::Shader;
using bave::Styles;
using bave::TextHeight;
using bave::Texture;

namespace ui = bave::ui;

Hud::Hud(Services const& services)
: ui::View(services), m_display(&services.get<IDisplay>()), m_layout(&services.get<Layout>()), m_styles(&services.get<Styles>()) {
create_background();
create_score(services);
create_lives_icon(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;
}

m_lives_icon.instances.resize(static_cast<std::size_t>(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;
}
}

void Hud::on_death() {
if (m_lives_icon.instances.empty()) { return; }
m_lives_icon.instances.pop_back();
}

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::create_background() {
auto background = std::make_unique<ui::OutlineQuad>();
m_background = background.get();
Expand Down Expand Up @@ -65,4 +94,17 @@ void Hud::create_score(Services const& services) {

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};
auto const& resources = services.get<Resources>();
if (auto const texture = resources.get<Texture>("images/player_ship_icon.png")) {
quad.size = texture->get_size();
m_lives_icon.set_texture(texture);
}
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;
}
} // namespace spaced
8 changes: 8 additions & 0 deletions src/spaced/spaced/game/hud.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once
#include <bave/graphics/instanced.hpp>
#include <bave/services/styles.hpp>
#include <bave/ui/outline_quad.hpp>
#include <bave/ui/text.hpp>
Expand All @@ -11,12 +12,17 @@ class Hud : public bave::ui::View {
public:
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);

private:
void render(bave::Shader& shader) const final;

void create_background();
void create_score(bave::Services const& services);
void create_lives_icon(bave::Services const& services);

bave::NotNull<bave::IDisplay const*> m_display;
bave::NotNull<Layout const*> m_layout;
Expand All @@ -26,5 +32,7 @@ class Hud : public bave::ui::View {
bave::Ptr<bave::ui::OutlineQuad> m_background{};
bave::Ptr<bave::ui::Text> m_score{};
bave::Ptr<bave::ui::Text> m_hi_score{};

bave::Instanced<bave::QuadShape> m_lives_icon{};
};
} // namespace spaced
73 changes: 53 additions & 20 deletions src/spaced/spaced/game/player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@ using bave::Shader;
using bave::Texture;

Player::Player(Services const& services, std::unique_ptr<IController> controller)
: m_services(&services), m_stats(&services.get<Stats>()), m_controller(std::move(controller)) {
: m_services(&services), m_stats(&services.get<Stats>()), m_controller(std::move(controller)), m_shield(services) {
auto const& layout = services.get<Layout>();
ship.transform.position.x = layout.player_x;

auto const& resources = services.get<Resources>();

if (auto const texture = services.get<Resources>().get<Texture>("images/player_ship.png")) { ship.set_texture(texture); }
ship.set_auto_size(ship_size);
if (auto const texture = services.get<Resources>().get<Texture>("images/player_ship.png")) {
ship.set_texture(texture);
ship.set_size(texture->get_size());
}

if (auto const exhaust = resources.get<ParticleEmitter>("particles/exhaust.json")) { m_exhaust = *exhaust; }
m_exhaust.set_position(get_exhaust_position());
m_exhaust.config.respawn = true;
m_exhaust.pre_warm();

if (auto const death = resources.get<ParticleEmitter>("particles/explode.json")) { m_death_source = *death; }
Expand All @@ -45,7 +48,7 @@ void Player::on_move(PointerMove const& pointer_move) { m_controller->on_move(po

void Player::on_tap(PointerTap const& pointer_tap) { m_controller->on_tap(pointer_tap); }

void Player::tick(State const& state, Seconds const dt) {
auto Player::tick(State const& state, Seconds const dt) -> bool {
if (m_death) {
m_death->tick(dt);
if (m_death->active_particles() == 0) { m_death.reset(); }
Expand All @@ -54,39 +57,41 @@ void Player::tick(State const& state, Seconds const dt) {
auto const round_state = IWeaponRound::State{
.targets = state.targets,
.muzzle_position = get_muzzle_position(),
.in_play = !health.is_dead(),
.in_play = !m_health.is_dead(),
};
m_arsenal.tick(round_state, m_controller->is_firing(), dt);

if (health.is_dead()) { return; }
m_shield.set_position(ship.transform.position);
m_shield.tick(dt);

m_exhaust.tick(dt);

if (m_health.is_dead()) { return false; }

auto const y_position = m_controller->tick(dt);
set_y(y_position);

auto const hitbox = Rect<>::from_size(hitbox_size, ship.transform.position);
for (auto const& target : state.targets) {
if (is_intersecting(target->get_bounds(), hitbox)) {
on_death(dt);
target->force_death();
return;
}
}

m_exhaust.set_position(get_exhaust_position());
m_exhaust.tick(dt);

auto ret = false;
auto const hitbox = Rect<>::from_size(hitbox_size, ship.transform.position);
for (auto const& target : state.targets) { ret |= check_hit(*target, hitbox, dt); }

for (auto const& powerup : state.powerups) {
if (is_intersecting(powerup->get_bounds(), ship.get_bounds())) {
powerup->activate(*this);
++m_stats->player.powerups_collected;
}
}

return ret;
}

void Player::draw(Shader& shader) const {
if (!health.is_dead()) {
m_exhaust.draw(shader);
m_exhaust.draw(shader);
if (!m_health.is_dead()) {
ship.draw(shader);
m_shield.draw(shader);
}
m_arsenal.draw(shader);
if (m_death) { m_death->draw(shader); }
Expand All @@ -103,14 +108,37 @@ void Player::set_controller(std::unique_ptr<IController> controller) {
m_controller = std::move(controller);
}

void Player::set_shield(Seconds const ttl) {
m_shield.ttl = ttl;
m_shield.set_position(ship.transform.position);
}

void Player::on_death(Seconds const dt) {
health = 0.0f;
m_health = 0.0f;
m_death = m_death_source;
m_death->set_position(ship.transform.position);
m_death->tick(dt);

m_exhaust.config.respawn = false;

++m_stats->player.death_count;
}

auto Player::check_hit(IDamageable& out, Rect<> const& hitbox, Seconds const dt) -> bool {
if (m_shield.is_active()) {
if (is_intersecting(out.get_bounds(), m_shield.get_bounds())) { out.force_death(); }
return false;
}

if (is_intersecting(out.get_bounds(), hitbox)) {
out.force_death();
on_death(dt);
return true;
}

return false;
}

void Player::do_inspect() {
if constexpr (bave::imgui_v) {
if (ImGui::TreeNodeEx("Controller", ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen)) {
Expand All @@ -126,8 +154,13 @@ void Player::do_inspect() {
m_arsenal.get_weapon().inspect();
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Shield", ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen)) {
auto ttl = m_shield.ttl.count();
if (ImGui::DragFloat("ttl", &ttl, 0.25f, 0.0f, 60.0f, "%.2f")) { m_shield.ttl = Seconds{ttl}; }
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Status", ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen)) {
health.inspect();
m_health.inspect();
ImGui::TreePop();
}
}
Expand Down
15 changes: 12 additions & 3 deletions src/spaced/spaced/game/player.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <spaced/game/controller.hpp>
#include <spaced/game/health.hpp>
#include <spaced/game/powerup.hpp>
#include <spaced/game/shield.hpp>

namespace spaced {
struct Stats;
Expand All @@ -23,7 +24,7 @@ class Player : public bave::IDrawable {
void on_move(bave::PointerMove const& pointer_move);
void on_tap(bave::PointerTap const& pointer_tap);

void tick(State const& state, bave::Seconds dt);
auto tick(State const& state, bave::Seconds dt) -> bool;
void draw(bave::Shader& shader) const final;

void set_y(float y);
Expand All @@ -37,28 +38,36 @@ class Player : public bave::IDrawable {

void set_special_weapon(std::unique_ptr<Weapon> weapon) { m_arsenal.set_special(std::move(weapon)); }

void set_shield(bave::Seconds ttl);

[[nodiscard]] auto is_dead() const -> bool { return m_health.is_dead(); }
[[nodiscard]] auto is_idle() const -> bool { return m_exhaust.active_particles() == 0; }

void on_death(bave::Seconds dt);

void inspect() {
if constexpr (bave::debug_v) { do_inspect(); }
}

bave::Sprite ship{};
glm::vec2 ship_size{100.0f};
glm::vec2 hitbox_size{75.0f};
Health health{};

private:
auto check_hit(IDamageable& out, bave::Rect<> const& hitbox, bave::Seconds dt) -> bool;

void do_inspect();

bave::Logger m_log{"Player"};
bave::NotNull<bave::Services const*> m_services;
bave::NotNull<Stats*> m_stats;
std::unique_ptr<IController> m_controller;
bave::ParticleEmitter m_exhaust{};
Shield m_shield;

bave::ParticleEmitter m_death_source{};
std::optional<bave::ParticleEmitter> m_death{};

Arsenal m_arsenal{*m_services};
Health m_health{};
};
} // namespace spaced
Loading

0 comments on commit 3cf1c29

Please sign in to comment.