Skip to content

About

An OpenGL 2D game engine built with GLFW in C++. The true successor of Waffle Engine.

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Repository files navigation

Pancake Game Engine

Description

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.

Example

Demo

Dependencies

The engine requires the following dependencies:

Getting Started

To use the engine in your project, follow these steps:

  1. Clone or download the project template from here.
  2. In the root directory of the project run the following command to download all submodules.
git submodule update --init --recursive
  1. Construct your own game components by creating new classes inheriting from the Component Class.
  2. Construct a Scene Initialiser Method which will be executed on game start.
  3. In the src/main file in the template, update the template to run the game.
  4. 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

Building the Engine on its Own

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.

Example

#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();
}

Features

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.

Working with Entities

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 */ });

Collisions

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);

Camera and Gamepads

// 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

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);

Prefabs and Hot Reload

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.

Debugging Tools

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) {
    // ...
});

Upgrading

  • 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.
  • 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 collision is 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::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.

About

An OpenGL 2D game engine built with GLFW in C++. The true successor of Waffle Engine.

Topics

Resources

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages