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 strategyTablet Landscape (1024x768)
Visible area: MEDIUM
Balanced viewMobile Landscape (812x375)
Visible area: WIDE but short
More horizontal space
Good for platformers, runnersMobile Portrait (375x812)
Visible area: NARROW but tall
More vertical space
Good for tower defense, puzzle gamesPractical 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 viewportTesting Adaptivity
In Browser
- Run game:
bash
pnpm devOpen DevTools (F12)
Enable Device Toolbar (Ctrl+Shift+M / Cmd+Shift+M)
Switch devices:
- iPhone SE (375x667) - Portrait
- iPhone 14 Pro (430x932) - Portrait
- iPad (768x1024) - Portrait/Landscape
- Desktop (1920x1080) - Landscape
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:
Fair Play
- Larger monitor = more visibility
- Mobile = less visibility (but controls adapted)
Comfortable Play
- No need to scroll
- Everything visible immediately
- No black bars
Convenience
- Rotate phone → adaptation
- Resize window → adaptation
- Any resolution supported
For Developers:
One Codebase
- No separate versions for mobile/desktop
- Automatic adaptation
Simplicity
- ViewportManager handles everything
- Just subscribe to callback
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 orientationTechnical 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 neededRESIZE - 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:
- Grid 64x64 - shows world coordinates
- Coordinate markers - every 256 pixels
- Instructions - show current viewport size
- World 2000x2000 - large for exploration
- 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.166Handling 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):
- Use setScrollFactor(0) for UI
typescript
const uiElement = this.add.text(10, 10, "Score: 0");
uiElement.setScrollFactor(0); // Doesn't move with camera- 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
}- 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):
- Don't use fixed positions for UI
typescript
// Will break on resize
const button = this.add.text(1200, 680, "Click");- Don't forget to update camera
typescript
// Forgot to update camera
this.scale.on("resize", (size) => {
// this.cameras.resize(size.width, size.height); // FORGOT!
});- 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 ║
║ ║
╚══════════════════════════════════════════════════╝Related Files
src/platform/base.config.ts- RESIZE mode configurationsrc/core/utils/ViewportManager.ts- management utilitysrc/game/scenes/GameScene.ts- usage examplesrc/game/scenes/MainMenuSceneVue.ts- adaptive menuindex.html- fullscreen CSS
Now visible area changes in real-time!