关于 phaser-ui
phaser-ui 是面向 Phaser 4 游戏界面的开发技能,适用于需要构建 HUD、控件和覆盖层的开发者。它提供基于游戏对象或独立 HUDScene 的实现模式,并涵盖响应式布局与交互层级问题。
主要功能
- 01使用游戏对象构建 UI
- 02提供固定滚动对象与 HUDScene 模式
- 03涵盖血条、按钮、对话框和小地图
- 04支持 DOM 输入覆盖层
- 05说明响应式布局与交互深度
适用场景
- 01为游戏场景添加分数和生命值 HUD
- 02实现按钮、对话框或进度条
- 03排查覆盖层遮挡交互的问题
- 04构建适应窗口变化的界面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yakoub-ai/ phaser4-gamedev --skill phaser-ui安装后在 Claude Code 中输入/phaser-ui确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/hit-test-and-depth.mdpanel-rebuild-patterns.mdui-patterns.md详细介绍
Phaser 4 UI 开发
Phaser 没有内置的 UI 工具包。你可以使用游戏对象(Text、Graphics、Image、Container)构建 UI,也可以使用并行的 HUDScene。两种基本方式:
- 滚动固定对象:将游戏对象添加到场景,调用
.setScrollFactor(0),使其在摄像机移动时保持固定。 - 并行 HUDScene:在游戏场景旁启动一个独立场景。该场景的摄像机不会移动,因此其中的所有内容天然固定。复杂 UI 适用此方式。
务必对 UI 元素调用 .setDepth(100)(或更高值),确保它们渲染在游戏对象上方。
血条(基于 Graphics)
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();
}
}
每次玩家受到伤害或恢复生命值时,都调用 setHealth(newValue)。
分数和文本显示
// 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}`);
为了适应不同布局,请根据 this.scale.width / this.scale.height 定位所有 HUD 文本(参见下文的“响应式 UI”)。
交互式按钮
方法 1 — 将文本用作按钮
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');
});
方法 2 — 使用纹理帧状态的图片按钮
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');
});
对话框
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
});
将缩放值从 0 动画过渡到 1,即可实现显示动画——动画版本参见 references/ui-patterns.md。
小地图(基于 Graphics 的点阵地图)
基于摄像机的小地图需要 WebGL 和渲染纹理。对大多数游戏来说,使用 Graphics 对象绘制点阵地图更简单,而且兼容 Canvas 和 WebGL:
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 })),
]);
通用进度条
可用于生命值、XP、加载进度、耐力等任何比例指标:
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', ...)
使用 BitmapText 提升性能
每次调用 setText 时,Phaser.GameObjects.Text 都会重新绘制到画布上。BitmapText 则会在纹理图集中切换 UV 坐标——对于每帧都会更新的数值(分数、帧计数器、伤害数字),性能开销低得多。
// 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}`);
适合使用 BitmapText 的内容:分数、计时器、浮动伤害数字、连击计数器。适合使用 Text 的内容:长段落、对话文本,以及任何使用系统字体或自定义 CSS 样式的内容。
使用 this.add.dom() 创建 DOM 覆盖层
需要使用原生 HTML 输入框的设置表单或聊天输入框:
// 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();
}
});
在游戏配置中启用 DOM:
const config: Phaser.Types.Core.GameConfig = {
// ...
dom: { createContainer: true },
};
注意:DOM 元素在移动设备和某些浏览器中存在 z-index 方面的问题。除非需要原生输入元素,否则 UI 应优先使用 Phaser 游戏对象。
使用 Scale Manager 实现响应式 UI
关于画布加 HUD 双层响应式尺寸规则的完整说明(包括可避免 iOS PWA 横屏错误的
100dvh+ 实时摄像机模式,以及模块级GAME_WIDTH常量冻结问题),请参阅skills/phaser-scene/references/scene-patterns.md→ 响应式尺寸:双层。请勿重复该内容——此处为权威说明。
切勿为 HUD 元素硬编码像素位置。请使用 this.scale.width / this.scale.height,并监听尺寸变化:
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);
});
}
两个独立的尺寸层
Phaser UI 有两个不同的尺寸问题,不可混为一谈:
- 画布缩放 — 由
ScaleManager(this.scale.width/height)处理。控制游戏画布如何映射到浏览器窗口。 - HUD / 覆盖层位置 — 创建时必须从
this.cameras.main.width/height获取(并在尺寸变化时更新)。切勿使用模块级常量(const GAME_W = 800)设置 HUD 位置——它们会冻结为启动时的值,无法适应画布尺寸变化。
// 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);
HUDScene 作为并行覆盖层时,其摄像机独立于 GameScene 摄像机。在 HUDScene 中从 this.cameras.main 获取的 HUD 位置始终反映 HUD 摄像机当前的视口——即使窗口尺寸变化后也仍然正确。
将 HUD 设为并行场景(复杂 UI 推荐)
对于分数文本和血条以外的 UI,请将所有 UI 移入专用的 HUDScene。这样做的好处包括:清晰分离、无需反复调整滚动因子,以及更方便地暂停或恢复游戏场景而不影响 UI。
// In GameScene.create():
this.scene.launch('HUDScene');
// Communicate via events:
this.events.emit('healthChanged', this.player.health);
this.events.emit('scoreChanged', this.score);
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);
}
}
完整的 HUDScene 启动模式和场景通信选项,请参阅 phaser-scene 技能。
拖动覆盖层吞噬事件
用于检测滑动或拖动关闭的全视口透明区域,如果层级高于按钮,就会悄无声息地吞噬所有指针事件。Phaser 默认的 topOnly 命中测试会将该位置的事件专门路由到层级最高的交互对象。
诊断方法: this.input.enableDebug(overlayZone)——如果调试轮廓覆盖整个视口,且其深度高于按钮的深度,原因就在这里。
修复方法(按优先顺序):
overlayZone.setDepth(-9999)——将区域置于所有交互对象后方。- 交互面板打开时销毁覆盖层区域;关闭时重新创建。
- 仅在此场景中使用
this.input.setTopOnly(false)——允许所有重叠的交互对象接收事件(注意其他场景中可能会重复触发)。
关于深度和 topOnly 语义的完整说明,请参阅 references/hit-test-and-depth.md。
其他资源
参考文件
references/ui-patterns.md— 可用于生产环境的组件类:动画生命值条、按钮类、动画对话框、浮动文本、面板、物品栏网格references/hit-test-and-depth.md— PhasertopOnly命中测试语义、透明区域深度模式、拖动覆盖层吞噬点击的诊断清单。当按钮或交互子项悄无声息地停止响应时请阅读。references/panel-rebuild-patterns.md— 面板内容的原位重建(切换标签页、购买),避免闪烁;首次访问时跳过打字机对话的模式;保留面板装饰的container.list.slice(base)惯用写法。当面板在内容更改时闪闭再打开,请阅读。