Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4557121

Browse files
authoredApr 20, 2024
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‎

Lines changed: 1 addition & 1 deletion
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‎

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 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‎

Lines changed: 4 additions & 1 deletion
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‎

Lines changed: 8 additions & 0 deletions
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‎

Lines changed: 2 additions & 0 deletions
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‎

Lines changed: 5 additions & 0 deletions
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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 4 additions & 3 deletions
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‎

Lines changed: 0 additions & 1 deletion
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
};
Lines changed: 10 additions & 0 deletions
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
Lines changed: 16 additions & 0 deletions
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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 9 additions & 0 deletions
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‎

Lines changed: 7 additions & 2 deletions
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‎

Lines changed: 9 additions & 9 deletions
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‎

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 8 additions & 6 deletions
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");
Lines changed: 3 additions & 1 deletion
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‎

Lines changed: 2 additions & 0 deletions
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;
Lines changed: 28 additions & 0 deletions
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

‎src/spaced/spaced/spaced.cpp‎

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <bave/json_io.hpp>
33
#include <bave/loader.hpp>
44
#include <spaced/scenes/home.hpp>
5+
#include <spaced/services/audio.hpp>
56
#include <spaced/services/gamepad_provider.hpp>
67
#include <spaced/services/layout.hpp>
78
#include <spaced/services/resources.hpp>
@@ -13,6 +14,9 @@
1314
namespace spaced {
1415
namespace {
1516
using bave::App;
17+
using bave::AudioClip;
18+
using bave::AudioDevice;
19+
using bave::AudioStreamer;
1620
using bave::Gamepad;
1721
using bave::KeyInput;
1822
using bave::Loader;
@@ -23,19 +27,7 @@ using bave::PointerTap;
2327
using bave::Rect;
2428
using bave::RenderDevice;
2529
using bave::RenderView;
26-
27-
struct SceneSwitcherImpl : ISceneSwitcher {
28-
App& app; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members)
29-
Services const& services; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members)
30-
std::unique_ptr<Scene> next_scene{};
31-
32-
explicit SceneSwitcherImpl(App& app, Services const& services) : app(app), services(services) {}
33-
34-
void switch_to_scene(std::unique_ptr<Scene> new_scene) final { next_scene = std::move(new_scene); }
35-
36-
[[nodiscard]] auto get_app() const -> App& final { return app; }
37-
[[nodiscard]] auto get_services() const -> Services const& final { return services; }
38-
};
30+
using bave::Seconds;
3931

4032
struct GamepadProvider : IGamepadProvider {
4133
bave::App& app; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members)
@@ -72,8 +64,50 @@ struct Layout : ILayout {
7264

7365
void set_framebuffer_size(glm::vec2 const size) final { framebuffer_size = size; }
7466
};
67+
68+
struct Audio : IAudio {
69+
NotNull<AudioDevice*> audio_device;
70+
NotNull<AudioStreamer*> audio_streamer;
71+
NotNull<Resources const*> resources;
72+
73+
explicit Audio(NotNull<AudioDevice*> audio_device, NotNull<AudioStreamer*> audio_streamer, NotNull<Resources const*> resources)
74+
: audio_device(audio_device), audio_streamer(audio_streamer), resources(resources) {}
75+
76+
[[nodiscard]] auto get_sfx_gain() const -> float final { return audio_device->sfx_gain; }
77+
void set_sfx_gain(float const gain) final { audio_device->sfx_gain = gain; }
78+
79+
[[nodiscard]] auto get_music_gain() const -> float final { return audio_streamer->gain; }
80+
void set_music_gain(float const gain) final { audio_streamer->gain = gain; }
81+
82+
void play_sfx(std::string_view const uri) final {
83+
auto const clip = resources->get<AudioClip>(uri);
84+
if (!clip) { return; }
85+
audio_device->play_once(*clip);
86+
}
87+
88+
void play_music(std::string_view const uri, Seconds const crossfade) final {
89+
auto const clip = resources->get<AudioClip>(uri);
90+
if (!clip) { return; }
91+
audio_streamer->play(clip, crossfade);
92+
}
93+
94+
void stop_music() final { audio_streamer->stop(); }
95+
};
7596
} // namespace
7697

98+
struct SceneSwitcher : ISceneSwitcher {
99+
App& app; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members)
100+
Services const& services; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members)
101+
std::unique_ptr<Scene> next_scene{};
102+
103+
explicit SceneSwitcher(App& app, Services const& services) : app(app), services(services) {}
104+
105+
void switch_to_scene(std::unique_ptr<Scene> new_scene) final { next_scene = std::move(new_scene); }
106+
107+
[[nodiscard]] auto get_app() const -> App& final { return app; }
108+
[[nodiscard]] auto get_services() const -> Services const& final { return services; }
109+
};
110+
77111
void Spaced::set_bindings([[maybe_unused]] Serializer& serializer) {}
78112

79113
Spaced::Spaced(App& app) : Driver(app), m_scene(std::make_unique<Scene>(app, m_services)) {
@@ -99,9 +133,10 @@ void Spaced::tick() {
99133
m_layout->set_framebuffer_size(get_app().get_framebuffer_size());
100134

101135
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast)
102-
if (auto& switcher = static_cast<SceneSwitcherImpl&>(m_services.get<ISceneSwitcher>()); switcher.next_scene) {
136+
if (m_scene_switcher->next_scene) {
103137
m_resources->clear();
104-
m_scene = std::move(switcher.next_scene);
138+
m_audio->stop_music();
139+
m_scene = std::move(m_scene_switcher->next_scene);
105140
}
106141

107142
m_scene->tick_frame(dt);
@@ -156,12 +191,16 @@ void Spaced::create_services() {
156191
set_bindings(*serializer);
157192
m_services.bind<Serializer>(std::move(serializer));
158193

159-
auto gamepad_provider = std::make_unique<GamepadProvider>(get_app());
160-
m_services.bind<IGamepadProvider>(std::move(gamepad_provider));
194+
m_services.bind<IGamepadProvider>(std::make_unique<GamepadProvider>(get_app()));
195+
196+
auto audio = std::make_unique<Audio>(&get_app().get_audio_device(), &get_app().get_audio_streamer(), m_resources);
197+
m_audio = audio.get();
198+
m_services.bind<IAudio>(std::move(audio));
161199
}
162200

163201
void Spaced::set_scene() {
164-
auto switcher = std::make_unique<SceneSwitcherImpl>(get_app(), m_services);
202+
auto switcher = std::make_unique<SceneSwitcher>(get_app(), m_services);
203+
m_scene_switcher = switcher.get();
165204
switcher->switch_to<Home>();
166205
m_services.bind<ISceneSwitcher>(std::move(switcher));
167206
}

‎src/spaced/spaced/spaced.hpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
namespace spaced {
77
class Serializer;
88
struct Resources;
9+
class IAudio;
10+
struct SceneSwitcher;
911

1012
class Spaced : public bave::Driver {
1113
public:
@@ -31,7 +33,9 @@ class Spaced : public bave::Driver {
3133
bave::Logger m_log{"Spaced"};
3234
Services m_services{};
3335
bave::Ptr<ILayout> m_layout{};
36+
bave::Ptr<SceneSwitcher> m_scene_switcher{};
3437
bave::Ptr<Resources> m_resources{};
38+
bave::Ptr<IAudio> m_audio{};
3539
std::unique_ptr<Scene> m_scene{};
3640
};
3741
} // namespace spaced

0 commit comments

Comments
 (0)
Please sign in to comment.