from box2d._box2d import lib, ffi
from .math import Vec2, Rot, Transform, VectorLike, to_vec2
from .shape import Box, Circle, Capsule, Segment, Polygon, Chain
from .shape_def import PolygonDef
from .collision_filter import CollisionFilter
[docs]
class BodyBuilder:
"""Builder for creating Box2D bodies with chained configuration methods.
Example:
>>> body = world.new_body()
>>> body.dynamic()
>>> body.position(2, 3)
>>> body.box(width=2, height=1)
>>> body.circle(radius=0.5, center=(1, 0))
>>> body = body.build()
"""
[docs]
def __init__(self, world):
"""Initialize the BodyBuilder with the world context.
Args:
world: The World instance where the body will be created.
"""
self.world = world
self._def = lib.b2DefaultBodyDef()
self._shape_defs = []
[docs]
@classmethod
def extend(cls, func):
"""
Decorator that adds a new method to the BodyBuilder class.
When decorating a function with @BodyBuilder.extend, the function is attached as a new method
to the BodyBuilder class. This enables you to extend the builder with custom configuration methods
that can be chained with the built-in methods.
Example::
>>> @BodyBuilder.extend
>>> def custom_shape(self, value):
>>> # Custom functionality
>>> return self
>>> builder = world.new_body().custom_shape(10)
Args:
func (callable): A function to be added as a method to BodyBuilder. The function should accept
'self' as its first argument.
Returns:
callable: The unchanged original function.
"""
setattr(cls, func.__name__, func)
return func
[docs]
def dynamic(self):
"""Set the body type to dynamic.
Dynamic bodies are affected by forces and impulses. Returns the builder instance.
"""
self._def.type = lib.b2_dynamicBody
return self
[docs]
def static(self):
"""Set the body type to static.
Static bodies cannot move and are unaffected by forces. Returns the builder instance.
"""
self._def.type = lib.b2_staticBody
return self
[docs]
def kinematic(self):
"""Set the body type to kinematic.
Kinematic bodies are moved by setting their velocity. Returns the builder instance.
"""
self._def.type = lib.b2_kinematicBody
return self
[docs]
def fixed_rotation(self, fixed=True):
"""Set whether the body has fixed rotation.
Fixed rotation bodies will not rotate. Useful for objects like characters.
Args:
fixed: Boolean indicating whether rotation should be fixed
Returns:
The builder instance
"""
self._def.fixedRotation = fixed
return self
[docs]
def bullet(self, bullet=True):
"""Set whether the body is treated as a bullet.
Bullet bodies perform continuous collision detection, suitable for fast-moving objects.
Args:
bullet: Boolean indicating whether to enable bullet behavior
Returns:
The builder instance
"""
self._def.isBullet = bullet
return self
[docs]
def gravity_scale(self, scale):
"""Set the gravity scale for the body.
Adjusts the effect of gravity on this body relative to the world gravity.
Args:
scale: Float value representing the gravity scale factor
Returns:
The builder instance
"""
self._def.gravityScale = scale
return self
[docs]
def position(self, x: float, y: float):
"""Set the initial position of the body.
Args:
x: The x-coordinate of the body's position
y: The y-coordinate of the body's position
Returns:
The builder instance
"""
self._def.position.x = x
self._def.position.y = y
return self
[docs]
def rotation(self, rotation: float):
"""Set the initial rotation of the body.
Args:
rotation: The rotation in radians
Returns:
The builder instance
"""
self._def.rotation = Rot(rotation).b2Rot[0]
return self
[docs]
def linear_velocity(self, x: float, y: float):
"""Set the initial linear velocity of the body.
Args:
x: The x-component of the velocity
y: The y-component of the velocity
Returns:
The builder instance
"""
self._def.linearVelocity.x = x
self._def.linearVelocity.y = y
return self
[docs]
def angular_velocity(self, radians: float):
"""Set the initial angular velocity of the body.
Args:
radians: The angular velocity in radians per second
Returns:
The builder instance
"""
self._def.angularVelocity = radians
return self
[docs]
def linear_damping(self, damping: float):
"""Set the linear damping of the body.
Damping reduces the linear velocity over time.
Args:
damping: Float value for linear damping
Returns:
The builder instance
"""
self._def.linearDamping = damping
return self
[docs]
def angular_damping(self, damping: float):
"""Set the angular damping of the body.
Damping reduces the angular velocity over time.
Args:
damping: Float value for angular damping
Returns:
The builder instance
"""
self._def.angularDamping = damping
return self
[docs]
def enable_sleep(self, enable: bool):
"""Set whether the body is allowed to sleep.
Sleeping bodies are skipped in simulations for performance optimization.
Args:
enable: Boolean indicating whether sleeping is enabled
Returns:
The builder instance
"""
self._def.enableSleep = enable
return self
[docs]
def sleep_threshold(self, threshold: float):
"""Set the sleep threshold for the body.
The minimum velocity below which the body will go to sleep.
Args:
threshold: Float value representing the sleep threshold
Returns:
The builder instance
"""
self._def.sleepThreshold = threshold
return self
[docs]
def box(
self,
width: float,
height: float,
radius=0.0,
offset=(0, 0),
angle=0.0,
density: float = 1.0,
friction: float = 0.2,
restitution: float = 0.0,
is_sensor: bool = False,
collision_filter: CollisionFilter = None,
):
"""Add a box shape to the body during construction.
Args:
width: Full width of the box.
height: Full height of the box.
radius: The radius of the rounded corners (default: 0.0).
offset: The offset of the box from the body's position (default: (0, 0)).
angle: The angle of the box (default: 0.0).
density: Mass density (kg/m²).
friction: Friction coefficient (0-1).
restitution: Bounciness (0-1).
is_sensor: True for sensor shape (no collision response).
collision_filter: Optional CollisionFilter instance for collision filtering.
Returns:
Self for method chaining.
"""
self._shape_defs.append(
{
"type": "box",
"params": (width, height),
"kwargs": {
"radius": radius,
"offset": offset,
"angle": angle,
"density": density,
"friction": friction,
"restitution": restitution,
"is_sensor": is_sensor,
"collision_filter": collision_filter,
},
}
)
return self
[docs]
def circle(
self,
radius: float,
center: tuple = (0, 0),
density: float = 1.0,
friction: float = 0.2,
restitution: float = 0.0,
is_sensor: bool = False,
collision_filter: CollisionFilter = None,
):
"""Add a circle shape to the body during construction.
Args:
radius: Radius of the circle.
center: Local center position (x, y).
density: Mass density (kg/m²).
friction: Friction coefficient (0-1).
restitution: Bounciness (0-1).
is_sensor: True for sensor shape.
collision_filter: Optional CollisionFilter instance for collision filtering.
Returns:
Self for method chaining.
"""
self._shape_defs.append(
{
"type": "circle",
"params": (radius, center),
"kwargs": {
"density": density,
"friction": friction,
"restitution": restitution,
"is_sensor": is_sensor,
"collision_filter": collision_filter,
},
}
)
return self
[docs]
def capsule(
self,
point1: tuple,
point2: tuple,
radius: float,
density: float = 1.0,
friction: float = 0.2,
restitution: float = 0.0,
is_sensor: bool = False,
collision_filter: CollisionFilter = None,
):
"""Add a vertical capsule shape (cylinder with hemispherical ends).
Args:
point1: The first endpoint of the capsule.
point2: The second endpoint of the capsule.
radius: Radius of the hemispherical ends.
density: Mass density (kg/m²).
friction: Friction coefficient (0-1).
restitution: Bounciness (0-1).
is_sensor: True for sensor shape.
collision_filter: Optional CollisionFilter instance for collision filtering.
Returns:
Self for method chaining.
"""
self._shape_defs.append(
{
"type": "capsule",
"params": (point1, point2, radius),
"kwargs": {
"density": density,
"friction": friction,
"restitution": restitution,
"is_sensor": is_sensor,
"collision_filter": collision_filter,
},
}
)
return self
[docs]
def polygon(
self,
vertices: list[tuple],
radius: float = 0.0,
density: float = 1.0,
friction: float = 0.2,
restitution: float = 0.0,
is_sensor: bool = False,
collision_filter: CollisionFilter = None,
):
"""Add a convex polygon shape.
The given vertices are processed to compute their convex hull. An exception will be raised
if the provided vertices do not form a valid convex polygon.
Args:
vertices: List of points that define the polygon shape.
radius: The radius of the rounded corners (default: 0.0).
density: Mass density (kg/m²).
friction: Friction coefficient (0-1).
restitution: Bounciness (0-1).
is_sensor: True for sensor shape.
collision_filter: Optional CollisionFilter instance for collision filtering.
Returns:
Self for method chaining.
Raises:
Exception: If the vertices cannot form a convex polygon.
"""
# Validate convexity (this will raise an exception if the polygon is not convex)
PolygonDef(vertices)
self._shape_defs.append(
{
"type": "polygon",
"params": (vertices,),
"kwargs": {
"radius": radius,
"density": density,
"friction": friction,
"restitution": restitution,
"is_sensor": is_sensor,
"collision_filter": collision_filter,
},
}
)
return self
[docs]
def segment(
self,
start: tuple,
end: tuple,
density: float = 0.0,
friction: float = 0.2,
restitution: float = 0.0,
is_sensor: bool = False,
collision_filter: CollisionFilter = None,
):
"""Add a line segment shape with optional edge radius.
Args:
start: Starting point (x, y) in local coordinates.
end: Ending point (x, y) in local coordinates.
density: Typically 0 for static segments.
friction: Friction coefficient (0-1).
restitution: Bounciness (0-1).
is_sensor: True for sensor shape.
collision_filter: Optional CollisionFilter instance for collision filtering.
Returns:
Self for method chaining.
"""
self._shape_defs.append(
{
"type": "segment",
"params": (start, end),
"kwargs": {
"density": density,
"friction": friction,
"restitution": restitution,
"is_sensor": is_sensor,
"collision_filter": collision_filter,
},
}
)
return self
[docs]
def chain(
self,
vertices: list[tuple],
loop: bool = False,
friction: float = 0.2,
restitution: float = 0.0,
collision_filter: CollisionFilter = None,
):
"""Add a chain shape to the body during construction.
Args:
vertices: List of points that define the chain shape. Must contain at least 4 vertices.
loop: Boolean indicating whether the chain should be closed (looped). Default is False.
friction: Friction coefficient (0-1).
restitution: Bounciness (0-1).
collision_filter: Optional CollisionFilter instance for collision filtering.
Returns:
Self for method chaining.
"""
self._shape_defs.append(
{
"type": "chain",
"params": (vertices,),
"kwargs": {
"loop": loop,
"friction": friction,
"restitution": restitution,
"collision_filter": collision_filter,
},
}
)
return self
[docs]
def build(self) -> "Body":
"""Finalize the body creation and attach configured shapes.
Creates and configures the body in the world using the specified properties.
Returns:
The newly created Body instance
"""
body = Body(self.world, self._def)
# Apply additional properties after creation
if self._def.fixedRotation:
lib.b2Body_SetFixedRotation(body._body_id, True)
if self._def.isBullet:
lib.b2Body_SetBullet(body._body_id, True)
lib.b2Body_SetGravityScale(body._body_id, self._def.gravityScale)
# Create shapes
for shape_def in self._shape_defs:
method = getattr(body, f'add_{shape_def["type"]}')
method(*shape_def["params"], **shape_def["kwargs"])
return body
[docs]
class Body:
"""Represents a rigid body in the 2D physics simulation.
Bodies can be dynamic, kinematic, or static, and can have various forces,
impulses, and constraints applied to them.
"""
[docs]
def __init__(self, world, body_def):
"""
Initialize a Body instance.
Args:
world: The World instance in which this body exists.
body_def: The body definition used to create this body.
"""
self.world = world
self.body_def = body_def
self._body_id = lib.b2CreateBody(
self.world._world_id, ffi.addressof(self.body_def)
)
self._handle = ffi.addressof(self._body_id)
lib.b2Body_SetUserData(self._body_id, self._handle)
self._shapes = []
self.world._track_body(self)
@property
def shapes(self):
"""Get the shapes attached to this body."""
return self._shapes
@property
def position(self):
"""Get the world position of the body."""
pos = lib.b2Body_GetPosition(self._body_id)
return Vec2(pos.x, pos.y)
@position.setter
def position(self, value: VectorLike):
"""Set the world position of the body."""
rot = lib.b2Body_GetRotation(self._body_id)
value = to_vec2(value)
lib.b2Body_SetTransform(self._body_id, value.b2Vec2[0], rot)
@property
def linear_velocity(self):
"""Get the linear velocity of the body."""
vel = lib.b2Body_GetLinearVelocity(self._body_id)
return Vec2(vel.x, vel.y)
@linear_velocity.setter
def linear_velocity(self, value: VectorLike):
"""Set the linear velocity of the body."""
value = to_vec2(value).b2Vec2[0]
lib.b2Body_SetLinearVelocity(self._body_id, value)
@property
def angular_velocity(self):
"""Get angular velocity in radians/sec."""
return lib.b2Body_GetAngularVelocity(self._body_id)
@angular_velocity.setter
def angular_velocity(self, value):
"""Set angular velocity in radians/sec."""
lib.b2Body_SetAngularVelocity(self._body_id, float(value))
@property
def linear_damping(self):
"""Get the current linear damping value."""
return lib.b2Body_GetLinearDamping(self._body_id)
@linear_damping.setter
def linear_damping(self, value: float):
"""Set the linear damping value."""
lib.b2Body_SetLinearDamping(self._body_id, float(value))
@property
def angular_damping(self):
"""Get the current angular damping value."""
return lib.b2Body_GetAngularDamping(self._body_id)
@angular_damping.setter
def angular_damping(self, value: float):
"""Set the angular damping value."""
lib.b2Body_SetAngularDamping(self._body_id, float(value))
@property
def sleep_threshold(self):
"""Get the sleep threshold value."""
return lib.b2Body_GetSleepThreshold(self._body_id)
@sleep_threshold.setter
def sleep_threshold(self, value: float):
"""Set the sleep threshold value."""
lib.b2Body_SetSleepThreshold(self._body_id, float(value))
@property
def type(self):
"""Get the body type as a string ('dynamic', 'kinematic', or 'static')."""
body_type = lib.b2Body_GetType(self._body_id)
if body_type == lib.b2_dynamicBody:
return "dynamic"
elif body_type == lib.b2_kinematicBody:
return "kinematic"
else:
return "static"
@property
def fixed_rotation(self):
"""Check if the body has fixed rotation."""
return lib.b2Body_IsFixedRotation(self._body_id)
@fixed_rotation.setter
def fixed_rotation(self, value):
"""Set whether the body has fixed rotation."""
lib.b2Body_SetFixedRotation(self._body_id, value)
@property
def is_bullet(self):
"""Check if the body is treated as a bullet."""
return lib.b2Body_IsBullet(self._body_id)
@is_bullet.setter
def is_bullet(self, value):
"""Set whether the body is treated as a bullet."""
lib.b2Body_SetBullet(self._body_id, value)
@property
def gravity_scale(self):
"""Get the gravity scale factor for this body."""
return lib.b2Body_GetGravityScale(self._body_id)
@gravity_scale.setter
def gravity_scale(self, value):
"""Set the gravity scale factor for this body."""
lib.b2Body_SetGravityScale(self._body_id, value)
@property
def awake(self):
"""Get the awake state of the body.
Returns:
bool: True if the body is awake, False otherwise.
"""
return lib.b2Body_IsAwake(self._body_id)
@awake.setter
def awake(self, value: bool):
"""Set the awake state of the body.
Args:
value (bool): True to wake the body, False to put the body to sleep.
"""
lib.b2Body_SetAwake(self._body_id, value)
@property
def enabled(self):
"""Get whether the body is enabled.
Returns:
bool: True if the body is enabled, False otherwise.
"""
return lib.b2Body_IsEnabled(self._body_id)
@enabled.setter
def enabled(self, value: bool):
"""Set whether the body is enabled.
Args:
value (bool): True to enable the body, False to disable it.
"""
if value:
lib.b2Body_Enable(self._body_id)
else:
lib.b2Body_Disable(self._body_id)
@property
def rotation(self):
"""Get the world rotation of the body in radians.
Returns:
float: The body's rotation angle in radians.
"""
rot = lib.b2Body_GetRotation(self._body_id)
return Rot.from_b2Rot(rot).angle_radians
@rotation.setter
def rotation(self, angle: float):
"""Set the world rotation of the body in radians.
Args:
angle (float): The new rotation angle in radians.
"""
pos = lib.b2Body_GetPosition(self._body_id)
rot = Rot(angle).b2Rot
lib.b2Body_SetTransform(self._body_id, pos, rot[0])
@property
def mass(self):
"""Get the mass of the body in kilograms"""
return lib.b2Body_GetMass(self._body_id)
@property
def rotational_inertia(self):
"""Get the rotational inertia of the body."""
return lib.b2Body_GetRotationalInertia(self._body_id)
@property
def transform(self) -> Transform:
"""Get the body Transform. You can use it to convert world coordinates to body coordinates."""
b2transform = lib.b2Body_GetTransform(self._body_id)
return Transform.from_b2Transform(b2transform)
[docs]
def apply_force(self, force, point=None, wake=True):
"""Apply a force at a world point.
Args:
force: Tuple representing the force vector (Fx, Fy).
point: Tuple representing the application point (x, y). Defaults to center.
wake: Boolean indicating whether to wake the body.
"""
x, y = force
if point is None:
lib.b2Body_ApplyForceToCenter(self._body_id, (x, y), wake)
else:
fx, fy = point
lib.b2Body_ApplyForce(self._body_id, (x, y), (fx, fy), wake)
[docs]
def apply_torque(self, torque, wake=True):
"""Apply a torque to the body.
Args:
torque: Float value representing the torque.
wake: Boolean indicating whether to wake the body.
"""
lib.b2Body_ApplyTorque(self._body_id, torque, wake)
[docs]
def apply_linear_impulse(self, impulse, point=None, wake=True):
"""Apply a linear impulse at a world point.
Args:
impulse: Tuple representing the impulse vector (Ix, Iy).
point: Tuple representing the application point (x, y). Defaults to center.
wake: Boolean indicating whether to wake the body.
"""
x, y = impulse
if point is None:
lib.b2Body_ApplyLinearImpulseToCenter(self._body_id, (x, y), wake)
else:
fx, fy = point
lib.b2Body_ApplyLinearImpulse(self._body_id, (x, y), (fx, fy), wake)
[docs]
def add_box(
self,
width: float,
height: float,
radius: float = 0.0,
offset: tuple = (0, 0),
angle: float = 0.0,
density: float = None,
friction: float = None,
restitution: float = None,
is_sensor: bool = None,
collision_filter=None,
custom_color=None,
):
"""Add a box shape to the body.
Args:
width: Full width of the box.
height: Full height of the box.
radius: The radius of the rounded corners (default: 0.0).
offset: The offset of the box from the body's position (default: (0, 0)).
angle: The rotation angle of the box in radians (default: 0.0).
density: Mass density of the shape.
friction: Friction coefficient.
restitution: Bounciness.
is_sensor: Flag indicating whether the shape is a sensor.
collision_filter: Optional CollisionFilter instance for collision filtering.
custom_color: Optional custom debug draw color (uint32_t).
Returns:
The created box shape.
"""
shape = Box.create(
self,
width,
height,
radius,
offset,
angle,
density,
friction,
restitution,
is_sensor,
collision_filter,
custom_color,
)
self._shapes.append(shape)
return shape
[docs]
def add_circle(
self,
radius: float,
center: tuple = (0, 0),
density: float = None,
friction: float = None,
restitution: float = None,
is_sensor: bool = None,
collision_filter=None,
custom_color=None,
):
"""Add a circle shape to the body.
Args:
radius: Radius of the circle.
center: Center of the circle (default: (0, 0)).
density: Mass density of the shape.
friction: Friction coefficient.
restitution: Bounciness.
is_sensor: Flag indicating whether the shape is a sensor.
collision_filter: Optional CollisionFilter instance for collision filtering.
custom_color: Optional custom debug draw color (uint32_t).
Returns:
The created circle shape.
"""
shape = Circle.create(
self,
radius,
center,
density,
friction,
restitution,
is_sensor,
collision_filter,
custom_color,
)
self._shapes.append(shape)
return shape
[docs]
def add_capsule(
self,
point1: tuple,
point2: tuple,
radius: float,
density: float = None,
friction: float = None,
restitution: float = None,
is_sensor: bool = None,
collision_filter=None,
custom_color=None,
):
"""Add a capsule shape to the body.
Args:
point1: First endpoint of the capsule.
point2: Second endpoint of the capsule.
radius: Radius of the capsule.
density: Mass density of the shape.
friction: Friction coefficient.
restitution: Bounciness.
is_sensor: Flag indicating whether the shape is a sensor.
collision_filter: Optional CollisionFilter instance for collision filtering.
custom_color: Optional custom debug draw color (uint32_t).
Returns:
The created capsule shape.
"""
shape = Capsule.create(
self,
point1,
point2,
radius,
density,
friction,
restitution,
is_sensor,
collision_filter,
custom_color,
)
self._shapes.append(shape)
return shape
[docs]
def add_polygon(
self,
vertices: list[tuple],
radius: float = 0.0,
density: float = None,
friction: float = None,
restitution: float = None,
is_sensor: bool = None,
collision_filter=None,
custom_color=None,
):
"""Add a convex polygon shape to the body.
Args:
vertices: List of vertices defining the polygon.
radius: Optional radius for rounded corners (default: 0.0).
density: Mass density of the shape.
friction: Friction coefficient.
restitution: Bounciness.
is_sensor: Flag indicating whether the shape is a sensor.
collision_filter: Optional CollisionFilter instance for collision filtering.
custom_color: Optional custom debug draw color (uint32_t).
Returns:
The created polygon shape.
"""
shape = Polygon.create(
self,
vertices,
radius,
density,
friction,
restitution,
is_sensor,
collision_filter,
custom_color,
)
self._shapes.append(shape)
return shape
[docs]
def add_segment(
self,
point1: tuple,
point2: tuple,
density: float = None,
friction: float = None,
restitution: float = None,
is_sensor: bool = None,
collision_filter=None,
custom_color=None,
):
"""Add a line segment shape to the body.
Args:
point1: Starting point of the segment.
point2: Ending point of the segment.
density: Mass density of the shape.
friction: Friction coefficient.
restitution: Bounciness.
is_sensor: Flag indicating whether the shape is a sensor.
collision_filter: Optional CollisionFilter instance for collision filtering.
custom_color: Optional custom debug draw color (uint32_t).
Returns:
The created segment shape.
"""
shape = Segment.create(
self,
point1,
point2,
density,
friction,
restitution,
is_sensor,
collision_filter,
custom_color,
)
self._shapes.append(shape)
return shape
[docs]
def add_chain(
self,
vertices: list[tuple],
loop: bool = False,
friction: float = None,
restitution: float = None,
collision_filter=None,
custom_color=None,
):
"""Add a chain shape to the body.
Args:
vertices: List of vertices defining the chain (must contain at least 4 vertices).
loop: Boolean indicating whether the chain should be closed (looped).
friction: Friction coefficient.
restitution: Bounciness.
collision_filter: Optional CollisionFilter instance for collision filtering.
custom_color: Optional custom debug draw color (uint32_t).
Returns:
The created chain shape.
"""
shape = Chain.create(
self, vertices, loop, friction, restitution, collision_filter, custom_color
)
self._shapes.append(shape)
return shape
[docs]
def remove_shape(self, shape):
"""Remove a shape from the body."""
if shape in self._shapes:
lib.b2DestroyShape(shape._shape_id, True) # Update body mass
self._shapes.remove(shape)
[docs]
def is_sleep_enabled(self):
"""Check if the body is allowed to sleep."""
return lib.b2Body_IsSleepEnabled(self._body_id)
[docs]
def destroy(self):
"""
Destroy this body and remove it from the world.
"""
if getattr(self, "_body_id", None) is None:
return
lib.b2DestroyBody(self._body_id)
if hasattr(self.world, "_bodies"):
self.world._bodies.pop(self._body_id, None)
self._body_id = None
# TODO: currently is segfaults one of the tests. need to figure out why.
# def __del__(self):
# Attempt to clean up if destroy() wasn't explicitly called.
# self.destroy()