From 79acf65d55bff773c20595fd0f276b9b8691f695 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 01:20:13 +0000 Subject: [PATCH] Add typed component access, entity names, release events and lambda loads Batch 1 of ROADMAP.md, the queue of planned engine improvements. - Entity::getComponent(), getComponents(), hasComponent() and addComponent(args...). They match subclasses and skip dead components. - Entities have an optional name, serialised only when set so older save files load unchanged. Scene::findEntity, findEntities and getEntitiesWith() look entities up. Names show in the inspector, where they can be edited, and in the entities console command. - KeyListener::isKeyEndDown and MouseListener::isMouseEndDown report a release for one frame. A press and release within one frame now still reports begin down. Mouse world coordinates are skipped when no scene exists, so MouseListener::endFrame is safe before start. - Pancake::load and Window::load take std::function, so capturing lambdas work. The load queue is moved out before running, so a load function that queues another can't reallocate the vector holding the running function. Tests: new unit test test_entity, and test_engine now covers name lookup across a save and reload, getEntitiesWith, and capturing and nested loads. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015cNAkNqoDWA5gFReD1AsqV --- CLAUDE.md | 3 +- README.md | 25 +++++++- ROADMAP.md | 34 ++++++++++ examples/demo.cpp | 6 +- include/pancake/core/engine.hpp | 4 +- include/pancake/core/entity.hpp | 47 +++++++++++++- include/pancake/core/listener.hpp | 2 + include/pancake/core/scene.hpp | 20 +++++- include/pancake/core/window.hpp | 4 +- src/core/console.cpp | 4 +- src/core/engine.cpp | 2 +- src/core/entity.cpp | 20 ++++++ src/core/inspector.cpp | 1 + src/core/listener.cpp | 31 +++++++-- src/core/scene.cpp | 15 +++++ src/core/window.cpp | 18 ++++-- tests/CMakeLists.txt | 1 + tests/test_engine.cpp | 36 +++++++++-- tests/test_entity.cpp | 103 ++++++++++++++++++++++++++++++ 19 files changed, 347 insertions(+), 29 deletions(-) create mode 100644 ROADMAP.md create mode 100644 tests/test_entity.cpp diff --git a/CLAUDE.md b/CLAUDE.md index 74e7357..a68dafc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`. 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`. 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()`, 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. @@ -74,4 +74,5 @@ There is no external test framework. `tests/test.hpp` provides `TEST("name") {.. - Include `` 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. diff --git a/README.md b/README.md index 7d18537..34091d5 100644 --- a/README.md +++ b/README.md @@ -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(); +scene->addEntity(player); + +Entity* found = scene->findEntity("Player"); // Or findEntities for every match. +Rigidbody* r = found->getComponent(); // nullptr if it has none. Subclasses match too. +for (Entity* e : scene->getEntitiesWith()) {} + +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. @@ -164,6 +183,8 @@ Console::registerCommand("spawn", "spawn : 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` 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. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..2572969 --- /dev/null +++ b/ROADMAP.md @@ -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()`, `getComponents()`, `hasComponent()`, `addComponent(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()`. Names show in the inspector and the `entities` command. +- [x] `load` accepts any callable (`std::function`), 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 `, `tp `, `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. diff --git a/examples/demo.cpp b/examples/demo.cpp index 70a5024..b656abf 100644 --- a/examples/demo.cpp +++ b/examples/demo.cpp @@ -49,12 +49,12 @@ class ImpactFlash : public Component, public CollisionListener { } void collision(Entity* with, CollisionManifold manifold) override { - Rigidbody* r = dynamic_cast(this->getEntity()->getComponent("Rigidbody")); + Rigidbody* r = this->getEntity()->getComponent(); if (r != nullptr && glm::length(r->getVelocity()) > 6.0f) {this->flash = 0.15f;} } void update(float dt) override { - SpriteRenderer* sprite = dynamic_cast(this->getEntity()->getComponent("SpriteRenderer")); + SpriteRenderer* sprite = this->getEntity()->getComponent(); if (sprite == nullptr) {return;} if (this->flash > 0.0f) { this->flash -= dt; @@ -79,7 +79,7 @@ class BallOutline : public Component { } void update(float dt) override { - Rigidbody* r = dynamic_cast(this->getEntity()->getComponent("Rigidbody")); + Rigidbody* r = this->getEntity()->getComponent(); if (r == nullptr) {return;} for (Collider* c : r->getColliders()) { CircleCollider* circle = dynamic_cast(c); diff --git a/include/pancake/core/engine.hpp b/include/pancake/core/engine.hpp index 4812121..42c321b 100644 --- a/include/pancake/core/engine.hpp +++ b/include/pancake/core/engine.hpp @@ -1,12 +1,14 @@ #pragma once +#include + #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 method); void load(string filename); void save(string filename); void reset(); diff --git a/include/pancake/core/entity.hpp b/include/pancake/core/entity.hpp index e84e3f3..c13cf9c 100644 --- a/include/pancake/core/entity.hpp +++ b/include/pancake/core/entity.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -14,6 +15,7 @@ namespace Pancake { private: int id; + std::string name; bool started; std::vector components; glm::vec2 position; @@ -44,6 +46,7 @@ namespace Pancake { // Getter Methods. int getId(); + std::string getName(); std::vector getComponents(); glm::vec2 getPosition(); glm::vec2 getSize(); @@ -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); @@ -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(). + template T* getComponent(); + template std::vector getComponents(); + template bool hasComponent(); + template T* addComponent(Args&&... args); + }; } -#include "pancake/core/component.hpp" \ No newline at end of file +#include "pancake/core/component.hpp" + +namespace Pancake { + + template + T* Entity::getComponent() { + for (Component* c : this->components) { + T* t = dynamic_cast(c); + if (t != nullptr && !c->isDead()) {return t;} + } + return nullptr; + } + + template + std::vector Entity::getComponents() { + std::vector result; + for (Component* c : this->components) { + T* t = dynamic_cast(c); + if (t != nullptr && !c->isDead()) {result.push_back(t);} + } + return result; + } + + template + bool Entity::hasComponent() { + return this->getComponent() != nullptr; + } + + template + T* Entity::addComponent(Args&&... args) { + T* component = new T(std::forward(args)...); + this->addComponent(component); + return component; + } + +} diff --git a/include/pancake/core/listener.hpp b/include/pancake/core/listener.hpp index c97b726..79e67e6 100644 --- a/include/pancake/core/listener.hpp +++ b/include/pancake/core/listener.hpp @@ -9,6 +9,7 @@ namespace Pancake { bool isKeyDown(int key); bool isKeyDown(); bool isKeyBeginDown(int key); + bool isKeyEndDown(int key); void endFrame(); } @@ -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(); diff --git a/include/pancake/core/scene.hpp b/include/pancake/core/scene.hpp index 5b7cb86..627f042 100644 --- a/include/pancake/core/scene.hpp +++ b/include/pancake/core/scene.hpp @@ -61,6 +61,11 @@ namespace Pancake { std::unordered_map* getComponents(); std::vector getEntities(); + // Lookups by name or component. Dead entities are skipped. + Entity* findEntity(std::string name); + std::vector findEntities(std::string name); + template std::vector getEntitiesWith(); + // Spatial queries. An entity's bounds cover its transform and its rigidbody, if it has one. std::pair getBounds(Entity* entity); std::vector getEntitiesIn(glm::vec2 min, glm::vec2 max); @@ -69,4 +74,17 @@ namespace Pancake { }; -} \ No newline at end of file +} + +namespace Pancake { + + template + std::vector Scene::getEntitiesWith() { + std::vector result; + for (Entity* e : this->entities) { + if (!e->isDead() && e->hasComponent()) {result.push_back(e);} + } + return result; + } + +} diff --git a/include/pancake/core/window.hpp b/include/pancake/core/window.hpp index 8954be5..c815bb7 100644 --- a/include/pancake/core/window.hpp +++ b/include/pancake/core/window.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "pancake/core/console.hpp" #include "pancake/core/scene.hpp" @@ -7,7 +9,7 @@ namespace Pancake { namespace Window { - void load(void(*method)(Scene* scene)); + void load(std::function method); void load(string filename); void save(string filename); void reset(); diff --git a/src/core/console.cpp b/src/core/console.cpp index 5383aa0..2952e23 100644 --- a/src/core/console.cpp +++ b/src/core/console.cpp @@ -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); } diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 0cc0102..6694afe 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -10,7 +10,7 @@ namespace Pancake { - void load(void (*method)(Scene* scene)) { + void load(std::function method) { Window::load(method); } diff --git a/src/core/entity.cpp b/src/core/entity.cpp index 312d7b9..8e8e22b 100644 --- a/src/core/entity.cpp +++ b/src/core/entity.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -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; @@ -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); @@ -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"]) { @@ -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: "); @@ -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 Entity::getComponents() { return this->components; } diff --git a/src/core/inspector.cpp b/src/core/inspector.cpp index 0ca2e7b..2dbbd5f 100644 --- a/src/core/inspector.cpp +++ b/src/core/inspector.cpp @@ -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();} } diff --git a/src/core/listener.cpp b/src/core/listener.cpp index 8a302cc..ca702cb 100644 --- a/src/core/listener.cpp +++ b/src/core/listener.cpp @@ -7,6 +7,7 @@ namespace { bool keyDown[350] = {0}; bool keyBeginDown[350] = {0}; + bool keyEndDown[350] = {0}; int keyPressCount = 0; double scrollX = 0.0; @@ -21,6 +22,7 @@ namespace { double lastWorldY = 0.0; bool mouseDown[9] = {0}; bool mouseBeginDown[9] = {0}; + bool mouseEndDown[9] = {0}; int mouseDownCount = 0; bool mouseDragging = 0; @@ -41,7 +43,7 @@ namespace Pancake { else if (action == GLFW_RELEASE) { if (keyDown[key]) {keyPressCount--;} keyDown[key] = false; - keyBeginDown[key] = false; + keyEndDown[key] = true; } } @@ -60,13 +62,23 @@ namespace Pancake { return keyBeginDown[key]; } + bool KeyListener::isKeyEndDown(int key) { + if (key < 0 || key > 349) {return false;} + return keyEndDown[key]; + } + void KeyListener::endFrame() { - // A key is only "beginning down" for the frame it was pressed in. - for (int i = 0; i < 350; i++) {keyBeginDown[i] = false;} + // A key is only "beginning down" or "ending down" for the frame it was pressed or released in. + // Both are kept until here, so a tap that presses and releases within one frame is still seen. + for (int i = 0; i < 350; i++) { + keyBeginDown[i] = false; + keyEndDown[i] = false; + } } void MouseListener::calcOrthoX() { + if (Window::getScene() == nullptr || Window::getWidth() <= 0) {return;} float currentX = ((float) x / Window::getWidth()) * 2.0f - 1.0f; Camera* camera = Window::getScene()->getCamera(); worldX = camera->getPosition().x + (currentX * 0.5f) * (camera->getProjectionSize().x / camera->getZoom()); @@ -75,6 +87,7 @@ namespace Pancake { void MouseListener::calcOrthoY() { + if (Window::getScene() == nullptr || Window::getHeight() <= 0) {return;} float currentY = ((float) y / Window::getHeight()) * 2.0f - 1.0f; Camera* camera = Window::getScene()->getCamera(); worldY = camera->getPosition().y + (currentY * -0.5f) * (camera->getProjectionSize().y / camera->getZoom()); @@ -111,7 +124,7 @@ namespace Pancake { else if (action == GLFW_RELEASE) { if (mouseDown[button]) {mouseDownCount--;} mouseDown[button] = false; - mouseBeginDown[button] = false; + mouseEndDown[button] = true; mouseDragging = false; } @@ -137,6 +150,11 @@ namespace Pancake { return mouseBeginDown[button]; } + bool MouseListener::isMouseEndDown(int button) { + if (button < 0 || button > 8) {return false;} + return mouseEndDown[button]; + } + bool MouseListener::hasMouseMoved() { return x != lastX || y != lastY; } @@ -187,7 +205,10 @@ namespace Pancake { void MouseListener::endFrame() { - for (int i = 0; i < 9; i++) {mouseBeginDown[i] = false;} + for (int i = 0; i < 9; i++) { + mouseBeginDown[i] = false; + mouseEndDown[i] = false; + } scrollX = 0.0; scrollY = 0.0; lastX = x; diff --git a/src/core/scene.cpp b/src/core/scene.cpp index f847c39..96d6714 100644 --- a/src/core/scene.cpp +++ b/src/core/scene.cpp @@ -238,6 +238,21 @@ namespace Pancake { return this->entities; } + Entity* Scene::findEntity(std::string name) { + for (Entity* e : this->entities) { + if (!e->isDead() && e->getName() == name) {return e;} + } + return nullptr; + } + + std::vector Scene::findEntities(std::string name) { + std::vector result; + for (Entity* e : this->entities) { + if (!e->isDead() && e->getName() == name) {result.push_back(e);} + } + return result; + } + std::pair Scene::computeBounds(Entity* entity) { // The entity's transform, as an axis aligned box around its rotated size. diff --git a/src/core/window.cpp b/src/core/window.cpp index 43dc55e..281070e 100644 --- a/src/core/window.cpp +++ b/src/core/window.cpp @@ -34,7 +34,7 @@ namespace Pancake { bool loadFlag = false; vector loadFilenames; - vector loadMethods; + vector> loadMethods; bool saveFlag = false; string saveFilename; @@ -119,7 +119,7 @@ namespace Pancake { namespace Window { - void load(void(*method)(Scene* scene)) { + void load(std::function method) { loadFlag = true; loadMethods.push_back(method); } @@ -265,10 +265,16 @@ namespace Pancake { } if (loadFlag) { - for (int i = 0; i < loadMethods.size(); i++) {loadMethods[i](scene);} - for (int i = 0; i < loadFilenames.size(); i++) {scene->load(loadFilenames[i]);} - loadMethods.clear(); - loadFilenames.clear(); + // Move the queues out before running them, so a load method that queues another load + // can't reallocate the vector holding the function that is running. Queued loads still run this frame. + while (!loadMethods.empty() || !loadFilenames.empty()) { + vector> methods; + vector filenames; + methods.swap(loadMethods); + filenames.swap(loadFilenames); + for (auto& method : methods) {method(scene);} + for (string& filename : filenames) {scene->load(filename);} + } loadFlag = false; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c128867..182e2dd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -32,4 +32,5 @@ pancake_test(test_spatial) pancake_test(test_physics) pancake_test(test_raycast) pancake_test(test_serialise) +pancake_test(test_entity) pancake_engine_test(test_engine) diff --git a/tests/test_engine.cpp b/tests/test_engine.cpp index 3b3da6e..1b57dc6 100644 --- a/tests/test_engine.cpp +++ b/tests/test_engine.cpp @@ -16,6 +16,7 @@ namespace { int settled = -1; // The frame the box came to rest on, which later steps are timed from. int boxId = -1; int groundId = -1; + int captured = 0; // Set by capturing load lambdas, including one queued from inside another load. void log(const char* message) { std::printf("frame %d: %s\n", frame, message); @@ -25,11 +26,12 @@ namespace { Rigidbody* rigidbodyOf(int entityId) { Entity* e = Pancake::getEntity(entityId); if (e == nullptr) {return nullptr;} - return dynamic_cast(e->getComponent("Rigidbody")); + return e->getComponent(); } - void addBody(Scene* scene, vec2 position, vec2 size, float mass, int* id) { + void addBody(Scene* scene, std::string name, vec2 position, vec2 size, float mass, int* id) { Entity* e = new Entity(position); + e->setName(name); Rigidbody* r = new Rigidbody(); BoxCollider* b = new BoxCollider(); b->setSize(size); @@ -79,6 +81,11 @@ class Driver : public Component { for (Entity* e : near) {found = found || e->getId() == boxId;} CHECK(found); + Entity* named = Pancake::getScene()->findEntity("Box"); + CHECK(named != nullptr && named->getId() == boxId); + CHECK(Pancake::getScene()->findEntity("Nobody") == nullptr); + CHECK(Pancake::getScene()->getEntitiesWith().size() == 2); + Pancake::save(SAVE_FILE); } @@ -96,6 +103,8 @@ class Driver : public Component { CHECK(box != nullptr); if (box != nullptr) {CHECK_NEAR(box->getEntity()->getPosition().y, -4.0, 0.1);} CHECK(rigidbodyOf(groundId) != nullptr); + Entity* named = Pancake::getScene()->findEntity("Ground"); + CHECK(named != nullptr && named->getId() == groundId); } if (t == 50) { @@ -131,15 +140,30 @@ class Driver : public Component { } if (t == 80) { - log("reset again, then stop"); + log("reset again with capturing load lambdas, then stop"); Pancake::reset(); - Pancake::load([](Scene* scene) { + int value = 3; + Pancake::load([value](Scene* scene) { + captured += value; Entity* d = new Entity(); d->addComponent(new Driver()); scene->addEntity(d); + // A load queued from inside a load still runs in the same frame. + std::string name = "Queued"; + Pancake::load([name](Scene* scene) { + Entity* e = new Entity(); + e->setName(name); + scene->addEntity(e); + captured += 4; + }); }); } + if (t == 81) { + CHECK(captured == 7); + CHECK(Pancake::getScene()->findEntity("Queued") != nullptr); + } + if (t == 85) {Pancake::stop();} } @@ -151,8 +175,8 @@ REGISTER(Component, Driver); void init(Scene* scene) { scene->setName("Engine Test"); scene->getPhysics()->setGravity(vec2(0.0f, -9.0f)); - addBody(scene, vec2(0.0f, -5.0f), vec2(10.0f, 1.0f), 0.0f, &groundId); - addBody(scene, vec2(0.0f, 0.0f), vec2(1.0f, 1.0f), 1.0f, &boxId); + addBody(scene, "Ground", vec2(0.0f, -5.0f), vec2(10.0f, 1.0f), 0.0f, &groundId); + addBody(scene, "Box", vec2(0.0f, 0.0f), vec2(1.0f, 1.0f), 1.0f, &boxId); Entity* d = new Entity(); d->addComponent(new Driver()); scene->addEntity(d); diff --git a/tests/test_entity.cpp b/tests/test_entity.cpp new file mode 100644 index 0000000..dc1866b --- /dev/null +++ b/tests/test_entity.cpp @@ -0,0 +1,103 @@ +// Entity and input APIs that need no window: typed component access, names, and key and mouse events. + +#include "test.hpp" +#include "pancake/pancake.hpp" + +using namespace Pancake; + +TEST("typed component access matches the class and its subclasses") { + Entity e; + Rigidbody* r = e.addComponent(); + Gravity* g = e.addComponent(); + g->setAcceleration(0.0f, -3.0f); + + CHECK(r->getEntity() == &e); + CHECK(e.getComponent() == r); + CHECK(e.getComponent() == g); + CHECK(e.getComponent() == g); + CHECK(e.hasComponent()); + CHECK(!e.hasComponent()); + CHECK(e.getComponent() == nullptr); + CHECK(e.getComponents().size() == 2); + CHECK(e.getComponents().size() == 1); + + // Dead components are not returned, even before the update loop deletes them. + g->kill(); + CHECK(!e.hasComponent()); + CHECK(e.getComponents().size() == 1); +} + +TEST("entity names round trip through json and are optional") { + Entity named(glm::vec2(1.0f, 2.0f)); + named.setName("Player"); + nlohmann::json j = named.serialise(); + CHECK(j["name"] == "Player"); + Entity* loaded = Entity::load(j); + CHECK(loaded != nullptr); + if (loaded != nullptr) { + CHECK(loaded->getName() == "Player"); + CHECK(loaded->serialise() == j); + delete loaded; + } + + // Unnamed entities don't write a name, so older save files load unchanged. + Entity unnamed; + nlohmann::json k = unnamed.serialise(); + CHECK(!k.contains("name")); + loaded = Entity::load(k); + CHECK(loaded != nullptr); + if (loaded != nullptr) { + CHECK(loaded->getName().empty()); + delete loaded; + } + + // A name that isn't a string is malformed. + k["name"] = 5; + CHECK(Entity::load(k) == nullptr); +} + +TEST("keys report begin and end down for one frame") { + KeyListener::keyCallback(nullptr, GLFW_KEY_SPACE, 0, GLFW_PRESS, 0); + CHECK(KeyListener::isKeyDown(GLFW_KEY_SPACE)); + CHECK(KeyListener::isKeyBeginDown(GLFW_KEY_SPACE)); + CHECK(!KeyListener::isKeyEndDown(GLFW_KEY_SPACE)); + KeyListener::endFrame(); + CHECK(KeyListener::isKeyDown(GLFW_KEY_SPACE)); + CHECK(!KeyListener::isKeyBeginDown(GLFW_KEY_SPACE)); + + KeyListener::keyCallback(nullptr, GLFW_KEY_SPACE, 0, GLFW_RELEASE, 0); + CHECK(!KeyListener::isKeyDown(GLFW_KEY_SPACE)); + CHECK(KeyListener::isKeyEndDown(GLFW_KEY_SPACE)); + KeyListener::endFrame(); + CHECK(!KeyListener::isKeyEndDown(GLFW_KEY_SPACE)); + CHECK(!KeyListener::isKeyDown()); +} + +TEST("a key tapped within one frame still begins and ends down") { + KeyListener::keyCallback(nullptr, GLFW_KEY_J, 0, GLFW_PRESS, 0); + KeyListener::keyCallback(nullptr, GLFW_KEY_J, 0, GLFW_RELEASE, 0); + CHECK(!KeyListener::isKeyDown(GLFW_KEY_J)); + CHECK(KeyListener::isKeyBeginDown(GLFW_KEY_J)); + CHECK(KeyListener::isKeyEndDown(GLFW_KEY_J)); + KeyListener::endFrame(); + CHECK(!KeyListener::isKeyBeginDown(GLFW_KEY_J)); + CHECK(!KeyListener::isKeyEndDown(GLFW_KEY_J)); + CHECK(!KeyListener::isKeyEndDown(-1)); +} + +TEST("mouse buttons report begin and end down for one frame") { + MouseListener::mouseButtonCallback(nullptr, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, 0); + CHECK(MouseListener::isMouseBeginDown(GLFW_MOUSE_BUTTON_LEFT)); + MouseListener::endFrame(); // Safe without a scene. + CHECK(MouseListener::isMouseDown(GLFW_MOUSE_BUTTON_LEFT)); + CHECK(!MouseListener::isMouseBeginDown(GLFW_MOUSE_BUTTON_LEFT)); + + MouseListener::mouseButtonCallback(nullptr, GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0); + CHECK(!MouseListener::isMouseDown(GLFW_MOUSE_BUTTON_LEFT)); + CHECK(MouseListener::isMouseEndDown(GLFW_MOUSE_BUTTON_LEFT)); + MouseListener::endFrame(); + CHECK(!MouseListener::isMouseEndDown(GLFW_MOUSE_BUTTON_LEFT)); + CHECK(!MouseListener::isMouseEndDown(99)); +} + +RUN_TESTS()