Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ There is no external test framework. `tests/test.hpp` provides `TEST("name") {..
**Engine/Window loop** (`core/engine.cpp`, `core/window.cpp`). `Pancake::*` free functions are a thin facade over the `Window` namespace, which holds all global state (GLFW window, current `Scene*`, shaders, entity-picking framebuffer).
- Calls such as `load`, `save`, `reset`, `title`, and `size` only **set flags**, which the main loop applies at the top of the next frame. `load(fn)` runs the init function against the current scene, and `reset()` replaces the scene with an empty one.
- A component that resets the scene deletes itself, so tests and demos must `load` a new driver in the same frame.
- Each frame does `scene->update(dt)`, then renders twice: once into an RGB32F framebuffer with the entity shader (sprite entity IDs for `Window::readPixel`) and once to the screen. Then `DebugDraw::render()` runs, followed by ImGui (`Console::render`, `Inspector::render`).
- Each frame does `scene->update(dt)`, where `dt` is the frame time × time scale, zero while paused, or exactly one physics step for each `step()` while paused. Components keep updating while paused. Then the frame renders twice: once into an RGB32F framebuffer with the entity shader (sprite entity IDs for `Window::readPixel`) and once to the screen. Then `DebugDraw::render()` runs, followed by ImGui (`Console::render`, `Inspector::render`).
- Shutdown deletes the scene, then the GL resources, then ImGui.

**Scene → Entity → Component** (`core/`). `Scene` owns `Entity*`s plus a `Camera`, a `Renderer` (sprite batches), a physics `World`, and a `SpatialHashGrid<Entity*>`. That grid is refreshed every frame for entities whose bounds (transform plus rigidbody) changed, and it backs `getEntitiesIn/Near/At`. Entities own `Component*`s, looked up by type name (`getComponent("Rigidbody")`) or by class (`getComponent<Rigidbody>()`, which skips dead components and matches subclasses). Entities have an optional `name`, found with `Scene::findEntity`. IDs come from static counters, and loading from JSON bumps the counter past loaded IDs. Lifecycle:
- `start()` runs when the entity is added to a started scene. Most components self-register with scene systems in `start()` through the global `Window::getScene()` (e.g. `SpriteRenderer` → `getRenderer()->add`, `Rigidbody` → `getPhysics()->add`) and unregister in `end()`.
- `Entity::update` only updates the components it had when the frame began, so a component added during an update (e.g. a chained `Tween`) first runs next frame.
- `Scene::after`/`every` timers run after entity updates, advance with the scene `dt`, and are cancelled when their owner entity dies. They aren't serialised. `Tween` (`components/tween.hpp`) is a non-serialised component that kills itself when done, and reads its start value when it begins, after its delay.
- `kill()` marks a component dead and calls `end()`. The owning update loop deletes dead entities and components afterwards.
- `Scene::~Scene` kills every entity before deleting any systems, so components can unregister while the systems still exist.
- `TransformableComponent` adds a position/size/rotation offset relative to the entity.
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ The engine provides the following features:
- Entity-component system for managing game objects, with typed component access and named entities.
- Resource loading from file system.
- JSON serialization and deserialization for game data, including custom components.
- An in-game console with built-in commands (`help`, `save`, `load`, `reset`, `debug`, `gravity`, `entities`, `inspector`) and support for your own.
- Time scale, pause and single stepping, with timers (`scene->after`, `scene->every`) and tweens with easing that follow them.
- An in-game console with built-in commands (`help`, `save`, `load`, `reset`, `debug`, `gravity`, `entities`, `inspector`, `pause`, `step`, `timescale`, `spawn`, `kill`, `tp`, `fps`, `vsync`, `fullscreen`) and support for your own.
- An entity inspector: click an entity to view and edit its components.
- Debug drawing for collision shapes and other debugging purposes.

Expand All @@ -167,11 +168,34 @@ int level = 2;
Pancake::load([level](Scene* scene) { /* build the level */ });
```

## Time, Timers and Tweens
```c++
Pancake::timeScale(0.25f); // Slow motion. Components get the scaled delta time.
Pancake::pause(true); // Components keep updating with a delta time of zero, so a pause menu can still read input.
Pancake::step(); // While paused, advance one physics step on the next frame.
float real = Pancake::getUnscaledDeltaTime();

// Timers follow the time scale. With an owner, a timer is cancelled when the owner dies.
int timer = scene->after(2.0f, []() { /* ... */ }, entity);
scene->every(0.5f, []() { /* ... */ });
scene->cancel(timer);

// Tweens are components, so they die with their entity. Start values are read after the delay.
Tween::position(entity, vec2(0.0f, 5.0f), 0.5f, Ease::outQuad)
->setDelay(1.0f)
->onComplete([entity]() { Tween::rotation(entity, 3.14f, 0.25f); });
Tween::colour(sprite, vec4(1.0f, 0.0f, 0.0f, 1.0f), 0.2f);
Tween::value(entity, 1.0f, [](float t) { /* t eases from 0 to 1 */ }, Ease::inOutSine);
```

## Debugging Tools
```c++
Pancake::console(true); // Open the console. Pancake::console() toggles it.
Pancake::inspector(true); // Open the inspector. Pancake::inspector() toggles it.
Pancake::debug(true); // Draw physics colliders, bounds and contacts.
Pancake::vsync(false); // Uncap the frame rate.
Pancake::fullscreen(true); // Fill the primary monitor. Pancake::fullscreen() toggles it.
Console::execute("fps on"); // Show the frame rate overlay.

// Add a console command.
Console::registerCommand("spawn", "spawn <x> <y>: Spawn a crate.", [](std::vector<std::string> args) {
Expand All @@ -184,6 +208,7 @@ Console::registerCommand("spawn", "spawn <x> <y>: Spawn a crate.", [](std::vecto
- The collision solver was rewritten, so restitution and friction may feel slightly different. Bounces below 1 unit per second are no longer applied, so resting bodies don't jitter.
- `KeyListener::isKeyBeginDown` and `MouseListener::isMouseBeginDown` are now true for the whole frame a key or button was pressed in, rather than only for the first caller.
- A key or mouse button pressed and released within one frame now still reports `isKeyBeginDown`/`isMouseBeginDown` for that frame, along with the new `isKeyEndDown`/`isMouseEndDown`.
- Components added to an entity during its update now first update on the next frame, instead of the same frame.
- `Pancake::load` and `Window::load` take a `std::function<void(Scene*)>` instead of a function pointer. Existing calls still compile.

## Contributions
Expand Down
6 changes: 3 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ Tick an item off in the same branch that implements it.

## Batch 2: time and debugging

- [ ] Time scale and pause: `Pancake::timeScale(float)`, `Pancake::pause(bool)`, single frame step, console `pause` / `timescale`.
- [ ] More console commands: `spawn`, `kill <id>`, `tp <id> <x> <y>`, `fps` overlay, `vsync on|off`, `fullscreen`.
- [ ] Timers and tweens: `scene->after(seconds, fn)`, tweening position and colour with easing.
- [x] Time scale and pause: `Pancake::timeScale(float)`, `Pancake::pause(bool)`, `Pancake::step()`, console `pause` / `step` / `timescale`.
- [x] More console commands: `spawn`, `kill <id|name>`, `tp <id|name> <x> <y>`, `fps` overlay, `vsync on|off`, `fullscreen`.
- [x] Timers and tweens: `scene->after` / `every` / `cancel`, `Tween` for position, size, rotation, colour and custom values, with `Ease` functions.

## Batch 3: physics for gameplay

Expand Down
10 changes: 10 additions & 0 deletions examples/demo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
// D toggle physics debug drawing
// I toggle the inspector
// ` toggle the console (try "help")
// P pause or resume
// N step one physics frame (pauses)
// F toggle the frame rate overlay
// R reset the sandbox
// Escape quit
//
Expand Down Expand Up @@ -115,6 +118,10 @@ void spawnBox(Scene* scene, vec2 position, vec2 size, float mass, vec4 colour) {
ImpactFlash* flash = new ImpactFlash();
flash->colour = colour;
e->addComponent(flash);

// Pop in: the sprite grows to full size while the collider is full size from the start.
e->setSize(0.2f * size);
Tween::size(e, size, 0.25f, Ease::outBack);
}

scene->addEntity(e);
Expand Down Expand Up @@ -155,6 +162,9 @@ class Sandbox : public Component {
if (KeyListener::isKeyBeginDown(GLFW_KEY_D)) {Console::execute("debug");}
if (KeyListener::isKeyBeginDown(GLFW_KEY_I)) {Pancake::inspector();}
if (KeyListener::isKeyBeginDown(GLFW_KEY_R)) {Pancake::reset(); Pancake::load(build);}
if (KeyListener::isKeyBeginDown(GLFW_KEY_P)) {Pancake::pause();}
if (KeyListener::isKeyBeginDown(GLFW_KEY_N)) {Pancake::step();}
if (KeyListener::isKeyBeginDown(GLFW_KEY_F)) {Console::execute("fps");}
}
if (KeyListener::isKeyBeginDown(GLFW_KEY_GRAVE_ACCENT)) {Pancake::console();}

Expand Down
77 changes: 77 additions & 0 deletions include/pancake/components/tween.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#pragma once

#include <functional>
#include <glm/glm.hpp>

#include "pancake/core/component.hpp"
#include "pancake/graphics/spriterenderer.hpp"

namespace Pancake {

// Easing functions map progress in [0, 1] to eased progress, starting at 0 and ending at 1.
namespace Ease {

typedef float (*Function)(float t);

float linear(float t);
float inQuad(float t);
float outQuad(float t);
float inOutQuad(float t);
float inCubic(float t);
float outCubic(float t);
float inOutCubic(float t);
float inSine(float t);
float outSine(float t);
float inOutSine(float t);
float outBack(float t);
float outBounce(float t);

}

// Animates a value over time, then kills itself. It advances with the scene's delta time, so it follows
// the time scale and stops while paused. Tweens live on an entity, so they die with it, and are not saved.
// Start values are read when the tween begins, after its delay, so delayed tweens can be chained.
class Tween : public Component {

private:

float duration;
float delay;
float elapsed;
bool begun;
bool finished;
Ease::Function ease;
std::function<void()> beginCallback;
std::function<void(float)> applyCallback;
std::function<void()> completeCallback;

public:

Tween();
Tween(float duration, std::function<void(float)> apply, Ease::Function ease = Ease::linear);

void update(float dt) override;
void imgui() override;

float getDuration();
float getDelay();
float getProgress();
bool isFinished();

Tween* setDelay(float delay);
Tween* setEase(Ease::Function ease);
Tween* onBegin(std::function<void()> callback);
Tween* onComplete(std::function<void()> callback);

// Tween an entity's transform, or a sprite's colour. The tween is added to the entity that owns the target.
static Tween* position(Entity* entity, glm::vec2 to, float duration, Ease::Function ease = Ease::linear);
static Tween* size(Entity* entity, glm::vec2 to, float duration, Ease::Function ease = Ease::linear);
static Tween* rotation(Entity* entity, float to, float duration, Ease::Function ease = Ease::linear);
static Tween* colour(SpriteRenderer* sprite, glm::vec4 to, float duration, Ease::Function ease = Ease::linear);

// Call apply with eased progress from 0 to 1, for anything else.
static Tween* value(Entity* entity, float duration, std::function<void(float)> apply, Ease::Function ease = Ease::linear);

};

}
16 changes: 16 additions & 0 deletions include/pancake/core/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ namespace Pancake {
void inspector(bool state);
void inspector();


// Time. Pausing keeps components updating, with a delta time of zero, so physics, timers and tweens stop.
void timeScale(float scale);
float getTimeScale();
void pause(bool state);
void pause();
bool isPaused();
void step(int frames = 1); // Pause if needed, then advance the given number of physics steps, one per frame.
float getFps();
float getUnscaledDeltaTime();

void vsync(bool state);
void fullscreen(bool state);
void fullscreen();
bool isFullscreen();

Scene* getScene();
Entity* getEntity(int id);
Component* getComponent(int id);
Expand Down
25 changes: 25 additions & 0 deletions include/pancake/core/scene.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <string>
#include <vector>
#include <unordered_map>
#include <functional>
#include <nlohmann/json.hpp>

#include "pancake/core/camera.hpp"
Expand All @@ -18,6 +19,16 @@ namespace Pancake {

private:

struct Timer {
int id;
float remaining;
float interval;
bool repeat;
int owner; // The entity the timer belongs to, or -1.
bool cancelled;
std::function<void()> callback;
};

std::string name;
std::vector<Entity*> entities;
std::unordered_map<int, Entity*> entityIndex;
Expand All @@ -31,7 +42,14 @@ namespace Pancake {
SpatialHashGrid<Entity*>* grid;
std::unordered_map<Entity*, std::pair<glm::vec2, glm::vec2>> gridBounds;

std::vector<Timer> timers;
std::vector<Timer> addedTimers; // Timers added by timer callbacks, merged in after the callbacks run.
bool updatingTimers;
int nextTimerId;

std::pair<glm::vec2, glm::vec2> computeBounds(Entity* entity);
void updateTimers(float dt);
int addTimer(float seconds, bool repeat, std::function<void()> callback, Entity* owner);
void updateGrid(Entity* entity);

public:
Expand Down Expand Up @@ -66,6 +84,13 @@ namespace Pancake {
std::vector<Entity*> findEntities(std::string name);
template<class T> std::vector<Entity*> getEntitiesWith();

// Timers advance with the scene's delta time, so they follow the time scale and stop while paused.
// A timer with an owner is cancelled when the owner dies. Timers are not saved with the scene.
int after(float seconds, std::function<void()> callback, Entity* owner = nullptr);
int every(float seconds, std::function<void()> callback, Entity* owner = nullptr);
void cancel(int timer);
bool hasTimer(int timer);

// Spatial queries. An entity's bounds cover its transform and its rigidbody, if it has one.
std::pair<glm::vec2, glm::vec2> getBounds(Entity* entity);
std::vector<Entity*> getEntitiesIn(glm::vec2 min, glm::vec2 max);
Expand Down
15 changes: 15 additions & 0 deletions include/pancake/core/window.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ namespace Pancake {
void setProjectionHeight(float height);
int readPixel(int x, int y);

// Time. The scene is updated with the frame time multiplied by the time scale, or zero while paused.
void setTimeScale(float scale);
float getTimeScale();
void setPaused(bool paused);
bool isPaused();
void step(int frames);
float getFps();
float getUnscaledDeltaTime();

// Display.
void setVsync(bool vsync);
bool hasVsync();
void setFullscreen(bool fullscreen);
bool isFullscreen();

void openConsole();
void closeConsole();

Expand Down
1 change: 1 addition & 0 deletions include/pancake/pancake.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include "pancake/components/animation.hpp"
#include "pancake/components/transition.hpp"
#include "pancake/components/tween.hpp"

#include "pancake/core/camera.hpp"
#include "pancake/core/component.hpp"
Expand Down
1 change: 1 addition & 0 deletions include/pancake/physics/world.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ namespace Pancake {

void update(float dt);
void render();
float getTimeStep();
glm::vec2 getGravity();
void setGravity(glm::vec2 gravity);
bool hasDebugDraw();
Expand Down
Loading
Loading