Skip to content

Commit ecc47da

Browse files
authored
Add World and related tech. (#25)
* bave update (unstable). * Load emitters from data files. * Support multi-stage async loads. * Add and use `AssetList` for staged loading. * Add and use `EnemyFactory`. * Add and use `World`. Refactor enemy spawner factory to be an `IEnemyFactory`, able to customize multiple virtual functions. * Use a vector of enemy spawners in `Game`. Delegate inspection and auto-spawn to spawner / factory. * Rename `World` to `WorldSpec`. * Add and use `World`. * CI build fixes. * Load player tint from world spec. * Update bave to stable 0.4.8. * Use `type_name` instead of `type`.
1 parent dbaaf57 commit ecc47da

30 files changed

+737
-203
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
bave
1313
GIT_REPOSITORY https://github.com/karnkaul/bave
14-
GIT_TAG 13ef94fe81e8914335b903c37c58bb2207fd786d # v0.4.7
14+
GIT_TAG 8717d1eafd2ac581c7b90fa3af384eb66cd7896a # v0.4.8
1515
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/bave"
1616
)
1717

assets/particles/exhaust.json

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"asset_type": "ParticleEmitter",
3+
"texture": "images/foam_bubble.png",
4+
"config": {
5+
"initial": {
6+
"position": {
7+
"lo": [
8+
0.000000,
9+
0.000000
10+
],
11+
"hi": [
12+
0.000000,
13+
0.000000
14+
]
15+
},
16+
"rotation": 0.000000
17+
},
18+
"velocity": {
19+
"linear": {
20+
"angle": {
21+
"lo": 80.000000,
22+
"hi": 100.000000
23+
},
24+
"speed": {
25+
"lo": -360.000000,
26+
"hi": -270.000000
27+
}
28+
},
29+
"angular": {
30+
"lo": -90.000000,
31+
"hi": 90.000000
32+
}
33+
},
34+
"lerp": {
35+
"tint": {
36+
"lo": "#231d2aff",
37+
"hi": "#231d2aff"
38+
},
39+
"scale": {
40+
"lo": [
41+
1.000000,
42+
1.000000
43+
],
44+
"hi": [
45+
0.500000,
46+
0.500000
47+
]
48+
}
49+
},
50+
"ttl": {
51+
"lo": 2.000000,
52+
"hi": 3.000000
53+
},
54+
"quad_size": [
55+
80.000000,
56+
80.000000
57+
],
58+
"count": 80,
59+
"respawn": true
60+
}
61+
}

assets/particles/explode.json

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"asset_type": "ParticleEmitter",
3+
"texture": "images/foam_bubble.png",
4+
"config": {
5+
"initial": {
6+
"position": {
7+
"lo": [
8+
0.000000,
9+
0.000000
10+
],
11+
"hi": [
12+
0.000000,
13+
0.000000
14+
]
15+
},
16+
"rotation": 0.000000
17+
},
18+
"velocity": {
19+
"linear": {
20+
"angle": {
21+
"lo": -180.000000,
22+
"hi": 180.000000
23+
},
24+
"speed": {
25+
"lo": -360.000000,
26+
"hi": -80.000000
27+
}
28+
},
29+
"angular": {
30+
"lo": -90.000000,
31+
"hi": 90.000000
32+
}
33+
},
34+
"lerp": {
35+
"tint": {
36+
"lo": "#f75c03ff",
37+
"hi": "#e5cdaeff"
38+
},
39+
"scale": {
40+
"lo": [
41+
1.000000,
42+
1.000000
43+
],
44+
"hi": [
45+
0.700000,
46+
0.700000
47+
]
48+
}
49+
},
50+
"ttl": {
51+
"lo": 0.500000,
52+
"hi": 3.000000
53+
},
54+
"quad_size": [
55+
50.000000,
56+
50.000000
57+
],
58+
"count": 40,
59+
"respawn": true
60+
}
61+
}

assets/worlds/playground.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "Playground",
3+
"background_tint": "mocha",
4+
"player": {
5+
"tint": "black",
6+
"exhaust_emitter": "particles/exhaust.json"
7+
},
8+
"enemy_factories": [
9+
{
10+
"type_name": "BasicCreepFactory",
11+
"tints": [
12+
"orange",
13+
"milk"
14+
],
15+
"spawn_rate": 2,
16+
"death_emitter": "particles/explode.json"
17+
}
18+
]
19+
}

src/spaced/spaced/async_exec.cpp

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,60 @@
11
#include <spaced/async_exec.hpp>
2+
#include <cassert>
3+
#include <iterator>
4+
#include <numeric>
25

36
namespace spaced {
47
using namespace std::chrono_literals;
58

6-
AsyncExec::AsyncExec(std::span<std::function<void()>> tasks) {
9+
AsyncExec::AsyncExec(std::span<Task const> tasks) {
10+
if (tasks.empty()) { return; }
11+
12+
m_total = static_cast<int>(tasks.size());
13+
enqueue(tasks);
14+
}
15+
16+
AsyncExec::AsyncExec(std::span<Stage> stages) {
17+
if (stages.empty()) { return; }
18+
std::move(stages.begin(), stages.end(), std::back_inserter(m_stages));
19+
m_total = std::accumulate(m_stages.begin(), m_stages.end(), 0, [](int count, auto const& tasks) { return static_cast<int>(tasks.size()) + count; });
20+
start_next_stage();
21+
}
22+
23+
auto AsyncExec::update() -> Status {
24+
if (m_remain.empty()) {
25+
if (m_stages.empty()) { return Status{.remain = 0, .total = m_total}; }
26+
start_next_stage();
27+
}
28+
std::erase_if(m_remain, [](std::future<void> const& future) { return !future.valid() || future.wait_for(0s) == std::future_status::ready; });
29+
return Status{.remain = m_total - m_completed, .total = m_total};
30+
}
31+
32+
void AsyncExec::start_next_stage() {
33+
if (m_stages.empty()) { return; }
34+
35+
auto get_next_stage = [&] {
36+
auto ret = std::move(m_stages.front());
37+
m_stages.pop_front();
38+
return ret;
39+
};
40+
41+
auto stage = get_next_stage();
42+
while (stage.empty() && !m_stages.empty()) { stage = get_next_stage(); }
43+
44+
enqueue(stage);
45+
}
46+
47+
void AsyncExec::enqueue(std::span<Task const> tasks) {
48+
assert(m_remain.empty());
749
if (tasks.empty()) { return; }
850

951
m_remain.reserve(tasks.size());
10-
for (auto& task : tasks) {
11-
auto func = [task = std::move(task), this] {
52+
for (auto const& task : tasks) {
53+
auto func = [task = task, this] {
1254
task();
1355
++m_completed;
1456
};
1557
m_remain.push_back(std::async(std::move(func)));
1658
}
17-
18-
m_total = static_cast<int>(m_remain.size());
19-
}
20-
21-
auto AsyncExec::update() -> Status {
22-
if (m_remain.empty()) { return Status{.remain = 0, .total = m_total}; }
23-
std::erase_if(m_remain, [](std::future<void> const& future) { return !future.valid() || future.wait_for(0s) == std::future_status::ready; });
24-
return Status{.remain = static_cast<int>(m_remain.size()), .total = m_total};
2559
}
2660
} // namespace spaced

src/spaced/spaced/async_exec.hpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22
#include <atomic>
3+
#include <deque>
34
#include <functional>
45
#include <future>
56
#include <span>
@@ -8,14 +9,22 @@
89
namespace spaced {
910
class AsyncExec {
1011
public:
12+
using Task = std::function<void()>;
13+
using Stage = std::vector<Task>;
14+
1115
struct Status;
1216

13-
explicit AsyncExec(std::span<std::function<void()>> tasks);
17+
explicit AsyncExec(std::span<Task const> tasks);
18+
explicit AsyncExec(std::span<Stage> stages);
1419

1520
auto update() -> Status;
1621

1722
private:
23+
void start_next_stage();
24+
void enqueue(std::span<Task const> tasks);
25+
1826
std::vector<std::future<void>> m_remain{};
27+
std::deque<Stage> m_stages{};
1928
std::atomic<int> m_completed{};
2029
int m_total{};
2130
};

src/spaced/spaced/game/asset_list.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#include <spaced/game/asset_list.hpp>
2+
#include <spaced/game/asset_loader.hpp>
3+
#include <spaced/services/resources.hpp>
4+
5+
namespace spaced {
6+
using bave::Loader;
7+
8+
AssetList::AssetList(Loader loader, Services const& services) : m_loader(std::move(loader)), m_resources(&services.get<Resources>()) {}
9+
10+
auto AssetList::add_texture(std::string uri, bool const mip_map) -> AssetList& {
11+
if (uri.empty()) { return *this; }
12+
m_textures.insert(Tex{.uri = std::move(uri), .mip_map = mip_map});
13+
return *this;
14+
}
15+
16+
auto AssetList::add_font(std::string uri) -> AssetList& {
17+
if (uri.empty()) { return *this; }
18+
m_fonts.insert(std::move(uri));
19+
return *this;
20+
}
21+
22+
auto AssetList::add_particle_emitter(std::string uri) -> AssetList& {
23+
if (uri.empty()) { return *this; }
24+
25+
auto const json = m_loader.load_json(uri);
26+
if (!json) { return *this; }
27+
28+
// emitters require textures (stage 0) to be loaded, and must be loaded in stage 1
29+
if (auto const& texture = json["texture"]) { add_texture(texture.as<std::string>()); }
30+
m_emitters.insert(std::move(uri));
31+
return *this;
32+
}
33+
34+
auto AssetList::read_world_spec(std::string_view const uri) -> WorldSpec {
35+
if (uri.empty()) { return {}; }
36+
37+
auto const json = m_loader.load_json(uri);
38+
if (!json) { return {}; }
39+
40+
auto ret = WorldSpec{};
41+
ret.name = json["name"].as_string();
42+
ret.background_tint = json["background_tint"].as_string();
43+
44+
if (auto const& player = json["player"]) {
45+
ret.player.tint = player["tint"].as_string();
46+
ret.player.exhaust_emitter = player["exhaust_emitter"].as_string();
47+
add_particle_emitter(ret.player.exhaust_emitter);
48+
}
49+
50+
for (auto const& enemy_factory : json["enemy_factories"].array_view()) {
51+
add_particle_emitter(enemy_factory["death_emitter"].as<std::string>());
52+
ret.enemy_factories.push_back(enemy_factory);
53+
}
54+
55+
return ret;
56+
}
57+
58+
auto AssetList::build_task_stages() const -> std::vector<AsyncExec::Stage> {
59+
auto ret = std::vector<AsyncExec::Stage>{};
60+
ret.reserve(2);
61+
auto asset_loader = AssetLoader{m_loader, m_resources};
62+
ret.push_back(build_stage_0(asset_loader));
63+
ret.push_back(build_stage_1(asset_loader));
64+
return ret;
65+
}
66+
67+
auto AssetList::build_stage_0(AssetLoader& asset_loader) const -> AsyncExec::Stage {
68+
auto ret = AsyncExec::Stage{};
69+
for (auto const& texture : m_textures) { ret.push_back(asset_loader.make_load_texture(texture.uri, texture.mip_map)); }
70+
for (auto const& font : m_fonts) { ret.push_back(asset_loader.make_load_font(font)); }
71+
return ret;
72+
}
73+
74+
auto AssetList::build_stage_1(AssetLoader& asset_loader) const -> AsyncExec::Stage {
75+
auto ret = AsyncExec::Stage{};
76+
for (auto const& emitter : m_emitters) { ret.push_back(asset_loader.make_load_particle_emitter(emitter)); }
77+
return ret;
78+
}
79+
} // namespace spaced

src/spaced/spaced/game/asset_list.hpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#pragma once
2+
#include <bave/loader.hpp>
3+
#include <spaced/async_exec.hpp>
4+
#include <spaced/game/world_spec.hpp>
5+
#include <spaced/services/services.hpp>
6+
#include <set>
7+
8+
namespace spaced {
9+
struct Resources;
10+
class AssetLoader;
11+
12+
class AssetList {
13+
public:
14+
explicit AssetList(bave::Loader loader, Services const& services);
15+
16+
auto add_texture(std::string uri, bool mip_map = false) -> AssetList&;
17+
auto add_font(std::string uri) -> AssetList&;
18+
auto add_particle_emitter(std::string uri) -> AssetList&;
19+
20+
auto read_world_spec(std::string_view uri) -> WorldSpec;
21+
22+
[[nodiscard]] auto build_task_stages() const -> std::vector<AsyncExec::Stage>;
23+
24+
private:
25+
struct Tex {
26+
std::string uri{};
27+
bool mip_map{};
28+
29+
// MacOS doesn't provide operator<=> for strings :/
30+
auto operator==(Tex const& rhs) const -> bool { return uri == rhs.uri; }
31+
auto operator<(Tex const& rhs) const -> bool { return uri < rhs.uri; }
32+
};
33+
34+
auto build_stage_0(AssetLoader& asset_loader) const -> AsyncExec::Stage;
35+
auto build_stage_1(AssetLoader& asset_loader) const -> AsyncExec::Stage;
36+
37+
bave::Loader m_loader;
38+
bave::NotNull<Resources*> m_resources;
39+
40+
std::set<Tex> m_textures{};
41+
std::set<std::string> m_fonts{};
42+
std::set<std::string> m_emitters{};
43+
};
44+
} // namespace spaced

0 commit comments

Comments
 (0)