A 2D game engine
written in C++

A free and open-source engine for building 2D games — with a C++ core and gameplay scripted in Lua.

Windows Linux macOS

Rendering

A modern 2D renderer powered by SDL3.

GPU-accelerated 2D pipeline

A modern 2D pipeline handles sprite sorting, drawing, and compositing on the GPU — then scales the result to any screen.

Materials & HLSL shaders

Per-material custom sprite shaders, authored in HLSL and cross-compiled at runtime — no offline build step.

Sprite animation

Define animation clips and play them per entity with a simple sprite animator.

Idle sprite animation

Physics

Real 2D physics powered by Box2D.

Rigid-body simulation

Rigid bodies and colliders driven by a fixed-timestep loop with configurable sub-steps.

Raycasting & queries

Cast rays into the world for line-of-sight, ground checks, and gameplay queries.

Collision & trigger events

Components receive on_collision_enter/exit and on_trigger_enter/exit callbacks automatically.

Entity & Components

A straightforward composition model: each entity owns a list of components.

Lifecycle hooks

Components hook into init, enter_play, exit_play, tick, physics_tick, and more.

Composition over inheritance

Build behaviour by attaching components — mix and match without deep class trees.

C++ or Lua components

Write components in C++ for the engine core or in Lua for gameplay — both plug into the same lifecycle hooks.

Lua Scripting

All gameplay code is written in Lua, with hot reload while the game runs.

Component inheritance

Share behaviour across components with single inheritance.

Data-driven prefabs

Compose entities from component blocks as declarative data — no boilerplate constructor code.

Hot reload

Edit scripts, assets, and config and see changes without restarting the engine.

A component with lifecycle hooks
DefineComponent.Player = {
    speed = 7.0,
}
---@class Player : LuaComponent
local Player = Player

function Player:init() end
function Player:enter_play() end
function Player:exit_play() end

function Player:tick(delta_time)
    -- per-frame game logic
end

function Player:physics_tick(fixed_delta_time)
    -- fixed-timestep movement
end
A data-driven prefab
DefineEntity.Player = {
    ticking = true,
    input = {},
    character_body = {
        collision_layer = Collision.Kinematic,
        collision_mask = Collision.Static | Collision.Dynamic,
        solver_ignore_mask = Collision.Trigger,
        capsule = Capsule(Vector2.zero(), Vector2.zero(), 1.2),
    },
    sprite = {
        texture = Textures.PlayerIdle01,
        material = Materials.WhiteOutline,
        z_index = 1,
    },
    -- attach the Player component
    lua_components = { Components.Player },
}
Spawn it at runtime
-- spawn the prefab at a world position
EntitySpawner.spawn_entity(Entities.Player, Vector2(0.0, 0.0))

UI

Build interfaces with RmlUi — HTML-like RML markup styled by RCSS, driven from Lua.

RML + RCSS

Author layouts in familiar HTML/CSS-like markup and stylesheets, hot-reloaded while the game runs.

RCSS transitions

Animate properties with CSS-style transition rules — no per-frame tweening code.

MVVM data binding

Bind {{ fields }} to a view model; update it from Lua and the view refreshes automatically — no manual DOM updates.

assets/ui/healthbar.rml
<rml>
<head>
    <link type="text/rcss" href="ui/healthbar.rcss"/>
</head>
<body data-model="player_hud">
    <div id="healthbar">
        <div id="health_fill" data-style-width="fill_width"></div>
        <span id="health_text">{{ health }} / {{ max_health }}</span>
    </div>
</body>
</rml>
assets/ui/healthbar.rcss
#healthbar {
    position: absolute;
    left: 40px;
    top: 40px;
    width: 320px;
    height: 36px;
    background-color: #401010;
    border: 2px #000000;
}

#health_fill {
    width: 100%;
    height: 100%;
    background-color: #d03030;
    transition: width 0.2s linear-in-out;
}
100 / 100
Drive it from Lua
function Player:enter_play()
    -- create the data model, then load the RML that binds to it
    self.hud_model = UI.create_model("player_hud", {
        health = self.health,
        max_health = self.max_health,
        fill_width = "100%",
    })
    self.hud_doc = UI.load_document("ui/healthbar.rml")
    UI.show_document(self.hud_doc)
end

function Player:set_health(value)
    self.health = math.max(0, math.min(self.max_health, value))
    local percent = math.floor(self.health / self.max_health * 100)

    -- update the model; the UI refreshes automatically
    UI.set(self.hud_model, "health", math.floor(self.health))
    UI.set(self.hud_model, "fill_width", string.format("%d%%", percent))
end

Input Mapping

Bind keys to named actions and axes in JSON — no rebuild to remap controls.

Named actions

Map multiple keys to a single action so gameplay code never references raw key codes.

Smoothed axes

Axis mappings blend opposing keys with configurable acceleration and deceleration.

config/input_config.json
{
  "action_mappings": {
    "jump":     ["Space", "W"],
    "interact": ["E"],
    "cancel":   ["Escape"]
  },
  "axis_mappings": {
    "horizontal": {
      "acceleration": 10,
      "deceleration": 10,
      "positive": ["Right", "D"],
      "negative": ["Left", "A"]
    }
  }
}
Bind it in Lua
function Player:enter_play()
    local input = self.entity:get_input()

    -- react to a smoothed axis
    input:bind_axis("horizontal", function(axis)
        self.movement.x = axis
    end)

    -- react to a named action
    input:bind_action("jump", InputEventType.Pressed, function()
        -- ...
    end)
end

Dev Tooling

A debug-first workflow baked into the engine, powered by Dear ImGui.

Dev console

A drop-down developer console, toggled with the backtick key, for commands and live tweaking.

Typed CVars

Bool / Int / Float / String console variables with Archive, ReadOnly, and Cheat flags.

Debug drawing

Draw world-space lines, circles, and text that stay crisp across DPI and resolution.

The drop-down ImGui developer console