Skip to content

Commit 3cf1c29

Browse files
authored
Multiple lives (#49)
* 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.
1 parent 40801e4 commit 3cf1c29

File tree

17 files changed

+286
-98
lines changed

17 files changed

+286
-98
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ include(FetchContent)
1111
FetchContent_Declare(
1212
bgf
1313
GIT_REPOSITORY https://github.com/karnkaul/bgf
14-
GIT_TAG v0.1.3
14+
GIT_TAG v0.1.4
1515
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/bgf"
1616
)
1717

assets/images/player_ship.png

198 Bytes
Loading

assets/images/player_ship_icon.png

6.54 KB
Loading

assets/images/shield.png

7.65 KB
Loading

assets/particles/exhaust.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
},
3434
"lerp": {
3535
"tint": {
36-
"lo": "#f48018ff",
36+
"lo": "#36bbf5ff",
3737
"hi": "#00000000"
3838
},
3939
"scale": {
@@ -58,4 +58,4 @@
5858
"count": 200,
5959
"respawn": true
6060
}
61-
}
61+
}

src/android/app/src/main/res/values-night/themes.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<resources xmlns:tools="http://schemas.android.com/tools">
22
<!-- Base application theme. -->
3-
<style name="Theme.Spaced" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
3+
<style name="Theme.Spaced" parent="Theme.MaterialComponents.DayNight.NoActionBar">
44
<!-- Primary brand color. -->
55
<item name="colorPrimary">@color/purple_200</item>
66
<item name="colorPrimaryVariant">@color/purple_700</item>
@@ -12,5 +12,7 @@
1212
<!-- Status bar color. -->
1313
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
1414
<!-- Customize your theme here. -->
15+
<item name="android:windowNoTitle">true</item>
16+
<item name="android:windowFullscreen">true</item>
1517
</style>
1618
</resources>

src/android/app/src/main/res/values/themes.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<resources xmlns:tools="http://schemas.android.com/tools">
22
<!-- Base application theme. -->
3-
<style name="Theme.Spaced" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
3+
<style name="Theme.Spaced" parent="Theme.MaterialComponents.DayNight.NoActionBar">
44
<!-- Primary brand color. -->
55
<item name="colorPrimary">@color/purple_500</item>
66
<item name="colorPrimaryVariant">@color/purple_700</item>
@@ -12,5 +12,7 @@
1212
<!-- Status bar color. -->
1313
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
1414
<!-- Customize your theme here. -->
15+
<item name="android:windowNoTitle">true</item>
16+
<item name="android:windowFullscreen">true</item>
1517
</style>
1618
</resources>

src/spaced/spaced/game/hud.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,60 @@
11
#include <fmt/format.h>
2+
#include <bave/services/resources.hpp>
23
#include <bave/services/styles.hpp>
34
#include <bave/ui/button.hpp>
45
#include <spaced/game/hud.hpp>
56
#include <spaced/services/layout.hpp>
67

78
namespace spaced {
89
using bave::IDisplay;
10+
using bave::Resources;
911
using bave::Seconds;
1012
using bave::Services;
13+
using bave::Shader;
1114
using bave::Styles;
1215
using bave::TextHeight;
16+
using bave::Texture;
1317

1418
namespace ui = bave::ui;
1519

1620
Hud::Hud(Services const& services)
1721
: ui::View(services), m_display(&services.get<IDisplay>()), m_layout(&services.get<Layout>()), m_styles(&services.get<Styles>()) {
1822
create_background();
1923
create_score(services);
24+
create_lives_icon(services);
2025

2126
block_input_events = false;
2227
render_view = m_display->get_world_view();
2328
}
2429

30+
void Hud::set_lives(int const lives) {
31+
if (lives <= 0) {
32+
m_lives_icon.instances.clear();
33+
return;
34+
}
35+
36+
m_lives_icon.instances.resize(static_cast<std::size_t>(lives));
37+
auto x_offset = 0.0f;
38+
for (auto& instance : m_lives_icon.instances) {
39+
instance.transform.position.x += x_offset;
40+
x_offset += 2.0f * m_lives_icon.get_shape().size.x;
41+
}
42+
}
43+
44+
void Hud::on_death() {
45+
if (m_lives_icon.instances.empty()) { return; }
46+
m_lives_icon.instances.pop_back();
47+
}
48+
2549
void Hud::set_score(std::int64_t const score) { m_score->text.set_string(fmt::format("{}", score)); }
2650

2751
void Hud::set_hi_score(std::int64_t const score) { m_hi_score->text.set_string(fmt::format("HI {}", score)); }
2852

53+
void Hud::render(Shader& shader) const {
54+
View::render(shader);
55+
m_lives_icon.draw(shader);
56+
}
57+
2958
void Hud::create_background() {
3059
auto background = std::make_unique<ui::OutlineQuad>();
3160
m_background = background.get();
@@ -65,4 +94,17 @@ void Hud::create_score(Services const& services) {
6594

6695
push(std::move(text));
6796
}
97+
98+
void Hud::create_lives_icon(Services const& services) {
99+
auto quad = m_lives_icon.get_shape();
100+
quad.size = glm::vec2{20.0f};
101+
auto const& resources = services.get<Resources>();
102+
if (auto const texture = resources.get<Texture>("images/player_ship_icon.png")) {
103+
quad.size = texture->get_size();
104+
m_lives_icon.set_texture(texture);
105+
}
106+
m_lives_icon.set_shape(quad);
107+
m_lives_icon.transform.position = m_layout->hud_area.centre();
108+
m_lives_icon.transform.position.x = m_layout->hud_area.lt.x + 100.0f;
109+
}
68110
} // namespace spaced

src/spaced/spaced/game/hud.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#pragma once
2+
#include <bave/graphics/instanced.hpp>
23
#include <bave/services/styles.hpp>
34
#include <bave/ui/outline_quad.hpp>
45
#include <bave/ui/text.hpp>
@@ -11,12 +12,17 @@ class Hud : public bave::ui::View {
1112
public:
1213
explicit Hud(bave::Services const& services);
1314

15+
void set_lives(int lives);
16+
void on_death();
1417
void set_score(std::int64_t score);
1518
void set_hi_score(std::int64_t score);
1619

1720
private:
21+
void render(bave::Shader& shader) const final;
22+
1823
void create_background();
1924
void create_score(bave::Services const& services);
25+
void create_lives_icon(bave::Services const& services);
2026

2127
bave::NotNull<bave::IDisplay const*> m_display;
2228
bave::NotNull<Layout const*> m_layout;
@@ -26,5 +32,7 @@ class Hud : public bave::ui::View {
2632
bave::Ptr<bave::ui::OutlineQuad> m_background{};
2733
bave::Ptr<bave::ui::Text> m_score{};
2834
bave::Ptr<bave::ui::Text> m_hi_score{};
35+
36+
bave::Instanced<bave::QuadShape> m_lives_icon{};
2937
};
3038
} // namespace spaced

src/spaced/spaced/game/player.cpp

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,20 @@ using bave::Shader;
2222
using bave::Texture;
2323

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

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

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

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

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

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

48-
void Player::tick(State const& state, Seconds const dt) {
51+
auto Player::tick(State const& state, Seconds const dt) -> bool {
4952
if (m_death) {
5053
m_death->tick(dt);
5154
if (m_death->active_particles() == 0) { m_death.reset(); }
@@ -54,39 +57,41 @@ void Player::tick(State const& state, Seconds const dt) {
5457
auto const round_state = IWeaponRound::State{
5558
.targets = state.targets,
5659
.muzzle_position = get_muzzle_position(),
57-
.in_play = !health.is_dead(),
60+
.in_play = !m_health.is_dead(),
5861
};
5962
m_arsenal.tick(round_state, m_controller->is_firing(), dt);
6063

61-
if (health.is_dead()) { return; }
64+
m_shield.set_position(ship.transform.position);
65+
m_shield.tick(dt);
66+
67+
m_exhaust.tick(dt);
68+
69+
if (m_health.is_dead()) { return false; }
6270

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

66-
auto const hitbox = Rect<>::from_size(hitbox_size, ship.transform.position);
67-
for (auto const& target : state.targets) {
68-
if (is_intersecting(target->get_bounds(), hitbox)) {
69-
on_death(dt);
70-
target->force_death();
71-
return;
72-
}
73-
}
74-
7574
m_exhaust.set_position(get_exhaust_position());
76-
m_exhaust.tick(dt);
75+
76+
auto ret = false;
77+
auto const hitbox = Rect<>::from_size(hitbox_size, ship.transform.position);
78+
for (auto const& target : state.targets) { ret |= check_hit(*target, hitbox, dt); }
7779

7880
for (auto const& powerup : state.powerups) {
7981
if (is_intersecting(powerup->get_bounds(), ship.get_bounds())) {
8082
powerup->activate(*this);
8183
++m_stats->player.powerups_collected;
8284
}
8385
}
86+
87+
return ret;
8488
}
8589

8690
void Player::draw(Shader& shader) const {
87-
if (!health.is_dead()) {
88-
m_exhaust.draw(shader);
91+
m_exhaust.draw(shader);
92+
if (!m_health.is_dead()) {
8993
ship.draw(shader);
94+
m_shield.draw(shader);
9095
}
9196
m_arsenal.draw(shader);
9297
if (m_death) { m_death->draw(shader); }
@@ -103,14 +108,37 @@ void Player::set_controller(std::unique_ptr<IController> controller) {
103108
m_controller = std::move(controller);
104109
}
105110

111+
void Player::set_shield(Seconds const ttl) {
112+
m_shield.ttl = ttl;
113+
m_shield.set_position(ship.transform.position);
114+
}
115+
106116
void Player::on_death(Seconds const dt) {
107-
health = 0.0f;
117+
m_health = 0.0f;
108118
m_death = m_death_source;
109119
m_death->set_position(ship.transform.position);
110120
m_death->tick(dt);
121+
122+
m_exhaust.config.respawn = false;
123+
111124
++m_stats->player.death_count;
112125
}
113126

127+
auto Player::check_hit(IDamageable& out, Rect<> const& hitbox, Seconds const dt) -> bool {
128+
if (m_shield.is_active()) {
129+
if (is_intersecting(out.get_bounds(), m_shield.get_bounds())) { out.force_death(); }
130+
return false;
131+
}
132+
133+
if (is_intersecting(out.get_bounds(), hitbox)) {
134+
out.force_death();
135+
on_death(dt);
136+
return true;
137+
}
138+
139+
return false;
140+
}
141+
114142
void Player::do_inspect() {
115143
if constexpr (bave::imgui_v) {
116144
if (ImGui::TreeNodeEx("Controller", ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen)) {
@@ -126,8 +154,13 @@ void Player::do_inspect() {
126154
m_arsenal.get_weapon().inspect();
127155
ImGui::TreePop();
128156
}
157+
if (ImGui::TreeNodeEx("Shield", ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen)) {
158+
auto ttl = m_shield.ttl.count();
159+
if (ImGui::DragFloat("ttl", &ttl, 0.25f, 0.0f, 60.0f, "%.2f")) { m_shield.ttl = Seconds{ttl}; }
160+
ImGui::TreePop();
161+
}
129162
if (ImGui::TreeNodeEx("Status", ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_DefaultOpen)) {
130-
health.inspect();
163+
m_health.inspect();
131164
ImGui::TreePop();
132165
}
133166
}

src/spaced/spaced/game/player.hpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <spaced/game/controller.hpp>
77
#include <spaced/game/health.hpp>
88
#include <spaced/game/powerup.hpp>
9+
#include <spaced/game/shield.hpp>
910

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

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

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

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

41+
void set_shield(bave::Seconds ttl);
42+
43+
[[nodiscard]] auto is_dead() const -> bool { return m_health.is_dead(); }
44+
[[nodiscard]] auto is_idle() const -> bool { return m_exhaust.active_particles() == 0; }
45+
4046
void on_death(bave::Seconds dt);
4147

4248
void inspect() {
4349
if constexpr (bave::debug_v) { do_inspect(); }
4450
}
4551

4652
bave::Sprite ship{};
47-
glm::vec2 ship_size{100.0f};
4853
glm::vec2 hitbox_size{75.0f};
49-
Health health{};
5054

5155
private:
56+
auto check_hit(IDamageable& out, bave::Rect<> const& hitbox, bave::Seconds dt) -> bool;
57+
5258
void do_inspect();
5359

5460
bave::Logger m_log{"Player"};
5561
bave::NotNull<bave::Services const*> m_services;
5662
bave::NotNull<Stats*> m_stats;
5763
std::unique_ptr<IController> m_controller;
5864
bave::ParticleEmitter m_exhaust{};
65+
Shield m_shield;
66+
5967
bave::ParticleEmitter m_death_source{};
6068
std::optional<bave::ParticleEmitter> m_death{};
6169

6270
Arsenal m_arsenal{*m_services};
71+
Health m_health{};
6372
};
6473
} // namespace spaced

0 commit comments

Comments
 (0)