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
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ 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.
- Gamepads are polled after `glfwPollEvents` each frame (`GamepadListener::poll`). Begin and end down compare against the previous poll, so they need no `endFrame`, and `setState` lets tests fake a pad.
- 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.

Expand All @@ -43,6 +44,7 @@ There is no external test framework. `tests/test.hpp` provides `TEST("name") {..
- `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.
- `Camera::shake` offsets the view only (`getView`). `getPosition` stays the logical position, which `CameraFollow` and mouse world coordinates use. `CameraFollow` (`components/camerafollow.hpp`) is serialised; its logic is in `apply(camera, target, dt)` so it can be tested without a window.
- `TransformableComponent` adds a position/size/rotation offset relative to the entity.

**Serialisation / reflection** (`core/factory.hpp`). `REGISTER(Base, Derived)` registers a default constructor under the class name. `FACTORY(Component).create(type)` is used by `Entity::load`, and `FACTORY(Collider)` by `Rigidbody::load`. Every serialisable component needs a default constructor, a `serialise()`/`load(json)` pair that chains to its base class, and a `REGISTER` line. New fields added to existing components should be optional on load, so older save files keep loading (see `gravityScale`). `Scene::serialise` also writes gravity and the asset pools (fonts, spritesheets, sprites, audio).
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,13 @@ The engine provides the following features:
- Collision listeners with enter and exit events, sensors, collision layers and masks, 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, with pressed, held and released events.
- Input management for keyboard, mouse and gamepads, with pressed, held and released events.
- A camera that can follow an entity, with smoothing, a dead zone and bounds, and shake.
- 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.
- 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 in-game console with built-in commands (`help`, `save`, `load`, `reset`, `debug`, `gravity`, `entities`, `inspector`, `pause`, `step`, `timescale`, `spawn`, `kill`, `tp`, `fps`, `vsync`, `fullscreen`, `shake`, `gamepads`) 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 Down Expand Up @@ -189,6 +190,22 @@ coin->setCollisionLayer(PICKUP)->setCollisionMask(PLAYER)->setSensor(true);
RaycastResult hit = scene->getPhysics()->raycast(Ray(origin, direction), 10.0f, ALL_LAYERS & ~PICKUP);
```

## Camera and Gamepads
```c++
// Follow the player. Smoothing is in seconds, the dead zone and bounds in world units. It is saved with the scene.
player->addComponent<CameraFollow>()
->setSmoothing(0.2f)
->setDeadZone(vec2(1.0f, 0.5f))
->setBounds(vec2(-50.0f, -10.0f), vec2(50.0f, 30.0f));

scene->getCamera()->shake(0.3f, 0.4f); // Up to 0.3 units, fading over 0.4 seconds. Only the view moves.

// Gamepad 0 by default. Sticks have the dead zone removed and up is positive y. Triggers go from 0 to 1.
vec2 move = GamepadListener::getLeftStick();
if (GamepadListener::isButtonBeginDown(GLFW_GAMEPAD_BUTTON_A)) {}
float throttle = GamepadListener::getAxis(GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER);
```

## Time, Timers and Tweens
```c++
Pancake::timeScale(0.25f); // Slow motion. Components get the scaled delta time.
Expand Down
6 changes: 3 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ Tick an item off in the same branch that implements it.

## 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.
- [x] `CameraFollow` component: target entity, smoothing, dead zone, bounds clamp, offset.
- [x] Camera shake: `shake(intensity, duration)`, and a `shake` console command.
- [x] Gamepad input through `glfwGetGamepadState`, with begin, down and end semantics, dead zones, and a `gamepads` console command.

## Batch 5: editor tooling

Expand Down
9 changes: 9 additions & 0 deletions examples/demo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// N step one physics frame (pauses)
// F toggle the frame rate overlay
// R reset the sandbox
// Gamepad left stick pans, A drops a box
// Escape quit
//
// Run with --frames N to quit automatically after N frames.
Expand Down Expand Up @@ -55,6 +56,7 @@ class ImpactFlash : public Component, public CollisionListener {

void collisionEnter(Entity* with, CollisionManifold manifold) override {
if (this->speed > 6.0f) {this->flash = 0.15f;}
if (this->speed > 12.0f) {Pancake::getScene()->getCamera()->shake(0.15f, 0.25f);}
}

void update(float dt) override {
Expand Down Expand Up @@ -171,6 +173,13 @@ class Sandbox : public Component {
}
if (KeyListener::isKeyBeginDown(GLFW_KEY_GRAVE_ACCENT)) {Pancake::console();}

// Gamepad: the left stick pans, even while paused, and A drops a box at the centre of the view.
Camera* view = Pancake::getScene()->getCamera();
view->addPosition(GamepadListener::getLeftStick() * 10.0f * Pancake::getUnscaledDeltaTime());
if (GamepadListener::isButtonBeginDown(GLFW_GAMEPAD_BUTTON_A)) {
spawnBox(Pancake::getScene(), view->getPosition(), vec2(0.8f, 0.8f), 1.0f, randomColour());
}

if (ImGui::GetIO().WantCaptureMouse) {return;}
Camera* camera = Pancake::getScene()->getCamera();
vec2 mouse = vec2(MouseListener::getWorldX(), MouseListener::getWorldY());
Expand Down
59 changes: 59 additions & 0 deletions include/pancake/components/camerafollow.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#pragma once

#include <glm/glm.hpp>

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

namespace Pancake {

// Moves the scene camera to follow an entity: its own entity by default, or another by id.
// Pausing the scene stops smoothed movement, as it advances with the scene's delta time.
class CameraFollow : public Component {

private:

int target; // The entity to follow, or -1 for this component's entity.
glm::vec2 offset; // Added to the target's position.
float smoothing; // Seconds for the camera to close most of the gap (about 63%). Zero snaps.
glm::vec2 deadZone; // Half size of a box around the camera centre the target can move in freely.
bool bounded;
glm::vec2 boundsMin; // The view is kept inside these bounds, or centred on them if it is larger.
glm::vec2 boundsMax;
bool snapNext; // Ignore smoothing on the next update, so the camera starts on the target.

public:

CameraFollow();

void start() override;
void update(float dt) override;
json serialise() override;
bool load(json j) override;
void imgui() override;

// Move the camera towards the target. Called by update with the scene camera and the resolved target.
void apply(Camera* camera, Entity* target, float dt);

int getTarget();
glm::vec2 getOffset();
float getSmoothing();
glm::vec2 getDeadZone();
bool hasBounds();
glm::vec2 getBoundsMin();
glm::vec2 getBoundsMax();

CameraFollow* setTarget(Entity* entity); // nullptr follows this component's entity.
CameraFollow* setTarget(int entityId);
CameraFollow* setOffset(glm::vec2 offset);
CameraFollow* setSmoothing(float seconds);
CameraFollow* setDeadZone(glm::vec2 halfSize);
CameraFollow* setBounds(glm::vec2 min, glm::vec2 max);
CameraFollow* clearBounds();
CameraFollow* snap(); // Jump straight to the target on the next update.

};

REGISTER(Component, CameraFollow);

}
12 changes: 12 additions & 0 deletions include/pancake/core/camera.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <random>
#include <glm/glm.hpp>
#include <nlohmann/json.hpp>

Expand Down Expand Up @@ -31,13 +32,24 @@ namespace Pancake {
float timeHalf;
float distance;
float maxVelocity;

// Shake moves the view, not the camera's position, so following and picking aren't disturbed.
float shakeIntensity;
float shakeDuration;
float shakeTime;
vec2 shakeOffset;
std::minstd_rand shakeRandom;

public:

Camera(vec2 position, vec2 projectionSize, float zoom);
void adjustProjection();
void update(float dt);
void move(vec2 to, float t);
void shake(float intensity, float duration); // Up to intensity world units, fading out over duration seconds.
void stopShake();
bool isShaking();
vec2 getShakeOffset();
json serialise();
void load(json j);

Expand Down
27 changes: 27 additions & 0 deletions include/pancake/core/listener.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once

#include <string>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>

namespace Pancake {

Expand Down Expand Up @@ -38,4 +40,29 @@ namespace Pancake {
void endFrame();
}

// Gamepads with a standard mapping, polled once a frame. Buttons and axes use GLFW's names, such as
// GLFW_GAMEPAD_BUTTON_A and GLFW_GAMEPAD_AXIS_LEFT_X. Gamepads are numbered 0 to GLFW_JOYSTICK_LAST.
namespace GamepadListener {

void poll();

// Replace a gamepad's state, as poll does. Begin and end down compare it with the previous state.
void setState(int gamepad, bool connected, const GLFWgamepadstate& state, std::string name = "");

bool isConnected(int gamepad = 0);
std::string getName(int gamepad = 0);
bool isButtonDown(int button, int gamepad = 0);
bool isButtonBeginDown(int button, int gamepad = 0);
bool isButtonEndDown(int button, int gamepad = 0);

// Stick axes are in [-1, 1] with the dead zone removed. Triggers are in [0, 1], at rest at 0.
float getAxis(int axis, int gamepad = 0);
glm::vec2 getLeftStick(int gamepad = 0); // Up is positive y, unlike GLFW's raw axes.
glm::vec2 getRightStick(int gamepad = 0);

void setDeadZone(float deadZone);
float getDeadZone();

}

}
1 change: 1 addition & 0 deletions include/pancake/pancake.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "pancake/audio/audiowave.hpp"

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

Expand Down
Loading
Loading