# src/box3d/world.py
from ._box2d import lib, ffi
from .body import BodyBuilder, Body
from .joint import (
MouseJoint,
WeldJoint,
RevoluteJoint,
PrismaticJoint,
WheelJoint,
DistanceJoint,
MotorJoint,
)
from .math import Vec2, VectorLike, AABB, Transform, to_vec2
from .debug_draw import DebugDraw
from .collision_filter import CollisionFilter
from .shape_def import CircleDef
from .shape import Shape
from dataclasses import dataclass
from dataclasses import dataclass
from .math import Vec2
from ._box2d import ffi, lib
@dataclass
class RayCastResult:
"""Result from a ray cast query."""
shape: "Shape" # Shape that was hit
point: Vec2 # Hit point in world coords
normal: Vec2 # Surface normal at hit point
fraction: float # Fraction along ray where hit occurred (0-1)
@dataclass
class SensorEvent:
"""Event generated by a sensor contact."""
sensor: "Shape" # Sensor shape generating the event
visitor: "Shape" # Other shape involved in the event
begin: bool # True if the sensor is starting to overlap the other shape False if ending
@dataclass
class SensorEvents:
"""Collection of sensor events generated during a time step."""
begin: list[SensorEvent]
end: list[SensorEvent]
def make_ray_cast_callback(results: list):
"""Create a ray cast callback function."""
@ffi.callback("float(b2ShapeId, b2Vec2, b2Vec2, float, void*)")
def ray_cast_callback(shape_id, point, normal, fraction, _):
shape = ffi.from_handle(lib.b2Shape_GetUserData(shape_id))
result = RayCastResult(
shape=shape,
point=Vec2(point.x, point.y),
normal=Vec2(normal.x, normal.y),
fraction=fraction,
)
results.append(result)
return 1.0 # Continue querying
return ray_cast_callback
def make_overlap_callback(results: list, max_results: int = None):
"""
Create an overlap callback that appends detected shapes to the results
list and stops querying when max_results is reached.
Args:
results: A list to collect overlapping shapes.
max_results: Optional maximum number of shapes to collect. Once reached,
the callback returns False to stop further processing.
Returns:
A Box2D-compatible callback function.
"""
@ffi.callback("bool(b2ShapeId, void*)")
def overlap_callback(shape_id, _):
shape = ffi.from_handle(lib.b2Shape_GetUserData(shape_id))
results.append(shape)
if max_results is not None and len(results) >= max_results:
return False # Stop querying further shapes
return True # Continue querying
return overlap_callback
[docs]
class World:
"""2D physics world containing bodies, joints, and simulation parameters.
Manages Box2D world state and provides body creation through a builder pattern.
Wraps Box2D's b2World functionality with Pythonic interfaces.
Example:
>>> world = World(gravity=(0, -9.81))
>>> body = world.new_body().dynamic().position(0, 5).box(1, 1).build()
>>> for _ in range(60):
... world.step(1/60, 4)
"""
[docs]
def __init__(self, gravity: VectorLike = (0, -10), threads: int = 1):
"""Initialize physics world with specified gravity vector.
Args:
gravity: Initial gravitational acceleration (x,y) in m/s².
Example:
>>> world = World(gravity=(0, -9.8))
>>> world.gravity
Vec2(0.0, -9.8)
"""
world_def = lib.b2DefaultWorldDef()
world_def.gravity.x, world_def.gravity.y = gravity
self._threads = threads
if threads > 1:
# Use the C-level task scheduler
lib.setup_threadpool(threads)
self._use_c_scheduler = True
world_def.workerCount = threads
world_def.enqueueTask = lib.c_enqueue_tasks
world_def.finishTask = lib.c_finish_tasks
self._user_task_ctx = ffi.new_handle(self)
world_def.userTaskContext = self._user_task_ctx
else:
self._use_c_scheduler = False
self._world_id = lib.b2CreateWorld(ffi.addressof(world_def))
# Store default simulation parameters
self._enable_sleep = world_def.enableSleep
self._enable_continuous = world_def.enableContinuous
self._restitution_threshold = world_def.restitutionThreshold
self._hit_event_threshold = world_def.hitEventThreshold
self._contact_hertz = world_def.contactHertz
self._contact_damping_ratio = world_def.contactDampingRatio
self._contact_push_velocity = world_def.contactPushMaxSpeed
self._bodies = {}
@property
def gravity(self):
"""World gravity vector (m/s²).
Example:
>>> world = World()
>>> world.gravity = (0, -9.81)
>>> world.gravity
Vec2(0.0, -9.81)
"""
g = lib.b2World_GetGravity(self._world_id)
return Vec2(g.x, g.y)
@gravity.setter
def gravity(self, value: VectorLike):
"""Set world gravity vector"""
value = to_vec2(value)
lib.b2World_SetGravity(self._world_id, value.b2Vec2[0])
[docs]
def step(self, time_step, substep_count=4):
"""Advance simulation by time step.
Args:
time_step: Time to simulate (seconds)
substep_count: Number of solver iterations (default 4)
Higher values improve stability at cost of performance
Example:
>>> world = World()
>>> world.step(1/60) # Default 4 substeps
>>> world.step(0.016, 6) # Custom substep count
"""
lib.b2World_Step(self._world_id, time_step, substep_count)
[docs]
def new_body(self):
"""Create BodyBuilder for constructing bodies. Entry point for body creation.
Returns:
BodyBuilder: Fluent interface for body configuration
Example:
>>> world = World()
>>> builder = world.new_body()
>>> body = builder.dynamic().position(2,3).circle(1).build()
"""
return BodyBuilder(self)
[docs]
def add_mouse_joint(
self,
body,
target,
max_force=1000.0,
damping_ratio=0.7,
hertz=5,
) -> MouseJoint:
"""Create a mouse joint for interactive dragging between bodies
Args:
body: Body to drag
target: Initial target position in world coordinates
max_force: Maximum constraint force (default 1000.0)
damping_ratio: Response damping ratio (0-1, default 0.7)
herts: Spring stiffness in Hz (higher = stiffer movement)
Example:
>>> world = World()
>>> box = world.new_body().dynamic().position(0,5).build()
>>> mouse_joint = world.add_mouse_joint(box, (0,5))
"""
if body._body_id not in self._bodies:
raise ValueError("Bodies must belong to this world")
return MouseJoint(self, body, target, max_force, damping_ratio, hertz)
[docs]
def add_weld_joint(
self,
body_a,
body_b,
local_anchor_a=None,
local_anchor_b=None,
anchor=None,
collide_connected=False,
linear_hertz=None,
linear_damping_ratio=None,
angular_hertz=None,
angular_damping_ratio=None,
reference_angle=None,
) -> WeldJoint:
"""Create a weld joint that rigidly connects two bodies.
Args:
body_a: The first body to connect (must belong to this world)
body_b: The second body to connect (must belong to this world)
local_anchor_a (tuple): Local coordinates (x, y) on body_a where the joint attaches.
local_anchor_b (tuple): Local coordinates (x, y) on body_b where the joint attaches.
anchor: World coordinates (x, y) where the joint attaches. (Used without local anchors)
collide_connected (bool, optional): If True, the connected bodies will collide.
linear_hertz (float, optional): Linear spring stiffness in Hertz (0 means rigid).
linear_damping_ratio (float, optional): Linear damping ratio (non-dimensional).
angular_hertz (float, optional): Angular spring stiffness in Hertz (0 means rigid).
angular_damping_ratio (float, optional): Angular damping ratio (non-dimensional).
reference_angle (float, optional): The body_b angle minus body_a angle in the reference state (radians)
Returns:
WeldJoint: The created weld joint connecting the two bodies.
Example:
>>> world = World()
>>> body_a = world.new_body().dynamic().position(0, 0).build()
>>> body_b = world.new_body().dynamic().position(1, 1).build()
>>> weld_joint = world.add_weld_joint(body_a, body_b, anchor=(0.5, 0.5))
"""
if (body_a._body_id not in self._bodies) or (
body_b._body_id not in self._bodies
):
raise ValueError("Both bodies must belong to this world")
if anchor is not None:
if local_anchor_a is not None or local_anchor_b is not None:
raise ValueError(
"You can't set local anchors when setting a world anchor"
)
local_anchor_a = body_a.transform.inverse(anchor)
local_anchor_b = body_b.transform.inverse(anchor)
return WeldJoint(
self,
body_a,
body_b,
local_anchor_a,
local_anchor_b,
collide_connected,
linear_hertz,
linear_damping_ratio,
angular_hertz,
angular_damping_ratio,
reference_angle,
)
[docs]
def add_revolute_joint(
self,
body_a,
body_b,
local_anchor_a=None,
local_anchor_b=None,
anchor=None,
collide_connected=False,
lower_angle=None,
upper_angle=None,
enable_limit=None,
motor_speed=None,
max_motor_torque=None,
enable_motor=None,
reference_angle=None,
) -> RevoluteJoint:
"""
Create a revolute joint connecting two bodies, allowing relative rotation about an anchor point.
Args:
body_a: The first body to connect (must belong to this world).
body_b: The second body to connect (must belong to this world).
local_anchor_a (tuple): Local coordinates (x, y) on body_a for the joint.
local_anchor_b (tuple): Local coordinates (x, y) on body_b for the joint.
collide_connected (bool, optional): Whether the connected bodies should collide with each other.
Defaults to False.
lower_angle (float, optional): Lower joint limit in radians.
upper_angle (float, optional): Upper joint limit in radians.
enable_limit (bool, optional): Enable joint limits if True.
motor_speed (float, optional): Desired motor speed in radians per second.
max_motor_torque (float, optional): Maximum motor torque in newton-meters.
enable_motor (bool, optional): Enable the joint motor if True.
reference_angle (float, optional): Reference angle between the two bodies.
Returns:
RevoluteJoint: The created revolute joint connecting body_a and body_b.
Example:
>>> world = World()
>>> body_a = world.new_body().dynamic().position(0, 0).build()
>>> body_b = world.new_body().dynamic().position(1, 0).build()
>>> revolute_joint = world.add_revolute_joint(
... body_a, body_b, (0, 0), (0, 0),
... collide_connected=False,
... enable_limit=True,
... lower_angle=-0.5,
... upper_angle=0.5
... )
"""
if (body_a._body_id not in self._bodies) or (
body_b._body_id not in self._bodies
):
raise ValueError("Both bodies must belong to this world")
if anchor is not None:
if local_anchor_a is not None or local_anchor_b is not None:
raise ValueError(
"You can't set local anchors when setting a world anchor"
)
local_anchor_a = body_a.transform.inverse(anchor)
local_anchor_b = body_b.transform.inverse(anchor)
return RevoluteJoint(
self,
body_a,
body_b,
local_anchor_a,
local_anchor_b,
collide_connected,
lower_angle,
upper_angle,
enable_limit,
motor_speed,
max_motor_torque,
enable_motor,
reference_angle,
)
[docs]
def add_prismatic_joint(
self,
body_a,
body_b,
local_anchor_a=None,
local_anchor_b=None,
axis=(1, 0),
anchor=None,
collide_connected=False,
lower_limit=None,
upper_limit=None,
enable_limit=None,
motor_speed=None,
max_motor_force=None,
enable_motor=None,
reference_angle=None,
enable_spring=None,
hertz=None,
damping_ratio=None,
) -> "PrismaticJoint":
"""Create a prismatic joint that allows sliding motion along an axis.
Args:
body_a: First body to connect (must belong to this world)
body_b: Second body to connect (must belong to this world)
local_anchor_a (tuple): Local coordinates (x,y) on body_a where joint attaches
local_anchor_b (tuple): Local coordinates (x,y) on body_b where joint attaches
axis (tuple): The axis defining allowed translation (x,y) in body A's frame
anchor: World coordinates (x,y) where joint attaches (alternative to local anchors)
collide_connected (bool): Whether bodies can collide with each other
lower_limit (float): Lower translation limit
upper_limit (float): Upper translation limit
enable_limit (bool): Whether to enable joint limits
motor_speed (float): Desired motor speed in meters/sec
max_motor_force (float): Maximum motor force in Newtons
enable_motor (bool): Whether to enable the joint motor
reference_angle (float): Reference angle between bodies in radians
enable_spring (bool): Enable spring behavior
hertz (float): Spring stiffness frequency in Hz
damping_ratio (float): Spring damping ratio
Returns:
PrismaticJoint: The created prismatic joint
Example:
>>> world = World()
>>> body_a = world.new_body().dynamic().position(0,0).build()
>>> body_b = world.new_body().dynamic().position(1,0).build()
>>> # Create sliding joint along x-axis
>>> joint = world.add_prismatic_joint(
... body_a, body_b,
... anchor=(0.5,0),
... axis=(1,0),
... enable_limit=True,
... lower_limit=-1,
... upper_limit=1
... )
"""
if (body_a._body_id not in self._bodies) or (
body_b._body_id not in self._bodies
):
raise ValueError("Both bodies must belong to this world")
if anchor is not None:
if local_anchor_a is not None or local_anchor_b is not None:
raise ValueError("Can't set local anchors when setting a world anchor")
local_anchor_a = body_a.transform.inverse(anchor)
local_anchor_b = body_b.transform.inverse(anchor)
return PrismaticJoint(
self,
body_a,
body_b,
local_anchor_a,
local_anchor_b,
axis,
collide_connected,
lower_limit,
upper_limit,
enable_limit,
motor_speed,
max_motor_force,
enable_motor,
reference_angle,
enable_spring,
hertz,
damping_ratio,
)
[docs]
def add_wheel_joint(
self,
body_a,
body_b,
local_anchor_a=None,
local_anchor_b=None,
axis=(1, 0),
anchor=None,
collide_connected=False,
enable_limit=False,
lower_translation=0.0,
upper_translation=0.0,
enable_motor=False,
motor_speed=0.0,
max_motor_torque=0.0,
enable_spring=False,
spring_hertz=0.0,
spring_damping_ratio=0.0,
) -> "WheelJoint":
"""Create a wheel joint for vehicle suspension simulation.
Args:
body_a: First body to connect (must belong to this world)
body_b: Second body to connect (must belong to this world)
local_anchor_a (tuple): Local coordinates (x,y) on body_a where joint attaches
local_anchor_b (tuple): Local coordinates (x,y) on body_b where joint attaches
axis (tuple): The axis defining translation (x,y) in body A's frame
anchor: World coordinates (x,y) where joint attaches (alternative to local anchors)
collide_connected (bool): Whether bodies can collide with each other
enable_limit (bool): Enable translation limits
lower_translation (float): Lower translation limit
upper_translation (float): Upper translation limit
enable_motor (bool): Enable the joint motor
motor_speed (float): Desired motor speed in radians/sec
max_motor_torque (float): Maximum motor torque in N-m
enable_spring (bool): Enable spring behavior
spring_hertz (float): Spring oscillation frequency in Hz
spring_damping_ratio (float): Spring damping ratio (non-dimensional)
Returns:
WheelJoint: The created wheel joint
Example:
>>> world = World()
>>> chassis = world.new_body().dynamic().position(0,1).box(2,0.5).build()
>>> wheel = world.new_body().dynamic().position(1,0).circle(0.4).build()
>>> # Create wheel suspension
>>> joint = world.add_wheel_joint(
... chassis, wheel,
... anchor=(1,0),
... axis=(0,1), # Vertical suspension movement
... enable_spring=True,
... spring_hertz=4.0,
... spring_damping_ratio=0.7
... )
"""
if (body_a._body_id not in self._bodies) or (
body_b._body_id not in self._bodies
):
raise ValueError("Both bodies must belong to this world")
if anchor is not None:
if local_anchor_a is not None or local_anchor_b is not None:
raise ValueError("Can't set local anchors when setting a world anchor")
local_anchor_a = body_a.transform.inverse(anchor)
local_anchor_b = body_b.transform.inverse(anchor)
return WheelJoint(
self,
body_a,
body_b,
local_anchor_a,
local_anchor_b,
axis,
collide_connected,
enable_limit,
lower_translation,
upper_translation,
enable_motor,
motor_speed,
max_motor_torque,
enable_spring,
spring_hertz,
spring_damping_ratio,
)
[docs]
def add_distance_joint(
self,
body_a,
body_b,
local_anchor_a=None,
local_anchor_b=None,
collide_connected=False,
length=None,
min_length=None,
max_length=None,
enable_limit=False,
enable_spring=False,
hertz=None,
damping_ratio=None,
enable_motor=False,
motor_speed=None,
max_motor_force=None,
) -> "DistanceJoint":
"""Create a distance joint that maintains or limits distance between points on two bodies.
Args:
body_a: First body to connect (must belong to this world)
body_b: Second body to connect (must belong to this world)
local_anchor_a (tuple): Local coordinates (x,y) on body_a where joint attaches
local_anchor_b (tuple): Local coordinates (x,y) on body_b where joint attaches
collide_connected (bool): Whether bodies can collide with each other
length (float): Rest length. Calculated from anchors if None.
min_length (float): Minimum allowed length when using limits
max_length (float): Maximum allowed length when using limits
enable_limit (bool): Whether to enable length limits
enable_spring (bool): Enable spring behavior
hertz (float): Spring oscillation frequency in Hz
damping_ratio (float): Spring damping ratio
enable_motor (bool): Enable the joint motor
motor_speed (float): Desired motor speed in meters/second
max_motor_force (float): Maximum motor force in Newtons
Returns:
DistanceJoint: The created distance joint
Example:
>>> world = World()
>>> body_a = world.new_body().dynamic().position(0,0).build()
>>> body_b = world.new_body().dynamic().position(2,0).build()
>>> # Create spring joint
>>> spring = world.add_distance_joint(
... body_a, body_b,
... anchor_a=(0,0),
... anchor_b=(2,0),
... enable_spring=True,
... hertz=4.0,
... damping_ratio=0.5
... )
>>> # Create rope joint
>>> rope = world.add_distance_joint(
... body_a, body_b,
... anchor_a=(0,1),
... anchor_b=(2,1),
... enable_limit=True,
... min_length=1.0,
... max_length=3.0
... )
"""
if (body_a._body_id not in self._bodies) or (
body_b._body_id not in self._bodies
):
raise ValueError("Both bodies must belong to this world")
return DistanceJoint(
self,
body_a,
body_b,
local_anchor_a,
local_anchor_b,
collide_connected,
length,
min_length,
max_length,
enable_limit,
enable_spring,
hertz,
damping_ratio,
enable_motor,
motor_speed,
max_motor_force,
)
[docs]
def add_motor_joint(
self,
body_a,
body_b,
linear_offset=None,
angular_offset=None,
max_force=None,
max_torque=None,
correction_factor=None,
collide_connected=False,
) -> "MotorJoint":
"""Create a motor joint to control relative motion between two bodies.
Args:
body_a: First body to connect
body_b: Second body to connect
linear_offset (tuple): Desired position of bodyB in bodyA's frame
angular_offset (float): Desired angle between bodies in radians
max_force (float): Maximum force in Newtons
max_torque (float): Maximum torque in Newton-meters
correction_factor (float): Position correction factor [0,1]
collide_connected (bool): Whether bodies can collide
Returns:
MotorJoint: The created motor joint
Example:
>>> world = World()
>>> ground = world.new_body().build()
>>> body = world.new_body().dynamic().position(0, 4).build()
>>> motor = world.add_motor_joint(
... ground, body,
... linear_offset=(1, 0), # Move body 1m right
... max_force=100
... )
"""
return MotorJoint(
self,
body_a,
body_b,
linear_offset,
angular_offset,
max_force,
max_torque,
correction_factor,
collide_connected,
)
def _track_body(self, body: "Body"):
"""Internal method to track body references. Called automatically during body creation.
Args:
body: Body instance to register in the world
"""
self._bodies[body._body_id] = body
@property
def bodies(self):
"""Get list of all active bodies in the world.
Returns:
list[Body]: Copies of registered body references
Example:
>>> world = World()
>>> box = world.new_body().dynamic().build()
>>> len(world.bodies)
1
"""
return list(self._bodies.values())
[docs]
def draw(self, debug_draw: DebugDraw):
"""Render world state using debug drawing interface.
Args:
debug_draw: Configured DebugDraw instance for visualization
Example:
>>> world = World()
>>> debug_draw = DebugDraw()
>>> world.draw(debug_draw)
"""
lib.b2World_Draw(self._world_id, ffi.addressof(debug_draw._debug_draw))
[docs]
def query_aabb(
self,
aabb: AABB,
collision_filter: "CollisionFilter" = None,
max_results: int = None,
) -> list:
"""Find shapes overlapping an axis-aligned bounding box using an optional collision filter.
Args:
aabb: Axis-aligned bounding box to query.
collision_filter: Optional CollisionFilter instance for filtering.
If None, a default CollisionFilter is used.
max_results: Optional maximum number of shapes to return.
The callback will return False when this limit is reached.
Returns:
list: Shapes with overlapping fixtures.
Example:
>>> world = World()
>>> box = world.new_body().dynamic().position(0, 0).box(1, 1).build()
>>> aabb = AABB(lower=Vec2(-1, -1), upper=Vec2(1, 1))
>>> overlaps = world.query_aabb(aabb, collision_filter=CollisionFilter(), max_results=10)
>>> len(overlaps) <= 10
True
"""
if collision_filter is None:
collision_filter = CollisionFilter()
results = []
overlap_callback = make_overlap_callback(results, max_results)
# Convert the CollisionFilter to a Box2D c_filter.
c_filter = collision_filter.b2QueryFilter
filter_dict = {
"categoryBits": c_filter.categoryBits,
"maskBits": c_filter.maskBits,
}
lib.b2World_OverlapAABB(
self._world_id,
aabb.b2AABB[0],
c_filter[0],
overlap_callback,
ffi.NULL,
)
return results
[docs]
def query_circle(
self,
position: VectorLike,
radius: float,
collision_filter: "CollisionFilter" = None,
max_results: int = None,
) -> list:
"""Find shapes overlapping a circle.
Args:
position: Center of the query circle in world coordinates.
radius: Radius of the query circle.
collision_filter: Optional CollisionFilter instance for filtering.
If None, a default CollisionFilter is used.
max_results: Optional maximum number of shapes to return.
The callback will return False when this limit is reached.
Returns:
list: Shapes with overlapping fixtures.
Example:
>>> world = World()
>>> # Create a body with a circle fixture
>>> box = world.new_body().dynamic().circle(1).build()
>>> overlaps = world.query_circle((0, 0), 1.5, collision_filter=CollisionFilter(), max_results=10)
>>> len(overlaps) <= 10
True
"""
if collision_filter is None:
collision_filter = CollisionFilter()
results = []
overlap_callback = make_overlap_callback(results, max_results)
circle = CircleDef(radius).circle
transform = Transform(position=position).b2Transform
c_filter = collision_filter.b2QueryFilter
lib.b2World_OverlapCircle(
self._world_id,
circle,
transform[0],
c_filter[0],
overlap_callback,
ffi.NULL,
)
return results
[docs]
def ray_cast(
self,
origin: VectorLike,
translation: VectorLike,
collision_filter: "CollisionFilter" = None,
first_hit_only: bool = False,
) -> list[RayCastResult]:
"""Cast a ray and find intersecting shapes.
Args:
origin: Starting point of the ray (x,y)
translation: The translation of the ray from the start point to the end point
collision_filter: Optional filter to control which shapes are tested
first_hit_only: If True, only return the first hit encountered
Returns:
List of RayCastResult objects containing hit information
Example:
>>> world = World()
>>> box = world.new_body().dynamic().position(5,0).box(1,1).build()
>>> hits = world.ray_cast((0,0), (10,0))
>>> len(hits) > 0
True
>>> hits[0].shape # First intersected shape
<Shape>
"""
if collision_filter is None:
collision_filter = CollisionFilter()
results = []
p1 = to_vec2(origin).b2Vec2[0]
p2 = to_vec2(translation).b2Vec2[0]
c_filter = collision_filter.b2QueryFilter
if first_hit_only:
result = lib.b2World_CastRayClosest(self._world_id, p1, p2, c_filter[0])
if result.hit:
results.append(
RayCastResult(
shape=ffi.from_handle(lib.b2Shape_GetUserData(result.shapeId)),
point=Vec2(result.point.x, result.point.y),
normal=Vec2(result.normal.x, result.normal.y),
fraction=result.fraction,
)
)
else:
callback = make_ray_cast_callback(results)
lib.b2World_CastRay(self._world_id, p1, p2, c_filter[0], callback, ffi.NULL)
results.sort(key=lambda r: r.fraction)
return results
[docs]
def get_sensor_events(self) -> list:
"""Retrieve all sensor events that occurred during the last time step.
Sensors are shapes marked with is_sensor=True. They detect overlap with
other shapes but don't generate collision responses. This method returns
all begin and end overlap events for sensors from the last simulation step.
Returns:
list: A list of SensorEvent objects with the following attributes:
- sensor: The sensor Shape that detected overlap
- visitor: The Shape that overlapped with the sensor
- begin: True if overlap began, False if overlap ended
Example:
>>> world = World()
>>> body = world.new_body().dynamic().position(0,0).build()
>>> # Create a sensor fixture
>>> sensor = body.add_circle(radius=1.0, is_sensor=True)
>>> # Run simulation
>>> world.step(1/60)
>>> # Get events
>>> events = world.get_sensor_events()
>>> for event in events:
... if event['begin']:
... print(f"Sensor began contact with object")
... else:
... print(f"Sensor ended contact with object")
"""
events = []
# Get the events from Box2D
sensor_events = lib.b2World_GetSensorEvents(self._world_id)
# Process the events if there are any
if sensor_events:
# Determine how many events we have
begin_count = sensor_events.beginCount
end_count = sensor_events.endCount
# Process each event
begin_ev = [sensor_events.beginEvents[i] for i in range(begin_count)]
end_ev = [sensor_events.endEvents[i] for i in range(end_count)]
begin_events = [
SensorEvent(
sensor=ffi.from_handle(
lib.b2Shape_GetUserData(event.sensorShapeId)
),
visitor=ffi.from_handle(
lib.b2Shape_GetUserData(event.visitorShapeId)
),
begin=True,
)
for event in begin_ev
]
end_events = [
SensorEvent(
sensor=ffi.from_handle(
lib.b2Shape_GetUserData(event.sensorShapeId)
),
visitor=ffi.from_handle(
lib.b2Shape_GetUserData(event.visitorShapeId)
),
begin=False,
)
for event in end_ev
]
return SensorEvents(begin=begin_events, end=end_events)
[docs]
def destroy(self):
"""Destroy the world.
Example:
>>> world = World()
>>> world.destroy()
"""
# If using the C-level scheduler, clean it up.
if getattr(self, "_use_c_scheduler", False):
lib.cleanup_threadpool()
self._use_c_scheduler = False
if hasattr(self, "_world_id"):
lib.b2DestroyWorld(self._world_id)
del self._world_id
def __del__(self):
"""Clean up world resources. Automatically called when instance is garbage collected.
Destroys Box2D world instance.
"""
self.destroy()
@property
def enable_sleep(self) -> bool:
"""Control whether bodies can enter sleep state to save computation.
When enabled, inactive bodies will stop simulating until awakened.
Example:
>>> world = World()
>>> world.enable_sleep = False # Disable sleeping entirely
"""
return self._enable_sleep
@enable_sleep.setter
def enable_sleep(self, value: bool):
self._enable_sleep = bool(value)
lib.b2World_EnableSleeping(self._world_id, self._enable_sleep)
@property
def enable_continuous(self) -> bool:
"""Toggle continuous collision detection for dynamic vs static bodies.
Helps prevent fast-moving objects from tunneling through static geometry.
Example:
>>> world = World()
>>> world.enable_continuous = False # Disable CCD for static
"""
return self._enable_continuous
@enable_continuous.setter
def enable_continuous(self, value: bool):
self._enable_continuous = bool(value)
lib.b2World_EnableContinuous(self._world_id, self._enable_continuous)
@property
def restitution_threshold(self) -> float:
"""Minimum collision speed for restitution effects (m/s).
Collisions slower than this threshold will have zero restitution.
Example:
>>> world = World()
>>> world.restitution_threshold = 2.0 # Only apply restitution above 2m/s
"""
return self._restitution_threshold
@restitution_threshold.setter
def restitution_threshold(self, value: float):
self._restitution_threshold = float(value)
lib.b2World_SetRestitutionThreshold(self._world_id, self._restitution_threshold)
@property
def hit_event_threshold(self) -> float:
"""Minimum collision speed to trigger hit events (m/s).
Collisions slower than this won't generate collision events.
Example:
>>> world = World()
>>> world.hit_event_threshold = 0.5 # Get events for slower impacts
"""
return self._hit_event_threshold
@hit_event_threshold.setter
def hit_event_threshold(self, value: float):
self._hit_event_threshold = float(value)
lib.b2World_SetHitEventThreshold(self._world_id, self._hit_event_threshold)
@property
def contact_hertz(self) -> float:
"""Contact constraint stiffness frequency (Hz).
Higher values make contacts stiffer/more rigid.
Example:
>>> world = World()
>>> world.contact_hertz = 30.0 # Softer contacts
"""
return self._contact_hertz
@contact_hertz.setter
def contact_hertz(self, value: float):
self._contact_hertz = float(value)
lib.b2World_SetContactTuning(
self._world_id,
self._contact_hertz,
self._contact_damping_ratio,
self._contact_push_velocity,
)
@property
def contact_damping_ratio(self) -> float:
"""Contact constraint damping ratio (0-1).
1.0 = critical damping (fastest oscillation reduction)
Example:
>>> world = World()
>>> world.contact_damping_ratio = 0.2 # Add some energy absorption
"""
return self._contact_damping_ratio
@contact_damping_ratio.setter
def contact_damping_ratio(self, value: float):
self._contact_damping_ratio = float(value)
lib.b2World_SetContactTuning(
self._world_id,
self._contact_hertz,
self._contact_damping_ratio,
self._contact_push_velocity,
)
@property
def contact_push_velocity(self) -> float:
"""Maximum velocity for pushing objects out of penetration (m/s).
Limits how fast contacts can separate penetrating bodies.
Example:
>>> world = World()
>>> world.contact_push_velocity = 2.0 # Allow faster separation
"""
return self._contact_push_velocity
@contact_push_velocity.setter
def contact_push_velocity(self, value: float):
self._contact_push_velocity = float(value)
lib.b2World_SetContactTuning(
self._world_id,
self._contact_hertz,
self._contact_damping_ratio,
self._contact_push_velocity,
)