Skip to content

Movement and Collision

Movement in cub3D is applied from intent, not directly from raw key events. The engine first computes frame-based displacement, then resolves collision against the map so the player never crosses blocking geometry.

Frame timing

`delta_time` turns speed per second into movement per frame

FPS varies with the machine and rendering workload. The engine measures elapsed time since the previous frame, then multiplies speed by that time to keep movement consistent.

01

Compare FPS

Lower FPS means a larger delta_time. One-frame movement increases, but the total over one real second remains equal to the chosen speed.

`delta_time` 0.0167 s
Movement per frame 0.1000 u
Total over 1 second 6.00 u/s
02

Without `delta_time` vs with it

Without delta_time, adding a fixed amount each frame makes the player faster on a computer that renders more frames.

Without `delta_time` 360 u
With `delta_time` 6 u
0 seconds 1 real second
03

Computed at the start of `draw_frame`

Time comes from gettimeofday. The first frame uses a default value, and large spikes are clamped with FRAME_DT_MAX.

now = get_time_seconds();
app->delta_time = now - app->last_frame_time;

if (app->delta_time > FRAME_DT_MAX)
    app->delta_time = FRAME_DT_MAX;

app->last_frame_time = now;
04

Consumed by movement

`MOVE_SPEED` is a speed in units per second. Multiplying it by `delta_time` gives the small distance to apply this frame.

move_with_collision(app,
    app->dir_x * MOVE_SPEED * app->delta_time,
    app->dir_y * MOVE_SPEED * app->delta_time);
FPS

Number of frames produced per second. It varies by machine.

`delta_time`

Elapsed time since the previous frame.

`MOVE_SPEED`

Desired player speed in units per second.

movement = 6 * 0.0167 = 0.1000 u per frame

Collision pipeline

Intent is computed first, then resolved axis by axis

Movement starts as a desired `(dx, dy)` from input and `delta_time`. Collision then validates each axis independently, which is what allows smooth wall sliding instead of hard rejection at corners.

01
compute intended movement

Movement helpers build frame-based displacement from camera vectors.

// move_forward / move_backward along dir
dx = dir_x * move_speed * delta_time
dy = dir_y * move_speed * delta_time

// strafe_left / strafe_right use the perpendicular direction
dx =  dir_y * move_speed * delta_time
dy = -dir_x * move_speed * delta_time
`W/S` → `dir` `A/D` → perpendicular move `delta_time` → frame-rate independent
02
resolve collision per axis

X and Y are validated separately inside small movement steps, so blocked corners still allow sliding.

X axis new_x = player.x + dx

apply X only if center and radius samples stay clear

Y axis new_y = player.y + dy

apply Y only if center and radius samples stay clear

Interactive collision preview

Click a floor cell to toggle a wall. The moving dot keeps trying to advance and demonstrates why split X/Y checks slide along geometry.

player (1.50, 3.00)
03
solidity source stays shared

Collision and raycasting must agree on what blocks the world.

wall tile

`1` or any mandatory solid blocks movement immediately

blocking
closed door

bonus door logic reports it as solid

blocking
open door

door query returns passable state

passable
moving door

collision depends on current open progress

dynamic
post-move invariants

The player remains in navigable space, corner cases are stable, and blocking logic stays aligned with the render path.

no wall tunneling wall sliding preserved corners handled safely door passability respected

Movement is driven by input flags and frame timing through app->delta_time.

The main motion components are:

  • forward / backward along the current view direction
  • strafe left / right on the perpendicular axis
  • rotation handled separately by the camera update path

This keeps controls stable across frame rates.

The current code uses frame-based displacement, sub-stepping, and radius sampling before committing player position.

Core flow:

  1. compute intended dx and dy
  2. split large movement into small steps to avoid tunneling
  3. test the player center and surrounding radius samples against collision rules
  4. apply only the safe axis updates

This prevents wall crossing, reduces corner clipping, and keeps movement stable when delta_time changes.

The engine does not validate one combined (x + dx, y + dy) jump only. Instead, it resolves axes independently.

That design matters because:

  • blocked corners still allow movement on the free axis
  • sliding along walls remains smooth
  • full-vector rejection does not freeze motion unnecessarily

This is why the player can “glide” along a wall instead of sticking the moment one side collides.

Collision logic must stay aligned with rendering logic.

That means blocking queries take into account:

  • mandatory wall tiles
  • bonus solid tiles
  • door passability state in bonus mode

If collision and raycasting disagree on solidity, the player would see geometry that does not match gameplay behavior.

After each collision-safe update:

  • the player remains in navigable space
  • no movement step tunnels through a wall
  • sliding behavior stays deterministic near corners
  • door state is respected at the current frame
  • srcs/input/move_player.c
  • srcs/input/move_collision.c
  • srcs/input/input_update.c
  • srcs/validation/validate_map_closed.c
  • srcs_bonus/doors/