Refactor game loop to use state machine scene manager

master
John Montagu, the 4th Earl of Sandvich 2026-03-15 20:37:52 -07:00
parent 7e954cd4de
commit 3bf8200d0f
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
5 changed files with 375 additions and 155 deletions

View File

@ -8,6 +8,7 @@
#include <algorithm>
#include <cmath>
#include <functional>
#include <vector>
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<std::function<void(Entity &)>> onCollision;
void Setup() override {}
void EmitCollision(Entity &other) {
for (auto &callback : onCollision) {
callback(other);
}
}
void AddCollisionListener(std::function<void(Entity &)> 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<TransformComponent>();
void Setup() override {
auto selfCollider = entity->GetComponent<ColliderComponent>();
auto probeTransform = context->probeEntity->GetComponent<TransformComponent>();
auto probeCollider = context->probeEntity->GetComponent<ColliderComponent>();
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<MeterComponent>();
if (!meter) {
return;
}
auto meter = context->hudEntity->GetComponent<MeterComponent>();
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<TransformComponent>();
void Setup() override {
auto selfCollider = entity->GetComponent<ColliderComponent>();
auto probeTransform = context->probeEntity->GetComponent<TransformComponent>();
auto probeCollider = context->probeEntity->GetComponent<ColliderComponent>();
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<TransformComponent>();
auto physics = entity->GetComponent<PhysicsComponent>();
if (!transform || !physics) {
if (!transform) {
return;
}
if (transform->get().y < -20.0f ||
transform->get().y > static_cast<float>(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<MeterComponent>();
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<CollectibleComponent>();
if (collectible) {
collectible->get().collected = false;
}
}
}
context->resetRequested = false;
}
void Cleanup() override {}
};

View File

@ -1,50 +1,79 @@
#pragma once
#include <functional>
#include <memory>
#include <vector>
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<std::shared_ptr<Entity>> *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<void()> onPlayerDeath;
/** @brief Callbacks fired whenever a collectible is picked up. */
std::vector<std::function<void(Entity &)>> collectiblePickedListeners;
/** @brief Callbacks fired whenever meter value changes. */
std::vector<std::function<void(float, float)>> 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<void(Entity &)> 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<void(float, float)> 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);
}
}
};

View File

@ -0,0 +1,219 @@
#pragma once
#include "Draw.hpp"
#include "Entities.hpp"
#include "Systems.hpp"
#include "raylib.h"
#include <memory>
#include <utility>
#include <vector>
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 <typename T, typename... Args> 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<Scene> 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<Scene> 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<std::shared_ptr<Entity>> 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<Scene> 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<GameplayScene>();
}
}
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<MeterComponent>()) {
meterValue = meter->get().value;
}
}
inline void GameplayScene::Update(float dt) {
UpdateAllSystems(entities, context, dt);
if (wantsDeathScene) {
manager.ChangeScene<DeathScene>();
}
}
inline void GameplayScene::Draw() {
DrawSceneOutline();
DrawText("Gravity Surfing", 14, 12, 20, Color{230, 238, 255, 255});
auto worldScroll = entities[0]->GetComponent<ScrollComponent>();
auto probeTransform = entities[2]->GetComponent<TransformComponent>();
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<CollectibleComponent>();
if (collectible && collectible->get().collected) {
continue;
}
auto render = entity->GetComponent<RenderComponent>();
if (render) {
render->get().Draw();
}
}
}
inline void DeathScene::Update(float) {
if (IsKeyPressed(KEY_ENTER) || IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
manager.ChangeScene<GameplayScene>();
return;
}
if (IsKeyPressed(KEY_ESCAPE)) {
manager.ChangeScene<StartMenuScene>();
}
}
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 <typename T, typename... Args> inline void SceneManager::ChangeScene(Args &&...args) {
ChangeScene(std::make_unique<T>(*this, std::forward<Args>(args)...));
}

View File

@ -26,4 +26,38 @@ void UpdateAllSystems(std::vector<std::shared_ptr<Entity>> &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<TransformComponent>();
auto aCollider = a->GetComponent<ColliderComponent>();
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<TransformComponent>();
auto bCollider = b->GetComponent<ColliderComponent>();
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);
}
}
}
}

View File

@ -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<std::shared_ptr<Entity>> 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<StartMenuScene>();
while (!window.ShouldClose()) {
float dt = window.GetFrameTime();
UpdateAllSystems(entities, context, dt);
auto worldScroll = entities[0]->GetComponent<ScrollComponent>();
auto probeTransform = entities[2]->GetComponent<TransformComponent>();
auto hudMeter = entities.back()->GetComponent<MeterComponent>();
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<CollectibleComponent>();
if (collectible && collectible->get().collected) {
continue;
}
auto render = entity->GetComponent<RenderComponent>();
if (render) {
render->get().Draw();
}
}
sceneManager.Draw();
window.EndDrawing();
}