phaser-ui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Phaser 4 UI Development

Phaser 4 UI 开发

Phaser has no built-in UI toolkit. Build UI from game objects — Text, Graphics, Image, Container — or use a parallel HUDScene. The two fundamental approaches:
  1. Scroll-fixed objects: Add game objects to the scene, call
    .setScrollFactor(0)
    so they stay fixed when the camera moves.
  2. Parallel HUDScene: Launch a separate scene alongside the game scene. That scene's camera never moves, so everything in it is inherently fixed. Use this for complex UIs.
Always call
.setDepth(100)
(or higher) on UI elements so they render above game objects.

Phaser 没有内置的 UI 工具包。可以通过游戏对象——Text、Graphics、Image、Container——构建 UI,或者使用并行的 HUDScene。两种基本实现方式:
  1. 滚动固定对象:将游戏对象添加到场景中,调用
    .setScrollFactor(0)
    ,这样相机移动时它们会保持固定。
  2. 并行 HUDScene:在游戏场景旁启动一个独立场景。该场景的相机从不移动,因此其中的所有元素天生就是固定的。复杂 UI 推荐使用这种方式。
始终为 UI 元素调用
.setDepth(100)
(或更高数值),确保它们在游戏对象上方渲染。

Health Bar (Graphics-based)

基于Graphics的生命值条

typescript
class HealthBar {
  private bar: Phaser.GameObjects.Graphics;
  private x: number;
  private y: number;
  private width = 200;
  private height = 20;
  private maxHealth: number;
  private health: number;

  constructor(scene: Phaser.Scene, x: number, y: number, maxHealth: number) {
    this.x = x;
    this.y = y;
    this.maxHealth = this.health = maxHealth;
    this.bar = scene.add.graphics().setScrollFactor(0).setDepth(100);
    this.draw();
  }

  setHealth(value: number): void {
    this.health = Phaser.Math.Clamp(value, 0, this.maxHealth);
    this.draw();
  }

  private draw(): void {
    this.bar.clear();
    // Background border
    this.bar.fillStyle(0x000000, 0.6);
    this.bar.fillRect(this.x - 2, this.y - 2, this.width + 4, this.height + 4);
    // Colored fill — green above 50%, yellow above 25%, red below
    const ratio = this.health / this.maxHealth;
    const color = ratio > 0.5 ? 0x00ff00 : ratio > 0.25 ? 0xffff00 : 0xff0000;
    this.bar.fillStyle(color, 1);
    this.bar.fillRect(this.x, this.y, this.width * ratio, this.height);
  }

  destroy(): void {
    this.bar.destroy();
  }
}
Use
setHealth(newValue)
every time the player takes damage or heals.

typescript
class HealthBar {
  private bar: Phaser.GameObjects.Graphics;
  private x: number;
  private y: number;
  private width = 200;
  private height = 20;
  private maxHealth: number;
  private health: number;

  constructor(scene: Phaser.Scene, x: number, y: number, maxHealth: number) {
    this.x = x;
    this.y = y;
    this.maxHealth = this.health = maxHealth;
    this.bar = scene.add.graphics().setScrollFactor(0).setDepth(100);
    this.draw();
  }

  setHealth(value: number): void {
    this.health = Phaser.Math.Clamp(value, 0, this.maxHealth);
    this.draw();
  }

  private draw(): void {
    this.bar.clear();
    // 背景边框
    this.bar.fillStyle(0x000000, 0.6);
    this.bar.fillRect(this.x - 2, this.y - 2, this.width + 4, this.height + 4);
    // 彩色填充——生命值高于50%为绿色,高于25%为黄色,低于25%为红色
    const ratio = this.health / this.maxHealth;
    const color = ratio > 0.5 ? 0x00ff00 : ratio > 0.25 ? 0xffff00 : 0xff0000;
    this.bar.fillStyle(color, 1);
    this.bar.fillRect(this.x, this.y, this.width * ratio, this.height);
  }

  destroy(): void {
    this.bar.destroy();
  }
}
玩家受到伤害或恢复生命值时,调用
setHealth(newValue)
更新生命值条。

Score and Text Display

分数与文本显示

typescript
// In create():
this.scoreText = this.add.text(16, 16, 'Score: 0', {
  fontSize: '24px',
  color: '#ffffff',
  stroke: '#000000',
  strokeThickness: 3,
}).setScrollFactor(0).setDepth(100);

// When score changes:
this.scoreText.setText(`Score: ${score}`);
Position all HUD text relative to
this.scale.width
/
this.scale.height
for responsive layouts (see Responsive UI below).

typescript
// 在create()方法中:
this.scoreText = this.add.text(16, 16, 'Score: 0', {
  fontSize: '24px',
  color: '#ffffff',
  stroke: '#000000',
  strokeThickness: 3,
}).setScrollFactor(0).setDepth(100);

// 分数变化时:
this.scoreText.setText(`Score: ${score}`);
为实现响应式布局,所有HUD文本的位置应基于
this.scale.width
/
this.scale.height
来设置(详见下方“响应式UI”部分)。

Interactive Buttons

交互按钮

Method 1 — Text as button

方法1 —— 文本作为按钮

typescript
const btn = this.add.text(400, 300, 'PLAY', {
  fontSize: '32px',
  backgroundColor: '#4a4a8a',
  padding: { x: 20, y: 10 },
})
  .setOrigin(0.5)
  .setInteractive({ useHandCursor: true })
  .on('pointerover', () => btn.setStyle({ color: '#ffff00' }))
  .on('pointerout',  () => btn.setStyle({ color: '#ffffff' }))
  .on('pointerdown', () => btn.setScale(0.95))
  .on('pointerup',   () => {
    btn.setScale(1);
    this.scene.start('GameScene');
  });
typescript
const btn = this.add.text(400, 300, 'PLAY', {
  fontSize: '32px',
  backgroundColor: '#4a4a8a',
  padding: { x: 20, y: 10 },
})
  .setOrigin(0.5)
  .setInteractive({ useHandCursor: true })
  .on('pointerover', () => btn.setStyle({ color: '#ffff00' }))
  .on('pointerout',  () => btn.setStyle({ color: '#ffffff' }))
  .on('pointerdown', () => btn.setScale(0.95))
  .on('pointerup',   () => {
    btn.setScale(1);
    this.scene.start('GameScene');
  });

Method 2 — Image button with texture frame states

方法2 —— 带纹理帧状态的图片按钮

typescript
const playBtn = this.add.image(400, 300, 'ui', 'btn-play-normal.png')
  .setInteractive({ useHandCursor: true });

playBtn.on('pointerover', () => playBtn.setFrame('btn-play-hover.png'));
playBtn.on('pointerout',  () => playBtn.setFrame('btn-play-normal.png'));
playBtn.on('pointerdown', () => playBtn.setFrame('btn-play-pressed.png'));
playBtn.on('pointerup',   () => {
  playBtn.setFrame('btn-play-normal.png');
  this.scene.start('GameScene');
});

typescript
const playBtn = this.add.image(400, 300, 'ui', 'btn-play-normal.png')
  .setInteractive({ useHandCursor: true });

playBtn.on('pointerover', () => playBtn.setFrame('btn-play-hover.png'));
playBtn.on('pointerout',  () => playBtn.setFrame('btn-play-normal.png'));
playBtn.on('pointerdown', () => playBtn.setFrame('btn-play-pressed.png'));
playBtn.on('pointerup',   () => {
  playBtn.setFrame('btn-play-normal.png');
  this.scene.start('GameScene');
});

Dialog Box

对话框

typescript
class DialogBox extends Phaser.GameObjects.Container {
  constructor(scene: Phaser.Scene, text: string, onClose: () => void) {
    super(scene, scene.scale.width / 2, scene.scale.height / 2);
    scene.add.existing(this);
    this.setDepth(200).setScrollFactor(0);

    const bg = scene.add.graphics();
    bg.fillStyle(0x222244, 0.95);
    bg.fillRoundedRect(-200, -80, 400, 160, 16);

    const label = scene.add.text(0, -30, text, {
      fontSize: '20px',
      color: '#ffffff',
      wordWrap: { width: 360 },
      align: 'center',
    }).setOrigin(0.5);

    const closeBtn = scene.add.text(0, 50, 'OK', {
      fontSize: '20px',
      backgroundColor: '#4488aa',
      padding: { x: 20, y: 8 },
    })
      .setOrigin(0.5)
      .setInteractive({ useHandCursor: true })
      .on('pointerup', () => {
        this.destroy();
        onClose();
      });

    this.add([bg, label, closeBtn]);
  }
}

// Usage:
new DialogBox(this, 'You found a treasure chest!', () => {
  // Resume game logic after dialog closes
});
Animate the appearance by tweening scale from 0 to 1 — see
references/ui-patterns.md
for the animated version.

typescript
class DialogBox extends Phaser.GameObjects.Container {
  constructor(scene: Phaser.Scene, text: string, onClose: () => void) {
    super(scene, scene.scale.width / 2, scene.scale.height / 2);
    scene.add.existing(this);
    this.setDepth(200).setScrollFactor(0);

    const bg = scene.add.graphics();
    bg.fillStyle(0x222244, 0.95);
    bg.fillRoundedRect(-200, -80, 400, 160, 16);

    const label = scene.add.text(0, -30, text, {
      fontSize: '20px',
      color: '#ffffff',
      wordWrap: { width: 360 },
      align: 'center',
    }).setOrigin(0.5);

    const closeBtn = scene.add.text(0, 50, 'OK', {
      fontSize: '20px',
      backgroundColor: '#4488aa',
      padding: { x: 20, y: 8 },
    })
      .setOrigin(0.5)
      .setInteractive({ useHandCursor: true })
      .on('pointerup', () => {
        this.destroy();
        onClose();
      });

    this.add([bg, label, closeBtn]);
  }
}

// 使用示例:
new DialogBox(this, '你找到了一个宝箱!', () => {
  // 对话框关闭后恢复游戏逻辑
});
可以通过补间动画将缩放从0变为1来实现对话框的出现效果——查看
references/ui-patterns.md
获取动画版本。

Minimap (Graphics-based dot map)

小地图(基于Graphics的点图)

A camera-based minimap requires WebGL and render textures. For most games, a dot-map on a
Graphics
object is simpler and works in both Canvas and WebGL:
typescript
class MiniMap {
  private gfx: Phaser.GameObjects.Graphics;
  private mapX = 10;
  private mapY = 10;
  private mapW = 150;
  private mapH = 100;
  private worldW: number;
  private worldH: number;

  constructor(scene: Phaser.Scene, worldW: number, worldH: number) {
    this.worldW = worldW;
    this.worldH = worldH;
    this.gfx = scene.add.graphics().setScrollFactor(0).setDepth(100);
  }

  update(entities: Array<{ x: number; y: number; color: number }>): void {
    this.gfx.clear();
    // Background
    this.gfx.fillStyle(0x000000, 0.5);
    this.gfx.fillRect(this.mapX, this.mapY, this.mapW, this.mapH);
    // Border
    this.gfx.lineStyle(2, 0xffffff, 0.8);
    this.gfx.strokeRect(this.mapX, this.mapY, this.mapW, this.mapH);
    // Entities as colored dots
    for (const e of entities) {
      const mx = this.mapX + (e.x / this.worldW) * this.mapW;
      const my = this.mapY + (e.y / this.worldH) * this.mapH;
      this.gfx.fillStyle(e.color, 1);
      this.gfx.fillCircle(mx, my, 2);
    }
  }

  destroy(): void {
    this.gfx.destroy();
  }
}

// In GameScene.update():
this.miniMap.update([
  { x: this.player.x, y: this.player.y, color: 0x00ff00 },
  ...this.enemies.getChildren().map(e => ({ x: (e as Enemy).x, y: (e as Enemy).y, color: 0xff0000 })),
]);

基于相机的小地图需要WebGL和渲染纹理。对于大多数游戏来说,在
Graphics
对象上实现的点图更简单,并且在Canvas和WebGL环境下都能工作:
typescript
class MiniMap {
  private gfx: Phaser.GameObjects.Graphics;
  private mapX = 10;
  private mapY = 10;
  private mapW = 150;
  private mapH = 100;
  private worldW: number;
  private worldH: number;

  constructor(scene: Phaser.Scene, worldW: number, worldH: number) {
    this.worldW = worldW;
    this.worldH = worldH;
    this.gfx = scene.add.graphics().setScrollFactor(0).setDepth(100);
  }

  update(entities: Array<{ x: number; y: number; color: number }>): void {
    this.gfx.clear();
    // 背景
    this.gfx.fillStyle(0x000000, 0.5);
    this.gfx.fillRect(this.mapX, this.mapY, this.mapW, this.mapH);
    // 边框
    this.gfx.lineStyle(2, 0xffffff, 0.8);
    this.gfx.strokeRect(this.mapX, this.mapY, this.mapW, this.mapH);
    // 实体为彩色圆点
    for (const e of entities) {
      const mx = this.mapX + (e.x / this.worldW) * this.mapW;
      const my = this.mapY + (e.y / this.worldH) * this.mapH;
      this.gfx.fillStyle(e.color, 1);
      this.gfx.fillCircle(mx, my, 2);
    }
  }

  destroy(): void {
    this.gfx.destroy();
  }
}

// 在GameScene.update()中:
this.miniMap.update([
  { x: this.player.x, y: this.player.y, color: 0x00ff00 },
  ...this.enemies.getChildren().map(e => ({ x: (e as Enemy).x, y: (e as Enemy).y, color: 0xff0000 })),
]);

Generic Progress Bar

通用进度条

Reuse for health, XP, loading progress, stamina — any ratio metric:
typescript
interface ProgressBarConfig {
  x: number;
  y: number;
  width?: number;
  height?: number;
  fillColor?: number;
  bgColor?: number;
  depth?: number;
}

class ProgressBar {
  private bar: Phaser.GameObjects.Graphics;
  private cfg: Required<ProgressBarConfig>;

  constructor(scene: Phaser.Scene, config: ProgressBarConfig) {
    this.cfg = {
      width: 200, height: 16,
      fillColor: 0x00aaff,
      bgColor: 0x333333,
      depth: 100,
      ...config,
    };
    this.bar = scene.add.graphics().setScrollFactor(0).setDepth(this.cfg.depth);
    this.setValue(1);
  }

  setValue(ratio: number): void {
    const { x, y, width, height, fillColor, bgColor } = this.cfg;
    const clamped = Phaser.Math.Clamp(ratio, 0, 1);
    this.bar.clear();
    this.bar.fillStyle(bgColor, 0.8);
    this.bar.fillRect(x, y, width, height);
    this.bar.fillStyle(fillColor, 1);
    this.bar.fillRect(x, y, width * clamped, height);
  }

  destroy(): void {
    this.bar.destroy();
  }
}

// Examples:
const xpBar   = new ProgressBar(scene, { x: 16, y: 50, fillColor: 0xaa00ff });
const loadBar = new ProgressBar(scene, { x: 160, y: 300, width: 320, height: 24, fillColor: 0x00ff88 });
xpBar.setValue(0.65);    // 65% XP
loadBar.setValue(value); // from load.on('progress', ...)

可复用在生命值、经验值、加载进度、耐力等任何比例指标上:
typescript
interface ProgressBarConfig {
  x: number;
  y: number;
  width?: number;
  height?: number;
  fillColor?: number;
  bgColor?: number;
  depth?: number;
}

class ProgressBar {
  private bar: Phaser.GameObjects.Graphics;
  private cfg: Required<ProgressBarConfig>;

  constructor(scene: Phaser.Scene, config: ProgressBarConfig) {
    this.cfg = {
      width: 200, height: 16,
      fillColor: 0x00aaff,
      bgColor: 0x333333,
      depth: 100,
      ...config,
    };
    this.bar = scene.add.graphics().setScrollFactor(0).setDepth(this.cfg.depth);
    this.setValue(1);
  }

  setValue(ratio: number): void {
    const { x, y, width, height, fillColor, bgColor } = this.cfg;
    const clamped = Phaser.Math.Clamp(ratio, 0, 1);
    this.bar.clear();
    this.bar.fillStyle(bgColor, 0.8);
    this.bar.fillRect(x, y, width, height);
    this.bar.fillStyle(fillColor, 1);
    this.bar.fillRect(x, y, width * clamped, height);
  }

  destroy(): void {
    this.bar.destroy();
  }
}

// 使用示例:
const xpBar   = new ProgressBar(scene, { x: 16, y: 50, fillColor: 0xaa00ff });
const loadBar = new ProgressBar(scene, { x: 160, y: 300, width: 320, height: 24, fillColor: 0x00ff88 });
xpBar.setValue(0.65);    // 65% 经验值
loadBar.setValue(value); // 来自load.on('progress', ...)

BitmapText for Performance

高性能BitmapText

Phaser.GameObjects.Text
redraws to canvas every
setText
call.
BitmapText
swaps UV coordinates on a texture atlas — much cheaper for values that update every frame (score, frame counter, damage numbers).
typescript
// Preload the font (in PreloaderScene):
this.load.bitmapFont('arcade', 'assets/fonts/arcade.png', 'assets/fonts/arcade.xml');

// Create:
const scoreText = this.add.bitmapText(16, 16, 'arcade', 'Score: 0', 32)
  .setScrollFactor(0)
  .setDepth(100);

// Update every frame with zero canvas overhead:
scoreText.setText(`Score: ${this.score}`);
Use
BitmapText
for: scores, timers, floating damage numbers, combo counters. Use
Text
for: long paragraphs, dialog text, anything using system fonts or custom CSS styles.

Phaser.GameObjects.Text
每次调用
setText
都会重新绘制到画布上。
BitmapText
则是交换纹理图集上的UV坐标——对于每帧更新的值(分数、帧计数器、伤害数值)来说,性能开销小得多。
typescript
// 预加载字体(在PreloaderScene中):
this.load.bitmapFont('arcade', 'assets/fonts/arcade.png', 'assets/fonts/arcade.xml');

// 创建文本:
const scoreText = this.add.bitmapText(16, 16, 'arcade', 'Score: 0', 32)
  .setScrollFactor(0)
  .setDepth(100);

// 每帧更新,无画布开销:
scoreText.setText(`Score: ${this.score}`);
BitmapText
适用于:分数、计时器、浮动伤害数值、连击计数器。
Text
适用于:长段落、对话文本、任何使用系统字体或自定义CSS样式的内容。

DOM Overlay with
this.add.dom()

使用
this.add.dom()
实现DOM覆盖层

For settings forms or chat inputs where native HTML inputs are needed:
typescript
// Preload (PreloaderScene or scene preload):
this.load.html('settings-form', 'assets/html/settings.html');

// In create():
const domEl = this.add.dom(400, 300).createFromCache('settings-form');
domEl.addListener('click');
domEl.on('click', (event: Event) => {
  const target = event.target as HTMLElement;
  if (target.id === 'submit-btn') {
    const input = domEl.getChildByName('username') as HTMLInputElement;
    console.log('Username:', input.value);
    domEl.destroy();
  }
});
Enable DOM in game config:
typescript
const config: Phaser.Types.Core.GameConfig = {
  // ...
  dom: { createContainer: true },
};
Caveat: DOM elements have z-index complications on mobile and in some browsers. Prefer Phaser game objects for all UI that doesn't need native input elements.

当需要原生HTML输入框的设置表单或聊天输入时:
typescript
// 预加载(在PreloaderScene或场景预加载中):
this.load.html('settings-form', 'assets/html/settings.html');

// 在create()方法中:
const domEl = this.add.dom(400, 300).createFromCache('settings-form');
domEl.addListener('click');
domEl.on('click', (event: Event) => {
  const target = event.target as HTMLElement;
  if (target.id === 'submit-btn') {
    const input = domEl.getChildByName('username') as HTMLInputElement;
    console.log('用户名:', input.value);
    domEl.destroy();
  }
});
在游戏配置中启用DOM:
typescript
const config: Phaser.Types.Core.GameConfig = {
  // ...
  dom: { createContainer: true },
};
注意:DOM元素在移动端和部分浏览器中存在z-index问题。除非需要原生输入元素,否则优先使用Phaser游戏对象实现UI。

Responsive UI with Scale Manager

使用Scale Manager实现响应式UI

For the full canvas-plus-HUD two-layer responsive sizing rule (including the
100dvh
+ live-camera pattern that avoids the iOS PWA landscape bug and the module-level
GAME_WIDTH
constant freeze), see
skills/phaser-scene/references/scene-patterns.md
Responsive Sizing: Two Layers. Do not duplicate that content — it's the canonical explanation.
Never hardcode pixel positions for HUD elements. Use
this.scale.width
/
this.scale.height
and listen for resize:
typescript
create(): void {
  const { width, height } = this.scale;

  this.scoreText = this.add.text(width * 0.02, height * 0.02, 'Score: 0', {
    fontSize: `${Math.round(height * 0.04)}px`,
    color: '#ffffff',
  }).setScrollFactor(0).setDepth(100);

  // Re-anchor on resize (e.g. when browser window resizes)
  this.scale.on('resize', (gameSize: Phaser.Structs.Size) => {
    this.scoreText.setPosition(gameSize.width * 0.02, gameSize.height * 0.02);
  });
}
如需了解完整的画布+HUD双层响应式尺寸规则(包括避免iOS PWA横屏bug的
100dvh
+实时相机模式,以及模块级
GAME_WIDTH
常量冻结问题),请查看
skills/phaser-scene/references/scene-patterns.md
响应式尺寸:双层结构。请勿重复该内容——这是权威说明。
永远不要为HUD元素硬编码像素位置。使用
this.scale.width
/
this.scale.height
并监听 resize 事件:
typescript
create(): void {
  const { width, height } = this.scale;

  this.scoreText = this.add.text(width * 0.02, height * 0.02, 'Score: 0', {
    fontSize: `${Math.round(height * 0.04)}px`,
    color: '#ffffff',
  }).setScrollFactor(0).setDepth(100);

  // 窗口大小改变时重新定位(例如浏览器窗口调整大小)
  this.scale.on('resize', (gameSize: Phaser.Structs.Size) => {
    this.scoreText.setPosition(gameSize.width * 0.02, gameSize.height * 0.02);
  });
}

Two Independent Sizing Layers

两个独立尺寸层

Phaser UI has two separate sizing concerns that must not be mixed:
  1. Canvas scale — handled by
    ScaleManager
    (
    this.scale.width/height
    ). Controls how the game canvas maps to the browser window.
  2. HUD / overlay positions — must come from
    this.cameras.main.width/height
    at creation time (and update on resize). Never use module-level constants (
    const GAME_W = 800
    ) for HUD positions — they freeze at the value from boot and don't adapt to canvas resize.
typescript
// BAD — module-level constant freezes at boot dimensions:
const OVERLAY_W = 1280;
backdrop.setSize(OVERLAY_W, OVERLAY_H);

// CORRECT — read from live camera:
const { width, height } = this.cameras.main;
backdrop.setSize(width, height);
When a HUDScene is a parallel overlay, its camera is independent from the GameScene camera. HUD positions read from
this.cameras.main
in the HUDScene always reflect the HUD camera's current viewport — correct even after window resize.

Phaser UI有两个必须分开处理的尺寸问题:
  1. 画布缩放——由
    ScaleManager
    this.scale.width/height
    )处理。控制游戏画布如何映射到浏览器窗口。
  2. HUD/覆盖层位置——创建时必须从
    this.cameras.main.width/height
    读取(并在resize时更新)。永远不要使用模块级常量(如
    const GAME_W = 800
    )设置HUD位置——它们会在启动时固定值,无法适应画布尺寸变化。
typescript
// 错误示例——模块级常量在启动时固定尺寸:
const OVERLAY_W = 1280;
backdrop.setSize(OVERLAY_W, OVERLAY_H);

// 正确示例——从实时相机读取:
const { width, height } = this.cameras.main;
backdrop.setSize(width, height);
当HUDScene作为并行覆盖层时,其相机独立于GameScene相机。在HUDScene中从
this.cameras.main
读取的HUD位置始终反映HUD相机的当前视口——即使窗口调整大小后也能保持正确。

HUD as Parallel Scene (recommended for complex UIs)

作为并行场景的HUD(复杂UI推荐)

For anything beyond a score text and health bar, move all UI into a dedicated
HUDScene
. Benefits: clean separation, no scroll-factor juggling, easier to pause/resume the game scene without affecting UI.
typescript
// In GameScene.create():
this.scene.launch('HUDScene');

// Communicate via events:
this.events.emit('healthChanged', this.player.health);
this.events.emit('scoreChanged', this.score);
typescript
export class HUDScene extends Phaser.Scene {
  private healthBar!: HealthBar;
  private scoreText!: Phaser.GameObjects.Text;

  constructor() { super({ key: 'HUDScene' }); }

  create(): void {
    this.healthBar = new HealthBar(this, 16, 16, 100);
    this.scoreText = this.add.text(16, 50, 'Score: 0', {
      fontSize: '20px', color: '#ffffff',
    }).setDepth(100);

    const game = this.scene.get('GameScene');
    game.events.on('healthChanged', (hp: number) => this.healthBar.setHealth(hp), this);
    game.events.on('scoreChanged',  (s: number)  => this.scoreText.setText(`Score: ${s}`), this);

    // Clean up listeners when GameScene shuts down
    game.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
      game.events.off('healthChanged');
      game.events.off('scoreChanged');
    }, this);
  }
}
See
phaser-scene
skill for the full HUDScene launch pattern and scene communication options.

除了分数文本和生命值条之外的任何UI,都建议将所有UI移至专用的
HUDScene
中。优势:清晰分离逻辑,无需处理滚动因子,更容易暂停/恢复游戏场景而不影响UI。
typescript
// 在GameScene.create()中:
this.scene.launch('HUDScene');

// 通过事件通信:
this.events.emit('healthChanged', this.player.health);
this.events.emit('scoreChanged', this.score);
typescript
export class HUDScene extends Phaser.Scene {
  private healthBar!: HealthBar;
  private scoreText!: Phaser.GameObjects.Text;

  constructor() { super({ key: 'HUDScene' }); }

  create(): void {
    this.healthBar = new HealthBar(this, 16, 16, 100);
    this.scoreText = this.add.text(16, 50, 'Score: 0', {
      fontSize: '20px', color: '#ffffff',
    }).setDepth(100);

    const game = this.scene.get('GameScene');
    game.events.on('healthChanged', (hp: number) => this.healthBar.setHealth(hp), this);
    game.events.on('scoreChanged',  (s: number)  => this.scoreText.setText(`Score: ${s}`), this);

    // GameScene关闭时清理监听器
    game.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
      game.events.off('healthChanged');
      game.events.off('scoreChanged');
    }, this);
  }
}
查看
phaser-scene
技能获取完整的HUDScene启动模式和场景通信选项。

Drag Overlay Event Swallowing

拖拽覆盖层事件吞灭

Full-viewport invisible zones used for swipe detection or drag-to-dismiss will silently swallow all pointer events if placed at a higher depth than your buttons. Phaser's default
topOnly
hit-test routes the event exclusively to the highest-depth interactive at that point.
Diagnosis:
this.input.enableDebug(overlayZone)
— if the debug outline covers the full viewport and its depth > your button depths, this is the cause.
Fixes (in order of preference):
  1. overlayZone.setDepth(-9999)
    — place the zone behind all interactives.
  2. Destroy the overlay zone while interactive panels are open; recreate on close.
  3. this.input.setTopOnly(false)
    in this scene only — allows all overlapping interactives to receive the event (watch for double-fire on other scenes).
See
references/hit-test-and-depth.md
for complete depth and topOnly semantics.
用于滑动检测或拖拽关闭的全屏不可见区域,如果深度高于按钮,会静默吞灭所有指针事件。Phaser默认的
topOnly
命中测试会将事件仅路由到该点深度最高的交互元素。
诊断方法
this.input.enableDebug(overlayZone)
——如果调试轮廓覆盖整个视口且其深度大于按钮深度,就是此问题导致。
修复方案(优先级从高到低)
  1. overlayZone.setDepth(-9999)
    ——将区域置于所有交互元素之后。
  2. 交互面板打开时销毁覆盖层,关闭时重新创建。
  3. 仅在当前场景中设置
    this.input.setTopOnly(false)
    ——允许所有重叠的交互元素接收事件(注意其他场景可能出现双击触发问题)。
查看
references/hit-test-and-depth.md
获取完整的深度和topOnly语义说明。

Additional Resources

额外资源

Reference Files

参考文件

  • references/ui-patterns.md
    — Production-ready component classes: animated HealthBar, Button class, animated DialogBox, FloatingText, Panel, InventoryGrid
  • references/hit-test-and-depth.md
    — Phaser
    topOnly
    hit-test semantics, invisible-zone depth patterns, drag-overlay click-swallow diagnostic checklist. Read when buttons or interactive children silently stop responding.
  • references/panel-rebuild-patterns.md
    — In-place content rebuild for panels (tab switches, purchases) without flicker, first-visit typewriter dialogue skip pattern, chrome-preserving
    container.list.slice(base)
    idiom. Read when panels flash closed and reopen on content change.
  • references/ui-patterns.md
    ——生产就绪的组件类:带动画的生命值条、Button类、带动画的对话框、浮动文本、面板、库存网格
  • references/hit-test-and-depth.md
    ——Phaser
    topOnly
    命中测试语义、不可见区域深度模式、拖拽覆盖层点击吞灭诊断清单。当按钮或交互子元素无响应时请阅读。
  • references/panel-rebuild-patterns.md
    ——面板内容原地重建(标签切换、购买操作)无闪烁方案、首次访问打字机对话跳过模式、保留框架的
    container.list.slice(base)
    用法。当面板内容变化时出现闪关闭再重新打开的情况时请阅读。