diff --git a/lib/include/chomper/engine.hpp b/lib/include/chomper/engine.hpp index f38fb52..59e796b 100644 --- a/lib/include/chomper/engine.hpp +++ b/lib/include/chomper/engine.hpp @@ -52,6 +52,10 @@ class Engine : public IDebugInspector, public klib::Pinned { return m_debugStats; } + [[nodiscard]] le::AssetLoader createAssetLoader() const { + return m_context->create_asset_loader(&getDataLoader()); + } + void run(); void setVsync(le::Vsync vsync); diff --git a/lib/include/chomper/manifest/asset_manifest.hpp b/lib/include/chomper/manifest/asset_manifest.hpp new file mode 100644 index 0000000..455f9ee --- /dev/null +++ b/lib/include/chomper/manifest/asset_manifest.hpp @@ -0,0 +1,11 @@ +#pragma once +#include +#include + +namespace chomper { +struct AssetManifest { + std::vector textures{}; + std::vector audio{}; + std::vector fonts{}; +}; +} // namespace chomper diff --git a/lib/include/chomper/manifest/manifest_loader.hpp b/lib/include/chomper/manifest/manifest_loader.hpp new file mode 100644 index 0000000..f2e19b0 --- /dev/null +++ b/lib/include/chomper/manifest/manifest_loader.hpp @@ -0,0 +1,62 @@ +#pragma once +#include "chomper/manifest/asset_manifest.hpp" +#include "chomper/resources.hpp" +#include +#include +#include +#include +#include +#include + +namespace chomper { +class ManifestLoader { + public: + struct Progress; + + explicit ManifestLoader(le::AssetLoader asset_loader); + + void startLoad(AssetManifest const& manifest); + + Progress update(); + [[nodiscard]] Progress getProgress() const; + + void transferTo(Resources& out); + + private: + struct Asset { + std::string uri{}; + std::unique_ptr asset{}; + }; + + template AssetTypeT> + void startLoads(std::span uris); + + klib::TypedLogger m_log{}; + + le::AssetLoader m_asset_loader{}; + + std::vector> m_loading{}; // assets being loaded. + std::vector m_loaded{}; // loaded assets. +}; + +struct ManifestLoader::Progress { + [[nodiscard]] constexpr std::int64_t getTotal() const { + return remaining + loaded; + } + + [[nodiscard]] constexpr float normalized() const { + auto const total = getTotal(); + if (total == 0) { + return 0.0f; + } + return float(loaded) / float(total); + } + + [[nodiscard]] constexpr bool isLoading() const { + return remaining > 0; + } + + std::int64_t remaining{}; + std::int64_t loaded{}; +}; +} // namespace chomper diff --git a/lib/include/chomper/resources.hpp b/lib/include/chomper/resources.hpp index 0cd6bbe..f0a183c 100644 --- a/lib/include/chomper/resources.hpp +++ b/lib/include/chomper/resources.hpp @@ -1,6 +1,7 @@ #pragma once #include "chomper/debug_inspector.hpp" #include +#include #include #include #include @@ -13,18 +14,16 @@ class Resources : public IDebugInspector { public: explicit Resources(le::AssetLoader assetLoader); + void store(std::string uri, std::unique_ptr asset); + template AssetTypeT> [[nodiscard]] AssetTypeT* load(std::string_view const uri) { auto it = m_assets.find(uri); - if (it == m_assets.end()) { - auto ret = m_assetLoader.load(uri); - if (!ret) { - return {}; - } - it = m_assets.insert_or_assign(std::string{uri}, std::move(ret)).first; + if (it != m_assets.end()) { + return dynamic_cast(it->second.get()); } - KLIB_ASSERT(it != m_assets.end()); - return dynamic_cast(it->second.get()); + m_log.warn("loading {} at runtime: {}", klib::demangled_name(), uri); + return reload(uri); } template AssetTypeT> @@ -35,6 +34,26 @@ class Resources : public IDebugInspector { throw std::runtime_error{std::format("Failed to load required {}: {}", klib::demangled_name(), uri)}; } + template AssetTypeT> + [[nodiscard]] AssetTypeT* reload(std::string_view const uri) { + auto asset = m_assetLoader.load(uri); + if (!asset) { + m_log.warn("failed to load {}: {}", klib::demangled_name(), uri); + return {}; + } + auto* ret = asset.get(); + m_assets.insert_or_assign(std::string{uri}, std::move(asset)); + return ret; + } + + template AssetTypeT> + [[nodiscard]] AssetTypeT& reloadRequired(std::string_view const uri) { + if (auto* ret = reload(uri)) { + return *ret; + } + throw std::runtime_error{std::format("Failed to load required {}: {}", klib::demangled_name(), uri)}; + } + [[nodiscard]] le::IFont& getMainFont() const { return *m_mainFont; } @@ -52,6 +71,8 @@ class Resources : public IDebugInspector { // IDebugInspector void debugInspect() final; + klib::TypedLogger m_log{}; + le::AssetLoader m_assetLoader{}; dj::StringTable> m_assets{}; diff --git a/lib/include/chomper/runtimes/entrypoint.hpp b/lib/include/chomper/runtimes/entrypoint.hpp index b9a6e21..e41b50d 100644 --- a/lib/include/chomper/runtimes/entrypoint.hpp +++ b/lib/include/chomper/runtimes/entrypoint.hpp @@ -1,6 +1,8 @@ #pragma once #include "chomper/engine.hpp" +#include "chomper/manifest/manifest_loader.hpp" #include "chomper/runtime.hpp" +#include "chomper/ui/progress_bar.hpp" #include namespace chomper::runtime { @@ -12,11 +14,15 @@ class Entrypoint : public IRuntime { void tick(kvf::Seconds dt) final; void render(le::IRenderer& renderer) const final; - void swingMainText(kvf::Seconds dt); + void setupMainText(); + void setupProgressBar(); gsl::not_null m_engine; + ManifestLoader m_manifestLoader; + le::drawable::Text m_mainText{}; - kvf::Seconds m_elapsed{}; + ui::ProgressBar m_progressBar{}; + float m_progress{}; }; } // namespace chomper::runtime diff --git a/lib/include/chomper/runtimes/main_menu.hpp b/lib/include/chomper/runtimes/main_menu.hpp new file mode 100644 index 0000000..f7c45f9 --- /dev/null +++ b/lib/include/chomper/runtimes/main_menu.hpp @@ -0,0 +1,22 @@ +#pragma once +#include "chomper/engine.hpp" +#include "chomper/runtime.hpp" +#include + +namespace chomper::runtime { +class MainMenu : public IRuntime { + public: + explicit MainMenu(gsl::not_null engine); + + private: + void tick(kvf::Seconds dt) final; + void render(le::IRenderer& renderer) const final; + + void swingMainText(kvf::Seconds dt); + + gsl::not_null m_engine; + + le::drawable::Text m_mainText{}; + kvf::Seconds m_elapsed{}; +}; +} // namespace chomper::runtime diff --git a/lib/include/chomper/theme.hpp b/lib/include/chomper/theme.hpp index 460eb75..1e900e3 100644 --- a/lib/include/chomper/theme.hpp +++ b/lib/include/chomper/theme.hpp @@ -3,4 +3,5 @@ namespace chomper::theme { constexpr auto clearColor_v = kvf::Color{glm::vec4{.34f, .54f, .2f, 1.f}}; +constexpr auto snakeBodyColor_v = kvf::Color(glm::vec4{0.f, 0.6f, 1.f, 1.f}); } // namespace chomper::theme diff --git a/lib/include/chomper/ui/progress_bar.hpp b/lib/include/chomper/ui/progress_bar.hpp new file mode 100644 index 0000000..fd528e8 --- /dev/null +++ b/lib/include/chomper/ui/progress_bar.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +namespace chomper::ui { +class ProgressBar { + public: + void setProgress(float value); + + void draw(le::IRenderer& renderer) const { + quad.draw(renderer); + } + + le::drawable::Quad quad{}; + + glm::vec2 targetSize{300.0f, 50.0f}; +}; +} // namespace chomper::ui diff --git a/lib/src/engine.cpp b/lib/src/engine.cpp index fcdd666..a55c1bc 100644 --- a/lib/src/engine.cpp +++ b/lib/src/engine.cpp @@ -91,6 +91,7 @@ void Engine::debugInspect() { ImGui::Separator(); if (ImGui::Button("restart")) { m_log.info("restarting"); + m_context->wait_idle(); setNextRuntime(&createEntrypoint); } } @@ -137,8 +138,7 @@ void Engine::createContext(CreateInfo const& createInfo) { } void Engine::createResources() { - auto assetLoader = m_context->create_asset_loader(m_dataLoader.get()); - m_resources = std::make_unique(std::move(assetLoader)); + m_resources = std::make_unique(createAssetLoader()); } void Engine::inspectStats() { diff --git a/lib/src/manifest/manifest_loader.cpp b/lib/src/manifest/manifest_loader.cpp new file mode 100644 index 0000000..9f62342 --- /dev/null +++ b/lib/src/manifest/manifest_loader.cpp @@ -0,0 +1,72 @@ +#include "chomper/manifest/manifest_loader.hpp" +#include +#include +#include +#include + +namespace chomper { +ManifestLoader::ManifestLoader(le::AssetLoader asset_loader) : m_asset_loader(std::move(asset_loader)) {} + +void ManifestLoader::startLoad(AssetManifest const& assetManifest) { + // call startLoads() for each field in the manifest. + startLoads(assetManifest.textures); + startLoads(assetManifest.audio); + startLoads(assetManifest.fonts); +} + +auto ManifestLoader::getProgress() const -> Progress { + return Progress{ + .remaining = std::int64_t(m_loading.size()), + .loaded = std::int64_t(m_loaded.size()), + }; +} + +auto ManifestLoader::update() -> Progress { + auto const isReady = [this](std::future& future) { + // should only have futures from std::async(). + KLIB_ASSERT(future.valid()); + + if (future.wait_for(0s) == std::future_status::ready) { // if future is ready, + m_loaded.push_back(future.get()); // store loaded asset, and + return true; // erase corresponding future. + } + return false; // else do nothing. + }; + std::erase_if(m_loading, isReady); + return getProgress(); +} + +void ManifestLoader::transferTo(Resources& out) { + // block until all pending loads have been completed. + for (auto& future : m_loading) { + m_loaded.push_back(future.get()); + } + m_loading.clear(); + + // transfer valid loaded assets to out. + auto count = std::int64_t{}; + for (auto& asset : m_loaded) { + if (!asset.asset) { + continue; + } + out.store(std::move(asset.uri), std::move(asset.asset)); + ++count; + } + m_loaded.clear(); + + m_log.info("{} asset(s) transferred to {}", count, klib::demangled_name(out)); +} + +template AssetTypeT> +void ManifestLoader::startLoads(std::span uris) { + for (auto const& uri : uris) { + if (uri.empty()) { + continue; + } + auto const loadAsset = [this, uri] { + return Asset{.uri = std::move(uri), .asset = m_asset_loader.load(uri)}; + }; + m_loading.push_back(std::async(loadAsset)); + } +} +} // namespace chomper diff --git a/lib/src/resources.cpp b/lib/src/resources.cpp index 9a3a5d6..a4818e9 100644 --- a/lib/src/resources.cpp +++ b/lib/src/resources.cpp @@ -4,7 +4,14 @@ #include namespace chomper { -Resources::Resources(le::AssetLoader assetLoader) : m_assetLoader(std::move(assetLoader)), m_mainFont(&loadRequired("fonts/main.ttf")) {} +Resources::Resources(le::AssetLoader assetLoader) : m_assetLoader(std::move(assetLoader)), m_mainFont(&reloadRequired("fonts/main.ttf")) {} + +void Resources::store(std::string uri, std::unique_ptr asset) { + if (uri.empty() || !asset) { + return; + } + m_assets.insert_or_assign(std::move(uri), std::move(asset)); +} bool Resources::unload(std::string_view const uri) { auto const it = m_assets.find(uri); diff --git a/lib/src/runtimes/entrypoint.cpp b/lib/src/runtimes/entrypoint.cpp index c4db3e5..0459eeb 100644 --- a/lib/src/runtimes/entrypoint.cpp +++ b/lib/src/runtimes/entrypoint.cpp @@ -1,38 +1,59 @@ #include "chomper/runtimes/entrypoint.hpp" -#include "chomper/runtimes/game.hpp" +#include "chomper/runtimes/main_menu.hpp" +#include "chomper/theme.hpp" +#include "chomper/viewport.hpp" +#include #include +#include namespace chomper::runtime { -Entrypoint::Entrypoint(gsl::not_null engine) : m_engine(engine) { - static constexpr auto textParams_v = le::drawable::Text::Params{ - .height = le::TextHeight{60}, - }; - m_mainText.set_string(engine->getResources().getMainFont(), "START", textParams_v); +namespace { +auto const assetManifest = AssetManifest{ + .textures = + { + "images/apple.png", + }, +}; +} // namespace + +Entrypoint::Entrypoint(gsl::not_null engine) : m_engine(engine), m_manifestLoader(engine->createAssetLoader()) { + setupMainText(); + setupProgressBar(); + m_manifestLoader.startLoad(assetManifest); } void Entrypoint::tick(kvf::Seconds const dt) { - swingMainText(dt); - - auto const visitor = klib::SubVisitor{[this](le::event::Key const& key) { - if (key.action != GLFW_PRESS) { - return; - } - m_engine->setNextRuntime(); - }}; - for (auto const& event : m_engine->getContext().event_queue()) { - std::visit(visitor, event); + auto const loadProgress = m_manifestLoader.update(); + auto const t = std::clamp(50.0f * dt.count(), 0.5f, 1.0f); + m_progress = std::lerp(m_progress, loadProgress.normalized(), t); + m_progressBar.setProgress(m_progress); + + static constexpr auto progressTrigger_v{0.95f}; + auto const aestheticWait = m_progress < progressTrigger_v; // functionally useless, looks good + if (loadProgress.isLoading() || aestheticWait) { + return; } + + m_manifestLoader.transferTo(m_engine->getResources()); + m_engine->setNextRuntime(); } void Entrypoint::render(le::IRenderer& renderer) const { + m_progressBar.draw(renderer); m_mainText.draw(renderer); } -void Entrypoint::swingMainText(kvf::Seconds const dt) { - static constexpr auto speed_v = 5.0f; - static constexpr auto amplitude_v = 20.0f; - m_elapsed += dt; - auto const offset = glm::sin(speed_v * m_elapsed.count()) * amplitude_v; - m_mainText.transform.position.x = offset; +void Entrypoint::setupMainText() { + static constexpr auto textParams_v = le::drawable::Text::Params{ + .height = le::TextHeight{60}, + }; + m_mainText.set_string(m_engine->getResources().getMainFont(), "LOADING...", textParams_v); +} + +void Entrypoint::setupProgressBar() { + m_progressBar.targetSize = {0.8f * viewport_v.world_size.x, 50.0f}; + m_progressBar.quad.transform.position = {-0.5f * m_progressBar.targetSize.x, -200.0f}; + m_progressBar.quad.tint = theme::snakeBodyColor_v; + m_progressBar.setProgress(0.0f); } } // namespace chomper::runtime diff --git a/lib/src/runtimes/game.cpp b/lib/src/runtimes/game.cpp index cc89cbe..62dcf5d 100644 --- a/lib/src/runtimes/game.cpp +++ b/lib/src/runtimes/game.cpp @@ -1,6 +1,6 @@ #include "chomper/runtimes/game.hpp" #include "chomper/im_util.hpp" -#include "chomper/runtimes/entrypoint.hpp" +#include "chomper/runtimes/main_menu.hpp" #include "chomper/world_space.hpp" #include #include @@ -44,7 +44,7 @@ void Game::tick(kvf::Seconds const dt) { if (key.action != GLFW_PRESS) { return; } - m_engine->setNextRuntime(); + m_engine->setNextRuntime(); }}; for (auto const& event : m_engine->getContext().event_queue()) { std::visit(visitor, event); diff --git a/lib/src/runtimes/main_menu.cpp b/lib/src/runtimes/main_menu.cpp new file mode 100644 index 0000000..add4334 --- /dev/null +++ b/lib/src/runtimes/main_menu.cpp @@ -0,0 +1,38 @@ +#include "chomper/runtimes/main_menu.hpp" +#include "chomper/runtimes/game.hpp" +#include + +namespace chomper::runtime { +MainMenu::MainMenu(gsl::not_null engine) : m_engine(engine) { + static constexpr auto textParams_v = le::drawable::Text::Params{ + .height = le::TextHeight{60}, + }; + m_mainText.set_string(engine->getResources().getMainFont(), "START", textParams_v); +} + +void MainMenu::tick(kvf::Seconds const dt) { + swingMainText(dt); + + auto const visitor = klib::SubVisitor{[this](le::event::Key const& key) { + if (key.action != GLFW_PRESS) { + return; + } + m_engine->setNextRuntime(); + }}; + for (auto const& event : m_engine->getContext().event_queue()) { + std::visit(visitor, event); + } +} + +void MainMenu::render(le::IRenderer& renderer) const { + m_mainText.draw(renderer); +} + +void MainMenu::swingMainText(kvf::Seconds const dt) { + static constexpr auto speed_v = 5.0f; + static constexpr auto amplitude_v = 20.0f; + m_elapsed += dt; + auto const offset = glm::sin(speed_v * m_elapsed.count()) * amplitude_v; + m_mainText.transform.position.x = offset; +} +} // namespace chomper::runtime diff --git a/lib/src/snake.cpp b/lib/src/snake.cpp index e1b0e9e..2f71cf4 100644 --- a/lib/src/snake.cpp +++ b/lib/src/snake.cpp @@ -1,4 +1,5 @@ #include "chomper/snake.hpp" +#include "chomper/theme.hpp" #include "chomper/world_size.hpp" #include "chomper/world_space.hpp" #include @@ -7,13 +8,12 @@ namespace chomper { namespace { -constexpr auto snakeBodyColor_v = kvf::Color(glm::vec4{0.f, 0.6f, 1.f, 1.f}); constexpr auto headingToDir_v = klib::EnumArray{glm::vec2{1.f, 0.f}, glm::vec2{0.f, 1.f}, glm::vec2{-1.f, 0.f}, glm::vec2{0.f, -1.f}}; } // namespace Snake::Snake() { le::RenderInstance instance{}; - instance.tint = snakeBodyColor_v; + instance.tint = theme::snakeBodyColor_v; instance.transform.position = worldSpace::gridToWorld({0, 0.5f * worldSize_v.y}); m_instances.push_back(instance); while (m_instances.size() < m_baseSize) { @@ -27,7 +27,7 @@ void Snake::draw(le::IRenderer& renderer) const { void Snake::grow(Heading heading) { le::RenderInstance instance{}; - instance.tint = snakeBodyColor_v; + instance.tint = theme::snakeBodyColor_v; // no reason to move on initialization if (!m_instances.empty()) { instance.transform.position = worldSpace::gridToWorld(worldSpace::worldToGrid(m_instances.back().transform.position) + headingToDir_v[heading]); diff --git a/lib/src/ui/progress_bar.cpp b/lib/src/ui/progress_bar.cpp new file mode 100644 index 0000000..18196ec --- /dev/null +++ b/lib/src/ui/progress_bar.cpp @@ -0,0 +1,12 @@ +#include "chomper/ui/progress_bar.hpp" +#include + +namespace chomper::ui { +void ProgressBar::setProgress(float value) { + value = std::clamp(value, 0.0f, 1.0f); + auto const size = glm::vec2{value * targetSize.x, targetSize.y}; + auto const xOffset = 0.5f * size.x; + auto const rect = kvf::Rect<>::from_size(size, {xOffset, 0.0f}); + quad.create(rect); +} +} // namespace chomper::ui