Skip to content
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ include(FetchContent)
FetchContent_Declare(
bave
GIT_REPOSITORY https://github.com/karnkaul/bave
GIT_TAG 13ef94fe81e8914335b903c37c58bb2207fd786d # v0.4.7
GIT_TAG 8717d1eafd2ac581c7b90fa3af384eb66cd7896a # v0.4.8
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/bave"
)

Expand Down
61 changes: 61 additions & 0 deletions assets/particles/exhaust.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"asset_type": "ParticleEmitter",
"texture": "images/foam_bubble.png",
"config": {
"initial": {
"position": {
"lo": [
0.000000,
0.000000
],
"hi": [
0.000000,
0.000000
]
},
"rotation": 0.000000
},
"velocity": {
"linear": {
"angle": {
"lo": 80.000000,
"hi": 100.000000
},
"speed": {
"lo": -360.000000,
"hi": -270.000000
}
},
"angular": {
"lo": -90.000000,
"hi": 90.000000
}
},
"lerp": {
"tint": {
"lo": "#231d2aff",
"hi": "#231d2aff"
},
"scale": {
"lo": [
1.000000,
1.000000
],
"hi": [
0.500000,
0.500000
]
}
},
"ttl": {
"lo": 2.000000,
"hi": 3.000000
},
"quad_size": [
80.000000,
80.000000
],
"count": 80,
"respawn": true
}
}
61 changes: 61 additions & 0 deletions assets/particles/explode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"asset_type": "ParticleEmitter",
"texture": "images/foam_bubble.png",
"config": {
"initial": {
"position": {
"lo": [
0.000000,
0.000000
],
"hi": [
0.000000,
0.000000
]
},
"rotation": 0.000000
},
"velocity": {
"linear": {
"angle": {
"lo": -180.000000,
"hi": 180.000000
},
"speed": {
"lo": -360.000000,
"hi": -80.000000
}
},
"angular": {
"lo": -90.000000,
"hi": 90.000000
}
},
"lerp": {
"tint": {
"lo": "#f75c03ff",
"hi": "#e5cdaeff"
},
"scale": {
"lo": [
1.000000,
1.000000
],
"hi": [
0.700000,
0.700000
]
}
},
"ttl": {
"lo": 0.500000,
"hi": 3.000000
},
"quad_size": [
50.000000,
50.000000
],
"count": 40,
"respawn": true
}
}
19 changes: 19 additions & 0 deletions assets/worlds/playground.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "Playground",
"background_tint": "mocha",
"player": {
"tint": "black",
"exhaust_emitter": "particles/exhaust.json"
},
"enemy_factories": [
{
"type_name": "BasicCreepFactory",
"tints": [
"orange",
"milk"
],
"spawn_rate": 2,
"death_emitter": "particles/explode.json"
}
]
}
56 changes: 45 additions & 11 deletions src/spaced/spaced/async_exec.cpp
Original file line number Diff line number Diff line change
@@ -1,26 +1,60 @@
#include <spaced/async_exec.hpp>
#include <cassert>
#include <iterator>
#include <numeric>

namespace spaced {
using namespace std::chrono_literals;

AsyncExec::AsyncExec(std::span<std::function<void()>> tasks) {
AsyncExec::AsyncExec(std::span<Task const> tasks) {
if (tasks.empty()) { return; }

m_total = static_cast<int>(tasks.size());
enqueue(tasks);
}

AsyncExec::AsyncExec(std::span<Stage> stages) {
if (stages.empty()) { return; }
std::move(stages.begin(), stages.end(), std::back_inserter(m_stages));
m_total = std::accumulate(m_stages.begin(), m_stages.end(), 0, [](int count, auto const& tasks) { return static_cast<int>(tasks.size()) + count; });
start_next_stage();
}

auto AsyncExec::update() -> Status {
if (m_remain.empty()) {
if (m_stages.empty()) { return Status{.remain = 0, .total = m_total}; }
start_next_stage();
}
std::erase_if(m_remain, [](std::future<void> const& future) { return !future.valid() || future.wait_for(0s) == std::future_status::ready; });
return Status{.remain = m_total - m_completed, .total = m_total};
}

void AsyncExec::start_next_stage() {
if (m_stages.empty()) { return; }

auto get_next_stage = [&] {
auto ret = std::move(m_stages.front());
m_stages.pop_front();
return ret;
};

auto stage = get_next_stage();
while (stage.empty() && !m_stages.empty()) { stage = get_next_stage(); }

enqueue(stage);
}

void AsyncExec::enqueue(std::span<Task const> tasks) {
assert(m_remain.empty());
if (tasks.empty()) { return; }

m_remain.reserve(tasks.size());
for (auto& task : tasks) {
auto func = [task = std::move(task), this] {
for (auto const& task : tasks) {
auto func = [task = task, this] {
task();
++m_completed;
};
m_remain.push_back(std::async(std::move(func)));
}

m_total = static_cast<int>(m_remain.size());
}

auto AsyncExec::update() -> Status {
if (m_remain.empty()) { return Status{.remain = 0, .total = m_total}; }
std::erase_if(m_remain, [](std::future<void> const& future) { return !future.valid() || future.wait_for(0s) == std::future_status::ready; });
return Status{.remain = static_cast<int>(m_remain.size()), .total = m_total};
}
} // namespace spaced
11 changes: 10 additions & 1 deletion src/spaced/spaced/async_exec.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include <atomic>
#include <deque>
#include <functional>
#include <future>
#include <span>
Expand All @@ -8,14 +9,22 @@
namespace spaced {
class AsyncExec {
public:
using Task = std::function<void()>;
using Stage = std::vector<Task>;

struct Status;

explicit AsyncExec(std::span<std::function<void()>> tasks);
explicit AsyncExec(std::span<Task const> tasks);
explicit AsyncExec(std::span<Stage> stages);

auto update() -> Status;

private:
void start_next_stage();
void enqueue(std::span<Task const> tasks);

std::vector<std::future<void>> m_remain{};
std::deque<Stage> m_stages{};
std::atomic<int> m_completed{};
int m_total{};
};
Expand Down
79 changes: 79 additions & 0 deletions src/spaced/spaced/game/asset_list.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#include <spaced/game/asset_list.hpp>
#include <spaced/game/asset_loader.hpp>
#include <spaced/services/resources.hpp>

namespace spaced {
using bave::Loader;

AssetList::AssetList(Loader loader, Services const& services) : m_loader(std::move(loader)), m_resources(&services.get<Resources>()) {}

auto AssetList::add_texture(std::string uri, bool const mip_map) -> AssetList& {
if (uri.empty()) { return *this; }
m_textures.insert(Tex{.uri = std::move(uri), .mip_map = mip_map});
return *this;
}

auto AssetList::add_font(std::string uri) -> AssetList& {
if (uri.empty()) { return *this; }
m_fonts.insert(std::move(uri));
return *this;
}

auto AssetList::add_particle_emitter(std::string uri) -> AssetList& {
if (uri.empty()) { return *this; }

auto const json = m_loader.load_json(uri);
if (!json) { return *this; }

// emitters require textures (stage 0) to be loaded, and must be loaded in stage 1
if (auto const& texture = json["texture"]) { add_texture(texture.as<std::string>()); }
m_emitters.insert(std::move(uri));
return *this;
}

auto AssetList::read_world_spec(std::string_view const uri) -> WorldSpec {
if (uri.empty()) { return {}; }

auto const json = m_loader.load_json(uri);
if (!json) { return {}; }

auto ret = WorldSpec{};
ret.name = json["name"].as_string();
ret.background_tint = json["background_tint"].as_string();

if (auto const& player = json["player"]) {
ret.player.tint = player["tint"].as_string();
ret.player.exhaust_emitter = player["exhaust_emitter"].as_string();
add_particle_emitter(ret.player.exhaust_emitter);
}

for (auto const& enemy_factory : json["enemy_factories"].array_view()) {
add_particle_emitter(enemy_factory["death_emitter"].as<std::string>());
ret.enemy_factories.push_back(enemy_factory);
}

return ret;
}

auto AssetList::build_task_stages() const -> std::vector<AsyncExec::Stage> {
auto ret = std::vector<AsyncExec::Stage>{};
ret.reserve(2);
auto asset_loader = AssetLoader{m_loader, m_resources};
ret.push_back(build_stage_0(asset_loader));
ret.push_back(build_stage_1(asset_loader));
return ret;
}

auto AssetList::build_stage_0(AssetLoader& asset_loader) const -> AsyncExec::Stage {
auto ret = AsyncExec::Stage{};
for (auto const& texture : m_textures) { ret.push_back(asset_loader.make_load_texture(texture.uri, texture.mip_map)); }
for (auto const& font : m_fonts) { ret.push_back(asset_loader.make_load_font(font)); }
return ret;
}

auto AssetList::build_stage_1(AssetLoader& asset_loader) const -> AsyncExec::Stage {
auto ret = AsyncExec::Stage{};
for (auto const& emitter : m_emitters) { ret.push_back(asset_loader.make_load_particle_emitter(emitter)); }
return ret;
}
} // namespace spaced
44 changes: 44 additions & 0 deletions src/spaced/spaced/game/asset_list.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once
#include <bave/loader.hpp>
#include <spaced/async_exec.hpp>
#include <spaced/game/world_spec.hpp>
#include <spaced/services/services.hpp>
#include <set>

namespace spaced {
struct Resources;
class AssetLoader;

class AssetList {
public:
explicit AssetList(bave::Loader loader, Services const& services);

auto add_texture(std::string uri, bool mip_map = false) -> AssetList&;
auto add_font(std::string uri) -> AssetList&;
auto add_particle_emitter(std::string uri) -> AssetList&;

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

[[nodiscard]] auto build_task_stages() const -> std::vector<AsyncExec::Stage>;

private:
struct Tex {
std::string uri{};
bool mip_map{};

// MacOS doesn't provide operator<=> for strings :/
auto operator==(Tex const& rhs) const -> bool { return uri == rhs.uri; }
auto operator<(Tex const& rhs) const -> bool { return uri < rhs.uri; }
};

auto build_stage_0(AssetLoader& asset_loader) const -> AsyncExec::Stage;
auto build_stage_1(AssetLoader& asset_loader) const -> AsyncExec::Stage;

bave::Loader m_loader;
bave::NotNull<Resources*> m_resources;

std::set<Tex> m_textures{};
std::set<std::string> m_fonts{};
std::set<std::string> m_emitters{};
};
} // namespace spaced
Loading