Skip to content

forge3d.CharacterController

A character controller built on a capsule rigid body with ground detection via downward raycast. Use world.add_character() to create one.


Class reference

CharacterController

Kinematic character controller backed by a capsule rigid body.

Returned by :meth:forge3d.World.add_character.

Typical game loop::

cc = world.add_character(position=(0, 0, 2), height=1.8, radius=0.3)

while viewer.is_open:
    inp = viewer.input
    dx = inp.axis("right") - inp.axis("left")   # -1 … 1
    dy = inp.axis("up")    - inp.axis("down")
    cc.move(direction=(dx, dy, 0), speed=5.5, dt=viewer.dt)
    if inp.just_pressed("space"):
        cc.jump(impulse=6.4)
    world.step(viewer.dt)

Attributes

body : The underlying :class:Body (capsule). is_grounded : True when the character is standing on solid ground. is_airborne : True when the character is not grounded. velocity : Current linear velocity (3,).

position property

position: ndarray

Current world position (3,).

velocity property

velocity: ndarray

Current linear velocity (3,) in m/s.

is_grounded property

is_grounded: bool

True if the character is touching the ground.

is_airborne property

is_airborne: bool

True if the character is in the air.

move

move(direction: Any, speed: float, dt: float) -> None

Apply horizontal movement toward direction at speed m/s.

Also carries the character on moving platforms automatically — no manual delta-passing needed.

Parameters

direction : (3,) movement vector (only x/y components used unless z != 0). Does not need to be normalised. speed : Maximum movement speed in m/s. dt : Frame delta-time in seconds.

jump

jump(impulse: float = 5.0) -> None

Apply an upward velocity impulse if grounded.

Parameters

impulse : Upward speed added in m/s (think: initial jump velocity).

glide

glide(target_fall_speed: float = -1.5, dt: float = 1 / 60) -> None

Reduce falling speed to target_fall_speed for a glide effect.

Call each frame while glide input is held.

Parameters

target_fall_speed : Target downward z-velocity (negative = downward). dt : Frame delta-time (used to smooth the transition).


Creating a character

import forge3d as f3d

world = f3d.World(gravity=(0, 0, -9.81))
world.add_ground()

cc = world.add_character(
    position=(0, 0, 2),
    height=1.8,       # capsule height (m)
    radius=0.3,       # capsule radius (m)
    mass=70.0,        # kg
    name="player",
    ground_layer_mask=f3d.CollisionLayer.TERRAIN,
)

Game loop integration

viewer = f3d.Viewer(world, width=1280, height=720, title="Character Demo")
cam = f3d.FollowCamera(cc._body, offset=(-5, 0, 2.5), frame="local")

while viewer.is_open:
    inp = viewer.input
    dt = viewer.dt

    # Horizontal movement
    dx = float(inp.key_held(f3d.Key.D)) - float(inp.key_held(f3d.Key.A))
    dy = float(inp.key_held(f3d.Key.W)) - float(inp.key_held(f3d.Key.S))

    cc.move(direction=(dx, dy, 0), speed=5.5, dt=dt)

    # Jump only if on the ground
    if inp.key_pressed(f3d.Key.SPACE) and cc.is_grounded:
        cc.jump(impulse=6.0)

    # Slow fall when holding CTRL
    if inp.key_held(f3d.Key.CTRL) and cc.is_airborne:
        cc.glide(target_fall_speed=1.0, dt=dt)

    world.step(dt)
    viewer.set_camera(cam.to_snapshot(dt=dt))
    viewer.draw()

    viewer.draw_text(
        f"Grounded: {cc.is_grounded}  Speed: {cc.velocity[0]:.1f} m/s",
        x=10, y=10, size=18,
    )

Properties reference

Property Type Description
position ndarray (3,) World-space capsule base position
velocity ndarray (3,) Linear velocity (m/s)
is_grounded bool True when standing on a surface
is_airborne bool not is_grounded

Method reference

Method Description
move(direction, speed, dt) Apply horizontal velocity; direction is automatically normalized
jump(impulse) Apply an upward velocity impulse; only effective when grounded
glide(target_fall_speed, dt) Dampen downward velocity for a glide / slow-fall effect
set_position(pos) Teleport the character to a new world position

Ground detection

The controller shoots a downward ray of length height/2 + radius + 0.15 m from the capsule centre. is_grounded is True when this ray hits a body matching ground_layer_mask (default: CollisionLayer.TERRAIN | CollisionLayer.DEFAULT).

To stand on non-terrain bodies, adjust the mask:

cc = world.add_character(
    position=(0, 0, 2),
    ground_layer_mask=(
        f3d.CollisionLayer.TERRAIN |
        f3d.CollisionLayer.DEFAULT |
        f3d.CollisionLayer.ENEMY   # can stand on top of enemies
    ),
)