Skip to content

What's New in v2.2.0

v2.2 absorbs game-loop boilerplate into the library — inputs, camera, and character movement are now one-liners. All changes are fully backward-compatible.


New features

Input.axis() — compact movement input

# before
move_x = float(inp.key_held(f3d.Key.D)) - float(inp.key_held(f3d.Key.A))

# after
move_x = inp.axis(f3d.Key.A, f3d.Key.D)   # → float in [-1, 1]
move_y = inp.axis(f3d.Key.S, f3d.Key.W)

Also available on ScriptedInput for deterministic testing.

OrbitCamera.handle_input() — camera in one call

# before (per-frame event parsing)
dx, dy = inp.mouse_delta()
if inp.mouse_button(1):
    cam.rotate(d_azimuth=dx * 0.5, d_elevation=-dy * 0.5)
cam.zoom(-inp.scroll_delta() * 0.5)

# after
cam.handle_input(inp, dt)   # drag + Q/E rotate + scroll zoom, all built in

CharacterController.move_camera_relative() — camera-space movement

# before — manual yaw computation
import math
yaw = math.atan2(cam_dir[1], cam_dir[0])
fx = math.cos(yaw) * move_x + ...

# after
cc.move_camera_relative(inp, cam, speed=5.5, dt=dt)

Viewer.show_grid — public property

viewer = f3d.Viewer(world, show_grid=True)   # constructor parameter
viewer.show_grid = False                      # or toggle at runtime

Replaces internal viewer._renderer._show_grid access.

Viewer.draw_text — anchor-based layout

viewer.draw_text("Score: 100",  anchor="topcenter",    margin=10)
viewer.draw_text("Lives: 3",    anchor="topleft",       margin=10)
viewer.draw_text("GAME OVER",   anchor="bottomcenter",  margin=40)
viewer.draw_text("Press ENTER", anchor="bottomright",   margin=10)

x and y are still accepted for pixel-exact placement; omit them to use anchor.

App render parameters at construction

app = f3d.App(
    "My Game",
    width=1920, height=1080,
    substeps=4,
    shadow_resolution=2048,
    sky_color=(0.1, 0.15, 0.25),
    show_grid=False,
    max_dt=1/30,
)

Bug fix

Sentry hover drift (apps/game/enemies.py)

The hover raycast was missing layer_mask, causing sentries to detect their own body and drift upward. Fixed with:

hit = world.raycast(origin, (0, 0, -1), max_dist=2.0,
                    layer_mask=CollisionLayer.DEFAULT | CollisionLayer.TERRAIN)

Migration

No breaking changes. All existing code runs unchanged.

Old pattern v2.2 alternative
float(inp.key_held(D)) - float(inp.key_held(A)) inp.axis(A, D)
Per-frame camera event parsing cam.handle_input(inp, dt)
Manual yaw + cc.move(...) cc.move_camera_relative(inp, cam, speed, dt)
viewer._renderer._show_grid = True viewer.show_grid = True
draw_text(text, x=640, y=10, ...) draw_text(text, anchor="topcenter", margin=10)