Keel’s 2D physics layer is a thin, ECS-first bridge over pymunk (Chipmunk2D). You describe bodies and shapes as plain ECS components, and the bridge keeps pymunk in sync automatically: every frame it reads your component data into pymunk, advances the simulation, and writes the results back intoDocumentation 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.
Transform2D. No pymunk objects ever appear in game code — only Keel components and events do.
Setting up physics
Callsetup_physics_2d once, immediately after creating your App. It registers a physics_2d_system at Phase.POST_UPDATE and returns the Physics2D bridge object in case you need direct control later.
setup_physics_2d is idempotent — a second call returns the existing bridge without registering a duplicate system. The function signature is:
The default gravity of
-980.0 is in pixels per second squared, matching a screen where y increases upward. Adjust to taste — some games use -200.0 for floaty feel.Components
Every physics entity needs exactly three components:Transform2D for position, RigidBody2D for body parameters, and Collider2D for shape parameters. Missing any one of them prints a one-time RuntimeWarning and skips the entity.
RigidBody2D
body_type with the keel.BodyType enum for clarity:
moment defaults to 0, which tells the bridge to compute an appropriate moment of inertia automatically from the shape. Supply a positive value to override it.
Collider2D
shape_type selects which size fields the bridge reads:
CIRCLE
Uses
radius. Good for balls, projectiles, enemies.BOX
Uses
width and height. Good for platforms, paddles, walls.SEGMENT
A thin capsule along the X axis.
width sets total length.Sensors
Settingsensor=True makes the shape a trigger volume: dynamic bodies pass through it, but the contact still fires a CollisionEvent2D on the frame the overlap begins. Use sensors for pickups, checkpoints, or damage zones where you want detection without physical response.
Collision filtering
category_bits and mask_bits work like pymunk’s ShapeFilter. A shape collides only with other shapes whose category_bits match its own mask_bits. The defaults (category=1, mask=0xFFFF) mean “collide with everything.” Divide entities into groups by assigning distinct power-of-two bits:
Spawning physics entities
Spawn all three components together. Structural changes are deferred, so callworld.flush() after spawning if you need the entity to participate in physics before the end of the frame:
Collision events
CollisionEvent2D is emitted by the bridge every frame a contact is active.
Phase.POST_UPDATE:
impulse (magnitude of the total impulse vector). Sensor contacts set impulse=0.0, normal_x=0.0, and normal_y=0.0 because pymunk skips the solver for them.
Identifying entities in collision handlers
CollisionEvent2D carries raw entity IDs. Track them at spawn time so your collision system can dispatch in O(1):
Physics material presets
PhysicsMaterial2D bundles a friction and elasticity pair into a reusable preset. Apply one to an entity immediately after spawning and flushing — the bridge uses the material’s values instead of the Collider2D component fields when it builds the pymunk shape:
| Preset | Friction | Elasticity |
|---|---|---|
PhysicsMaterial2D.DEFAULT | 0.50 | 0.30 |
PhysicsMaterial2D.BOUNCY | 0.30 | 0.90 |
PhysicsMaterial2D.ICE | 0.05 | 0.10 |
PhysicsMaterial2D.RUBBER | 0.90 | 0.80 |
PhysicsMaterial2D.WOOD | 0.60 | 0.20 |
PhysicsMaterial2D.METAL | 0.30 | 0.10 |
Direct physics controls
ThePhysics2D bridge object (returned by setup_physics_2d) exposes lower-level helpers for gameplay code that needs to drive bodies directly:
Raycasting
Physics2D.raycast_2d performs a pymunk segment query and returns hits sorted nearest-first by alpha (0.0 = start point, 1.0 = end point):
dict with keys: entity_id, point, normal, and alpha.
ECS tick order
The bridge runs entirely inside thePhase.POST_UPDATE system in this fixed order:
sync_to_physics
Reads
Transform2D, RigidBody2D, and Collider2D from every matching archetype and creates or updates the corresponding pymunk bodies and shapes. Entities removed from the ECS have their pymunk objects removed here too.step
Advances the pymunk
Space by dt seconds. The collision handler appends raw tuples to an internal buffer during this step.sync_from_physics
Writes pymunk body state (position, rotation, velocity, angular velocity) back into
Transform2D and RigidBody2D in place. Static bodies are skipped — they never move.ECS data is the source of truth going in. Physics owns the result on the way out. Never read a body’s position directly from pymunk — always query
Transform2D after Phase.POST_UPDATE.Kinematic body warning
When a second kinematic body joins the simulation alongside any existing kinematic or static body, Keel prints a one-timeUserWarning:
Full example: bouncing ball
A complete program — a ball that falls under gravity and bounces on a static floor. Save asmain.py and run with python main.py. Press F3 to toggle the physics debug-draw overlay.
Troubleshooting
CollisionEvent2D never fires
CollisionEvent2D never fires
The most common cause is body type.
CollisionEvent2D only fires when at least one body is DYNAMIC. Check the Body Types page for the full matrix. If both bodies are KINEMATIC or STATIC, change at least one to DYNAMIC.Ball passes through walls at high speed
Ball passes through walls at high speed
Tunneling occurs when the ball moves more than its own diameter in a single tick (~1500 px/s at 60 Hz). Workarounds in order of effort: reduce speed, reduce the simulation fixed timestep, or manage ball position manually each frame and push it into pymunk via
phys.set_position / phys.set_velocity (keeping CollisionEvent2D for detection only).RuntimeWarning: entity N has Collider2D without RigidBody2D
RuntimeWarning: entity N has Collider2D without RigidBody2D
Every physics entity needs all three components:
Transform2D, RigidBody2D, and Collider2D. A Collider2D without RigidBody2D (or vice versa) is silently skipped, but the bridge emits a one-time RuntimeWarning to flag the misconfiguration.