This is a lightweight 2D game engine written in C++. The engine is designed to be easy to use and highly customizable. It provides a set of basic functionalities to get you started with creating a 2D game. The engine includes a graphics renderer, audio manager, input manager and entity-component system.
The engine requires the following dependencies:
To use the engine in your project, follow these steps:
- Clone or download the project template from here.
- In the root directory of the project run the following command to download all submodules.
git submodule update --init --recursive
- Construct your own game components by creating new classes inheriting from the Component Class.
- Construct a Scene Initialiser Method which will be executed on game start.
- In the src/main file in the template, update the template to run the game.
- To build the game, in the build directory in the template run the following commands.
cmake ..
make
On Linux, GLFW and SoLoud need the X11, OpenGL and ALSA development packages:
sudo apt-get install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev libasound2-dev
The engine can also be built directly, which builds its tests and a demo sandbox.
git submodule update --init --recursive
cmake -S . -B build
cmake --build build -j
ctest --test-dir build --output-on-failure
./build/examples/pancake_demo
| Option | Default | Description |
|---|---|---|
PANCAKE_BUILD_TESTS |
On when top level | Build the unit and engine tests. |
PANCAKE_BUILD_EXAMPLES |
On when top level | Build the demo sandbox in examples/. |
PANCAKE_ASAN |
Off | Build the engine, and everything linking it, with AddressSanitizer. |
The engine tests open a real window. On a machine without a display, they run inside xvfb-run when it is installed.
#include "pancake/pancake.hpp"
using namespace Pancake;
class MousePanHandler : public Component {
public:
MousePanHandler() : Component("MousePanHandler") {
}
void update(float dt) override {
if (MouseListener::isMouseDragging()) {
if (MouseListener::getDx() != 0) {Window::getScene()->getCamera()->addPosition(vec2(-MouseListener::getWorldDx(), 0.0f));}
if (MouseListener::getDy() != 0) {Window::getScene()->getCamera()->addPosition(vec2(0.0f, -MouseListener::getWorldDy()));}
}
}
};
// This line allows the MousePanHandler component to be loaded from a save file if necessary.
REGISTER(Component, MousePanHandler);
void TestInit(Scene* scene) {
Entity* entity;
MousePanHandler* mousepanhandler;
Rigidbody* rigidbody;
BoxCollider* box;
// Set the scene name
scene->setName("Test Scene");
// Mouse Movement Handler
entity = new Entity();
mousepanhandler = new MousePanHandler();
entity->addComponent(mousepanhandler);
scene->addEntity(entity);
// Gravity applies to every body by default. It can be changed per scene, or per body with setGravityScale.
scene->getPhysics()->setGravity(vec2(0.0f, -10.0f));
// Ground. A body with no mass is static.
entity = new Entity(vec2(0.0f, -5.0f), vec2(1.0f, 1.0f), 0.0f);
rigidbody = new Rigidbody();
box = new BoxCollider();
box->setSize(vec2(10.0f, 1.0f));
rigidbody->addCollider(box);
entity->addComponent(rigidbody);
scene->addEntity(entity);
// Falling BoxCollider
entity = new Entity(vec2(-1.0f, 2.0f), vec2(1.0f, 1.0f), 0.0f);
rigidbody = new Rigidbody();
box = new BoxCollider();
box->setMass(1.0f);
box->setSize(vec2(0.5f, 1.0f));
box->setRotationOffset(0.5f);
rigidbody->addCollider(box);
rigidbody->setFriction(0.15f);
rigidbody->setRestitution(0.3f);
entity->addComponent(rigidbody);
scene->addEntity(entity);
}
int main(int argc, char* argv[]) {
Pancake::load(TestInit);
Pancake::start();
}The engine provides the following features:
- 2D graphics rendering using OpenGL.
- 2D rigidbody physics with box and circle colliders, combinable into compound bodies, solved with warm started sequential impulses so stacks stay stable.
- World gravity, and force generator components for custom forces that act only on the bodies that opt in.
- 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, 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,shake,gamepads,clone,prefab,saveprefab,hotreload,reload) and support for your own. - An entity inspector: click an entity to view and edit its components, drag it to move it, duplicate or delete it, and pause, step, save and load from its toolbar.
- Entity cloning and prefabs, and hot reloading of textures and audio when their files change.
- Debug drawing for collision shapes and other debugging purposes.
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 */ });class Coin : public Component, public CollisionListener {
public:
Coin() : Component("Coin") {}
void collisionEnter(Entity* with, CollisionManifold manifold) override {
if (with->getName() == "Player") {this->getEntity()->kill();} // Safe: listeners run after the step.
}
void collisionExit(Entity* with) override {} // Also sent when the other body is removed.
void collision(Entity* with, CollisionManifold manifold) override {} // Every contact, every step.
};
// Layers are bits. Two bodies collide only if each one's layer is in the other's mask.
const uint32_t PLAYER = 1 << 1, ENEMY = 1 << 2, PICKUP = 1 << 3;
player->setCollisionLayer(PLAYER)->setCollisionMask(ALL_LAYERS & ~PLAYER);
coin->setCollisionLayer(PICKUP)->setCollisionMask(PLAYER)->setSensor(true);
// Raycasts can skip layers too.
RaycastResult hit = scene->getPhysics()->raycast(Ray(origin, direction), 10.0f, ALL_LAYERS & ~PICKUP);// 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);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);Entity* copy = entity->clone(); // New ids, not yet in a scene.
scene->addEntity(copy);
Prefab::save(entity, "assets/prefabs/crate.json");
Entity* crate = Pancake::spawn("assets/prefabs/crate.json", vec2(3.0f, 4.0f)); // Read again whenever the file changes.
Pancake::hotReload(true); // Reload textures and audio when their files change, while the game runs.Prefabs don't include assets, so load any sprites or spritesheets they use first.
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) {
// ...
});- Physics scenes now have world gravity, defaulting to
(0, -10). If you previously added aGravityforce generator to every body to get gravity, remove it or set the world gravity to zero withscene->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::isKeyBeginDownandMouseListener::isMouseBeginDownare 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/isMouseBeginDownfor that frame, along with the newisKeyEndDown/isMouseEndDown. - Collision listeners are now called at the end of each physics step, after the solver, rather than during it. A body's velocity read in
collisionis the resolved one. Listeners can now kill entities safely. - An entity is marked dead before its components end, so its own listeners don't receive collision exits while it is being killed.
- Bodies moved by game code, such as with
setPosition, now collide from their new position in the next step rather than one step later. - Components added to an entity during its update now first update on the next frame, instead of the same frame.
Pancake::loadandWindow::loadtake astd::function<void(Scene*)>instead of a function pointer. Existing calls still compile.
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.
