Skip to content

Adaptive Viewport

Dynamic visible area changes in real-time

Concept

Idea: Viewport size = browser window size

Large Screen (Desktop):
╔══════════════════════════════════╗
║  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  ║
║  ░░░ VISIBLE AREA LARGER ░░░░  ║
║  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  ║
╚══════════════════════════════════╝

Mobile Landscape:
╔═══════════════════════════╗
║  ░░░░░░░░░░░░░░░░░░░░░░  ║
║  ░░ WIDER BUT SHORTER ░░  ║
╚═══════════════════════════╝

Mobile Portrait:
╔═══════════╗
║  ░░░░░░░  ║
║  ░░░░░░░  ║
║  ░NARROW░  ║
║  ░ BUT  ░  ║
║  ░TALLER░  ║
║  ░░░░░░░  ║
╚═══════════╝

How It Works

1. Phaser Configuration

typescript
// src/platform/base.config.ts
export const baseConfig = {
  // Use browser window size
  width: window.innerWidth,
  height: window.innerHeight,

  scale: {
    mode: Phaser.Scale.RESIZE, // Key setting!
    autoCenter: Phaser.Scale.CENTER_BOTH,
    width: window.innerWidth,
    height: window.innerHeight,
  },
};

2. ViewportManager

typescript
// src/core/utils/ViewportManager.ts
class ViewportManager {
  init(callback?: (width: number, height: number) => void) {
    // Subscribe to resize events
    this.scene.scale.on("resize", this.handleResize);
  }

  private handleResize(gameSize: Phaser.Structs.Size) {
    // Update camera
    this.scene.cameras.resize(width, height);

    // Call callback to update UI
    this.onResizeCallback?.(width, height);
  }
}

3. Usage in Scenes

typescript
// src/game/scenes/GameScene.ts
create() {
  this.viewportManager = new ViewportManager(this);

  this.viewportManager.init((width, height) => {
    // This callback is called on every change!
    console.log(`New size: ${width}x${height}`);
    this.repositionUI();
  });
}

Device Adaptation

Desktop (1920x1080)

Visible area: LARGE
More game world visible
Better overview for strategy

Tablet Landscape (1024x768)

Visible area: MEDIUM
Balanced view

Mobile Landscape (812x375)

Visible area: WIDE but short
More horizontal space
Good for platformers, runners

Mobile Portrait (375x812)

Visible area: NARROW but tall
More vertical space
Good for tower defense, puzzle games

Practical Examples

Example 1: UI Adaptation on Resize

typescript
export class GameScene extends Phaser.Scene {
  private infoText!: Phaser.GameObjects.Text;
  private minimap!: Phaser.GameObjects.Container;

  create() {
    this.viewportManager.init((width, height) => {
      // Reposition UI elements
      this.repositionUI(width, height);
    });
  }

  private repositionUI(width: number, height: number) {
    // Info in top-right corner
    if (this.infoText) {
      this.infoText.setPosition(width - 16, 16);
    }

    // Minimap in bottom-right corner
    if (this.minimap) {
      this.minimap.setPosition(width - 216, height - 216);
    }

    // Adapt UI size based on device
    const deviceType = this.viewportManager.getDeviceType();

    if (deviceType === "mobile") {
      // Smaller UI on mobile
      this.infoText.setFontSize(12);
      this.minimap.setScale(0.7);
    } else {
      // Normal size on desktop
      this.infoText.setFontSize(16);
      this.minimap.setScale(1);
    }
  }
}

Example 2: Gameplay Logic Adaptation

typescript
export class GameScene extends Phaser.Scene {
  create() {
    this.viewportManager.init((width, height) => {
      this.adjustGameplay(width, height);
    });
  }

  private adjustGameplay(width: number, height: number) {
    // On mobile - simplify
    if (this.viewportManager.getDeviceType() === "mobile") {
      this.maxEnemies = 10;
      this.spawnRate = 3000; // Less frequent
    } else {
      this.maxEnemies = 50;
      this.spawnRate = 1000; // More frequent
    }

    // In landscape - more horizontal spawns
    if (this.viewportManager.isLandscape()) {
      this.spawnZone = { x: width * 0.8, y: height * 0.5 };
    } else {
      // In portrait - more vertical spawns
      this.spawnZone = { x: width * 0.5, y: height * 0.8 };
    }
  }
}

Example 3: Camera Following Player

typescript
create() {
  // Camera follows player
  this.cameras.main.startFollow(this.player, true, 0.1, 0.1);

  // On resize - more/less world visible
  this.viewportManager.init((width, height) => {
    // Camera adapts automatically!
    // NO need to do anything - Phaser handles it

    // But can adjust zoom if needed
    if (width < 600) {
      // On small screen - zoom out to see more
      this.cameras.main.setZoom(0.8);
    } else {
      this.cameras.main.setZoom(1);
    }
  });
}

ViewportManager API

Methods

typescript
viewportManager.getSize();
// → { width: 1920, height: 1080 }

viewportManager.isLandscape();
// → true (if width > height)

viewportManager.isPortrait();
// → false

viewportManager.getAspectRatio();
// → 1.777... (16:9)

viewportManager.getDeviceType();
// → "desktop" | "tablet" | "mobile"

viewportManager.calculateOptimalZoom();
// → 1.5 (calculated zoom)

viewportManager.setWorldBoundsProportional(2000);
// Sets world proportional to viewport

Testing Adaptivity

In Browser

  1. Run game:
bash
pnpm dev
  1. Open DevTools (F12)

  2. Enable Device Toolbar (Ctrl+Shift+M / Cmd+Shift+M)

  3. Switch devices:

    • iPhone SE (375x667) - Portrait
    • iPhone 14 Pro (430x932) - Portrait
    • iPad (768x1024) - Portrait/Landscape
    • Desktop (1920x1080) - Landscape
  4. Resize window manually - visible area changes in real-time!


What You'll See

On Large Screen (Desktop):

╔═══════════════════════════════════════════════╗
║                                               ║
║         More grid visible                     ║
║         More game world                       ║
║         Better overview                       ║
║                                               ║
║                     ■ (player)                ║
║                                               ║
║         Coordinates visible further           ║
║                                               ║
╚═══════════════════════════════════════════════╝

On Mobile Landscape:

╔═══════════════════════════════════╗
║                                   ║
║  Wider but shorter                ║
║  Good for horizontal movement     ║
║         ■ (player)                ║
╚═══════════════════════════════════╝

On Mobile Portrait:

╔═══════════╗
║           ║
║  Narrow   ║
║  but      ║
║  taller   ║
║           ║
║    ■      ║
║ (player)  ║
║           ║
║  Good     ║
║  for      ║
║ vertical  ║
╚═══════════╝

Benefits

For Players:

  1. Fair Play

    • Larger monitor = more visibility
    • Mobile = less visibility (but controls adapted)
  2. Comfortable Play

    • No need to scroll
    • Everything visible immediately
    • No black bars
  3. Convenience

    • Rotate phone → adaptation
    • Resize window → adaptation
    • Any resolution supported

For Developers:

  1. One Codebase

    • No separate versions for mobile/desktop
    • Automatic adaptation
  2. Simplicity

    • ViewportManager handles everything
    • Just subscribe to callback
  3. Flexibility

    • Can customize behavior per device
    • Easy to test different resolutions

Recommendations

For Different Genres:

Strategy / God Game (like Populous)

typescript
// RECOMMENDED: Larger screen = more visible
create() {
  // DON'T use zoom
  // Just allow viewing entire viewport

  this.viewportManager.init((width, height) => {
    // On mobile - fewer units
    if (this.viewportManager.getDeviceType() === "mobile") {
      this.maxUnits = 50;
    } else {
      this.maxUnits = 200;
    }
  });
}

Platformer / Action

typescript
// RECOMMENDED: Fixed zoom + follow player
create() {
  this.cameras.main.startFollow(this.player);
  this.cameras.main.setZoom(2); // Fixed zoom

  // Viewport changes, but zoom stays
  // Larger screen = more overview around player
}

RPG / Adventure

typescript
// RECOMMENDED: Adaptive zoom
create() {
  this.cameras.main.startFollow(this.player);

  this.viewportManager.init((width, height) => {
    // On mobile - zoom in to see details
    if (width < 600) {
      this.cameras.main.setZoom(1.5);
    } else {
      this.cameras.main.setZoom(1);
    }
  });
}

Quick Start

Test Adaptivity:

bash
# 1. Run game
pnpm dev

# 2. Play demo

# 3. Resize browser window
#    → Viewport adapts in real-time!

# 4. Check console:
#    → See size change logs

# 5. Check instructions in top-left:
#    → Shows current size and orientation

Technical Details

Scale Modes in Phaser 3

typescript
Phaser.Scale.NONE; // Fixed size
Phaser.Scale.FIT; // Scales preserving aspect ratio
Phaser.Scale.ENVELOP; // Fills entire screen
Phaser.Scale.RESIZE; // OUR SETTING - dynamic size
Phaser.Scale.EXPAND; // Expands when needed

RESIZE - ideal for adaptive games:

  • Canvas size = window size
  • Dynamic resizing
  • Larger screen = more visible area

Handling Resize

typescript
// Automatically called on size change
this.scale.on("resize", (gameSize: Phaser.Structs.Size) => {
  const width = gameSize.width;
  const height = gameSize.height;

  // Update camera
  this.cameras.resize(width, height);

  // Update UI
  this.updateUI(width, height);
});

Visual Demonstration

What's in GameScene:

  1. Grid 64x64 - shows world coordinates
  2. Coordinate markers - every 256 pixels
  3. Instructions - show current viewport size
  4. World 2000x2000 - large for exploration
  5. Camera follows player - smooth scrolling

Try It:

1. Run game (pnpm dev)
2. Play demo
3. Move with arrows
4. Resize browser window
   → Visible area changes!
5. Rotate phone (in DevTools)
   → Landscape ↔ Portrait adaptation!

Advanced Techniques

1. Adaptive UI Positions

typescript
class HUD {
  private healthBar: Phaser.GameObjects.Container;
  private minimap: Phaser.GameObjects.Container;

  updateLayout(width: number, height: number) {
    // Health bar always in top-left
    this.healthBar.setPosition(16, 16);

    // Minimap adapts
    if (width < 600) {
      // Mobile - small map bottom-right
      this.minimap.setPosition(width - 110, height - 110).setScale(0.5);
    } else {
      // Desktop - large map top-right
      this.minimap.setPosition(width - 216, 16).setScale(1);
    }
  }
}

2. Adaptive Camera Zoom

typescript
class CameraManager {
  updateZoom(width: number, height: number) {
    // Base sizes for reference
    const baseWidth = 1280;
    const baseHeight = 720;

    // Calculate zoom
    const scaleX = width / baseWidth;
    const scaleY = height / baseHeight;

    // For strategy - no zoom (see more)
    this.camera.setZoom(1);

    // For action - zoom to preserve scale
    // this.camera.setZoom(Math.min(scaleX, scaleY));
  }
}

3. Adaptive Object Count

typescript
class EnemySpawner {
  private maxEnemies = 100;

  adjustForViewport(width: number, height: number) {
    // Larger viewport = more enemies
    const area = width * height;
    const baseArea = 1280 * 720; // reference

    this.maxEnemies = Math.floor((area / baseArea) * 100);

    console.log(`Max enemies adjusted: ${this.maxEnemies}`);
  }
}

Aspect Ratios

Common Aspect Ratios:

16:9  (Desktop, TV)           → 1.777
16:10 (Mac, some laptops)     → 1.6
4:3   (Old monitors)          → 1.333
21:9  (Ultrawide)             → 2.333
9:16  (Mobile Portrait)       → 0.562
19.5:9 (Modern mobiles)       → 2.166

Handling Extreme Ratios:

typescript
adjustForAspectRatio() {
  const ratio = this.viewportManager.getAspectRatio();

  if (ratio > 2) {
    // Ultrawide - lots of side space
    console.log("Ultrawide detected!");
    // Can place UI on sides
  }

  if (ratio < 0.6) {
    // Very narrow portrait
    console.log("Narrow portrait!");
    // UI bottom/top
  }
}

Best Practices

DO (Good Practices):

  1. Use setScrollFactor(0) for UI
typescript
const uiElement = this.add.text(10, 10, "Score: 0");
uiElement.setScrollFactor(0); // Doesn't move with camera
  1. Store references to UI elements
typescript
private healthBar: Phaser.GameObjects.Rectangle;

create() {
  this.healthBar = this.add.rectangle(10, 10, 100, 20, 0xff0000);
  // Can reposition later on resize
}
  1. Use relative positions
typescript
// GOOD - relative to viewport size
this.minimap.setPosition(width - 216, height - 216);

// BAD - fixed position
this.minimap.setPosition(1064, 504);

DON'T (Bad Practices):

  1. Don't use fixed positions for UI
typescript
// Will break on resize
const button = this.add.text(1200, 680, "Click");
  1. Don't forget to update camera
typescript
// Forgot to update camera
this.scale.on("resize", (size) => {
  // this.cameras.resize(size.width, size.height); // FORGOT!
});
  1. Don't make UI too large
typescript
// Will fill entire mobile screen
const panel = this.add.rectangle(0, 0, 800, 600, 0x000000);

Debugging

Show Viewport Bounds

typescript
create() {
  // Debug rectangle showing viewport
  const debugRect = this.add.rectangle(
    0, 0,
    this.cameras.main.width,
    this.cameras.main.height,
    0xff0000,
    0
  );
  debugRect.setStrokeStyle(4, 0xff0000);
  debugRect.setOrigin(0, 0);
  debugRect.setScrollFactor(0);

  this.viewportManager.init((width, height) => {
    debugRect.setSize(width, height);
  });
}

Mobile Specifics

Touch Controls

typescript
create() {
  // On mobile - add virtual controls
  if (this.viewportManager.getDeviceType() === "mobile") {
    this.createVirtualControls();
  }
}

private createVirtualControls() {
  const { width, height } = this.viewportManager.getSize();

  // Virtual joystick bottom-left
  const joystick = this.add.circle(100, height - 100, 50, 0xffffff, 0.3);
  joystick.setScrollFactor(0);

  // Buttons bottom-right
  const btnA = this.add.circle(width - 100, height - 100, 40, 0xff0000, 0.5);
  btnA.setScrollFactor(0);
  btnA.setInteractive();

  btnA.on("pointerdown", () => {
    // Action
  });
}

Summary

╔══════════════════════════════════════════════════╗
║                                                  ║
║  WHAT'S IMPLEMENTED:                            ║
║                                                  ║
║  ✓ Phaser.Scale.RESIZE mode                     ║
║  ✓ ViewportManager utility                      ║
║  ✓ All scenes adapted                           ║
║  ✓ Real-time dynamic resizing                   ║
║  ✓ All orientations supported                   ║
║  ✓ Change logging                               ║
║                                                  ║
║  RESULT:                                         ║
║                                                  ║
║  Larger screen = larger visible area            ║
║  Rotate phone = adaptation                      ║
║  Resize window = instant reaction               ║
║  One code for all devices                       ║
║                                                  ║
╚══════════════════════════════════════════════════╝

  • src/platform/base.config.ts - RESIZE mode configuration
  • src/core/utils/ViewportManager.ts - management utility
  • src/game/scenes/GameScene.ts - usage example
  • src/game/scenes/MainMenuSceneVue.ts - adaptive menu
  • index.html - fullscreen CSS

Now visible area changes in real-time!

MIT Licensed