Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/VKSFY/keel/llms.txt

Use this file to discover all available pages before exploring further.

This guide takes you from a fresh Python environment to a running Keel game in five steps. You will build a movable sprite, add a bouncing physics ball, and turn on the built-in developer tools — all without creating any image or audio files. Every example on this page is a complete, runnable program.
1

Install Keel

Install Keel from PyPI. The distribution name is keelpy; the import name you use in code is keel.
pip install keelpy
Need 3D physics or the ImGui dev tools? See the Installation page for optional extras.
2

Scaffold a project

The Keel CLI creates the standard project layout and a hot-reloading dev loop in one command:
keel new mygame
cd mygame
This produces:
mygame/
├── main.py
├── pyproject.toml
├── README.md
├── assets/
│   └── .gitkeep
└── scenes/
    └── .gitkeep
assets/ is monitored by the asset hot-reload system. scenes/ is the conventional home for Scene.save JSON output. You can skip scaffolding and write a main.py directly — the scaffold just saves the boilerplate.
3

Minimal sprite example

Replace main.py (or create it fresh) with the following. It opens an 800×600 window and lets you drive a white square around with WASD.
texture_id=0 is always the engine’s built-in 1×1 white pixel texture. The renderer multiplies the sprite’s RGB tint into it, giving you solid-colored quads with zero asset setup. You never need an image file to get started.
main.py
import keel
from keel.renderer import setup_renderer_2d

app = keel.App(title="Hello Keel", width=800, height=600)
setup_renderer_2d(app)

@keel.component
class Player:
    speed: float = 200.0

player = app.world.spawn(
    keel.Transform2D(x=400.0, y=300.0),
    keel.Sprite(texture_id=0, width=32.0, height=32.0),
    Player(),
)

@app.system(keel.Phase.UPDATE)
def move(world, dt):
    for transform, player in world.query(keel.Transform2D, Player):
        if app.input.is_key_down(keel.KEY_D):
            transform['x'] += player['speed'] * dt
        if app.input.is_key_down(keel.KEY_A):
            transform['x'] -= player['speed'] * dt
        if app.input.is_key_down(keel.KEY_W):
            transform['y'] += player['speed'] * dt
        if app.input.is_key_down(keel.KEY_S):
            transform['y'] -= player['speed'] * dt

app.run()
Run it:
python main.py
A few things to notice:
  • @keel.component turns a plain dataclass into an ECS component. float fields map to float64 numpy columns automatically.
  • world.query(keel.Transform2D, Player) returns per-archetype numpy array views — the in-place assignments (transform['x'] += ...) write directly back to ECS storage.
  • app.input.is_key_down checks held state every simulation tick at 60 Hz. For one-shot actions (fire, jump), use is_key_pressed instead.
4

Physics example: bouncing ball

This standalone program adds 2D physics. A dynamic ball falls under gravity and bounces on a static floor. Paste it into a new file or replace main.py.
main.py
import keel
from keel.renderer import setup_renderer_2d
from keel.physics import setup_physics_2d

app = keel.App(title="Bouncing Ball", width=800, height=600)
setup_renderer_2d(app)
setup_physics_2d(app, gravity_y=-980.0)

tools = keel.dev_tools(app)
tools.debug_draw.set_visible(True)

# Static floor — never moves, generates collision events with dynamic bodies.
app.world.spawn(
    keel.Transform2D(x=400.0, y=50.0),
    keel.RigidBody2D(body_type=keel.BodyType.STATIC),
    keel.Collider2D(
        shape_type=keel.ShapeType2D.BOX,
        width=600.0,
        height=20.0,
        elasticity=0.6,
    ),
)

# Dynamic ball — affected by gravity and collision.
app.world.spawn(
    keel.Transform2D(x=400.0, y=500.0),
    keel.RigidBody2D(mass=1.0, body_type=keel.BodyType.DYNAMIC),
    keel.Collider2D(
        shape_type=keel.ShapeType2D.CIRCLE,
        radius=20.0,
        elasticity=0.75,
    ),
)

@app.system(keel.Phase.UPDATE)
def log_bounces(world, dt):
    for event in world.read_events(keel.CollisionEvent2D):
        if event.impulse > 100.0:
            print(f"bounce: impulse={event.impulse:.0f}")

app.run()
Run it:
python main.py
With tools.debug_draw.set_visible(True) you will see the collider outlines drawn over the window: green for dynamic bodies, gray for static ones. Press F3 to toggle the debug draw at runtime.Key concepts from this example:
  • setup_physics_2d must be called before keel.dev_tools for the debug draw to attach automatically.
  • keel.BodyType.DYNAMIC is required for CollisionEvent2D to fire. STATIC vs STATIC and KINEMATIC vs STATIC do not emit events — only pairings involving at least one DYNAMIC body do.
  • world.read_events(keel.CollisionEvent2D) drains the event queue for the current frame. The queue is cleared at the start of every new frame.
5

Enable developer tools

The keel.dev_tools call registers the profiler, inspector, and (if physics is set up) debug draw in one line. Add it right after your setup calls:
main.py
import keel
from keel.renderer import setup_renderer_2d

app = keel.App(title="My Game", width=800, height=600)
setup_renderer_2d(app)

tools = keel.dev_tools(app)  # F1 inspector, F2 profiler, F3 debug draw

# ... your components, entities, and systems ...

app.run()
Once the window is open:
ShortcutToolWhat it shows
F1World InspectorEvery archetype and entity with live component field values. Filter by component name.
F2Frame ProfilerPer-system 60-frame rolling average in milliseconds with a bar chart.
F3Debug DrawCollider outlines colored by body type (green = dynamic, gray = static, blue = kinematic).
tools is a DevTools instance. You can access sub-tools directly:
tools.profiler     # FrameProfiler — call tools.profiler.get_stats() for programmatic access
tools.inspector    # WorldInspector
tools.debug_draw   # DebugDraw2D, or None if physics was not set up before dev_tools
The F-key shortcuts use edge-detected polling via app.input.is_key_down, checked once per simulation tick. They do not rely on KeyEvent from the event bus, so they fire reliably even on visual frames where no simulation tick ran.
6

Run with hot reload

Instead of python main.py, use the Keel CLI for a hot-reloading dev loop:
keel run
keel run watches every .py file in the current directory and restarts the process automatically on save. This means you can edit a system, hit save, and see the result in the window within a second — no manual restart needed.Other useful CLI commands:
keel new mygame     # scaffold a new project
keel run            # hot-reload dev server (watches *.py)
keel build          # package the project for distribution

What’s Next?

ECS Concepts

Deep dive into archetypes, queries, the command buffer, events, and resource injection. Learn when to use query, query_one, get, and set.

App & Loop

How the fixed-timestep loop works, phase execution order, the FIXED_DT constant, and App properties — everything that drives your game each frame.

Systems & Phases

Phase ordering from PRE_UPDATE through POST_RENDER, after dependencies for intra-phase ordering, and resource injection into system parameters.

Events

Define custom events with @keel.event, emit and read them within a frame, and use the built-in KeyEvent, CollisionEvent2D, and more.

Build docs developers (and LLMs) love