From 3bf8200d0f3871fc77fd4b87373e7e10d748bf0d Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Sun, 15 Mar 2026 20:37:52 -0700 Subject: [PATCH] Refactor game loop to use state machine scene manager --- as6/Components.hpp | 138 ++++++++++++--------------- as6/GameContext.hpp | 81 +++++++++++----- as6/SceneManager.hpp | 219 +++++++++++++++++++++++++++++++++++++++++++ as6/Systems.hpp | 34 +++++++ as6/main.cpp | 58 +----------- 5 files changed, 375 insertions(+), 155 deletions(-) create mode 100644 as6/SceneManager.hpp diff --git a/as6/Components.hpp b/as6/Components.hpp index 692c55d..d501dc4 100644 --- a/as6/Components.hpp +++ b/as6/Components.hpp @@ -8,6 +8,7 @@ #include #include #include +#include struct TransformComponent : public Component { float x = 0.0f; @@ -85,6 +86,19 @@ struct MeterComponent : public Component { float drainRate = 14.0f; float gainPerStar = 28.0f; void Setup() override {} + + void SetValue(float newValue) { + const float oldValue = value; + value = newValue; + if (context && oldValue != value) { + context->EmitMeterChanged(oldValue, value); + } + } + + void AddValue(float delta) { SetValue(std::clamp(value + delta, 0.0f, maxValue)); } + + void Drain(float amount) { AddValue(-amount); } + void Update(float) override {} void Cleanup() override {} }; @@ -98,7 +112,19 @@ struct NullZoneComponent : public Component { struct ColliderComponent : public Component { float radius = 8.0f; + std::vector> onCollision; void Setup() override {} + + void EmitCollision(Entity &other) { + for (auto &callback : onCollision) { + callback(other); + } + } + + void AddCollisionListener(std::function callback) { + onCollision.push_back(std::move(callback)); + } + void Update(float) override {} void Cleanup() override {} }; @@ -142,66 +168,57 @@ struct ProjectionComponent : public Component { struct CollectibleComponent : public Component { bool collected = false; - void Setup() override {} - void Update(float) override { - if (collected || !context || !context->probeEntity || entity == context->probeEntity) { - return; - } - auto selfTransform = entity->GetComponent(); + void Setup() override { auto selfCollider = entity->GetComponent(); - auto probeTransform = context->probeEntity->GetComponent(); - auto probeCollider = context->probeEntity->GetComponent(); - if (!selfTransform || !selfCollider || !probeTransform || !probeCollider) { + if (!selfCollider) { return; } - const float dx = selfTransform->get().x - probeTransform->get().x; - const float dy = selfTransform->get().y - probeTransform->get().y; - const float r = selfCollider->get().radius + probeCollider->get().radius; - if ((dx * dx + dy * dy) > (r * r)) { - return; - } + selfCollider->get().AddCollisionListener([this](Entity &other) { + if (collected || !context || !context->probeEntity || &other != context->probeEntity) { + return; + } - collected = true; + collected = true; + context->EmitCollectiblePicked(*entity); - if (!context->hudEntity) { - return; - } + if (!context->hudEntity) { + return; + } - auto meter = context->hudEntity->GetComponent(); - if (!meter) { - return; - } + auto meter = context->hudEntity->GetComponent(); + if (!meter) { + return; + } - meter->get().value = - std::min(meter->get().maxValue, meter->get().value + meter->get().gainPerStar); + meter->get().AddValue(meter->get().gainPerStar); + }); } + + void Update(float) override {} + void Cleanup() override {} }; struct HazardComponent : public Component { - void Setup() override {} - void Update(float) override { - if (!context || !context->probeEntity || entity == context->probeEntity) { - return; - } - - auto selfTransform = entity->GetComponent(); + void Setup() override { auto selfCollider = entity->GetComponent(); - auto probeTransform = context->probeEntity->GetComponent(); - auto probeCollider = context->probeEntity->GetComponent(); - if (!selfTransform || !selfCollider || !probeTransform || !probeCollider) { + if (!selfCollider) { return; } - const float dx = selfTransform->get().x - probeTransform->get().x; - const float dy = selfTransform->get().y - probeTransform->get().y; - const float r = selfCollider->get().radius + probeCollider->get().radius; - if ((dx * dx + dy * dy) <= (r * r)) { - context->resetRequested = true; - } + selfCollider->get().AddCollisionListener([this](Entity &other) { + if (!context || !context->probeEntity || &other != context->probeEntity) { + return; + } + + if (context->onPlayerDeath) { + context->onPlayerDeath(); + } + }); } + void Update(float) override {} void Cleanup() override {} }; @@ -285,7 +302,7 @@ struct GravityReceiverComponent : public Component { physics->get().vy += ny * force * dt; if (meterPtr) { - meterPtr->value = std::max(0.0f, meterPtr->value - meterPtr->drainRate * dt); + meterPtr->Drain(meterPtr->drainRate * dt); } } void Cleanup() override {} @@ -296,7 +313,6 @@ struct ProbeStateComponent : public Component { float spawnY = 230.0f; float spawnVx = 165.0f; float spawnVy = 0.0f; - void Setup() override {} void Update(float) override { if (!context || !context->probeEntity || entity != context->probeEntity) { @@ -304,47 +320,17 @@ struct ProbeStateComponent : public Component { } auto transform = entity->GetComponent(); - auto physics = entity->GetComponent(); - if (!transform || !physics) { + if (!transform) { return; } if (transform->get().y < -20.0f || transform->get().y > static_cast(GetScreenHeight() + 20) || transform->get().x < -20.0f) { - context->resetRequested = true; - } - - if (!context->resetRequested) { - return; - } - - transform->get().x = spawnX; - transform->get().y = spawnY; - physics->get().vx = spawnVx; - physics->get().vy = spawnVy; - - if (context->hudEntity) { - auto meter = context->hudEntity->GetComponent(); - if (meter) { - meter->get().value = 60.0f; + if (context->onPlayerDeath) { + context->onPlayerDeath(); } } - - if (context->entities) { - for (auto &other : *context->entities) { - if (!other) { - continue; - } - - auto collectible = other->GetComponent(); - if (collectible) { - collectible->get().collected = false; - } - } - } - - context->resetRequested = false; } void Cleanup() override {} }; diff --git a/as6/GameContext.hpp b/as6/GameContext.hpp index 77b028a..70f6ab8 100644 --- a/as6/GameContext.hpp +++ b/as6/GameContext.hpp @@ -1,50 +1,79 @@ #pragma once +#include #include #include struct Entity; /** - * Shared execution context that flows through every entity and component so - * they can read/write the global scroll, known entity handles, and reset state. + * Shared state for one gameplay scene instance. */ struct GameContext { /** - * Pointer to the global entity list stored in `main.cpp`, allowing systems - * or components to inspect/create entities. + * @brief Pointer to the entity list owned by the active gameplay scene. */ std::vector> *entities = nullptr; - /** - * Entity that owns the static world geometry (terrain, background, etc.). - */ + /** @brief World entity (scroll/spawn owner). */ Entity *worldEntity = nullptr; - - /** - * Entity representing the gravity well that drags collectibles/probe. - */ + /** @brief Gravity well entity. */ Entity *wellEntity = nullptr; - - /** - * Entity for the player's probe input/state representation. - */ + /** @brief Player probe entity. */ Entity *probeEntity = nullptr; - - /** - * HUD entity that draws meter/score/probe status overlays. - */ + /** @brief HUD entity. */ Entity *hudEntity = nullptr; - /** - * Global horizontal scroll offset that components read to position - * themselves on screen. - */ + /** @brief Shared horizontal world scroll value. */ float scrollX = 0.0f; + /** @brief Optional callback invoked when the probe dies. */ + std::function onPlayerDeath; + + /** @brief Callbacks fired whenever a collectible is picked up. */ + std::vector> collectiblePickedListeners; + + /** @brief Callbacks fired whenever meter value changes. */ + std::vector> meterChangedListeners; + /** - * Flag that a component (usually reset button) can toggle to request the - * gameplay systems reset the probe/meter/collectibles. + * Registers a collectible pickup listener. + * + * @param listener Callback receiving the collected entity. */ - bool resetRequested = false; + void AddCollectiblePickedListener(std::function listener) { + collectiblePickedListeners.push_back(std::move(listener)); + } + + /** + * Emits collectible pickup event. + * + * @param collectible The collected entity. + */ + void EmitCollectiblePicked(Entity &collectible) { + for (auto &listener : collectiblePickedListeners) { + listener(collectible); + } + } + + /** + * Registers a meter changed listener. + * + * @param listener Callback receiving old value then new value. + */ + void AddMeterChangedListener(std::function listener) { + meterChangedListeners.push_back(std::move(listener)); + } + + /** + * Emits meter changed event. + * + * @param oldValue Meter value before mutation. + * @param newValue Meter value after mutation. + */ + void EmitMeterChanged(float oldValue, float newValue) { + for (auto &listener : meterChangedListeners) { + listener(oldValue, newValue); + } + } }; diff --git a/as6/SceneManager.hpp b/as6/SceneManager.hpp new file mode 100644 index 0000000..9ddf7b1 --- /dev/null +++ b/as6/SceneManager.hpp @@ -0,0 +1,219 @@ +#pragma once + +#include "Draw.hpp" +#include "Entities.hpp" +#include "Systems.hpp" + +#include "raylib.h" + +#include +#include +#include + +class SceneManager; + +class Scene { + public: + explicit Scene(SceneManager &owner) : manager(owner) {} + virtual ~Scene() = default; + + virtual void Enter() {} + virtual void Exit() {} + virtual void Update(float dt) = 0; + virtual void Draw() = 0; + + protected: + SceneManager &manager; +}; + +/** + * Manages the current active scene, using an object oriented state machine approach. + */ +class SceneManager { + public: + /** + * @brief Changes the current scene, invoking `Exit` on the old scene and `Enter` + * on the new scene. The new scene is constructed in-place with the provided arguments. + * + * @tparam T The type of the new scene, must derive from `Scene`. + * @param args Arguments forwarded to the constructor of the new scene. + */ + template void ChangeScene(Args &&...args); + + /** + * @brief Changes to the provided next scene, invoking `Exit` on the old scene and + * `Enter` on the new scene. + * + * @param nextScene The new scene to switch to. + */ + void ChangeScene(std::unique_ptr nextScene); + void Update(float dt); + void Draw(); + + private: + // use unique_ptr to enforce single ownership and ensure proper cleanup on + // scene change + std::unique_ptr current; +}; + +class StartMenuScene; +class GameplayScene; +class DeathScene; + +class StartMenuScene : public Scene { + public: + explicit StartMenuScene(SceneManager &owner) : Scene(owner) {} + void Update(float dt) override; + void Draw() override; +}; + +class GameplayScene : public Scene { + public: + explicit GameplayScene(SceneManager &owner) : Scene(owner) {} + void Enter() override; + void Update(float dt) override; + void Draw() override; + + private: + std::vector> entities; + GameContext context; + bool wantsDeathScene = false; + int collectedCount = 0; + float meterValue = 60.0f; +}; + +class DeathScene : public Scene { + public: + explicit DeathScene(SceneManager &owner) : Scene(owner) {} + void Update(float dt) override; + void Draw() override; +}; + +inline void SceneManager::ChangeScene(std::unique_ptr nextScene) { + if (current) { + current->Exit(); + } + + current = std::move(nextScene); + if (current) { + current->Enter(); + } +} + +inline void SceneManager::Update(float dt) { + if (current) { + current->Update(dt); + } +} + +inline void SceneManager::Draw() { + if (current) { + current->Draw(); + } +} + +inline void StartMenuScene::Update(float) { + if (IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + manager.ChangeScene(); + } +} + +inline void StartMenuScene::Draw() { + DrawSceneOutline(); + DrawText("Gravity Surfing", 24, 28, 36, Color{230, 238, 255, 255}); + DrawText("Click or press Enter to start", 24, 78, 22, Color{140, 198, 220, 255}); + DrawText("Left mouse controls gravity well", 24, 130, 18, Color{170, 185, 205, 255}); + DrawText("Collect stars and dodge hazards", 24, 156, 18, Color{170, 185, 205, 255}); +} + +inline void GameplayScene::Enter() { + entities.clear(); + entities.reserve(20); + context = {}; + wantsDeathScene = false; + collectedCount = 0; + meterValue = 60.0f; + + context.onPlayerDeath = [this]() { wantsDeathScene = true; }; + context.AddCollectiblePickedListener([this](Entity &) { ++collectedCount; }); + context.AddMeterChangedListener([this](float, float newValue) { meterValue = newValue; }); + + entities.push_back(CreateWorld()); + entities.push_back(CreateGravityWell()); + entities.push_back(CreateProbe()); + entities.push_back(CreateStar(900.0f, 120.0f)); + entities.push_back(CreateAsteroid(1100.0f, 330.0f)); + entities.push_back(CreateNullZone(1280.0f, 70.0f)); + entities.push_back(CreateHUD()); + + if (auto meter = entities.back()->GetComponent()) { + meterValue = meter->get().value; + } +} + +inline void GameplayScene::Update(float dt) { + UpdateAllSystems(entities, context, dt); + if (wantsDeathScene) { + manager.ChangeScene(); + } +} + +inline void GameplayScene::Draw() { + DrawSceneOutline(); + DrawText("Gravity Surfing", 14, 12, 20, Color{230, 238, 255, 255}); + + auto worldScroll = entities[0]->GetComponent(); + auto probeTransform = entities[2]->GetComponent(); + + if (worldScroll) { + DrawText(TextFormat("scrollX: %.2f", worldScroll->get().scrollX), 14, 40, 16, + Color{125, 225, 205, 255}); + } + + if (probeTransform) { + const auto &probe = probeTransform->get(); + DrawText(TextFormat("probe: (%.1f, %.1f)", probe.x, probe.y), 14, 60, 16, + Color{235, 215, 125, 255}); + } + + DrawText(TextFormat("meter: %.1f", meterValue), 14, 80, 16, Color{120, 210, 190, 255}); + DrawText(TextFormat("stars: %i", collectedCount), 14, 100, 16, Color{255, 223, 86, 255}); + + for (auto &entity : entities) { + if (!entity) { + continue; + } + + auto collectible = entity->GetComponent(); + if (collectible && collectible->get().collected) { + continue; + } + + auto render = entity->GetComponent(); + if (render) { + render->get().Draw(); + } + } +} + +inline void DeathScene::Update(float) { + if (IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + manager.ChangeScene(); + return; + } + + if (IsKeyPressed(KEY_ESCAPE)) { + manager.ChangeScene(); + } +} + +inline void DeathScene::Draw() { + DrawSceneOutline(); + DrawText("PROBE LOST", 24, 28, 36, Color{255, 208, 208, 255}); + DrawText("Press Enter or click to restart", 24, 84, 22, Color{255, 170, 170, 255}); + DrawText("Press Esc for menu", 24, 116, 20, Color{200, 188, 188, 255}); +} + +template inline void SceneManager::ChangeScene(Args &&...args) { + ChangeScene(std::make_unique(*this, std::forward(args)...)); +} diff --git a/as6/Systems.hpp b/as6/Systems.hpp index ccd4fd0..776b5e4 100644 --- a/as6/Systems.hpp +++ b/as6/Systems.hpp @@ -26,4 +26,38 @@ void UpdateAllSystems(std::vector> &entities, GameContex entity->Update(deltaTime); } + + for (size_t i = 0; i < entities.size(); ++i) { + auto &a = entities[i]; + if (!a) { + continue; + } + + auto aTransform = a->GetComponent(); + auto aCollider = a->GetComponent(); + if (!aTransform || !aCollider) { + continue; + } + + for (size_t j = i + 1; j < entities.size(); ++j) { + auto &b = entities[j]; + if (!b) { + continue; + } + + auto bTransform = b->GetComponent(); + auto bCollider = b->GetComponent(); + if (!bTransform || !bCollider) { + continue; + } + + const float dx = aTransform->get().x - bTransform->get().x; + const float dy = aTransform->get().y - bTransform->get().y; + const float r = aCollider->get().radius + bCollider->get().radius; + if ((dx * dx + dy * dy) <= (r * r)) { + aCollider->get().EmitCollision(*b); + bCollider->get().EmitCollision(*a); + } + } + } } diff --git a/as6/main.cpp b/as6/main.cpp index 5faee34..a4ac757 100644 --- a/as6/main.cpp +++ b/as6/main.cpp @@ -3,6 +3,7 @@ #include "Entities.hpp" #include "Entity.hpp" #include "GameContext.hpp" +#include "SceneManager.hpp" #include "Systems.hpp" #include "raylib-cpp.hpp" @@ -12,67 +13,18 @@ int main() { window.SetState(FLAG_WINDOW_RESIZABLE); window.SetTargetFPS(60); - std::vector> entities; - entities.reserve(20); - - GameContext context; - - entities.push_back(CreateWorld()); - entities.push_back(CreateGravityWell()); - entities.push_back(CreateProbe()); - entities.push_back(CreateStar(900.0f, 120.0f)); - entities.push_back(CreateAsteroid(1100.0f, 330.0f)); - entities.push_back(CreateNullZone(1280.0f, 70.0f)); - entities.push_back(CreateHUD()); + SceneManager sceneManager; + sceneManager.ChangeScene(); while (!window.ShouldClose()) { float dt = window.GetFrameTime(); - UpdateAllSystems(entities, context, dt); - - auto worldScroll = entities[0]->GetComponent(); - auto probeTransform = entities[2]->GetComponent(); - auto hudMeter = entities.back()->GetComponent(); + sceneManager.Update(dt); window.BeginDrawing(); window.ClearBackground(raylib::Color(11, 15, 26, 255)); - DrawSceneOutline(); - - raylib::DrawText("Gravity Surfing", 14, 12, 20, raylib::Color::RayWhite()); - - if (worldScroll) { - // debug readout for early loop validation - raylib::DrawText(TextFormat("scrollX: %.2f", worldScroll->get().scrollX), 14, 40, 16, - raylib::Color(125, 225, 205, 255)); - } - - if (probeTransform) { - const auto &probe = probeTransform->get(); - raylib::DrawText(TextFormat("probe: (%.1f, %.1f)", probe.x, probe.y), 14, 60, 16, - raylib::Color(235, 215, 125, 255)); - } - - if (hudMeter) { - raylib::DrawText(TextFormat("meter: %.1f", hudMeter->get().value), 14, 80, 16, - raylib::Color(120, 210, 190, 255)); - } - - for (auto &entity : entities) { - if (!entity) { - continue; - } - - auto collectible = entity->GetComponent(); - if (collectible && collectible->get().collected) { - continue; - } - - auto render = entity->GetComponent(); - if (render) { - render->get().Draw(); - } - } + sceneManager.Draw(); window.EndDrawing(); }