$npx -y skills add Yakoub-ai/phaser4-gamedev --skill phaser-uiThis skill should be used when the user asks to "add health bar", "create menu", "UI elements", "dialog box", "inventory system", "create buttons", "HUD overlay", "score display", "minimap", "progress bar", "bitmap text", "interactive button", or "UI layout".
| 1 | # Phaser 4 UI Development |
| 2 | |
| 3 | 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: |
| 4 | |
| 5 | 1. **Scroll-fixed objects**: Add game objects to the scene, call `.setScrollFactor(0)` so they stay fixed when the camera moves. |
| 6 | 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. |
| 7 | |
| 8 | Always call `.setDepth(100)` (or higher) on UI elements so they render above game objects. |
| 9 | |
| 10 | --- |
| 11 | |
| 12 | ## Health Bar (Graphics-based) |
| 13 | |
| 14 | ```typescript |
| 15 | class HealthBar { |
| 16 | private bar: Phaser.GameObjects.Graphics; |
| 17 | private x: number; |
| 18 | private y: number; |
| 19 | private width = 200; |
| 20 | private height = 20; |
| 21 | private maxHealth: number; |
| 22 | private health: number; |
| 23 | |
| 24 | constructor(scene: Phaser.Scene, x: number, y: number, maxHealth: number) { |
| 25 | this.x = x; |
| 26 | this.y = y; |
| 27 | this.maxHealth = this.health = maxHealth; |
| 28 | this.bar = scene.add.graphics().setScrollFactor(0).setDepth(100); |
| 29 | this.draw(); |
| 30 | } |
| 31 | |
| 32 | setHealth(value: number): void { |
| 33 | this.health = Phaser.Math.Clamp(value, 0, this.maxHealth); |
| 34 | this.draw(); |
| 35 | } |
| 36 | |
| 37 | private draw(): void { |
| 38 | this.bar.clear(); |
| 39 | // Background border |
| 40 | this.bar.fillStyle(0x000000, 0.6); |
| 41 | this.bar.fillRect(this.x - 2, this.y - 2, this.width + 4, this.height + 4); |
| 42 | // Colored fill — green above 50%, yellow above 25%, red below |
| 43 | const ratio = this.health / this.maxHealth; |
| 44 | const color = ratio > 0.5 ? 0x00ff00 : ratio > 0.25 ? 0xffff00 : 0xff0000; |
| 45 | this.bar.fillStyle(color, 1); |
| 46 | this.bar.fillRect(this.x, this.y, this.width * ratio, this.height); |
| 47 | } |
| 48 | |
| 49 | destroy(): void { |
| 50 | this.bar.destroy(); |
| 51 | } |
| 52 | } |
| 53 | ``` |
| 54 | |
| 55 | Use `setHealth(newValue)` every time the player takes damage or heals. |
| 56 | |
| 57 | --- |
| 58 | |
| 59 | ## Score and Text Display |
| 60 | |
| 61 | ```typescript |
| 62 | // In create(): |
| 63 | this.scoreText = this.add.text(16, 16, 'Score: 0', { |
| 64 | fontSize: '24px', |
| 65 | color: '#ffffff', |
| 66 | stroke: '#000000', |
| 67 | strokeThickness: 3, |
| 68 | }).setScrollFactor(0).setDepth(100); |
| 69 | |
| 70 | // When score changes: |
| 71 | this.scoreText.setText(`Score: ${score}`); |
| 72 | ``` |
| 73 | |
| 74 | Position all HUD text relative to `this.scale.width` / `this.scale.height` for responsive layouts (see Responsive UI below). |
| 75 | |
| 76 | --- |
| 77 | |
| 78 | ## Interactive Buttons |
| 79 | |
| 80 | ### Method 1 — Text as button |
| 81 | |
| 82 | ```typescript |
| 83 | const btn = this.add.text(400, 300, 'PLAY', { |
| 84 | fontSize: '32px', |
| 85 | backgroundColor: '#4a4a8a', |
| 86 | padding: { x: 20, y: 10 }, |
| 87 | }) |
| 88 | .setOrigin(0.5) |
| 89 | .setInteractive({ useHandCursor: true }) |
| 90 | .on('pointerover', () => btn.setStyle({ color: '#ffff00' })) |
| 91 | .on('pointerout', () => btn.setStyle({ color: '#ffffff' })) |
| 92 | .on('pointerdown', () => btn.setScale(0.95)) |
| 93 | .on('pointerup', () => { |
| 94 | btn.setScale(1); |
| 95 | this.scene.start('GameScene'); |
| 96 | }); |
| 97 | ``` |
| 98 | |
| 99 | ### Method 2 — Image button with texture frame states |
| 100 | |
| 101 | ```typescript |
| 102 | const playBtn = this.add.image(400, 300, 'ui', 'btn-play-normal.png') |
| 103 | .setInteractive({ useHandCursor: true }); |
| 104 | |
| 105 | playBtn.on('pointerover', () => playBtn.setFrame('btn-play-hover.png')); |
| 106 | playBtn.on('pointerout', () => playBtn.setFrame('btn-play-normal.png')); |
| 107 | playBtn.on('pointerdown', () => playBtn.setFrame('btn-play-pressed.png')); |
| 108 | playBtn.on('pointerup', () => { |
| 109 | playBtn.setFrame('btn-play-normal.png'); |
| 110 | this.scene.start('GameScene'); |
| 111 | }); |
| 112 | ``` |
| 113 | |
| 114 | --- |
| 115 | |
| 116 | ## Dialog Box |
| 117 | |
| 118 | ```typescript |
| 119 | class DialogBox extends Phaser.GameObjects.Container { |
| 120 | constructor(scene: Phaser.Scene, text: string, onClose: () => void) { |
| 121 | super(scene, scene.scale.width / 2, scene.scale.height / 2); |
| 122 | scene.add.existing(this); |
| 123 | this.setDepth(200).setScrollFactor(0); |
| 124 | |
| 125 | const bg = scene.add.graphics(); |
| 126 | bg.fillStyle(0x222244, 0.95); |
| 127 | bg.fillRoundedRect(-200, -80, 400, 160, 16); |
| 128 | |
| 129 | const label = scene.add.text(0, -30, text, { |
| 130 | fontSize: '20px', |
| 131 | color: '#ffffff', |
| 132 | wordWrap: { width: 360 }, |
| 133 | align: 'center', |
| 134 | }).setOrigin(0.5); |
| 135 | |
| 136 | const closeBtn = scene.add.text(0, 50, 'OK', { |
| 137 | fontSize: '20px', |
| 138 | backgroundColor: '#4488aa', |
| 139 | padding: { x: 20, y: 8 }, |
| 140 | }) |
| 141 | .setOrigin(0.5) |
| 142 | .setInteractive({ useHandCursor: true }) |
| 143 | .on('pointerup', () => { |
| 144 | this.destroy(); |
| 145 | onClose(); |
| 146 | }); |
| 147 | |
| 148 | this.add([bg, label, closeBtn]); |
| 149 | } |
| 150 | } |
| 151 | |
| 152 | // Usage: |
| 153 | new DialogBox(this, 'You found a treasure chest!', () => { |
| 154 | // Resume game logic after dialog closes |
| 155 | }); |
| 156 | ``` |
| 157 | |
| 158 | Animate the appearance by tweening scale from 0 to 1 — see `references/ui-patterns.md` for the animated version. |
| 159 | |
| 160 | --- |
| 161 | |
| 162 | ## Minimap (Graphics-based dot map) |
| 163 | |
| 164 | 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 |