Skip to content

Commit ea2d65c

Browse files
authored
Add foam particles to player and for enemy explosions. (#21)
* Add foam particles. * Add `AssetLoader`. * Add `ResourceMap`, `EnemySpawner`. Refactor `Resources` to inherit from `ResourceMap`. * Clear transient resources before switching scene. * Remove `ext/` from git history and ignore it. * Move enemy inspection into `EnemySpawner::inspect()`.
1 parent 1b034a1 commit ea2d65c

19 files changed

+311
-37
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
**/.vscode/*
33
build/*
44
out/*
5-
ext/*
5+
/ext
66
screenshots
77
.cache
88
.DS_Store

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
bave
1313
GIT_REPOSITORY https://github.com/karnkaul/bave
14-
GIT_TAG 5002fc271533a3268592c47c5bc8653ae094dc6b # v0.4.6
14+
GIT_TAG 2b1b7ce206a031aa2c2c957f67c8bf92cc031b10 # v0.4.6
1515
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/bave"
1616
)
1717

assets/images/foam_bubble.png

33.7 KB
Loading

assets/styles.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"grey": "#535151ff",
55
"mocha": "#6f5a48ff",
66
"milk": "#e5cdaeff",
7-
"ice": "#0xd6dbe1e1"
7+
"ice": "#0xd6dbe1e1",
8+
"orange": "#f75c03ff"
89
},
910
"buttons": {
1011
"default": {

ext/bave

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#include <bave/logger.hpp>
2+
#include <spaced/game/asset_loader.hpp>
3+
#include <mutex>
4+
5+
namespace spaced {
6+
using bave::Loader;
7+
using bave::Logger;
8+
using bave::NotNull;
9+
10+
struct AssetLoader::Impl {
11+
Logger log{"AssetLoader"};
12+
Loader loader;
13+
NotNull<Resources*> resources;
14+
std::mutex mutex{};
15+
};
16+
17+
AssetLoader::AssetLoader(Loader loader, NotNull<Resources*> resources) : m_impl(new Impl{.loader = std::move(loader), .resources = resources}) {}
18+
19+
auto AssetLoader::make_load_font(std::string uri, bool reload) -> LoadTask {
20+
auto const load = [](Loader const& loader, std::string_view const uri) { return loader.load_font(uri); };
21+
return make_load_task(std::move(uri), reload, load);
22+
}
23+
24+
auto AssetLoader::make_load_texture(std::string uri, bool mip_map, bool reload) -> LoadTask {
25+
auto const load = [mip_map](Loader const& loader, std::string_view const uri) { return loader.load_texture(uri, mip_map); };
26+
return make_load_task(std::move(uri), reload, load);
27+
}
28+
29+
auto AssetLoader::make_load_texture_atlas(std::string uri, bool mip_map, bool reload) -> LoadTask {
30+
auto const load = [mip_map](Loader const& loader, std::string_view const uri) { return loader.load_texture_atlas(uri, mip_map); };
31+
return make_load_task(std::move(uri), reload, load);
32+
}
33+
34+
template <typename FuncT>
35+
auto AssetLoader::make_load_task(std::string uri, bool reload, FuncT load) const -> LoadTask {
36+
return [impl = m_impl, uri = std::move(uri), reload, load] {
37+
auto lock = std::unique_lock{impl->mutex};
38+
if (!reload && impl->resources->contains(uri)) { return; }
39+
40+
lock.unlock();
41+
auto asset = load(impl->loader, uri);
42+
if (!asset) { return; }
43+
44+
lock.lock();
45+
impl->resources->add(uri, std::move(asset));
46+
};
47+
}
48+
} // namespace spaced
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#pragma once
2+
#include <bave/core/ptr.hpp>
3+
#include <bave/loader.hpp>
4+
#include <spaced/services/resources.hpp>
5+
#include <functional>
6+
#include <memory>
7+
8+
namespace spaced {
9+
class AssetLoader {
10+
public:
11+
using LoadTask = std::function<void()>;
12+
13+
explicit AssetLoader(bave::Loader loader, bave::NotNull<Resources*> resources);
14+
15+
[[nodiscard]] auto make_load_font(std::string uri, bool reload = false) -> LoadTask;
16+
[[nodiscard]] auto make_load_texture(std::string uri, bool mip_map = false, bool reload = false) -> LoadTask;
17+
[[nodiscard]] auto make_load_texture_atlas(std::string uri, bool mip_map = false, bool reload = false) -> LoadTask;
18+
19+
private:
20+
template <typename FuncT>
21+
auto make_load_task(std::string uri, bool reload, FuncT load) const -> LoadTask;
22+
23+
struct Impl;
24+
std::shared_ptr<Impl> m_impl{};
25+
};
26+
} // namespace spaced
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include <imgui.h>
2+
#include <bave/core/fixed_string.hpp>
3+
#include <spaced/game/enemy_spawner.hpp>
4+
5+
namespace spaced {
6+
using bave::FixedString;
7+
using bave::ParticleEmitter;
8+
using bave::Seconds;
9+
using bave::Shader;
10+
11+
EnemySpawner::EnemySpawner(Spawn spawn, ParticleEmitter explode) : m_spawn(std::move(spawn)), m_explode(std::move(explode)) {
12+
m_explode.config.respawn = false;
13+
}
14+
15+
void EnemySpawner::tick(Seconds const dt) {
16+
for (auto const& enemy : m_enemies) {
17+
enemy->tick(dt);
18+
if (enemy->is_destroyed()) { explode_at(enemy->get_bounds().centre()); }
19+
}
20+
21+
for (auto& emitter : m_explodes) { emitter.tick(dt); }
22+
23+
std::erase_if(m_enemies, [](auto const& enemy) { return enemy->is_destroyed(); });
24+
std::erase_if(m_explodes, [](ParticleEmitter const& emitter) { return emitter.active_particles() == 0; });
25+
}
26+
27+
void EnemySpawner::draw(Shader& shader) const {
28+
for (auto const& enemy : m_enemies) { enemy->draw(shader); }
29+
for (auto const& emitter : m_explodes) { emitter.draw(shader); }
30+
}
31+
32+
void EnemySpawner::append_targets(std::vector<bave::NotNull<IDamageable*>>& out) const {
33+
out.reserve(out.size() + m_enemies.size());
34+
for (auto const& enemy : m_enemies) { out.push_back(enemy.get()); }
35+
}
36+
37+
void EnemySpawner::explode_at(glm::vec2 const position) {
38+
auto& emitter = m_explodes.emplace_back(m_explode);
39+
emitter.set_position(position);
40+
}
41+
42+
void EnemySpawner::do_inspect() {
43+
if constexpr (bave::imgui_v) {
44+
for (std::size_t i = 0; i < m_enemies.size(); ++i) {
45+
if (ImGui::TreeNode(FixedString{"{}", i}.c_str())) {
46+
m_enemies.at(i)->inspect();
47+
ImGui::TreePop();
48+
}
49+
}
50+
}
51+
}
52+
} // namespace spaced
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#pragma once
2+
#include <bave/graphics/particle_system.hpp>
3+
#include <spaced/game/enemy.hpp>
4+
#include <functional>
5+
6+
namespace spaced {
7+
class EnemySpawner {
8+
public:
9+
using Spawn = std::function<std::unique_ptr<Enemy>()>;
10+
11+
explicit EnemySpawner(Spawn spawn, bave::ParticleEmitter explode);
12+
13+
void tick(bave::Seconds dt);
14+
void draw(bave::Shader& shader) const;
15+
16+
void spawn() { m_enemies.push_back(m_spawn()); }
17+
18+
[[nodiscard]] auto get_enemies() const -> std::span<std::unique_ptr<Enemy> const> { return m_enemies; }
19+
void append_targets(std::vector<bave::NotNull<IDamageable*>>& out) const;
20+
21+
void inspect() {
22+
if constexpr (bave::debug_v) { do_inspect(); }
23+
}
24+
25+
private:
26+
void explode_at(glm::vec2 position);
27+
void do_inspect();
28+
29+
Spawn m_spawn{};
30+
bave::ParticleEmitter m_explode{};
31+
std::vector<std::unique_ptr<Enemy>> m_enemies{};
32+
std::vector<bave::ParticleEmitter> m_explodes{};
33+
};
34+
} // namespace spaced

src/spaced/spaced/game/player.cpp

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,26 @@
22
#include <bave/imgui/im_text.hpp>
33
#include <bave/platform.hpp>
44
#include <spaced/game/player.hpp>
5+
#include <spaced/services/resources.hpp>
56

67
// temp for testing
78
#include <spaced/game/weapons/gun_beam.hpp>
89
#include <spaced/game/weapons/gun_kinetic.hpp>
910

1011
namespace spaced {
12+
using bave::Degrees;
1113
using bave::im_text;
1214
using bave::NotNull;
15+
using bave::ParticleEmitter;
1316
using bave::PointerMove;
1417
using bave::PointerTap;
1518
using bave::RoundedQuad;
1619
using bave::Seconds;
1720
using bave::Shader;
1821

1922
Player::Player(Services const& services, std::unique_ptr<IController> controller) : m_services(&services), m_controller(std::move(controller)) {
20-
auto const x = services.get<ILayout>().get_player_x();
21-
ship.transform.position.x = x;
22-
auto rounded_quad = RoundedQuad{};
23-
rounded_quad.size = glm::vec2{100.0f};
24-
rounded_quad.corner_radius = 20.0f;
25-
ship.set_shape(rounded_quad);
23+
setup_ship();
24+
setup_foam();
2625

2726
debug_switch_weapon();
2827
}
@@ -37,7 +36,7 @@ void Player::tick(std::span<NotNull<IDamageable*> const> targets, Seconds const
3736
auto const y_position = m_controller->tick(dt);
3837
set_y(y_position);
3938

40-
auto const muzzle_position = ship.transform.position + 0.5f * glm::vec2{ship.get_shape().size.x, 0.0f};
39+
auto const muzzle_position = get_muzzle_position();
4140
if (m_controller->is_firing() && m_debug.shots_remaining > 0) {
4241
if (auto round = m_weapon->fire(muzzle_position)) {
4342
m_weapon_rounds.push_back(std::move(round));
@@ -51,17 +50,26 @@ void Player::tick(std::span<NotNull<IDamageable*> const> targets, Seconds const
5150

5251
m_weapon->tick(dt);
5352

53+
foam_particles.set_position(get_exhaust_position());
54+
55+
foam_particles.tick(dt);
56+
5457
if (m_debug.shots_remaining <= 0) { debug_switch_weapon(); }
5558
}
5659

5760
void Player::draw(Shader& shader) const {
61+
foam_particles.draw(shader);
5862
ship.draw(shader);
5963

6064
for (auto const& round : m_weapon_rounds) { round->draw(shader); }
6165
}
6266

6367
void Player::set_y(float const y) { ship.transform.position.y = y; }
6468

69+
auto Player::get_muzzle_position() const -> glm::vec2 { return ship.transform.position + 0.5f * glm::vec2{ship.get_shape().size.x, 0.0f}; }
70+
71+
auto Player::get_exhaust_position() const -> glm::vec2 { return ship.transform.position - 0.5f * glm::vec2{ship.get_shape().size.x, 0.0f}; }
72+
6573
void Player::set_controller(std::unique_ptr<IController> controller) {
6674
if (!controller) { return; }
6775
m_controller = std::move(controller);
@@ -85,6 +93,29 @@ void Player::do_inspect() {
8593
}
8694
}
8795

96+
void Player::setup_ship() {
97+
auto const x = m_services->get<ILayout>().get_player_x();
98+
ship.transform.position.x = x;
99+
auto rounded_quad = RoundedQuad{};
100+
rounded_quad.size = glm::vec2{100.0f};
101+
rounded_quad.corner_radius = 20.0f;
102+
ship.set_shape(rounded_quad);
103+
}
104+
105+
void Player::setup_foam() {
106+
using Modifier = ParticleEmitter::Modifier;
107+
foam_particles.config.quad_size = glm::vec2{20.0f};
108+
foam_particles.config.velocity.linear.angle = {Degrees{80.0f}, Degrees{100.0f}};
109+
foam_particles.config.velocity.linear.speed = {-100.0f, -200.0f};
110+
foam_particles.config.ttl = {1s, 2s};
111+
foam_particles.config.lerp.tint.hi.channels.w = 0x0;
112+
foam_particles.config.lerp.scale.hi = {};
113+
foam_particles.config.count = 500;
114+
foam_particles.modifiers = {Modifier::eTranslate, Modifier::eTint, Modifier::eScale};
115+
foam_particles.set_position(get_exhaust_position());
116+
foam_particles.pre_warm();
117+
}
118+
88119
void Player::debug_switch_weapon() {
89120
if (m_weapon && !m_weapon->is_idle()) { return; }
90121

src/spaced/spaced/game/player.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/particle_system.hpp>
23
#include <bave/graphics/shape.hpp>
34
#include <bave/logger.hpp>
45
#include <spaced/game/controller.hpp>
@@ -24,13 +25,20 @@ class Player : public bave::IDrawable {
2425
void set_y(float y);
2526
[[nodiscard]] auto get_y() const -> float { return ship.transform.position.y; }
2627

28+
[[nodiscard]] auto get_muzzle_position() const -> glm::vec2;
29+
[[nodiscard]] auto get_exhaust_position() const -> glm::vec2;
30+
2731
void set_controller(std::unique_ptr<IController> controller);
2832
[[nodiscard]] auto get_controller() const -> IController const& { return *m_controller; }
2933

3034
bave::RoundedQuadShape ship{};
35+
bave::ParticleEmitter foam_particles{};
3136
Health health{};
3237

3338
private:
39+
void setup_ship();
40+
void setup_foam();
41+
3442
void do_inspect();
3543
void debug_switch_weapon();
3644

src/spaced/spaced/game/weapons/gun_beam.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ class LaserCharge : public IWeaponRound {
9797
};
9898
} // namespace
9999

100-
GunBeam::GunBeam(Services const& services) : Weapon(services, "GunBeam") { config.beam_tint = services.get<Styles>().rgbas["grey"]; }
100+
GunBeam::GunBeam(Services const& services) : Weapon(services, "GunBeam") {
101+
auto const& rgbas = services.get<Styles>().rgbas;
102+
config.beam_tint = rgbas.get_or("gun_beam", rgbas["grey"]);
103+
}
101104

102105
auto GunBeam::fire(glm::vec2 const muzzle_position) -> std::unique_ptr<Round> {
103106
if (!is_idle() || m_reload_remain > 0s) { return {}; }

src/spaced/spaced/resource_map.hpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#pragma once
2+
#include <bave/core/polymorphic.hpp>
3+
#include <spaced/string_map.hpp>
4+
#include <memory>
5+
#include <typeindex>
6+
7+
namespace spaced {
8+
class ResourceMap {
9+
public:
10+
template <typename Type>
11+
auto add(std::string uri, std::shared_ptr<Type> resource) {
12+
if (uri.empty() || !resource) { return; }
13+
m_resources.insert_or_assign(std::move(uri), std::make_unique<Model<Type>>(std::move(resource)));
14+
}
15+
16+
[[nodiscard]] auto contains(std::string_view const uri) const -> bool { return m_resources.contains(uri); }
17+
18+
template <typename Type>
19+
[[nodiscard]] auto contains(std::string_view const uri) const -> bool {
20+
auto const it = m_resources.find(uri);
21+
if (it == m_resources.end()) { return false; }
22+
return it->second->type_index == typeid(Type);
23+
}
24+
25+
template <typename Type>
26+
[[nodiscard]] auto get(std::string_view const uri, std::shared_ptr<Type> const& fallback = {}) const -> std::shared_ptr<Type> {
27+
auto const it = m_resources.find(uri);
28+
if (it == m_resources.end()) { return fallback; }
29+
auto const& resource = it->second;
30+
if (resource->type_index != typeid(Type)) { return fallback; }
31+
return static_cast<Model<Type> const&>(*it->second).resource;
32+
}
33+
34+
void clear() { m_resources.clear(); }
35+
36+
private:
37+
struct Base : bave::Polymorphic {
38+
std::type_index type_index;
39+
explicit Base(std::type_index type_index) : type_index(type_index) {}
40+
};
41+
42+
template <typename T>
43+
struct Model : Base {
44+
std::shared_ptr<T> resource;
45+
explicit Model(std::shared_ptr<T> t) : Base(typeid(T)), resource(std::move(t)) {}
46+
};
47+
48+
StringMap<std::unique_ptr<Base>> m_resources{};
49+
};
50+
} // namespace spaced

src/spaced/spaced/scene.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22
#include <bave/app.hpp>
33
#include <bave/core/polymorphic.hpp>
4+
#include <bave/loader.hpp>
45
#include <spaced/async_exec.hpp>
56
#include <spaced/services/services.hpp>
67
#include <spaced/ui/loading_screen.hpp>
@@ -25,6 +26,8 @@ class Scene : public bave::PolyPinned {
2526
[[nodiscard]] auto is_loading() const -> bool { return m_loading_screen.has_value(); }
2627
[[nodiscard]] auto is_ui_blocking_input() const -> bool;
2728

29+
[[nodiscard]] auto make_loader() const -> bave::Loader { return bave::Loader{&m_app.get_data_store(), &m_app.get_render_device()}; }
30+
2831
void push_view(std::unique_ptr<ui::View> view);
2932

3033
bave::Rgba clear_colour{bave::black_v};

0 commit comments

Comments
 (0)