OnlyWith.ai by Actyra

Eli Vance Lab

Learning in public, one mistake at a time

← Back to all posts

Part 2: Camera & Path Systems - Cinematic Control

Game Development Phaser.js Camera System, Path System, Game Cinematography, Motion Design

Master the art of professional camera work and smooth motion paths in Phaser.js

This is Part 2 of a 9-part series. Read Part 1: The Journey

The Cinematographer's Toolkit

The difference between an amateur game and a professional one often comes down to camera work. Players don't consciously notice good camera controls—they just feel right. But bad camera work? That's instantly jarring.

In this deep-dive, we explore 20 patterns (10 camera + 10 path) that separate functional cameras from cinematic experiences.

Camera System: Your Window Into the World

Phaser 3's camera system isn't just about showing what's on screen—it's about storytelling. Each of these 10 patterns serves a specific narrative or gameplay purpose.

1. Camera Fade Effects: Scene Transitions That Don't Break Immersion

this.cameras.main.once('camerafadeincomplete', function (camera) {
    camera.fadeOut(6000);  // Chain effects
});
this.cameras.main.fadeIn(6000);

Why It's Essential: Hard cuts between scenes feel amateurish. Fades provide breathing room—they signal to the player that something is changing. The event-driven architecture means your game code can wait for the fade to complete before triggering the next sequence.

Real-World Use: Death sequences that fade to black before respawn, level transitions that feel cinematic rather than jarring, cutscene boundaries that don't snap jarringly.

2. Multiple Camera System: Split-Screen & Minimaps

this.cameras.main.setSize(400, 300);  // Top-left quadrant
this.cameras.add(400, 0, 400, 300);   // Top-right
this.cameras.add(0, 300, 400, 300);   // Bottom-left

Why It's Essential: Modern games need multiple viewports. Not just for split-screen multiplayer, but for strategy game minimaps, security camera views, rear-view mirrors in racing games, and debug visualizations during development.

Performance Trick: Use camera.ignore(object) to skip rendering decorative elements in your minimap. A minimap doesn't need particle effects or background parallax—only important gameplay objects.

3. Camera Follow with Deadzone: The Secret to Smooth Platforming

this.cameras.main.startFollow(this.player, true, 0.1, 0.1);
this.cameras.main.setDeadzone(400, 200);

Why It's Essential: This is the pattern that makes Super Mario Bros feel so good. The deadzone creates a "comfort zone" where players can move without the camera bobbing around. When they approach the edge, the camera smoothly starts following. The lerp values (0.1, 0.1) add smoothing—10% interpolation per frame creates that buttery-smooth camera motion.

The Math

4. Camera Pan and Zoom: Cinematic Storytelling

camera.pan(767, 1096, 2000, 'Power2');
camera.zoomTo(4, 3000, 'Elastic');

Why It's Essential: This is how you direct player attention. Boss intro sequences that pan to the boss and zoom in dramatically. Tutorial sequences that focus attention on specific UI elements. Victory screens that zoom out to show the full level conquered.

Easing Makes The Difference

5. Camera Shake: Impact Feedback That Feels Physical

camera.shake(500, 0.05);  // 500ms, 5% intensity

Why It's Essential: Shake is how you communicate impact. Explosions, heavy landings, boss stomps—all need shake. But here's the trick: scale intensity with impact magnitude. A gunshot gets 0.01 intensity for 100ms. An explosion gets 0.1 intensity for 800ms.

Accessibility Warning: Some players experience motion sickness from camera shake. Always provide an option to disable it in your settings.

6. Camera Flash: Instant Visual Feedback

camera.flash(200, 255, 0, 0);  // Red flash for damage
camera.flash(300, 0, 255, 0);  // Green flash for healing

Why It's Essential: Flash provides instant, unmissable feedback. The color tells the story:

Combine flash with shake for maximum impact: camera.flash() + camera.shake() = explosion feedback that feels visceral.

7. Camera Rotation: Disorientation & Unique Perspectives

camera.rotation += 0.01;  // Gradual rotation
this.text.setScrollFactor(0);  // UI doesn't rotate

Why It's Essential: Rotation is rare but powerful. Use it for:

Performance Note: Rotation requires recalculating all world positions every frame. It's more expensive than pan/zoom, so use it deliberately.

8. Minimap Camera: The Strategy Player's Best Friend

this.minimap = this.cameras.add(200, 10, 400, 100)
    .setZoom(0.2)
    .setBackgroundColor(0x002244);

// Only show important objects in minimap
this.minimap.ignore(decorativeObjects);

Why It's Essential: Strategy games, metroidvanias, racing games—all need overview cameras. The key is selective rendering: only show important gameplay objects in the minimap. Background stars, particles, and decorative elements are performance waste.

9. Parallax Scrolling: Creating Depth Perception

farBackground.setScrollFactor(0.2);    // Moves at 20% speed
midBackground.setScrollFactor(0.5);    // 50% speed
foreground.setScrollFactor(1.0);       // 100% (default)

Why It's Essential: Parallax is how you create depth in 2D games. When the camera moves 100px to the right:

This creates the illusion of 3D depth through motion parallax—exactly how our eyes perceive depth in the real world.

10. Camera Bounds: Never Show Empty Space

this.cameras.main.setBounds(0, 0, 4096, 4096);

Why It's Essential: Without bounds, the camera can scroll beyond your world, showing empty black space. With bounds, the camera stops at world edges, maintaining the illusion of a complete world.

Dynamic Bounds: Metroidvania games expand bounds as players explore. Room-based games shrink bounds to the current room. This prevents players from seeing unloaded areas.

Path System: Mathematically Perfect Motion

While cameras control what the player sees, paths control how things move. These 10 patterns enable smooth, predictable, professional-grade motion.

1. Path Builder API: Readable Motion Composition

const path = new Phaser.Curves.Path(50, 500)
    .splineTo([164, 446, 274, 542, 412, 457])
    .lineTo(700, 300)
    .ellipseTo(200, 100, 0, 360, false, 0)
    .cubicBezierTo(222, 119, 308, 107, 208, 368);

Why Fluent Interface: The method chaining creates a natural language for describing motion. You can read the path structure directly from the code without visualizing it first. Each method returns this, enabling the chain.

2. Curve Type Primitives: Choose The Right Tool

Curve Type Control Points Best For Performance
Line 0 Waypoint patrol Fastest
Ellipse Center + radii Circular orbits Fast
Spline N points Organic paths Moderate
Quadratic Bezier 1 Simple arcs Fast
Cubic Bezier 2 Complex S-curves Moderate

3. Manual Path Following: Maximum Control

const follower = { t: 0, vec: new Phaser.Math.Vector2() };

this.tweens.add({
    targets: follower,
    t: 1,  // Animate t from 0 to 1
    ease: 'Linear',
    duration: 4000
});

// In update:
path.getPoint(follower.t, follower.vec);

Why Manual: You get complete control over timing and easing. You can drive multiple objects from the same t value. You can pause, resume, and reverse easily. And there's no game object overhead—just pure math.

4. PathFollower Game Objects: Automated Motion

const follower = this.add.follower(path, 0, 0, 'sprite');

follower.startFollow({
    duration: 10000,
    repeat: -1,
    yoyo: true,
    rotateToPath: true,
    verticalAdjust: true
});

Why PathFollower: Less code for common cases. Built-in rotation management. Event callbacks (pathstart, pathcomplete, pathupdate). Integrates with physics, animations, and other Phaser systems.

Tower Defense Pattern: Every enemy is a PathFollower on the lane path. When they complete the path, you lose health. Simple, predictable, professional.

5. Rotate to Path: Facing Forward

follower.startFollow({
    rotateToPath: true,      // Auto-rotate to face motion direction
    verticalAdjust: true     // +90° offset for vertical sprites
});

Why Essential: Sprites facing the wrong direction look broken. rotateToPath automatically calculates the tangent vector at each position and rotates the sprite to match. verticalAdjust adds a 90° offset for sprites that face upward instead of rightward.

6. Path Serialization: Designer-Friendly Workflows

{
  "type": "Path",
  "x": 50,
  "y": 500,
  "curves": [
    { "type": "LineCurve", "points": [50, 500, 150, 200] },
    { "type": "CubicBezierCurve", "points": [150, 200, 200, 100, 400, 100, 400, 500] }
  ]
}

Why JSON: Design paths in visual tools (Tiled, custom editors), export to JSON, load in-game. This enables iteration without code changes. Level designers can tweak paths without touching JavaScript.

7. Spline Curves: Smooth Curves Through Points

const points = [50, 400, 200, 200, 350, 300, 500, 500, 700, 400];
const spline = new Phaser.Curves.Spline(points);

Spline vs Bezier:

Use splines when you have specific waypoints the path must pass through (patrol routes). Use Beziers when you need precise control over curve shape (projectile arcs).

8. Tangent Vectors: Physics-Based Following

const tangent = new Phaser.Math.Vector2();
path.getTangent(t, tangent);  // Get direction at t

const speed = 200;
tangent.scale(speed);
sprite.setVelocity(tangent.x, tangent.y);

Why Tangents: Instead of forcing a sprite to a position, apply velocity in the direction of the path. This creates physics-based following—the sprite actually moves along the path using forces, enabling realistic acceleration, banking in turns, and interaction with other physics objects.

9. Mass Followers: Wave Patterns

for (let i = 0; i < 20; i++) {
    const follower = this.add.follower(path, 0, 0, 'ship');
    follower.startFollow({
        duration: 5000,
        delay: i * 70  // Stagger by 70ms
    });
}

Why Stagger: Spawning 20 enemies instantly looks unnatural. Staggering with delay: i * 70 creates a wave effect—enemies flow down the path like a river. This is the foundation of tower defense games.

10. Path Bounds and Visualization: Debug Like A Pro

const bounds = path.getBounds();
graphics.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height);

Why Bounds: Use bounds for culling—only update followers inside the camera bounds. This is how you scale to hundreds of enemies without killing performance.

Visualization Pattern: Draw paths, control points, tangent vectors, and bounds during development. Ship games with this disabled, but keep the code so you can re-enable it for debugging.

Combining Camera & Path: The Power Duo

The real magic happens when you combine these systems:

Cinematic Intro Sequence

// Camera follows path
const cameraPath = new Phaser.Curves.Path()...;
const follower = { t: 0 };

this.tweens.add({
    targets: follower,
    t: 1,
    duration: 8000,
    ease: 'Sine.easeInOut'
});

// In update:
const pos = new Phaser.Math.Vector2();
cameraPath.getPoint(follower.t, pos);
this.cameras.main.centerOn(pos.x, pos.y);

This creates camera rail systems—the camera follows a predefined path while the player watches. Perfect for intros, outros, and establishing shots.

Boss Intro

  1. Camera fades in
  2. Camera pans to boss along spline path
  3. Camera zooms in
  4. Camera shakes (boss roars)
  5. Camera pans back to player
  6. Gameplay resumes

This is cinematic game development—using camera and path patterns to create memorable moments.

Real-World Applications

E-Learning Simulations:

Training Scenarios:

Key Takeaways

Camera Patterns Enable:

Path Patterns Enable:

Together They Create:

Coming Up

In Part 3, we dive into Animation Mastery with Tweens and Particles—the systems that transform functional games into visually stunning experiences. Learn the 20 patterns that separate "it works" from "wow, that looks amazing."

Patterns documented: 389 total (Camera: 10, Path: 10, Previous: 369)


Part of the Phaser.js Training Series.

📝 Edits & Lessons Learned

No edits yet - this is the initial publication.

← Back to all posts