forge3d.World¶
The central object of every simulation. It manages rigid bodies, advances physics, and produces SceneSnapshots for rendering.
Class reference¶
World ¶
forge3d physics world — minimal public API.
Coordinate system: z-up, SI units. Start here::
world = forge3d.World()
world.add_ground()
box = world.add_box(size=(1, 1, 1), position=(0, 0, 5))
world.step() # advances by default_dt=1/60 s
snap = world.snapshot()
The internal PhysicsWorld is accessible via world._physics for
advanced use, but the public API never needs it.
profiler
property
¶
Lazy-created :class:~forge3d.profiler.PhysicsProfiler for this world.
Usage::
with world.profiler:
world.step(dt)
print(world.profiler.last)
add_ground ¶
add_ground(material: Material | str = 'ground', size: Any = (40.0, 40.0, 0.2), height: float = 0.0) -> Body
Add a static ground plane. By default: 40×40 m slab at z=0.
add_box ¶
add_box(size: Any = (1.0, 1.0, 1.0), position: Any = (0.0, 0.0, 0.0), mass: float = 1.0, material: Material | str = 'default', name: str = '', restitution: float = 0.3, friction: float = 0.5, static: bool = False) -> Body
Add a box-shaped rigid body.
Parameters¶
static : If True, creates an immovable static box (mass is ignored).
add_static_box ¶
add_static_box(size: Any = (1.0, 1.0, 1.0), position: Any = (0.0, 0.0, 0.0), material: Material | str = 'default', name: str = '', restitution: float = 0.3, friction: float = 0.5) -> Body
Add a static (non-moving) box and register it in world.bodies.
Equivalent to add_box(..., static=True) — exposes _physics.add_static_box
as a properly tracked public method.
add_sphere ¶
add_sphere(radius: float = 0.5, position: Any = (0.0, 0.0, 0.0), mass: float = 1.0, material: Material | str = 'default', name: str = '', restitution: float = 0.3, friction: float = 0.5, static: bool = False) -> Body
Add a sphere-shaped rigid body.
static=True creates a non-moving marker (e.g. target visualization).
add_capsule ¶
add_capsule(radius: float = 0.2, half_length: float = 0.5, position: Any = (0.0, 0.0, 0.0), quat: Any = None, mass: float = 1.0, material: Material | str = 'default', name: str = '', restitution: float = 0.3, friction: float = 0.5, static: bool = False) -> Body
Add a capsule-shaped rigid body (cylinder + two hemispherical caps).
The capsule axis is aligned with body-local +Z. Use quat to orient it.
Parameters¶
static : If True, creates an immovable static capsule.
add_cylinder ¶
add_cylinder(radius: float = 0.5, half_length: float = 0.5, position: Any = (0.0, 0.0, 0.0), quat: Any = None, mass: float = 1.0, material: Material | str = 'default', name: str = '', restitution: float = 0.3, friction: float = 0.5, static: bool = False) -> Body
Add a cylinder-shaped rigid body (flat end caps, axis along +Z).
The cylinder has radius radius and total length 2 × half_length.
Use quat to orient it — e.g. quat=[0.707,0.707,0,0] lays it on
its side along the X axis.
Physics uses GJK/EPA convex-hull collision (same as add_mesh).
Parameters¶
radius : Cylinder radius (m). half_length : Half the cylinder height (m).
add_cone ¶
add_cone(radius: float = 0.5, height: float = 1.0, position: Any = (0.0, 0.0, 0.0), quat: Any = None, mass: float = 1.0, material: Material | str = 'default', name: str = '', restitution: float = 0.3, friction: float = 0.5, static: bool = False) -> Body
Add a cone-shaped rigid body (circular base at z=−height/2, apex at z=+height/2).
Physics uses GJK/EPA convex-hull collision.
Parameters¶
radius : Base circle radius (m). height : Total height (m).
add_wedge ¶
add_wedge(size: Any = (1.0, 1.0, 1.0), position: Any = (0.0, 0.0, 0.0), quat: Any = None, mass: float = 1.0, material: Material | str = 'default', name: str = '', restitution: float = 0.3, friction: float = 0.5, static: bool = False) -> Body
Add a wedge (right-angle triangular prism / ramp).
The ramp slopes from the front-bottom edge up to the back-top edge.
size is (width_x, depth_y, height_z). Use quat to rotate.
Physics uses GJK/EPA convex-hull collision.
Parameters¶
size : (sx, sy, sz) total bounding dimensions (m).
Example::
ramp = world.add_wedge(size=(2, 3, 1), position=(0, 0, 0.5),
static=True)
add_convex ¶
add_convex(vertices: Any, position: Any = (0.0, 0.0, 0.0), quat: Any = None, mass: float = 1.0, material: Material | str = 'default', name: str = '', restitution: float = 0.3, friction: float = 0.5, static: bool = False, collision_layer: int = 1, collision_mask: int = 65535) -> Body
Add a rigid body from a custom convex point cloud.
Computes the convex hull of vertices, uses it for both rendering and physics. Ideal for simple custom shapes without loading a full OBJ file.
Parameters¶
vertices : (N, 3) array-like — point cloud in local frame.
Example::
import numpy as np
pts = np.array([
[-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1],
[ 0, 0, 1], # pyramid apex
], dtype=float)
pyramid = world.add_convex(pts, position=(0, 0, 2), mass=1.0)
add_mesh ¶
add_mesh(mesh_data: Any, position: Any = (0.0, 0.0, 0.0), quat: Any = None, mass: float = 1.0, material: Material | str = 'default', name: str = '', restitution: float = 0.3, friction: float = 0.5, static: bool = False, collision_layer: int = 1, collision_mask: int = 65535) -> Body
Add a convex-hull rigid body from a MeshData object.
Use collision_mask=0 for visual-only decorative bodies that should
not participate in physics collision checks (trees, rocks, props).
This is important for performance: mesh GJK is expensive and decorative
bodies with large AABBs would otherwise be checked every frame.
Typical use::
from forge3d.io import load_obj
mesh = load_obj("assets/models/cube.obj")
body = world.add_mesh(mesh, position=(0, 0, 3), mass=1.0)
add_terrain ¶
add_terrain(heights: Any, cell_size: float = 1.0, origin: Any = (0.0, 0.0, 0.0), material: Material | str = 'ground', friction: float = 0.8, layer: int = 8) -> Any
Add a heightfield terrain (static, collision-only).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
heights
|
Any
|
2D array of shape (rows, cols) with z-heights in metres. |
required |
cell_size
|
float
|
World-space size of each grid cell (m). |
1.0
|
origin
|
Any
|
World-space position of the (0, 0) grid corner. |
(0.0, 0.0, 0.0)
|
material
|
Material | str
|
Surface material for rendering. |
'ground'
|
friction
|
float
|
Coulomb friction coefficient (default 0.8). |
0.8
|
layer
|
int
|
Collision layer bit-flag (default CollisionLayer.TERRAIN = 0x0008). |
8
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Any
|
class: |
Example::
import numpy as np
rng = np.random.default_rng(42)
h = rng.uniform(0, 2, (32, 32)).astype(np.float32)
terrain = world.add_terrain(h, cell_size=0.5, origin=(-8, -8, 0),
friction=0.9, layer=f3d.CollisionLayer.TERRAIN)
add_character ¶
add_character(position: Any = (0.0, 0.0, 2.0), height: float = 1.8, radius: float = 0.3, mass: float = 70.0, name: str = 'character', ground_layer_mask: int = 65535, ground_check_hz: float = 60.0) -> Any
Add a capsule-based character controller.
Returns a :class:~forge3d.character.CharacterController with
move(), jump(), and glide() methods.
Parameters¶
position : Initial world position (3,). height : Total capsule height in metres (default 1.8 m). radius : Capsule radius in metres (default 0.3 m). mass : Body mass in kg (default 70.0). name : Body name for collision queries. ground_layer_mask : Layers considered "ground" for the raycast check.
Example::
cc = world.add_character(position=(0, 0, 2), height=1.8)
while viewer.is_open:
cc.move(direction=(inp.dx, inp.dy, 0), speed=5.5, dt=viewer.dt)
if inp.just_pressed("space"):
cc.jump(impulse=6.4)
world.step(viewer.dt)
print(cc.is_grounded)
remove ¶
Remove a body from the simulation.
.. warning::
The Python Body object you hold becomes stale after this
call — its _id no longer exists in the world. Any subsequent
call to :meth:apply_impulse, :meth:teleport, etc. with the old
handle will raise RuntimeError.
Always reassign (``ball = world.add_sphere(...)``) or guard with
:meth:`contains` before reusing a handle.
Parameters¶
body : Body handle returned by an add_* method.
clear ¶
Remove all bodies from the world.
.. warning::
All body handles in scope become stale after this call —
including variables like ball, ground, etc. created before
clear(). Calling :meth:apply_impulse or reading
body.position on a stale handle raises RuntimeError.
Always reassign every body reference after calling ``clear()``::
world.clear(keep_statics=False)
ball = world.add_sphere(...) # new handle — old variable is now stale
Parameters¶
keep_statics : If True, static bodies (ground planes, robot links) are kept; only dynamic bodies are removed.
contains ¶
Return True if body is still present in this world.
Body handles become stale after :meth:remove or :meth:clear.
Use this guard before calling any method that operates on the body::
if world.contains(ball):
world.apply_impulse(ball, force * dt)
Parameters¶
body : Body handle to check.
get_body ¶
step ¶
Advance simulation by dt seconds (default: 1/60 s).
Parameters¶
dt : Time delta in seconds (default 1/60 s).
substeps : Divide dt into this many equal sub-steps for stability.
substeps=4 is recommended for fast or lightweight objects.
Collision callbacks fire once per full step (not per sub-step).
Steps taken each call:
- Flush per-body force/torque accumulators.
- Apply per-body linear/angular damping.
- Physics integration (substeps times at dt/substeps each).
- Apply weld constraints.
- Dispatch collision events.
update ¶
Fixed-timestep accumulator update — call once per rendered frame.
Internally accumulates frame_dt and calls :meth:step with
fixed_dt repeatedly until the accumulated time is consumed.
Leftover time carries over to the next call.
Configure via :attr:fixed_dt and :attr:max_substeps (set on the
world instance)::
world.fixed_dt = 1 / 120 # default 1/120 s
world.max_substeps = 8 # default 8 (caps spiral-of-death)
Example::
world = forge3d.World()
world.fixed_dt = 1 / 120
while viewer.is_open:
world.update(viewer.dt) # frame_dt varies; physics is stable
viewer.draw()
apply_impulse ¶
Apply an instantaneous velocity impulse to body (Δv = impulse / mass).
Use this to apply per-frame forces from a game loop::
# Apply force F for time dt:
world.apply_impulse(ball, np.array([F_x, F_y, 0]) * dt)
world.step(dt)
.. note::
Raises RuntimeError if body has been removed from the world
(via :meth:remove or :meth:clear). Guard with
:meth:contains if the body's lifetime is uncertain::
if world.contains(ball):
world.apply_impulse(ball, force * dt)
teleport ¶
Instantly move a body to a new position (and optionally orientation).
weld ¶
Attach body kinematically to anchor (weld constraint).
After welding, body follows anchor rigidly each step(),
preserving both relative position and relative orientation.
Parameters¶
local_offset : Position offset in anchor local frame. Computed automatically from current positions if omitted. local_rotation : Quaternion [w, x, y, z] expressing body's rotation relative to anchor. Computed automatically if omitted.
release ¶
Remove weld constraint from body (body resumes normal physics).
add_joint ¶
add_joint(joint_type: str, body_a: Body, body_b: Body | None = None, anchor_a: Any = (0.0, 0.0, 0.0), anchor_b: Any = (0.0, 0.0, 0.0), axis: Any = (0.0, 0.0, 1.0), limits: tuple[float, float] | None = None, motor_velocity: float | None = None, motor_max_torque: float = 10.0, stiffness: float = 100.0, damping: float = 5.0, rest_length: float = 1.0, target_distance: float = 1.0) -> Any
Add a joint constraint between two bodies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
joint_type
|
str
|
One of |
required |
body_a
|
Body
|
First body (required). |
required |
body_b
|
Body | None
|
Second body. If |
None
|
anchor_a
|
Any
|
Attachment point in body_a local frame. |
(0.0, 0.0, 0.0)
|
anchor_b
|
Any
|
Attachment point in body_b local frame (or world frame if body_b is None). |
(0.0, 0.0, 0.0)
|
axis
|
Any
|
Hinge / slide axis in body_a local frame
(used for |
(0.0, 0.0, 1.0)
|
limits
|
tuple[float, float] | None
|
Angular limits (rad) for hinge or distance limits (m) for prismatic. |
None
|
motor_velocity
|
float | None
|
Target velocity for hinge/prismatic motor. |
None
|
motor_max_torque
|
float
|
Torque cap for hinge motor (N·m). |
10.0
|
stiffness
|
float
|
Spring constant k (N/m) for spring joint. |
100.0
|
damping
|
float
|
Damping coefficient c (N·s/m) for spring joint. |
5.0
|
rest_length
|
float
|
Natural spring length (m) for spring joint. |
1.0
|
target_distance
|
float
|
Target distance (m) for distance joint. |
1.0
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Any
|
class: |
Any
|
meth: |
Examples::
hinge = world.add_joint("hinge", door, frame,
anchor_a=(-0.5, 0, 0),
anchor_b=(0.5, 0, 0),
axis=(0, 0, 1))
spring = world.add_joint("spring", box, ceiling,
stiffness=200.0, damping=10.0,
rest_length=2.0)
remove_joint ¶
Remove a joint by its handle (returned from :meth:add_joint).
set_camera ¶
set_camera(position: Any, target: Any = (0.0, 0.0, 0.0), up: Any = (0.0, 0.0, 1.0), fov_deg: float = 45.0) -> None
Set the default camera pose for snapshots and the Viewer.
raycast ¶
raycast(origin: Any, direction: Any, max_dist: float = 100.0, layer_mask: int = 65535) -> Any | None
Cast a ray from origin along direction and return the first hit.
Tests the ray against all physics bodies and heightfield terrain, and
returns the closest intersection, or None if nothing is hit.
Parameters¶
origin : (3,) ray start in world frame (m). direction : (3,) ray direction — need not be normalised. max_dist : Maximum hit distance (m).
Returns¶
A :class:RayHit namedtuple with fields
(body, point, normal, distance) or None.
For terrain hits body is None.
Example::
hit = world.raycast((0, 0, 5), (0, 0, -1), max_dist=10)
if hit:
name = hit.body.name if hit.body else "terrain"
print(name, hit.distance)
raycast_all ¶
raycast_all(origin: Any, direction: Any, max_dist: float = 100.0, layer_mask: int = 65535) -> list[Any]
Cast a ray and return all hits sorted by distance (closest first).
Parameters¶
origin, direction, max_dist : same as :meth:raycast.
layer_mask : only bodies whose collision_layer & layer_mask != 0
are tested. Heightfields are tested when
CollisionLayer.TERRAIN & layer_mask is nonzero.
Returns¶
List of :class:RayHit namedtuples (body, point, normal, distance).
May be empty.
Example::
hits = world.raycast_all((0, 0, 5), (0, 0, -1), max_dist=20)
for hit in hits:
print(hit.body.name, hit.distance)
overlap_sphere ¶
Return all bodies whose origin is within radius of center.
Uses AABB/position check (fast) — not exact shape intersection.
Parameters¶
center : (3,) world-space position. radius : Search radius in metres. layer_mask : Filter by collision layer.
Example::
nearby = world.overlap_sphere(explosion_pos, radius=5.0)
for body in nearby:
body.apply_force(...)
overlap_box ¶
overlap_box(center: Any, half_extents: Any, orientation: Any = None, layer_mask: int = 65535) -> list[Body]
Return all bodies whose origin falls inside an AABB or OBB.
Parameters¶
center : (3,) world-space centre. half_extents: (3,) half-sizes of the query box. orientation : (4,) quaternion [w,x,y,z] rotating the box (None = axis-aligned). layer_mask : Filter by collision layer.
Example::
bodies_in_room = world.overlap_box(room_center, half_extents=(5, 5, 3))
save ¶
Save the current world state to a JSON file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Any
|
Output path (str or :class: |
required |
Example::
world.save("checkpoint.json")
load
classmethod
¶
Load a world from a JSON file saved by :meth:save.
Returns a brand-new :class:World with all bodies restored::
world = forge3d.World.load("checkpoint.json")
To restore an existing world instance in-place, use
:meth:restore instead::
world.restore("checkpoint.json")
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Any
|
Path to a JSON file. |
required |
restore ¶
Restore world state from path into this instance (clears existing bodies).
Unlike the classmethod :meth:load, this modifies the current world
in-place so existing Python references to self remain valid::
world = forge3d.World()
world.restore("checkpoint.json")
print(len(world.bodies)) # bodies from the file
on_collision_begin ¶
Register a callback for when two bodies first collide.
Can be used as a decorator::
@world.on_collision_begin
def hit(event: forge3d.CollisionEvent) -> None:
print(event.body_a.name, "hit", event.body_b.name)
on_collision_stay ¶
Register a callback called every step while two bodies remain in contact.
add_collision_handler ¶
Return a :class:~forge3d.events.CollisionHandler for a specific body pair.
Example::
handler = world.add_collision_handler(ball, floor)
handler.on_begin = lambda e: print("Hit!")
ignore_collision ¶
Permanently ignore physics collisions between two specific bodies.
add_trigger_zone ¶
add_trigger_zone(position: Any = (0.0, 0.0, 0.0), size: Any = (1.0, 1.0, 1.0), name: str = 'trigger', visual_material: Material | None = None) -> Any
Add an invisible trigger zone (no physics collision, events only).
Returns a :class:~forge3d.events.TriggerZone with on_enter and
on_exit decorator attributes.
Parameters¶
visual_material : If supplied, a render-only box of the same size is created at position so the zone is visible in the scene without participating in physics.
Example::
goal = world.add_trigger_zone(position=(5, 0, 0.5), size=(1, 1, 1),
visual_material=f3d.Material(color="green"))
@goal.on_enter
def scored(body: forge3d.Body) -> None:
print(f"GOAL! {body.name}")
Usage examples¶
Basic simulation loop¶
import forge3d as f3d
world = f3d.World(gravity=(0, 0, -9.81))
ground = world.add_ground()
box = world.add_box(size=(1, 1, 1), position=(0, 0, 5), mass=1.0)
for _ in range(600): # 10 seconds @ 60 Hz
world.step(dt=1/60)
print(f"Final z: {box.position[2]:.3f} m")
Static bodies (v1.1.0)¶
# All three create non-moving, collidable geometry — no mass needed
wall = world.add_box(size=(10, 0.5, 3), position=(0, 5, 1.5), static=True)
cap = world.add_capsule(radius=0.2, half_length=2, position=(0, 0, 3), static=True)
shelf = world.add_static_box(size=(4, 0.2, 0.1), position=(0, 0, 2))
Managing bodies¶
world.remove(box)
world.clear(keep_statics=True) # remove dynamics only
print(world.time) # elapsed simulation time (s)
Body handles become stale after remove / clear
The Python Body object you hold is invalidated when you call
world.remove(body) or world.clear(). Any subsequent call that uses
the old variable — apply_impulse, teleport, reading body.position,
etc. — raises RuntimeError: Body id=N not found in world.
Pattern: always reassign after clear
Pattern: guard with world.contains()
Weld constraints with rotation (v1.1.0)¶
parent = world.add_box(size=(2, 2, 1), position=(0, 0, 2), mass=10)
child = world.add_box(size=(0.5, 0.5, 0.5), position=(1, 0, 2), mass=0.5)
# Classic weld — child follows parent position and orientation
world.weld(child, parent)
# Weld with fixed relative rotation (e.g. angled windmill blade at 90°)
world.weld(blade, hub, local_rotation=[0.707, 0, 0, 0.707]) # [w, x, y, z]
Raycast (v1.1.0)¶
hit = world.raycast(origin=(0, 0, 10), direction=(0, 0, -1), max_dist=15)
if hit:
print(f"Hit {hit.body.name} at distance {hit.distance:.2f} m")
print(f"Contact point: {hit.point}")
print(f"Surface normal: {hit.normal}")
Heightfield terrain¶
import numpy as np
heights = np.random.default_rng(42).uniform(0, 3, (32, 32)).astype(np.float32)
terrain = world.add_terrain(
heights=heights,
cell_size=2.0,
origin=(-32, -32, 0),
material=f3d.Material(color=(0.3, 0.45, 0.2), roughness=0.9),
friction=0.9, # terrain-specific friction
layer=f3d.CollisionLayer.TERRAIN, # assign to TERRAIN layer
)
# terrain is visible in Viewer and collidable (sphere + box vs heightfield).
Built-in primitive shapes¶
import forge3d as f3d
world = f3d.World(gravity=(0, 0, -9.81))
world.add_ground()
# Cylinder — aligned along +Z; half_length is half the height
pillar = world.add_cylinder(radius=0.3, half_length=1.0,
position=(0, 0, 1), static=True)
drum = world.add_cylinder(radius=0.5, half_length=0.4,
position=(2, 0, 2), mass=2.0)
# Cone — base at bottom (-Z), apex at top (+Z)
cone = world.add_cone(radius=0.4, height=0.8, position=(0, 0, 2), mass=1.0)
# Wedge (triangular prism / ramp) — slant runs front-low → back-high (+Y rises)
ramp = world.add_wedge(size=(2.0, 1.5, 0.6), position=(0, 3, 0.3), static=True)
# Convex hull from arbitrary point cloud
import numpy as np
pts = np.array([
[1, 0, 0], [-1, 0, 0],
[0, 1, 0], [0, -1, 0],
[0, 0, 1], [0, 0, -1],
], dtype=float) # octahedron
gem = world.add_convex(pts, position=(0, 0, 3), mass=1.5)
# All add_* methods share the same common keyword arguments:
# mass, static, restitution, friction, material,
# collision_layer, collision_mask, name
Wedge orientation
The ramp face runs from low (−Y side) to high (+Y side).
Rotate via quat= to orient the slope in any direction.
Mesh bodies and collision filtering¶
from forge3d.io import load_obj
mesh = load_obj("assets/tree.obj")
# Decorative prop — visible but skips all collision checks (mask=0)
tree = world.add_mesh(mesh, position=(5, 3, 0), static=True,
collision_mask=0)
# Solid obstacle on the DEFAULT layer
rock = world.add_mesh(mesh, position=(0, 0, 0), mass=5.0)
# Sensor-only trigger (detected by PLAYER but doesn't block movement)
pickup = world.add_mesh(mesh, position=(2, 0, 0), static=True,
collision_layer=f3d.CollisionLayer.TRIGGER,
collision_mask=f3d.CollisionLayer.PLAYER)
Joints¶
hinge = world.add_joint(
"hinge", door, frame,
anchor_a=(-0.5, 0, 0), anchor_b=(0, 0, 0),
axis=(0, 0, 1),
limits=(-1.5, 0.0),
motor_velocity=1.0,
motor_max_torque=30.0,
)
spring = world.add_joint(
"spring", box, ceiling,
stiffness=200.0, damping=10.0, rest_length=2.0,
)
world.remove_joint(hinge)
Fixed timestep (stable physics)¶
# Option A: world.update() — accumulates wall time, steps at fixed intervals
world.fixed_dt = 1 / 120 # physics runs at 120 Hz (default)
world.max_substeps = 8 # cap spiral-of-death
while viewer.is_open:
world.update(viewer.dt) # call once per rendered frame
viewer.draw()
# Option B: world.step(substeps=4) — split one frame into 4 sub-steps
world.step(dt=viewer.dt, substeps=4)
Spatial queries¶
# Multi-hit raycast (all intersections, sorted by distance)
hits = world.raycast_all(
origin=(0, 0, 10),
direction=(0, 0, -1),
max_dist=20.0,
layer_mask=f3d.CollisionLayer.ALL,
)
for hit in hits:
print(hit.body.name, hit.distance)
# Overlap queries — find bodies within a region
nearby = world.overlap_sphere(center=explosion_pos, radius=5.0)
in_room = world.overlap_box(center=room_center, half_extents=(5, 5, 3))
for body in nearby:
body.apply_force((0, 0, 300)) # explosion push
Character controller¶
cc = world.add_character(
position=(0, 0, 2),
height=1.8,
radius=0.3,
mass=70.0,
)
while viewer.is_open:
dx = inp.axis("right") - inp.axis("left")
cc.move(direction=(dx, 0, 0), speed=5.5, dt=viewer.dt)
if inp.just_pressed("space") and cc.is_grounded:
cc.jump(impulse=6.0)
world.step(viewer.dt)
Physics profiler¶
world.profiler.step(dt=1/60) # measure one step
print(world.profiler.last)
# PhysicsProfile(total=1.23ms contacts=8)
# Or use as context manager
with world.profiler:
world.step(dt=1/60)
avg = world.profiler.average(n=60) # 1-second rolling average
Collision events¶
@world.on_collision_begin
def hit(event: f3d.CollisionEvent) -> None:
print(event.body_a.name, "->", event.body_b.name,
f"impact={event.relative_speed:.1f} m/s")
goal = world.add_trigger_zone(position=(5, 0, 0.5), size=(1, 1, 1))
@goal.on_enter
def scored(body: f3d.Body) -> None:
print(f"Goal: {body.name}")
# Move or disable a trigger zone at runtime
goal.set_position((10, 0, 0.5))
goal.enabled = False # pause detection without removing