Skip to content

Commit 4557121

Browse files
authored
Audio framework. (#32)
* Add audio service. * Groundwork for weapon fire and enemy death SFX. * Fix Android CI.
1 parent 9975f47 commit 4557121

24 files changed

+182
-45
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@ jobs:
5757
tar xvzf jdk-17_linux-x64_bin.tar.gz -C /opt
5858
- name: build
5959
run: |
60-
export JAVA_HOME=/opt/jdk-17.0.10
60+
export JAVA_HOME=$(find /opt -name "jdk-17.*")
6161
cd src/android
6262
./gradlew assembleDebug

CMakeLists.txt

+1-1
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 8717d1eafd2ac581c7b90fa3af384eb66cd7896a # v0.4.8
14+
GIT_TAG ff3978f8431649007d3309db513d3de3e72fe823 # v0.4.9
1515
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/bave"
1616
)
1717

assets/sfx/bubble.wav

26 KB
Binary file not shown.

assets/worlds/playground.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
],
1616
"spawn_rate": 2,
1717
"initial_health": 2,
18-
"death_emitter": "particles/explode.json"
18+
"death_emitter": "particles/explode.json",
19+
"death_sfx": [
20+
"sfx/bubble.wav"
21+
]
1922
}
2023
]
2124
}

src/spaced/spaced/game/asset_list.cpp

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ auto AssetList::add_particle_emitter(std::string uri) -> AssetList& {
3131
return *this;
3232
}
3333

34+
auto AssetList::add_audio_clip(std::string uri) -> AssetList& {
35+
if (uri.empty()) { return *this; }
36+
m_audio_clips.insert(std::move(uri));
37+
return *this;
38+
}
39+
3440
auto AssetList::read_world_spec(std::string_view const uri) -> WorldSpec {
3541
if (uri.empty()) { return {}; }
3642

@@ -51,6 +57,7 @@ auto AssetList::read_world_spec(std::string_view const uri) -> WorldSpec {
5157

5258
for (auto const& enemy_factory : json["enemy_factories"].array_view()) {
5359
add_particle_emitter(enemy_factory["death_emitter"].as<std::string>());
60+
for (auto const& death_sfx : enemy_factory["death_sfx"].array_view()) { add_audio_clip(death_sfx.as<std::string>()); }
5461
ret.enemy_factories.push_back(enemy_factory);
5562
}
5663

@@ -70,6 +77,7 @@ auto AssetList::build_stage_0(AssetLoader& asset_loader) const -> AsyncExec::Sta
7077
auto ret = AsyncExec::Stage{};
7178
for (auto const& texture : m_textures) { ret.push_back(asset_loader.make_load_texture(texture.uri, texture.mip_map)); }
7279
for (auto const& font : m_fonts) { ret.push_back(asset_loader.make_load_font(font)); }
80+
for (auto const& audio_clip : m_audio_clips) { ret.push_back(asset_loader.make_load_audio_clip(audio_clip)); }
7381
return ret;
7482
}
7583

src/spaced/spaced/game/asset_list.hpp

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class AssetList {
1616
auto add_texture(std::string uri, bool mip_map = false) -> AssetList&;
1717
auto add_font(std::string uri) -> AssetList&;
1818
auto add_particle_emitter(std::string uri) -> AssetList&;
19+
auto add_audio_clip(std::string uri) -> AssetList&;
1920

2021
auto read_world_spec(std::string_view uri) -> WorldSpec;
2122

@@ -40,5 +41,6 @@ class AssetList {
4041
std::set<Tex> m_textures{};
4142
std::set<std::string> m_fonts{};
4243
std::set<std::string> m_emitters{};
44+
std::set<std::string> m_audio_clips{};
4345
};
4446
} // namespace spaced

src/spaced/spaced/game/asset_loader.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ auto AssetLoader::make_load_particle_emitter(std::string uri, bool const reload)
5353
return make_load_task(std::move(uri), reload, load);
5454
}
5555

56+
auto AssetLoader::make_load_audio_clip(std::string uri, bool const reload) -> LoadTask {
57+
auto const load = [](Loader const& loader, std::string_view const uri) { return loader.load_audio_clip(uri); };
58+
return make_load_task(std::move(uri), reload, load);
59+
}
60+
5661
template <typename FuncT>
5762
auto AssetLoader::make_load_task(std::string uri, bool reload, FuncT load) const -> LoadTask {
5863
return [impl = m_impl, uri = std::move(uri), reload, load] {

src/spaced/spaced/game/asset_loader.hpp

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class AssetLoader {
1616
[[nodiscard]] auto make_load_texture(std::string uri, bool mip_map = false, bool reload = false) -> LoadTask;
1717
[[nodiscard]] auto make_load_texture_atlas(std::string uri, bool mip_map = false, bool reload = false) -> LoadTask;
1818
[[nodiscard]] auto make_load_particle_emitter(std::string uri, bool reload = false) -> LoadTask;
19+
[[nodiscard]] auto make_load_audio_clip(std::string uri, bool reload = false) -> LoadTask;
1920

2021
private:
2122
template <typename FuncT>

src/spaced/spaced/game/enemies/basic_creep_factory.cpp

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,18 @@ using bave::random_in_range;
1313
using bave::Seconds;
1414

1515
BasicCreepFactory::BasicCreepFactory(NotNull<Services const*> services, NotNull<IEnemyDeathListener*> listener, dj::Json const& json)
16-
: m_services(services), m_listener(listener) {
16+
: IEnemyFactory(services), m_listener(listener) {
1717
for (auto const& tint : json["tints"].array_view()) { tints.push_back(tint.as<std::string>()); }
1818
if (auto const in_death_emitter = services->get<Resources>().get<ParticleEmitter>(json["death_emitter"].as_string())) { death_emitter = *in_death_emitter; }
1919
spawn_rate = Seconds{json["spawn_rate"].as<float>(spawn_rate.count())};
2020
initial_health = json["initial_health"].as<float>(initial_health);
21+
for (auto const& death_sfx : json["death_sfx"].array_view()) { m_death_sfx.push_back(death_sfx.as<std::string>()); }
2122
}
2223

2324
auto BasicCreepFactory::spawn_enemy() -> std::unique_ptr<Enemy> {
24-
auto ret = std::make_unique<Creep>(*m_services, m_listener);
25+
auto ret = std::make_unique<Creep>(get_services(), m_listener);
2526
if (!tints.empty()) {
26-
auto const& rgbas = m_services->get<Styles>().rgbas;
27+
auto const& rgbas = get_services().get<Styles>().rgbas;
2728
auto const tint_index = random_in_range(std::size_t{}, tints.size() - 1);
2829
ret->shape.tint = rgbas[tints.at(tint_index)];
2930
ret->health = initial_health;

src/spaced/spaced/game/enemies/basic_creep_factory.hpp

-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ class BasicCreepFactory : public IEnemyFactory {
2323
private:
2424
void do_inspect() final;
2525

26-
bave::NotNull<Services const*> m_services;
2726
bave::NotNull<IEnemyDeathListener*> m_listener;
2827
bave::Seconds m_elapsed{};
2928
};
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#include <spaced/game/enemy_factory.hpp>
2+
#include <spaced/services/audio.hpp>
3+
4+
namespace spaced {
5+
using bave::NotNull;
6+
7+
IEnemyFactory::IEnemyFactory(NotNull<Services const*> services) : m_services(services), m_audio(&services->get<IAudio>()) {}
8+
9+
void IEnemyFactory::play_death_sfx() { m_audio->play_any_sfx(m_death_sfx); }
10+
} // namespace spaced
+16
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
11
#pragma once
22
#include <bave/graphics/particle_system.hpp>
33
#include <spaced/game/enemy.hpp>
4+
#include <string>
5+
#include <vector>
46

57
namespace spaced {
8+
class IAudio;
9+
610
class IEnemyFactory : public bave::Polymorphic {
711
public:
12+
explicit IEnemyFactory(bave::NotNull<Services const*> services);
13+
814
[[nodiscard]] virtual auto get_type_name() const -> std::string_view = 0;
915
[[nodiscard]] virtual auto spawn_enemy() -> std::unique_ptr<Enemy> = 0;
1016
[[nodiscard]] virtual auto get_death_emitter() const -> bave::ParticleEmitter const& = 0;
1117

18+
void play_death_sfx();
19+
1220
virtual auto tick(bave::Seconds dt) -> bool = 0;
1321

1422
void inspect() {
1523
if constexpr (bave::debug_v) { do_inspect(); }
1624
}
1725

26+
protected:
27+
[[nodiscard]] auto get_services() const -> Services const& { return *m_services; }
28+
29+
std::vector<std::string> m_death_sfx{};
30+
1831
private:
1932
virtual void do_inspect() {}
33+
34+
bave::NotNull<Services const*> m_services;
35+
bave::NotNull<IAudio*> m_audio;
2036
};
2137
} // namespace spaced

src/spaced/spaced/game/enemy_spawner.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ void EnemySpawner::explode_at(glm::vec2 const position) {
4242
auto& emitter = m_death_emitters.emplace_back(m_factory->get_death_emitter());
4343
emitter.config.respawn = false;
4444
emitter.set_position(position);
45+
m_factory->play_death_sfx();
4546
}
4647

4748
void EnemySpawner::do_inspect() {

src/spaced/spaced/game/player.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ void Player::setup(WorldSpec::Player const& spec) {
7373
m_exhaust.pre_warm();
7474

7575
if (auto const death = resources.get<ParticleEmitter>(spec.death_emitter)) { m_death_source = *death; }
76+
m_death_source.config.respawn = false;
7677
}
7778

7879
void Player::set_y(float const y) { ship.transform.position.y = y; }
@@ -99,7 +100,6 @@ void Player::on_death() {
99100
health = 0.0f;
100101
m_death = m_death_source;
101102
m_death->set_position(ship.transform.position);
102-
m_death->config.respawn = false;
103103
}
104104

105105
void Player::do_inspect() {

src/spaced/spaced/game/weapon.cpp

+9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
#include <bave/imgui/im_text.hpp>
22
#include <spaced/game/weapon.hpp>
3+
#include <spaced/services/audio.hpp>
34

45
namespace spaced {
56
using bave::im_text;
67

8+
Weapon::Weapon(Services const& services, std::string name) : m_log{std::move(name)}, m_layout(&services.get<ILayout>()), m_audio(&services.get<IAudio>()) {}
9+
10+
auto Weapon::fire(glm::vec2 const muzzle_position) -> std::unique_ptr<Round> {
11+
auto ret = do_fire(muzzle_position);
12+
if (ret) { m_audio->play_any_sfx(m_fire_sfx); }
13+
return ret;
14+
}
15+
716
void Weapon::do_inspect() {
817
if constexpr (bave::imgui_v) { im_text("rounds remaining: {}", get_rounds_remaining()); }
918
}

src/spaced/spaced/game/weapon.hpp

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
#include <spaced/services/services.hpp>
77

88
namespace spaced {
9+
class IAudio;
10+
911
class Weapon : public bave::Polymorphic {
1012
public:
1113
using Round = IWeaponRound;
1214

13-
explicit Weapon(Services const& services, std::string name) : m_log{std::move(name)}, m_layout(&services.get<ILayout>()) {}
15+
explicit Weapon(Services const& services, std::string name);
1416

1517
[[nodiscard]] auto get_rounds_remaining() const -> int { return rounds < 0 ? 1 : rounds; }
1618

17-
virtual auto fire(glm::vec2 muzzle_position) -> std::unique_ptr<Round> = 0;
19+
auto fire(glm::vec2 muzzle_position) -> std::unique_ptr<Round>;
1820
[[nodiscard]] virtual auto is_idle() const -> bool = 0;
1921

2022
virtual void tick(bave::Seconds dt) = 0;
@@ -28,11 +30,14 @@ class Weapon : public bave::Polymorphic {
2830
protected:
2931
[[nodiscard]] auto get_layout() const -> ILayout const& { return *m_layout; }
3032

33+
virtual auto do_fire(glm::vec2 muzzle_position) -> std::unique_ptr<Round> = 0;
3134
virtual void do_inspect();
3235

3336
bave::Logger m_log{};
37+
std::vector<std::string> m_fire_sfx{};
3438

3539
private:
3640
bave::NotNull<ILayout const*> m_layout;
41+
bave::NotNull<IAudio*> m_audio;
3742
};
3843
} // namespace spaced

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

+9-9
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,6 @@ GunBeam::GunBeam(Services const& services) : Weapon(services, "GunBeam") {
102102
config.beam_tint = rgbas.get_or("gun_beam", rgbas["grey"]);
103103
}
104104

105-
auto GunBeam::fire(glm::vec2 const muzzle_position) -> std::unique_ptr<Round> {
106-
if (!is_idle() || m_reload_remain > 0s || rounds == 0) { return {}; }
107-
108-
if (rounds > 0) { --rounds; }
109-
m_fire_remain = config.fire_duration;
110-
m_reload_remain = 0s;
111-
return std::make_unique<LaserCharge>(&get_layout(), config, muzzle_position);
112-
}
113-
114105
void GunBeam::tick(Seconds const dt) {
115106
if (m_fire_remain > 0s) {
116107
m_fire_remain -= dt;
@@ -122,6 +113,15 @@ void GunBeam::tick(Seconds const dt) {
122113
if (m_reload_remain > 0s) { m_reload_remain -= dt; }
123114
}
124115

116+
auto GunBeam::do_fire(glm::vec2 const muzzle_position) -> std::unique_ptr<Round> {
117+
if (!is_idle() || m_reload_remain > 0s || rounds == 0) { return {}; }
118+
119+
if (rounds > 0) { --rounds; }
120+
m_fire_remain = config.fire_duration;
121+
m_reload_remain = 0s;
122+
return std::make_unique<LaserCharge>(&get_layout(), config, muzzle_position);
123+
}
124+
125125
void GunBeam::do_inspect() {
126126
if constexpr (bave::imgui_v) {
127127
im_text("type: GunBeam");

src/spaced/spaced/game/weapons/gun_beam.hpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ class GunBeam final : public Weapon {
1414

1515
explicit GunBeam(Services const& services);
1616

17-
auto fire(glm::vec2 muzzle_position) -> std::unique_ptr<Round> final;
1817
[[nodiscard]] auto is_idle() const -> bool final { return m_fire_remain <= 0s; }
1918
void tick(bave::Seconds dt) final;
2019

2120
Config config{};
2221

2322
private:
23+
auto do_fire(glm::vec2 muzzle_position) -> std::unique_ptr<Round> final;
2424
void do_inspect() final;
2525

2626
bave::Seconds m_fire_remain{};

src/spaced/spaced/game/weapons/gun_kinetic.cpp

+8-6
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@ using bave::im_text;
77
using bave::Rgba;
88
using bave::Seconds;
99

10-
GunKinetic::GunKinetic(Services const& services) : Weapon(services, "GunKinetic") { projectile_config.tint = services.get<Styles>().rgbas["black"]; }
10+
GunKinetic::GunKinetic(Services const& services) : Weapon(services, "GunKinetic"), m_audio(&services.get<IAudio>()) {
11+
projectile_config.tint = services.get<Styles>().rgbas["black"];
12+
}
13+
14+
void GunKinetic::tick(Seconds const dt) {
15+
if (m_reload_remain > 0s) { m_reload_remain -= dt; }
16+
}
1117

12-
auto GunKinetic::fire(glm::vec2 const muzzle_position) -> std::unique_ptr<Round> {
18+
auto GunKinetic::do_fire(glm::vec2 const muzzle_position) -> std::unique_ptr<Round> {
1319
if (m_reload_remain > 0s || rounds == 0) { return {}; }
1420

1521
if (rounds > 0) { --rounds; }
1622
m_reload_remain = reload_delay;
1723
return std::make_unique<Projectile>(&get_layout(), projectile_config, muzzle_position);
1824
}
1925

20-
void GunKinetic::tick(Seconds const dt) {
21-
if (m_reload_remain > 0s) { m_reload_remain -= dt; }
22-
}
23-
2426
void GunKinetic::do_inspect() {
2527
if constexpr (bave::imgui_v) {
2628
im_text("type: GunKinetic");
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#pragma once
22
#include <spaced/game/weapon.hpp>
33
#include <spaced/game/weapons/projectile.hpp>
4+
#include <spaced/services/audio.hpp>
45

56
namespace spaced {
67
class GunKinetic final : public Weapon {
78
public:
89
explicit GunKinetic(Services const& services);
910

10-
auto fire(glm::vec2 muzzle_position) -> std::unique_ptr<Round> final;
1111
[[nodiscard]] auto is_idle() const -> bool final { return m_reload_remain <= 0s; }
1212

1313
void tick(bave::Seconds dt) final;
@@ -16,8 +16,10 @@ class GunKinetic final : public Weapon {
1616
bave::Seconds reload_delay{0.25s};
1717

1818
private:
19+
auto do_fire(glm::vec2 muzzle_position) -> std::unique_ptr<Round> final;
1920
void do_inspect() final;
2021

2122
bave::Seconds m_reload_remain{};
23+
bave::NotNull<IAudio*> m_audio;
2224
};
2325
} // namespace spaced

src/spaced/spaced/resource_map.hpp

+2
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ class ResourceMap {
1717

1818
template <typename Type>
1919
[[nodiscard]] auto contains(std::string_view const uri) const -> bool {
20+
if (uri.empty()) { return false; }
2021
auto const it = m_resources.find(uri);
2122
if (it == m_resources.end()) { return false; }
2223
return it->second->type_index == typeid(Type);
2324
}
2425

2526
template <typename Type>
2627
[[nodiscard]] auto get(std::string_view const uri, std::shared_ptr<Type> const& fallback = {}) const -> std::shared_ptr<Type> {
28+
if (uri.empty()) { return fallback; }
2729
auto const it = m_resources.find(uri);
2830
if (it == m_resources.end()) { return fallback; }
2931
auto const& resource = it->second;

src/spaced/spaced/services/audio.hpp

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#pragma once
2+
#include <bave/core/random.hpp>
3+
#include <bave/core/time.hpp>
4+
#include <spaced/services/service.hpp>
5+
#include <span>
6+
#include <string_view>
7+
8+
namespace spaced {
9+
class IAudio : public IService {
10+
public:
11+
[[nodiscard]] virtual auto get_sfx_gain() const -> float = 0;
12+
virtual void set_sfx_gain(float gain) = 0;
13+
14+
[[nodiscard]] virtual auto get_music_gain() const -> float = 0;
15+
virtual void set_music_gain(float gain) = 0;
16+
17+
virtual void play_sfx(std::string_view uri) = 0;
18+
virtual void play_music(std::string_view uri, bave::Seconds crossfade = 1s) = 0;
19+
virtual void stop_music() = 0;
20+
21+
void play_any_sfx(std::span<std::string const> uris) {
22+
if (uris.empty()) { return; }
23+
if (uris.size() == 1) { return play_sfx(uris.front()); }
24+
auto const index = bave::random_in_range(std::size_t{}, uris.size() - 1);
25+
play_sfx(uris[index]);
26+
}
27+
};
28+
} // namespace spaced

0 commit comments

Comments
 (0)