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
typescriptimport 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:
typescriptupdate(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:
typescriptupdate(): void { const cursors = this.input.keyboard?.createCursorKeys(); if (cursors?.left.isDown) { this.player.x -= 5; } }
Track time for spawning:
typescriptprivate 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:
typescriptupdate(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:
typescriptcreate(): 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:
- Scene is in the config array
- Scene name matches exactly (case-sensitive)
- 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():
typescriptinit(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:
typescriptshutdown(): void { this.input.keyboard?.off('keydown-ESC'); // Remove other event listeners }
Verify Your Understanding
You've completed this lesson if you can:
Next: Add sprites and create animations.