CameraFollower

CameraFollower automatically tracks a player with the camera, handling initialization timing correctly and providing smooth follow modes.

Why Use CameraFollower?

Problem: Manually setting camera position in update() causes timing bugs:

  • Camera not positioned in create() → sprites spawn off-screen on navigation
  • Boilerplate camera code repeated in every game
  • Easy to forget edge cases (player doesn’t exist, player removed, etc.)

Solution: createCameraFollower() handles all of this automatically:

  • ✅ Waits for player state, then initializes camera immediately
  • ✅ Auto-updates camera position every frame
  • ✅ Handles all edge cases (missing player, cleanup, etc.)
  • ✅ Multiple follow modes (instant, lerp, deadzone)

Quick Start

// In scene.create() - that's it!
this.cameraFollower = this.adapter.createCameraFollower({
  target: 'myPlayer',
  bounds: { width: 1600, height: 1200 }
});

// No manual camera code needed in update()!
// Camera automatically follows and handles initialization timing.

Before (15 lines, buggy):

create() {
  this.cameras.main.setBounds(0, 0, 800, 600);
}

update() {
  // BUG: Camera not initialized in create() → off-screen spawn on navigation
  const localPlayer = state.players[this.adapter.getMyPlayerId()];
  if (localPlayer) {
    this.cameras.main.scrollX = localPlayer.x - 400;
    this.cameras.main.scrollY = localPlayer.y - 300;
  }
}

After (3 lines, automatic):

create() {
  this.cameraFollower = this.adapter.createCameraFollower({
    target: 'myPlayer',
    bounds: { width: 800, height: 600 }
  });
}
// No update() code needed!

Configuration

CameraFollowerConfig

interface CameraFollowerConfig {
  target?: 'myPlayer' | CameraFollowerTarget;
  mode?: 'instant' | 'lerp' | 'deadzone';
  lerpFactor?: number;
  offset?: { x: number; y: number };
  bounds?: { width: number; height: number };
  deadzone?: { width: number; height: number };
  centerOnTarget?: boolean;
}

target

  • Type: 'myPlayer' | { stateKey?: string, playerId?: string }
  • Default: 'myPlayer'
  • Description: Which player to follow
// Follow local player (default)
this.adapter.createCameraFollower({ target: 'myPlayer' });

// Follow specific player
this.adapter.createCameraFollower({
  target: { stateKey: 'players', playerId: 'player-123' }
});

mode

  • Type: 'instant' | 'lerp' | 'deadzone'
  • Default: 'instant'
  • Description: Camera follow behavior
// Instant snap (no smoothing)
mode: 'instant'

// Smooth lerp (cinematic)
mode: 'lerp'

// Deadzone (camera only moves when player leaves center area)
mode: 'deadzone'

lerpFactor

  • Type: number (0-1)
  • Default: 0.1
  • Description: Smoothness for lerp mode (lower = smoother, higher = snappier)
this.adapter.createCameraFollower({
  mode: 'lerp',
  lerpFactor: 0.05  // Very smooth
});

offset

  • Type: { x: number; y: number }
  • Default: { x: 0, y: 0 }
  • Description: Camera offset from target center
this.adapter.createCameraFollower({
  offset: { x: 0, y: 50 }  // Offset camera 50px down
});

bounds

  • Type: { width: number; height: number }
  • Default: undefined
  • Description: World bounds (prevents showing outside world)
this.adapter.createCameraFollower({
  bounds: { width: 1600, height: 1200 }
});

deadzone

  • Type: { width: number; height: number }
  • Default: { width: 200, height: 150 }
  • Description: Deadzone size (only used when mode: 'deadzone')
this.adapter.createCameraFollower({
  mode: 'deadzone',
  deadzone: { width: 300, height: 200 }
});

centerOnTarget

  • Type: boolean
  • Default: true
  • Description: Whether to center camera on target

Follow Modes

Instant Mode (Default)

Camera snaps directly to target. Best for most games.

this.adapter.createCameraFollower({
  mode: 'instant'
});

Use when:

  • You want responsive, tight camera control
  • Player movement is smooth enough without camera smoothing

Lerp Mode

Camera smoothly interpolates to target. Cinematic feel.

this.adapter.createCameraFollower({
  mode: 'lerp',
  lerpFactor: 0.1  // Lower = smoother, higher = snappier
});

Use when:

  • You want smooth, cinematic camera movement
  • Player can change direction quickly (lerp smooths it out)

Lerp factor guide:

  • 0.05 - Very smooth, noticeable lag (cinematic)
  • 0.1 - Balanced smoothness (recommended)
  • 0.2 - Snappy, minimal lag
  • 0.5+ - Almost instant (defeats the purpose)

Deadzone Mode

Camera only moves when player leaves deadzone rectangle.

this.adapter.createCameraFollower({
  mode: 'deadzone',
  deadzone: { width: 200, height: 150 }
});

Use when:

  • You want player to move freely in center of screen
  • You want camera to feel less “twitchy”
  • Classic platformer/action game feel

API Reference

CameraFollower Instance

The object returned by createCameraFollower():

interface CameraFollower {
  update(): void;
  destroy(): void;
  setTarget(playerId: string): void;
  getTarget(): string | null;
}

update()

Manually update camera position. Automatically called every frame, rarely needed.

this.cameraFollower.update();

destroy()

Clean up and stop following. Call in scene.shutdown():

shutdown() {
  this.cameraFollower.destroy();
}

setTarget(playerId)

Change which player to follow:

// Switch to different player
this.cameraFollower.setTarget('player-456');

getTarget()

Get current target player ID:

const targetId = this.cameraFollower.getTarget();
console.log('Following:', targetId);

Complete Examples

Top-Down Game (Blob Battle Style)

export function createBlobBattleScene(runtime: GameRuntime) {
  return class BlobBattleScene extends Phaser.Scene {
    private adapter!: PhaserAdapter;
    private cameraFollower: any;

    create() {
      this.adapter = new PhaserAdapter(runtime, this);

      // Auto-follow local player
      this.cameraFollower = this.adapter.createCameraFollower({
        target: 'myPlayer',
        bounds: { width: 800, height: 600 }
      });

      // That's it! Camera automatically follows.
    }

    shutdown() {
      this.cameraFollower.destroy();
    }
  };
}

Platformer with Smooth Camera

create() {
  this.adapter = new PhaserAdapter(runtime, this);

  this.cameraFollower = this.adapter.createCameraFollower({
    target: 'myPlayer',
    mode: 'lerp',
    lerpFactor: 0.1,
    bounds: { width: 2400, height: 1600 }
  });
}

Racing Game with Deadzone

create() {
  this.adapter = new PhaserAdapter(runtime, this);

  this.cameraFollower = this.adapter.createCameraFollower({
    target: 'myPlayer',
    mode: 'deadzone',
    deadzone: { width: 300, height: 200 },
    bounds: { width: 3200, height: 2400 }
  });
}

Spectator Mode (Switch Targets)

create() {
  this.adapter = new PhaserAdapter(runtime, this);

  this.cameraFollower = this.adapter.createCameraFollower({
    target: 'myPlayer'
  });

  // Switch to different player on keypress
  this.input.keyboard.on('keydown-TAB', () => {
    const state = runtime.getState();
    const playerIds = Object.keys(state.players);
    const currentIdx = playerIds.indexOf(this.cameraFollower.getTarget());
    const nextIdx = (currentIdx + 1) % playerIds.length;
    this.cameraFollower.setTarget(playerIds[nextIdx]);
  });
}

Common Patterns

Center Camera on Spawn

create() {
  // Camera automatically centers on player when they spawn
  this.cameraFollower = this.adapter.createCameraFollower({
    target: 'myPlayer',
    bounds: { width: 1600, height: 1200 }
  });
  // That's it! No manual initialization needed.
}

Camera with Offset (Over-the-shoulder)

create() {
  this.cameraFollower = this.adapter.createCameraFollower({
    target: 'myPlayer',
    offset: { x: 0, y: -100 },  // Show more ahead of player
    mode: 'lerp',
    lerpFactor: 0.15
  });
}

Bounded World

create() {
  this.cameraFollower = this.adapter.createCameraFollower({
    target: 'myPlayer',
    bounds: { width: 2400, height: 1600 }  // Camera won't show beyond world
  });
}

Troubleshooting

Camera not following player

Cause: Player state doesn’t have x and y properties.

Fix: Ensure your game state has player position:

setup: ({ playerIds }) => ({
  players: Object.fromEntries(
    playerIds.map(id => [id, { x: 100, y: 100 }])  // ✅ Must have x, y
  )
})

Camera follows wrong player

Cause: Using wrong target configuration.

Fix: Verify target is correct:

// Follow local player (most common)
target: 'myPlayer'

// Follow specific player
target: { playerId: 'player-123' }

Camera too laggy (lerp mode)

Cause: lerpFactor is too low.

Fix: Increase lerp factor:

lerpFactor: 0.2  // More responsive (was 0.1)

Camera too twitchy (instant mode)

Cause: Instant mode has no smoothing.

Fix: Use lerp or deadzone mode:

mode: 'lerp',
lerpFactor: 0.1

Best Practices

✅ Do

  • Use CameraFollower for all camera tracking - Handles timing correctly
  • Set world bounds - Prevents showing outside world
  • Choose appropriate mode - Instant for most, lerp for cinematic, deadzone for classic
  • Call destroy() in shutdown - Clean up resources

❌ Don’t

  • Don’t manually set camera position - Let CameraFollower handle it
  • Don’t create in update() - Create once in create()
  • Don’t forget bounds - Set bounds to match your world size
  • Don’t use extreme lerp values - Stay between 0.05-0.2

Performance

CameraFollower is highly optimized:

  • Zero overhead when player doesn’t move
  • Automatic updates via Phaser events (no manual polling)
  • Waits for state before initializing (no wasted checks)
  • Instant cleanup via destroy()

See Also