Skip to content

Add World and related tech. #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 19, 2024
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