gale.physics
gale.physics is 2D physics for gale games, backed by Box2D — but
you never import or touch Box2D directly: everything here works in
plain pixel units and gale’s own vocabulary (World, Body,
BodyType, shapes, joints), plus a lightweight scene graph
(Node) for organizing physics entities. See examples/leap (a
platformer using all three body types) and examples/hillclimb (a
vehicle demo built on joints) for full games built on it.
Creating a world and bodies
from gale.physics.world import World
from gale.physics.shapes import BoxShape, CircleShape
world = World(gravity=(0, 900)) # positive y points down the screen
ground = world.create_static_body(200, 280, BoxShape(400, 20))
ball = world.create_dynamic_body(200, 50, CircleShape(radius=10, restitution=0.6))
# In your state:
def update(self, dt: float) -> None:
self.world.update(dt)
def render(self, surface) -> None:
pygame.draw.circle(surface, "white", ball.position, 10)
There are three body types, created through create_static_body/
create_dynamic_body/create_kinematic_body:
Body type |
Use it for |
|---|---|
Static |
Fixed level geometry — the ground, walls, platforms that never move. |
Dynamic |
Anything fully simulated — affected by gravity, forces, and collisions (a player, a projectile). |
Kinematic |
Scripted motion — moves exactly per its own set velocity, unaffected by gravity/forces, but pushes dynamic bodies resting on it (a moving platform, an elevator). |
BoxShape/CircleShape/PolygonShape (gale.physics.shapes)
describe a fixture — friction, density, restitution, whether it’s a
sensor — and can be passed to create_*_body(shape=...) immediately
or attached later with body.add_box/add_circle/add_polygon.
Stepping the simulation: update vs. fixed_update
Box2D’s solver wants a fixed timestep for stable results, but your
game loop’s dt is real and variable. World follows the same
split Unity uses: call update(dt) once a frame, same as every
other gale subsystem; it accumulates that time and calls
fixed_update() (a real Box2D step, at fixed_timestep, default
1/60) as many times as needed to consume it. fixed_update() is
itself a normal public method, useful in tests (or the rare game) that
want to drive the simulation one fixed step at a time directly:
world = World(fixed_timestep=1 / 60)
world.update(dt) # what your state calls every frame
world.fixed_update() # what update() calls internally, 0+ times
Reading and driving a body
body.position # pygame.Vector2, in pixels
body.angle # radians
body.velocity # pygame.Vector2, pixels/second
body.set_velocity(vx, vy)
body.apply_force(fx, fy) # continuous, call every frame
body.apply_impulse(ix, iy) # instantaneous, e.g. a jump
body.apply_torque(t)
Collision: callbacks vs. touching_bodies
For discrete events (a pickup, a goal, a hazard), register callbacks:
def on_begin(body_a, body_b) -> None:
...
world.on_collision_begin(on_begin)
world.on_collision_end(lambda body_a, body_b: ...)
For a per-frame check (e.g. “is the player standing on something” to decide if it can jump), poll instead — cheaper, no bookkeeping:
is_grounded = any(b.user_data == "ground" for b in player.touching_bodies)
Joints
Two joint types are wired up, exactly what gale’s own examples need:
RevoluteJoint (a motorized pin/pivot) and WheelJoint (a
motorized wheel with a spring/damper suspension) — see
examples/hillclimb for a full car built from two of the latter.
joint = world.create_wheel_joint(
chassis, wheel, wheel.position, axis=(0, 1),
frequencyHz=4, dampingRatio=0.7,
)
joint.enable_motor = True
joint.motor_speed = 20 # positive drives toward +x
joint.max_motor_torque = 2000
Scene graph
Node organizes physics entities: every node has an absolute
position; if it owns a Body, that position (and angle) are pulled
from the body every update() (physics is the source of truth for
anything simulated); children exist for grouping and for attaching a
decoration to a body that should follow it around:
from gale.physics.node import Node
chassis_node = Node(body=chassis_body)
exhaust_node = Node(x=-20, y=0) # a decoration, no body of its own
chassis_node.add_child(exhaust_node)
chassis_node.update(dt)
exhaust_node.world_position # chassis_body.position + (-20, 0)
Debug rendering
world.render_debug(surface) # draws every fixture's outline, no assets needed
Handy while building a level; both examples/leap and
examples/hillclimb draw their own shapes instead for a nicer look,
but render_debug is there whenever you just need to see the raw
physics.