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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ There is no external test framework. `tests/test.hpp` provides `TEST("name") {..
- 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`).
- 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. IDs come from static counters, and loading from JSON bumps the counter past loaded IDs. Lifecycle:
**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()`.
- `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.
Expand Down Expand Up @@ -74,4 +74,5 @@ There is no external test framework. `tests/test.hpp` provides `TEST("name") {..
- Include `<glad/glad.h>` before anything that pulls in GLFW (including `pancake/core/listener.hpp`), or glad's header guard errors.
- Prefer fully qualified `std::`/`glm::` names in new code. Some headers still export `using std::string; using glm::vec2;` etc., and other code relies on that.
- Single-line `if (...) {...}` braces; British spelling (`serialise`, `colour`).
- `ROADMAP.md` is the queue of planned work, in batches. Tick items off in the branch that implements them.
- Errors are printed as `ERROR::AREA::DETAIL` to stdout. Load functions return `false`/`nullptr` on malformed JSON instead of throwing.
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,33 @@ The engine provides the following features:
- Collision listeners, sensors and raycasts.
- Spatial queries to find entities in an area, near a point or under the mouse.
- Audio playback and management using SoLoud.
- Input management for keyboard and mouse.
- Entity-component system for managing game objects.
- Input management for keyboard and mouse, with pressed, held and released events.
- 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.
- An entity inspector: click an entity to view and edit its components.
- Debug drawing for collision shapes and other debugging purposes.

## Working with Entities
```c++
Entity* player = new Entity(vec2(0.0f, 0.0f));
player->setName("Player"); // Saved with the scene.
Rigidbody* body = player->addComponent<Rigidbody>();
scene->addEntity(player);

Entity* found = scene->findEntity("Player"); // Or findEntities for every match.
Rigidbody* r = found->getComponent<Rigidbody>(); // nullptr if it has none. Subclasses match too.
for (Entity* e : scene->getEntitiesWith<Rigidbody>()) {}

if (KeyListener::isKeyBeginDown(GLFW_KEY_SPACE)) {} // Pressed this frame.
if (KeyListener::isKeyEndDown(GLFW_KEY_SPACE)) {} // Released this frame.

// load accepts any callable, including capturing lambdas.
int level = 2;
Pancake::load([level](Scene* scene) { /* build the level */ });
```

## Debugging Tools
```c++
Pancake::console(true); // Open the console. Pancake::console() toggles it.
Expand All @@ -164,6 +183,8 @@ Console::registerCommand("spawn", "spawn <x> <y>: Spawn a crate.", [](std::vecto
- Physics scenes now have world gravity, defaulting to `(0, -10)`. If you previously added a `Gravity` force generator to every body to get gravity, remove it or set the world gravity to zero with `scene->getPhysics()->setGravity(vec2(0.0f, 0.0f))`.
- 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`.
- `Pancake::load` and `Window::load` take a `std::function<void(Scene*)>` instead of a function pointer. Existing calls still compile.

## Contributions
Contributions are welcome! If you find any bugs or have suggestions for improvements, feel free to open an issue or submit a pull request on GitHub.
34 changes: 34 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Roadmap

Planned improvements for using Pancake as a base for games, grouped into batches of related items.
Tick an item off in the same branch that implements it.

## Batch 1: entity and input API

- [x] Typed component access: `getComponent<T>()`, `getComponents<T>()`, `hasComponent<T>()`, `addComponent<T>(args...)`.
- [x] Release events: `KeyListener::isKeyEndDown`, `MouseListener::isMouseEndDown`. Taps that press and release within one frame still register as begin down.
- [x] Entity names and lookup: optional serialised `name`, `Scene::findEntity(name)`, `Scene::findEntities(name)`, `Scene::getEntitiesWith<T>()`. Names show in the inspector and the `entities` command.
- [x] `load` accepts any callable (`std::function<void(Scene*)>`), so capturing lambdas work.

## 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.

## Batch 3: physics for gameplay

- [ ] Collision enter and exit events (`collisionEnter` / `collisionExit`), from diffing contact pairs between steps.
- [ ] Collision layers and masks on `Rigidbody`, checked in the broad phase, serialised and shown in the inspector.

## Batch 4: camera and input devices

- [ ] `CameraFollow` component: target entity, smoothing, dead zone, bounds clamp.
- [ ] Camera shake: `shake(intensity, duration)`.
- [ ] Gamepad input through `glfwGetGamepadState`, with begin, down and end semantics.

## Batch 5: editor tooling

- [ ] `Entity::clone()` and prefabs: `Pancake::spawn("prefab.json", position)`.
- [ ] Inspector edit mode: drag to move, delete, duplicate, play/pause, save and load buttons.
- [ ] Hot reload of textures and audio when their files change.
6 changes: 3 additions & 3 deletions examples/demo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ class ImpactFlash : public Component, public CollisionListener {
}

void collision(Entity* with, CollisionManifold manifold) override {
Rigidbody* r = dynamic_cast<Rigidbody*>(this->getEntity()->getComponent("Rigidbody"));
Rigidbody* r = this->getEntity()->getComponent<Rigidbody>();
if (r != nullptr && glm::length(r->getVelocity()) > 6.0f) {this->flash = 0.15f;}
}

void update(float dt) override {
SpriteRenderer* sprite = dynamic_cast<SpriteRenderer*>(this->getEntity()->getComponent("SpriteRenderer"));
SpriteRenderer* sprite = this->getEntity()->getComponent<SpriteRenderer>();
if (sprite == nullptr) {return;}
if (this->flash > 0.0f) {
this->flash -= dt;
Expand All @@ -79,7 +79,7 @@ class BallOutline : public Component {
}

void update(float dt) override {
Rigidbody* r = dynamic_cast<Rigidbody*>(this->getEntity()->getComponent("Rigidbody"));
Rigidbody* r = this->getEntity()->getComponent<Rigidbody>();
if (r == nullptr) {return;}
for (Collider* c : r->getColliders()) {
CircleCollider* circle = dynamic_cast<CircleCollider*>(c);
Expand Down
4 changes: 3 additions & 1 deletion include/pancake/core/engine.hpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#pragma once

#include <functional>

#include "pancake/core/scene.hpp"
#include "pancake/core/entity.hpp"
#include "pancake/core/component.hpp"

namespace Pancake {

void load(void (*method)(Scene* scene));
void load(std::function<void(Scene*)> method);
void load(string filename);
void save(string filename);
void reset();
Expand Down
47 changes: 46 additions & 1 deletion include/pancake/core/entity.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <string>
#include <vector>
#include <utility>
#include <glm/glm.hpp>
#include <nlohmann/json.hpp>

Expand All @@ -14,6 +15,7 @@ namespace Pancake {
private:

int id;
std::string name;
bool started;
std::vector<Component*> components;
glm::vec2 position;
Expand Down Expand Up @@ -44,6 +46,7 @@ namespace Pancake {

// Getter Methods.
int getId();
std::string getName();
std::vector<Component*> getComponents();
glm::vec2 getPosition();
glm::vec2 getSize();
Expand All @@ -53,6 +56,7 @@ namespace Pancake {

// Setter Methods.
void setId(int id);
void setName(std::string name);
void setPosition(glm::vec2 position);
void setSize(glm::vec2 size);
void setRotation(float radians);
Expand All @@ -68,8 +72,49 @@ namespace Pancake {
Component* getComponent(std::string type);
void addComponent(Component* component);

// Typed component access. These match subclasses too, e.g. getComponent<ForceGenerator>().
template<class T> T* getComponent();
template<class T> std::vector<T*> getComponents();
template<class T> bool hasComponent();
template<class T, class... Args> T* addComponent(Args&&... args);

};

}

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

namespace Pancake {

template<class T>
T* Entity::getComponent() {
for (Component* c : this->components) {
T* t = dynamic_cast<T*>(c);
if (t != nullptr && !c->isDead()) {return t;}
}
return nullptr;
}

template<class T>
std::vector<T*> Entity::getComponents() {
std::vector<T*> result;
for (Component* c : this->components) {
T* t = dynamic_cast<T*>(c);
if (t != nullptr && !c->isDead()) {result.push_back(t);}
}
return result;
}

template<class T>
bool Entity::hasComponent() {
return this->getComponent<T>() != nullptr;
}

template<class T, class... Args>
T* Entity::addComponent(Args&&... args) {
T* component = new T(std::forward<Args>(args)...);
this->addComponent(component);
return component;
}

}
2 changes: 2 additions & 0 deletions include/pancake/core/listener.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Pancake {
bool isKeyDown(int key);
bool isKeyDown();
bool isKeyBeginDown(int key);
bool isKeyEndDown(int key);
void endFrame();
}

Expand All @@ -21,6 +22,7 @@ namespace Pancake {
bool isMouseDown(int button);
bool isMouseDown();
bool isMouseBeginDown(int button);
bool isMouseEndDown(int button);
bool hasMouseMoved();
bool isMouseDragging();
double getX();
Expand Down
20 changes: 19 additions & 1 deletion include/pancake/core/scene.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ namespace Pancake {
std::unordered_map<int, Component*>* getComponents();
std::vector<Entity*> getEntities();

// Lookups by name or component. Dead entities are skipped.
Entity* findEntity(std::string name);
std::vector<Entity*> findEntities(std::string name);
template<class T> std::vector<Entity*> getEntitiesWith();

// 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 All @@ -69,4 +74,17 @@ namespace Pancake {

};

}
}

namespace Pancake {

template<class T>
std::vector<Entity*> Scene::getEntitiesWith() {
std::vector<Entity*> result;
for (Entity* e : this->entities) {
if (!e->isDead() && e->hasComponent<T>()) {result.push_back(e);}
}
return result;
}

}
4 changes: 3 additions & 1 deletion include/pancake/core/window.hpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
#pragma once

#include <functional>

#include "pancake/core/console.hpp"
#include "pancake/core/scene.hpp"

namespace Pancake {

namespace Window {

void load(void(*method)(Scene* scene));
void load(std::function<void(Scene*)> method);
void load(string filename);
void save(string filename);
void reset();
Expand Down
4 changes: 3 additions & 1 deletion src/core/console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ namespace Pancake {
Scene* scene = Window::getScene();
if (scene == nullptr) {return;}
for (Entity* e : scene->getEntities()) {
std::string line = std::to_string(e->getId()) + ":";
std::string line = std::to_string(e->getId());
if (!e->getName().empty()) {line += " " + e->getName();}
line += ":";
for (Component* c : e->getComponents()) {line += " " + c->getType();}
Console::print(line);
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Pancake {

void load(void (*method)(Scene* scene)) {
void load(std::function<void(Scene*)> method) {
Window::load(method);
}

Expand Down
20 changes: 20 additions & 0 deletions src/core/entity.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <cmath>
#include <cstdio>
#include <deque>
#include <algorithm>
#include <unordered_set>
Expand All @@ -18,6 +19,7 @@ namespace Pancake {
void Entity::init(int id, glm::vec2 position, glm::vec2 size, float radians, bool load) {

this->id = id;
this->name = "";
this->started = false;
this->position = position;
this->size = size;
Expand Down Expand Up @@ -99,6 +101,7 @@ namespace Pancake {

nlohmann::json j;
j.emplace("id", this->id);
if (!this->name.empty()) {j.emplace("name", this->name);}

j.emplace("position", nlohmann::json::array());
j["position"].push_back(this->position.x);
Expand Down Expand Up @@ -133,9 +136,11 @@ namespace Pancake {
if (!j["size"][1].is_number()) {return nullptr;}

if (!j.contains("rotation") || !j["rotation"].is_number()) {return nullptr;}
if (j.contains("name") && !j["name"].is_string()) {return nullptr;}

Entity* e = new Entity();
e->init(j["id"], glm::vec2(j["position"][0], j["position"][1]), glm::vec2(j["size"][0], j["size"][1]), j["rotation"], true);
if (j.contains("name")) {e->name = j["name"];}

if (j.contains("components") && j["components"].is_array()) {
for (auto element : j["components"]) {
Expand All @@ -154,6 +159,13 @@ namespace Pancake {

void Entity::imgui() {

// Name
char name[64];
std::snprintf(name, sizeof(name), "%s", this->name.c_str());
ImGui::Text("Name: ");
ImGui::SameLine();
if (ImGui::InputText("##Name", name, sizeof(name))) {this->setName(name);}

// Position
glm::vec2 position = glm::vec2(this->position.x, this->position.y);
ImGui::Text("Position: ");
Expand Down Expand Up @@ -204,6 +216,14 @@ namespace Pancake {
return this->id;
}

std::string Entity::getName() {
return this->name;
}

void Entity::setName(std::string name) {
this->name = name;
}

std::vector<Component*> Entity::getComponents() {
return this->components;
}
Expand Down
1 change: 1 addition & 0 deletions src/core/inspector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ namespace Pancake {
if (ImGui::CollapsingHeader("Entities")) {
for (Entity* e : scene->getEntities()) {
std::string label = std::to_string(e->getId());
if (!e->getName().empty()) {label += " \"" + e->getName() + "\"";}
for (Component* c : e->getComponents()) {label += " " + c->getType();}
if (ImGui::Selectable(label.c_str(), e->getId() == selected)) {selected = e->getId();}
}
Expand Down
Loading
Loading