PVG
14. The Third Dimension — Program Video Games

14. The Third Dimension

Our 2D collision system, entity management, and movement logic work perfectly within their constraints. But extending these systems to operate in 3D space opens up new possibilities for level design, visual presentation, and spatial gameplay mechanics.

Today we're taking our existing 2D architecture and beginning the systematic extension to handle 3D coordinates, 3D collision detection, and 3D rendering whilst preserving the core logic we've already built.

The Fractal Code Cycle Applied

Recall our fundamental pattern:

Input → Processing → Output

For extending our systems to 3D:

  • Input: 2D movement input mapped to 3D space, 3D entity positions, 3D collision geometry
  • Processing: Extend existing algorithms to handle Vec3 instead of Vec2, adapt collision detection for 3D shapes (next)
  • Output: 3D positioned entities, 3D collision responses, 3D rendered output

Coordinate System Extension: Vec2 to Vec3

The fundamental change is extending our coordinate system from 2D to 3D:

// Before: 2D coordinates
Entity :: struct {
    position: Vec2,
    // ...
}

// After: 3D coordinates
Entity :: struct {
    position: Vec3,
    // ...
}

This change propagates through our entire system, but in predictable and manageable ways due to our modular architecture.

Movement System Extension

Input Mapping to 3D Space

Our 2D input system extends naturally to 3D by mapping screen-relative directions to world axes (note the reversed signs):

// 2D movement (before)
if gs.input.up do move_dir.y -= 1
if gs.input.down do move_dir.y += 1
if gs.input.left do move_dir.x -= 1
if gs.input.right do move_dir.x += 1

// 3D movement (after)
if gs.input.up do move_dir.y += 1      // Maps to positive Z
if gs.input.down do move_dir.y -= 1    // Maps to negative Z  
if gs.input.left do move_dir.x += 1    // Maps to positive X
if gs.input.right do move_dir.x -= 1   // Maps to negative X

Movement Vector Application

The movement calculation extends from 2D to 3D by mapping the 2D input vector to the XZ plane:

player_movement := rl.Vector2Normalize(move_dir) * player.movement_speed * gs.time.delta
next_player_position := player.position + Vec3{player_movement.x, 0, player_movement.y}

This preserves the Y coordinate (height) whilst applying movement in the horizontal plane.

Camera System Extension

From 2D to 3D Camera

The camera system transitions from 2D screen mapping to 3D perspective projection:

// Before: 2D camera
camera: rl.Camera2D,

// After: 3D camera with fixed perspective
camera: rl.Camera3D,
camera_base_offset: Vec3,
camera_rotation_y: f32,

Fixed Perspective Implementation

The 3D camera maintains a consistent viewing angle:

gs.camera_base_offset = Vec3{0, 4, -5}
gs.camera.up = Vec3{0, 1, 0}
gs.camera.projection = .PERSPECTIVE  
gs.camera.fovy = 45

Camera Following Logic

The camera follows the player using 3D position calculations:

Camera rotation is for our debug purposes only. The game design dictates a fixed-angle camera.

rotation_matrix := rl.MatrixRotateY(gs.camera_rotation_y)
rotated_offset := rl.Vector3Transform(gs.camera_base_offset, rotation_matrix)
gs.camera.target = player.position
gs.camera.position = gs.camera.target + rotated_offset

This demonstrates matrix transformation for 3D positioning whilst maintaining the follow camera behaviour from our 2D system.

Collision System Extension: 2D to 3D

Collider Evolution

Player collision extends from 2D circles to 3D spheres:

// Collision detection conceptually unchanged
// Circle becomes sphere, but distance-based calculations remain the same
collider_radius: f32,  // Same field, now represents sphere radius

Static Geometry Extension

Our OBB (Oriented Bounding Box) system extends to OBB3:

Keeping OBB as a 2D version of OBB3 in case we want it for UI.

// 2D OBB (before)
OBB :: struct {
    position: Vec2,
    size: Vec2,
    rotation: f32,
    debug_color: rl.Color,
}

// 3D OBB (after)
OBB3 :: struct {
    center: Vec3,
    size: Vec3,
    rotation: Vec3,  // Now supports rotation around all three axes
}

This demonstrates systematic extension: each 2D concept gains a third dimension whilst preserving the underlying structure.

Entity Positioning in 3D

Ground Plane Positioning

Entities now require proper positioning relative to the ground plane:

At the moment, the player sits just at the edge of the ground plane.
A solution to sloped movement will be incorporated in a future lesson.

player.movement_speed = 4                           // Scaled for 3D units
player.collider_radius = 0.25                      // Appropriate for 3D scale
player.position = Vec3{0, player.collider_radius * 0.5, 0}  // Sits on ground

The Y coordinate represents height, with entities positioned so their collision sphere rests on the Y=0 ground plane.

Rendering System Extension

3D Debug Rendering

Our debug rendering extends to handle 3D geometry:

// 3D grid for spatial reference
rl.DrawGrid(10, 1)

// 3D collision sphere
rl.DrawSphereWires(player.position, player.collider_radius, 8, 8, rl.GREEN)

// 3D bounding boxes
for obb in gs.physics.static_geometry {
    draw_obb3(obb, rl.GRAY)
}

OBB3 Rendering

The system optimises rendering based on box orientation:

is_obb3_axis_aligned :: proc(obb: OBB3) -> bool {
    EPSILON :: 0.001
    x_aligned := abs(math.mod(obb.rotation.x, math.PI / 2)) < EPSILON
    y_aligned := abs(math.mod(obb.rotation.y, math.PI / 2)) < EPSILON
    z_aligned := abs(math.mod(obb.rotation.z, math.PI / 2)) < EPSILON
    return x_aligned && y_aligned && z_aligned
}

Axis-aligned boxes use the existing raylib procedure rl.DrawBoundingBox, whilst arbitrary orientations require custom corner calculation and line drawing.

System Architecture Benefits

Modular Extension

Our system stack architecture allows clean extension:

  • Base Layer: Entity coordinates extend to Vec3
  • Rendering Layer: Camera system rebuilt for 3D
  • World Layer: Collision detection extended to 3D shapes
  • Higher Layers: Remain unchanged due to abstraction

Code Reuse

The extension leverages existing patterns:

  • Movement logic structure preserved
  • Collision detection concepts maintained
  • Debug rendering patterns extended
  • Input handling architecture unchanged

Debug Controls for Development

Development controls allow system verification:

if gs.input.debug_rotate_cw {
    gs.camera_rotation_y -= ROTATION_SPEED * gs.time.delta
}
if gs.input.debug_rotate_ccw {
    gs.camera_rotation_y += ROTATION_SPEED * gs.time.delta  
}
if gs.input.debug_rotate_reset {
    gs.camera_rotation_y = 0
}

These controls enable verification of 3D positioning and collision detection from multiple viewpoints.

Input Changes

New fields are added to the input struct:

debug_rotate_cw, debug_rotate_ccw, debug_rotate_reset: bool,

The input procedure is rewritten to use a procedure from core:mem to zero the inputs, setting them to false:

input :: proc() {
    mem.zero_item(&gs.input)

    if rl.IsKeyDown(.W) do gs.input.up = true
    if rl.IsKeyDown(.S) do gs.input.down = true
    if rl.IsKeyDown(.A) do gs.input.left = true
    if rl.IsKeyDown(.D) do gs.input.right = true
    if rl.IsKeyDown(.Q) do gs.input.debug_rotate_cw = true
    if rl.IsKeyDown(.E) do gs.input.debug_rotate_ccw = true
    if rl.IsKeyDown(.R) do gs.input.debug_rotate_reset = true
}

Extension vs Rewrite Philosophy

Extension isn't always possible, though when it is, the benefits are many:

  • Preserve working logic: Keep movement calculations, just add dimensions
  • Extend data structures: Vec2 becomes Vec3, OBB becomes OBB3
  • Maintain interfaces: Higher-level systems see same access patterns
  • Add capabilities: 3D rendering and collision without breaking existing features

The Big Idea

Systematic extension of 2D systems to 3D demonstrates the power of good architecture. By building modular systems with clear interfaces, we can add entire new dimensions (literally) without rewriting our core game logic.

The coordinate system change from Vec2 to Vec3 propagates predictably through our codebase, and our collision detection concepts translate directly from circles and rectangles to spheres and boxes.

This extension approach allows us to leverage everything we've built whilst opening up new possibilities for spatial gameplay and visual presentation.