Module 12

3D Platformer

Weeks 23-24 | Camera control, spatial design, and the art of the jump in three dimensions.

"In a 2D platformer, the camera is solved. In a 3D platformer, the camera IS the problem. Everything else is downstream of whether the player can see where they're going."


Prerequisites

ModuleWhat You Used From It
Module 02 - PlatformerGravity, jump arcs, grounded-state detection, tile collision, game feel
Module 11 - First-Person Game3D coordinate systems, transforms, 3D collision, basic lighting, working in an engine

Week 1: History & Design Theory

The Origin

Shigeru Miyamoto's team at Nintendo spent months on Super Mario 64 (Nintendo, 1996) doing something that sounds absurd: just making Mario feel good to move around. Before a single level was designed, before a single enemy was placed, they built a small room and tuned Mario's run, jump, triple-jump, long-jump, wall-jump, backflip, and ground-pound until the act of controlling him was intrinsically fun.

The camera system was revolutionary and openly acknowledged the problem it was solving. Lakitu, a character who had been a cloud-riding enemy in previous Mario games, was recast as a cameraman literally flying behind Mario with a camera on a fishing pole. This was Miyamoto telling the player: "Yes, the camera is a separate thing you manage. Here's a friendly face to make that feel natural."

How the Genre Evolved

Crash Bandicoot (Naughty Dog, 1996) solved the camera problem through a completely different philosophy: constrain it. Rather than giving players a free camera, Crash's camera was mostly fixed — behind the player on rails. By limiting the camera, Naughty Dog ensured the player always had a readable view.

Banjo-Kazooie (Rare, 1998) expanded the collect-a-thon design that Mario 64 had introduced. Multiple move types, dozens of collectible types, and ability unlocks that opened new paths in previously visited worlds demonstrated that 3D platformers could be about thorough exploration of dense spaces.

A Hat in Time (Gears for Breakfast, 2017) and Astro Bot (Team Asobi, 2024) prove the genre is alive and still evolving, pushing the boundaries of what a 3D platformer camera can do.

What Makes 3D Platformers Great

The 3D platformer is a negotiation between two systems in constant tension: character control and camera control. The player needs to execute precise jumps, but they also need to see where they are jumping. These goals frequently conflict. Great 3D platformers resolve this through generous player mechanics (double-jumps, air control, coyote time), smart camera behavior, and level design that is readable from multiple angles.

The Essential Mechanic

Jumping and landing on platforms while managing a camera that must make 3D space readable — the marriage of character control and camera control.


Week 2: Build the MVP

What You're Building

A small 3D level with platforms at varying heights and positions. The focus is on making the jump feel good, the camera feel helpful, and the space feel readable. A tight, well-designed space with 10-15 platforms is more valuable than a sprawling empty world.

Core Concepts (Must Implement)

1. Third-Person Camera System

An orbit camera that follows the player at a fixed distance and can be rotated by the player. The camera must handle collision — if it would clip through a wall, it moves closer to the player.

// Orbit camera each frame:
yaw   += inputX * rotateSpeed
pitch += inputY * rotateSpeed
pitch  = clamp(pitch, -30, 60)

// Desired camera position: offset behind and above player
offset.x = -sin(yaw) * cos(pitch) * distance
offset.y =  sin(pitch) * distance
offset.z = -cos(yaw) * cos(pitch) * distance

desiredPosition = player.position + offset

// Camera collision: raycast from player to desired position
hit = raycast(player.position, direction_to(desiredPosition), distance)
if hit:
  camera.position = hit.point + hit.normal * skinWidth
else:
  camera.position = desiredPosition

camera.lookAt(player.position + (0, 1, 0))

Why it matters: The third-person camera is arguably the hardest unsolved problem in game design. No automatic system works perfectly in all situations.

Interactive: Camera Orbit Demo

Drag on the canvas to orbit the camera around the player cube on the platform. The camera position coordinates update in real-time. The camera always looks at the player.

2. 3D Character Controller with Jump

Extend the Module 11 character controller with a jump system. Gravity pulls the player down each frame. When grounded and the jump button is pressed, apply an upward velocity impulse.

// Gravity:
velocity.y -= gravity * dt

// Ground detection:
groundHit = spherecast(player.position, DOWN, radius=0.3, distance=0.1)
isGrounded = groundHit != null

// Jump:
if isGrounded and jumpPressed:
  velocity.y = jumpForce

player.position += velocity * dt

Why it matters: The 3D jump is the direct evolution of Module 2's gravity and grounded-state detection. The core physics are identical but ground normals and slope handling add complexity.

3. Camera-Relative Movement

When the player pushes the stick forward, the character must move toward where the camera is facing, not toward a fixed world direction.

// Get camera's ground-plane directions:
camForward = camera.forward
camForward.y = 0
camForward = normalize(camForward)

camRight = camera.right
camRight.y = 0
camRight = normalize(camRight)

moveDir = camForward * stickY + camRight * stickX

if length(moveDir) > 0:
  moveDir = normalize(moveDir)
  player.rotation = look_rotation(moveDir)
  player.position += moveDir * moveSpeed * dt

This is THE key UX insight of 3D platformers. Without camera-relative movement, the player would need to mentally translate between "push stick up" and "character moves world-north."

Why it matters: Camera-relative movement is what separates a playable 3D game from a frustrating one. This same pattern applies to every third-person game.

Interactive: Camera-Relative Movement

Use arrow keys to move the player. Drag the canvas to orbit the camera. Notice how "Up" always moves the character toward where the camera faces, not world-north. A top-down minimap shows actual world directions vs. camera directions.

4. 3D Spatial Design and Level Layout

Design platforms and spaces that are readable from a dynamic camera angle. Key design principles:

platforms = [
  { position: (0, 0, 0),   size: (5, 0.5, 5),  color: "green"  },  // start
  { position: (4, 1, 3),   size: (2, 0.5, 2),  color: "green"  },  // step up
  { position: (8, 2.5, 1), size: (2, 0.5, 2),  color: "green"  },  // gap jump
  { position: (8, 4, 6),   size: (3, 0.5, 3),  color: "gold"   },  // goal
]

Why it matters: Level design in 3D is harder than in 2D because the player's viewpoint is variable. You must design spaces that communicate effectively from many possible camera angles.

5. Shadow / Blob Under Player

Project a dark circle directly below the player onto whatever surface is underneath. This tells the player exactly where they will land.

// Each frame, cast a ray down from the player:
shadowHit = raycast(player.position, DOWN, maxDistance=50)

if shadowHit:
  blobShadow.position = shadowHit.point + shadowHit.normal * 0.01
  blobShadow.rotation = align_to_normal(shadowHit.normal)
  blobShadow.visible = true
  height = player.position.y - shadowHit.point.y
  blobShadow.scale = max(0.5, 1.0 - height * 0.05)
else:
  blobShadow.visible = false

Why it matters: Without a shadow, players cannot tell if they are directly above a platform or five meters to the left. This is a "generous lie" — real shadows are complex and expensive, but a simple blob projected downward solves the gameplay problem. Nearly every 3D platformer uses this technique.

Interactive: Shadow Blob Demo

A character jumps back and forth across a gap between two platforms. Toggle the shadow blob on/off with the button to see how it helps judge landing position. Without it, gauging where you will land becomes much harder.

6. Moving Platforms in 3D

Create platforms that move along a path. When the player stands on a moving platform, they must move with it. This requires parent-child transform relationships.

// Moving platform update:
platform.t += speed * dt
platform.position = lerp(pointA, pointB, ping_pong(platform.t))

// When player lands on a moving platform:
if player.groundHit.object == platform:
  platformDelta = platform.position - platform.previousPosition
  player.position += platformDelta

Why it matters: Moving platforms introduce the critical distinction between local space and world space. This parent-child transform relationship is the same pattern used for any hierarchical attachment in 3D.

7. Collectibles in 3D Space

Place items throughout the level to guide the player. Collectibles serve as breadcrumbs — a trail of items leading toward a platform implicitly tells the player "jump here."

// Collectible behavior each frame:
collectible.rotation.y += spinSpeed * dt

dist = distance(player.position, collectible.position)
if dist < pickupRadius:
  player.score += collectible.value
  play_sound("collect")
  spawn_particles(collectible.position)
  collectible.active = false

Why it matters: Collectibles solve a navigation problem unique to 3D: the player can get lost. They provide implicit wayfinding and secondary objectives.

8. Double-Jump and Air Control

Give the player a second jump in mid-air and allow directional influence while airborne. Neither is physically realistic, but both are essential for making 3D platforming feel responsive and forgiving.

jumpsRemaining = maxJumps   // 2 for double-jump

if jumpPressed and jumpsRemaining > 0:
  velocity.y = jumpForce
  jumpsRemaining -= 1

if isGrounded:
  jumpsRemaining = maxJumps

// Air control:
if not isGrounded:
  airMoveDir = camForward * stickY + camRight * stickX
  velocity.x += airMoveDir.x * airControlStrength * dt
  velocity.z += airMoveDir.z * airControlStrength * dt

Why it matters: These are the same "generous lies" from Module 2, evolved for 3D. The added dimension makes precise jumping harder, so the game compensates by giving the player more tools.


Stretch Goals

MVP Spec

FeatureRequired
Third-person orbit camera with collisionYes
3D character with gravity and jumpYes
Camera-relative movementYes
At least 10 platforms at varying heightsYes
Blob shadow under the playerYes
At least one moving platformYes
Collectibles that guide the playerYes
Double-jump or air controlYes
Wall-jumpStretch
Checkpoints / respawn on fallStretch
Camera auto-adjustmentStretch
Character animation state machineStretch

Deliverable

A playable 3D platformer level with camera control, jumping, and collectibles. Write-up: What did you learn? How does designing for 3D space differ from 2D? What was your biggest camera challenge?


Analogies by Background

These analogies map 3D platformer concepts to patterns you already know. Find your background below.

For Backend Developers

ConceptAnalogy
Third-Person Camera SystemLike a load balancer health check that tracks the player but must avoid collisions, adjusting distance dynamically
3D Character Controller with JumpLike a state machine managing a connection lifecycle (IDLE, ACTIVE, CLOSING)
Camera-Relative MovementLike resolving relative paths — ./forward is interpreted relative to the current working directory (camera orientation)
3D Spatial Design / Level LayoutLike API design — the level must communicate its structure to the player through intuitive patterns
Shadow / Blob Under PlayerLike a lightweight status probe — a constant downward raycast giving real-time position feedback
Moving Platforms in 3DLike container orchestration parent-child — the player inherits the platform's position
Collectibles in 3D SpaceLike breadcrumb logging or distributed tracing — guiding through a complex path
Double-Jump / Air ControlLike retry policies — a second attempt and course correction rather than failing hard

For Frontend Developers

ConceptAnalogy
Third-Person Camera SystemLike a scroll-follow with intersection observers that detects overlaps and adjusts position
3D Character Controller with JumpLike a CSS transition with cubic-bezier easing — the jump arc is a curve
Camera-Relative MovementLike resolving CSS transform-origin — movement is relative to the camera's coordinate system
3D Spatial Design / Level LayoutLike responsive design — the level must be readable at different camera angles
Shadow / Blob Under PlayerLike a tooltip or cursor follower projected onto a surface
Moving Platforms in 3DLike a child element inside a CSS-transformed parent — position: relative and absolute
Collectibles in 3D SpaceLike visual affordances in UI — spinning, glowing items say "interact with me"
Double-Jump / Air ControlLike undo/redo in a text editor — a second chance after committing to an action

For Data / ML Engineers

ConceptAnalogy
Third-Person Camera SystemLike a constrained optimization problem — minimizing distance to a target subject to constraints
3D Character Controller with JumpLike numerical integration of a differential equation — Euler method with impulse
Camera-Relative MovementLike a change of basis — input in camera basis vectors transformed to world coordinates
3D Spatial Design / Level LayoutLike feature engineering for interpretability — reducing the dimensionality of navigation
Shadow / Blob Under PlayerLike a projection onto a lower-dimensional subspace
Moving Platforms in 3DLike reference frame transformations in physics simulation
Collectibles in 3D SpaceLike reward shaping in reinforcement learning — guiding toward the goal
Double-Jump / Air ControlLike regularization — relaxing constraints to get better generalization (playability)

Discussion Questions

  1. Super Mario 64 and Crash Bandicoot launched the same year with opposite approaches to the camera: free control vs. constrained rails. What are the tradeoffs?
  2. The blob shadow is a "fake." Why is this acceptable? What other "lies" does your game tell the player?
  3. Compare your 2D platformer from Module 2 to this 3D platformer. Which concepts transferred directly, and which required fundamentally different solutions?
  4. Camera-relative movement means "forward" changes meaning whenever the camera rotates. What happens if the camera rotates while the player is holding forward?