phaser-ui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePhaser 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:
- Scroll-fixed objects: Add game objects to the scene, call so they stay fixed when the camera moves.
.setScrollFactor(0) - 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 (or higher) on UI elements so they render above game objects.
.setDepth(100)Phaser 没有内置的 UI 工具包。可以通过游戏对象——Text、Graphics、Image、Container——构建 UI,或者使用并行的 HUDScene。两种基本实现方式:
- 滚动固定对象:将游戏对象添加到场景中,调用,这样相机移动时它们会保持固定。
.setScrollFactor(0) - 并行 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 every time the player takes damage or heals.
setHealth(newValue)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 / for responsive layouts (see Responsive UI below).
this.scale.widththis.scale.heighttypescript
// 在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文本的位置应基于/来设置(详见下方“响应式UI”部分)。
this.scale.widththis.scale.heightInteractive 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 for the animated version.
references/ui-patterns.mdtypescript
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.mdMinimap (Graphics-based dot map)
小地图(基于Graphics的点图)
A camera-based minimap requires WebGL and render textures. For most games, a dot-map on a object is simpler and works in both Canvas and WebGL:
Graphicstypescript
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和渲染纹理。对于大多数游戏来说,在对象上实现的点图更简单,并且在Canvas和WebGL环境下都能工作:
Graphicstypescript
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.TextsetTextBitmapTexttypescript
// 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 for: scores, timers, floating damage numbers, combo counters. Use for: long paragraphs, dialog text, anything using system fonts or custom CSS styles.
BitmapTextTextPhaser.GameObjects.TextsetTextBitmapTexttypescript
// 预加载字体(在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}`);BitmapTextTextDOM Overlay with this.add.dom()
this.add.dom()使用this.add.dom()
实现DOM覆盖层
this.add.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+ live-camera pattern that avoids the iOS PWA landscape bug and the module-level100dvhconstant freeze), seeGAME_WIDTH→ Responsive Sizing: Two Layers. Do not duplicate that content — it's the canonical explanation.skills/phaser-scene/references/scene-patterns.md
Never hardcode pixel positions for HUD elements. Use / and listen for resize:
this.scale.widththis.scale.heighttypescript
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元素硬编码像素位置。使用/并监听 resize 事件:
this.scale.widththis.scale.heighttypescript
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:
- Canvas scale — handled by (
ScaleManager). Controls how the game canvas maps to the browser window.this.scale.width/height - HUD / overlay positions — must come from at creation time (and update on resize). Never use module-level constants (
this.cameras.main.width/height) for HUD positions — they freeze at the value from boot and don't adapt to canvas resize.const GAME_W = 800
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 in the HUDScene always reflect the HUD camera's current viewport — correct even after window resize.
this.cameras.mainPhaser UI有两个必须分开处理的尺寸问题:
- 画布缩放——由(
ScaleManager)处理。控制游戏画布如何映射到浏览器窗口。this.scale.width/height - HUD/覆盖层位置——创建时必须从读取(并在resize时更新)。永远不要使用模块级常量(如
this.cameras.main.width/height)设置HUD位置——它们会在启动时固定值,无法适应画布尺寸变化。const GAME_W = 800
typescript
// 错误示例——模块级常量在启动时固定尺寸:
const OVERLAY_W = 1280;
backdrop.setSize(OVERLAY_W, OVERLAY_H);
// 正确示例——从实时相机读取:
const { width, height } = this.cameras.main;
backdrop.setSize(width, height);当HUDScene作为并行覆盖层时,其相机独立于GameScene相机。在HUDScene中从读取的HUD位置始终反映HUD相机的当前视口——即使窗口调整大小后也能保持正确。
this.cameras.mainHUD as Parallel Scene (recommended for complex UIs)
作为并行场景的HUD(复杂UI推荐)
For anything beyond a score text and health bar, move all UI into a dedicated . Benefits: clean separation, no scroll-factor juggling, easier to pause/resume the game scene without affecting UI.
HUDScenetypescript
// 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 skill for the full HUDScene launch pattern and scene communication options.
phaser-scene除了分数文本和生命值条之外的任何UI,都建议将所有UI移至专用的中。优势:清晰分离逻辑,无需处理滚动因子,更容易暂停/恢复游戏场景而不影响UI。
HUDScenetypescript
// 在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);
}
}查看技能获取完整的HUDScene启动模式和场景通信选项。
phaser-sceneDrag 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 hit-test routes the event exclusively to the highest-depth interactive at that point.
topOnlyDiagnosis: — if the debug outline covers the full viewport and its depth > your button depths, this is the cause.
this.input.enableDebug(overlayZone)Fixes (in order of preference):
- — place the zone behind all interactives.
overlayZone.setDepth(-9999) - Destroy the overlay zone while interactive panels are open; recreate on close.
- in this scene only — allows all overlapping interactives to receive the event (watch for double-fire on other scenes).
this.input.setTopOnly(false)
See for complete depth and topOnly semantics.
references/hit-test-and-depth.md用于滑动检测或拖拽关闭的全屏不可见区域,如果深度高于按钮,会静默吞灭所有指针事件。Phaser默认的命中测试会将事件仅路由到该点深度最高的交互元素。
topOnly诊断方法:——如果调试轮廓覆盖整个视口且其深度大于按钮深度,就是此问题导致。
this.input.enableDebug(overlayZone)修复方案(优先级从高到低):
- ——将区域置于所有交互元素之后。
overlayZone.setDepth(-9999) - 交互面板打开时销毁覆盖层,关闭时重新创建。
- 仅在当前场景中设置——允许所有重叠的交互元素接收事件(注意其他场景可能出现双击触发问题)。
this.input.setTopOnly(false)
查看获取完整的深度和topOnly语义说明。
references/hit-test-and-depth.mdAdditional Resources
额外资源
Reference Files
参考文件
- — Production-ready component classes: animated HealthBar, Button class, animated DialogBox, FloatingText, Panel, InventoryGrid
references/ui-patterns.md - — Phaser
references/hit-test-and-depth.mdhit-test semantics, invisible-zone depth patterns, drag-overlay click-swallow diagnostic checklist. Read when buttons or interactive children silently stop responding.topOnly - — In-place content rebuild for panels (tab switches, purchases) without flicker, first-visit typewriter dialogue skip pattern, chrome-preserving
references/panel-rebuild-patterns.mdidiom. Read when panels flash closed and reopen on content change.container.list.slice(base)
- ——生产就绪的组件类:带动画的生命值条、Button类、带动画的对话框、浮动文本、面板、库存网格
references/ui-patterns.md - ——Phaser
references/hit-test-and-depth.md命中测试语义、不可见区域深度模式、拖拽覆盖层点击吞灭诊断清单。当按钮或交互子元素无响应时请阅读。topOnly - ——面板内容原地重建(标签切换、购买操作)无闪烁方案、首次访问打字机对话跳过模式、保留框架的
references/panel-rebuild-patterns.md用法。当面板内容变化时出现闪关闭再重新打开的情况时请阅读。container.list.slice(base)