Phaser
30 minutes
linux
macos
windows

Scenes and Game Loop

Master Phaser scene lifecycle, state management, and the game loop update pattern.

What you'll accomplish

Understand scene lifecycle and manage multiple game scenes.

Prerequisites

  • Working Phaser project setup
  • Completed previous Phaser lessons

What is a Scene?

A scene is a self-contained section of your game. Think of scenes like screens or levels:

  • Boot Scene: Load initial assets, show loading bar
  • Main Menu: Title screen, play button
  • Game Scene: The actual gameplay
  • Pause Scene: Overlay when paused
  • Game Over: Score display, retry button

Each scene has its own:

  • Assets
  • Game objects
  • Logic
  • Update loop

Scene Lifecycle

Every scene goes through a lifecycle:

┌──────────────────────────────────────────┐
│                                          │
│  init()    → Called when scene starts    │
│     ↓                                    │
│  preload() → Load assets                 │
│     ↓                                    │
│  create()  → Set up game objects         │
│     ↓                                    │
│  update()  → Game loop (repeats)  ←──┐   │
│     ↓                                │   │
│  update()  → Game loop              ─┘   │
│     ↓                                    │
│  shutdown() → Cleanup when leaving       │
│                                          │
└──────────────────────────────────────────┘

Complete Scene Example

typescript
import Phaser from 'phaser';

export class GameScene extends Phaser.Scene {
  private score: number = 0;
  private scoreText!: Phaser.GameObjects.Text;

  constructor() {
    super('GameScene');
  }

  // Called first when scene starts
  init(data: { level?: number }): void {
    console.log('init called');
    console.log('Received data:', data);
    this.score = 0; // Reset score
  }

  // Load assets
  preload(): void {
    console.log('preload called');
    // Load images, sounds, etc.
  }

  // Create game objects (called once)
  create(): void {
    console.log('create called');

    this.scoreText = this.add.text(10, 10, 'Score: 0', {
      fontSize: '20px',
      color: '#fff',
    });

    // Example: Go to game over after 5 seconds
    this.time.delayedCall(5000, () => {
      this.scene.start('GameOverScene', { finalScore: this.score });
    });
  }

  // Game loop (called every frame, ~60x per second)
  update(time: number, delta: number): void {
    // time: Total elapsed time in ms
    // delta: Time since last frame in ms

    // Example: Increase score over time
    this.score += delta / 100;
    this.scoreText.setText(`Score: ${Math.floor(this.score)}`);
  }

  // Called when scene shuts down
  shutdown(): void {
    console.log('shutdown called');
    // Clean up event listeners, etc.
  }
}

The Game Loop (update)

The update() method is the heart of your game. It runs every frame:

typescript
update(time: number, delta: number): void {
  // time = total milliseconds since game started
  // delta = milliseconds since last update

  // Example: Move something at constant speed
  const speed = 200; // pixels per second
  const movement = speed * (delta / 1000); // Convert to per-frame

  this.player.x += movement;
}

Not all computers are the same speed. Without delta time:

  • Fast computer: 120 FPS → Player moves 120 times/second
  • Slow computer: 30 FPS → Player moves 30 times/second

The player moves faster on faster computers!

With delta time:

  • Fast computer: Small delta → Small movement per frame
  • Slow computer: Large delta → Large movement per frame

Result: Same speed regardless of frame rate!

Common Update Patterns

Check input every frame:

typescript
update(): void {
  const cursors = this.input.keyboard?.createCursorKeys();

  if (cursors?.left.isDown) {
    this.player.x -= 5;
  }
}

Track time for spawning:

typescript
private lastSpawn: number = 0;
private spawnInterval: number = 1000; // 1 second

update(time: number): void {
  if (time - this.lastSpawn > this.spawnInterval) {
    this.spawnEnemy();
    this.lastSpawn = time;
  }
}

Smooth animation:

typescript
update(time: number): void {
  // Bobbing motion using sine wave
  this.floatingItem.y = 300 + Math.sin(time / 500) * 20;
}

Multiple Scenes

Creating Multiple Scenes

typescript
// src/scenes/BootScene.ts
export class BootScene extends Phaser.Scene {
  constructor() {
    super('BootScene');
  }

  preload(): void {
    // Load assets needed for loading screen
    this.load.image('logo', 'assets/logo.png');
  }

  create(): void {
    this.scene.start('MenuScene');
  }
}

// src/scenes/MenuScene.ts
export class MenuScene extends Phaser.Scene {
  constructor() {
    super('MenuScene');
  }

  create(): void {
    this.add.text(400, 250, 'My Game', { fontSize: '48px' }).setOrigin(0.5);

    const playButton = this.add.text(400, 350, 'Play', { fontSize: '32px' })
      .setOrigin(0.5)
      .setInteractive({ useHandCursor: true });

    playButton.on('pointerdown', () => {
      this.scene.start('GameScene');
    });
  }
}

// src/scenes/GameScene.ts
export class GameScene extends Phaser.Scene {
  constructor() {
    super('GameScene');
  }

  create(): void {
    this.add.text(400, 300, 'Playing!', { fontSize: '32px' }).setOrigin(0.5);

    // Press ESC to return to menu
    this.input.keyboard?.on('keydown-ESC', () => {
      this.scene.start('MenuScene');
    });
  }
}

Register Scenes in Config

typescript
// src/main.ts
import { BootScene } from './scenes/BootScene';
import { MenuScene } from './scenes/MenuScene';
import { GameScene } from './scenes/GameScene';
import { GameOverScene } from './scenes/GameOverScene';

const config: Phaser.Types.Core.GameConfig = {
  // ...other config
  scene: [BootScene, MenuScene, GameScene, GameOverScene],
};

The first scene in the array starts automatically.

Scene Transitions

Start (Replace Current Scene)

typescript
// Stop current scene, start new one
this.scene.start('GameScene');

// Pass data to the new scene
this.scene.start('GameScene', { level: 5, difficulty: 'hard' });

// Receive in init()
init(data: { level: number; difficulty: string }): void {
  console.log(`Starting level ${data.level}`);
}

Launch (Parallel Scenes)

typescript
// Run another scene alongside current one (good for UI overlays)
this.scene.launch('HUDScene');

// The HUD scene runs in parallel

Pause and Resume

typescript
// Pause a scene
this.scene.pause('GameScene');

// Resume
this.scene.resume('GameScene');

// Pause self
this.scene.pause();

Sleep and Wake

typescript
// Sleep keeps the scene in memory but stops updates
this.scene.sleep('MenuScene');

// Wake up a sleeping scene
this.scene.wake('MenuScene');

Scene Order

Control which scene appears on top:

typescript
// Bring to top
this.scene.bringToTop('PauseScene');

// Send to back
this.scene.sendToBack('BackgroundScene');

Pause Menu Example

A common pattern: pause menu overlay.

typescript
// GameScene.ts
create(): void {
  // ... game setup

  // Press ESC to pause
  this.input.keyboard?.on('keydown-ESC', () => {
    this.scene.pause();
    this.scene.launch('PauseScene');
  });
}

// PauseScene.ts
export class PauseScene extends Phaser.Scene {
  constructor() {
    super('PauseScene');
  }

  create(): void {
    // Semi-transparent background
    this.add.rectangle(400, 300, 800, 600, 0x000000, 0.7);

    this.add.text(400, 250, 'PAUSED', { fontSize: '48px' }).setOrigin(0.5);

    const resumeBtn = this.add.text(400, 350, 'Resume', { fontSize: '32px' })
      .setOrigin(0.5)
      .setInteractive({ useHandCursor: true });

    resumeBtn.on('pointerdown', () => {
      this.scene.stop();
      this.scene.resume('GameScene');
    });

    // ESC also resumes
    this.input.keyboard?.on('keydown-ESC', () => {
      this.scene.stop();
      this.scene.resume('GameScene');
    });
  }
}

Scene Events

Listen for scene lifecycle events:

typescript
create(): void {
  // When this scene resumes from pause
  this.events.on('resume', () => {
    console.log('Scene resumed!');
  });

  // When this scene wakes from sleep
  this.events.on('wake', () => {
    console.log('Scene woke up!');
  });

  // When this scene is about to shut down
  this.events.on('shutdown', () => {
    console.log('Cleaning up...');
  });
}

Common Issues

Frequently Asked Questions

Check:

  1. Scene is in the config array
  2. Scene name matches exactly (case-sensitive)
  3. No errors in the scene's constructor

Common causes:

  • Forgot to use delta time
  • Variable scope issue (use this.)
  • Physics body not enabled

Make sure you receive data in init(), not create():

typescript
init(data: { score: number }): void {
  this.score = data.score;
}

Check scene ordering. Use scene.bringToTop() for overlays. Make sure you're stopping/starting the right scenes.

Clean up in shutdown:

typescript
shutdown(): void {
  this.input.keyboard?.off('keydown-ESC');
  // Remove other event listeners
}

Verify Your Understanding

You've completed this lesson if you can:

0/5

Next: Add sprites and create animations.