MenuScene:菜单场景与故障文字特效
> 214 行代码,把"菜单"做成了赛博朋克体验的第一入口。
## 场景概览
`MenuScene` 是 NEON RUNNER 2077 里最"花哨"的场景——故障文字、扫描线、全息面板、键盘/鼠标双输入、呼吸动画。它不是一个简单的"开始游戏"按钮列表,而是一个**沉浸式的菜单界面**。
```ts
// src/scenes/MenuScene.ts
export class MenuScene extends Phaser.Scene {
private buttons: MenuButton[] = [];
private selectedIndex = 0;
private titleGlitch!: GlitchText;
private subtitle!: Phaser.GameObjects.Text;
```
## 视觉层次:从背景到前景
`MenuScene` 的 `create()` 里,视觉元素按以下顺序叠加:
1. **背景网格**(`drawBackgroundGrid`)— 紫色细线,60px 间距,透明度 0.25
2. **扫描线**(`drawScanLines`)— 黑色横线,每 3px 一条,透明度 0.08
3. **故障标题**(`titleGlitch`)— GlitchText 组件,88px Orbitron
4. **副标题**(`subtitle`)— "// 2 0 7 7 //",呼吸动画
5. **全息面板**(`HoloPanel`)— 青色角标框,扫描浮线
6. **菜单按钮**(3 个 Container)— 键盘/鼠标可选
7. **底部信息**(版本号 + 操作提示)
这个顺序不是随意的——`setDepth()` 决定了谁在谁上面。背景网格 `setDepth(0)`,扫描线 `setDepth(20)`,按钮 `setDepth(6-7)`,底部信息 `setDepth(10)`。扫描线在网格之上但在按钮之下,所以按钮文字不会被扫描线遮挡。
## 扫描线效果:CRT 显示器的数字回声
```ts
private drawScanLines(width: number, height: number): void {
const g = this.add.graphics().setDepth(20).setAlpha(0.08);
g.fillStyle(0x000000, 1);
for (let y = 0; y < height; y += 3) {
g.fillRect(0, y, width, 1);
}
}
```
每 3 像素画一条 1 像素高的黑线,透明度 0.08。这个效果模拟的是 CRT 显示器的扫描线——老式电视的电子束从左到右、从上到下扫过屏幕,留下一条条暗线。
在赛博朋克语境里,扫描线不只是"复古",它暗示**这是一个模拟信号的世界**——不是干净的数字界面,而是有噪点、有干扰的系统。
## 菜单按钮:键盘 + 鼠标双输入
`MenuScene` 的按钮系统是整个项目里最完整的输入处理:
```ts
const labels = ['> JACK IN // START GAME', '> SYSTEM // OPTIONS', '> DISCONNECT // EXIT'];
const startY = height * 0.58 - 70;
labels.forEach((label, i) => {
const y = startY + i * 60;
const bg = this.add.rectangle(width / 2, y, 420, 48, COLORS.DARK_LINE, 0.6)
.setStrokeStyle(1, COLORS.NEON_CYAN, 0.4)
.setDepth(6);
const text = this.add.text(width / 2, y, label, {
fontFamily: FONTS.MONO,
fontSize: '20px',
color: '#FFFFFF',
}).setOrigin(0.5).setDepth(7);
const container = this.add.container(0, 0, [bg, text]).setDepth(6);
this.buttons.push({ container, text, bg, label });
});
```
每个按钮是一个 `Container`,包含背景矩形和文字。`Container` 的好处是:移动容器时,里面的所有子对象一起移动。
### 键盘导航
```ts
keyboard.on('keydown-UP', () => this.moveSelection(-1));
keyboard.on('keydown-DOWN', () => this.moveSelection(1));
keyboard.on('keydown-W', () => this.moveSelection(-1));
keyboard.on('keydown-S', () => this.moveSelection(1));
keyboard.on('keydown-ENTER', () => this.confirmSelection());
keyboard.on('keydown-SPACE', () => this.confirmSelection());
```
WASD 和方向键都支持,Enter 和 Space 都确认。这是"玩家友好"的最低标准——不要让玩家猜"哪个键能用"。
### 鼠标点击
```ts
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
for (let i = 0; i < this.buttons.length; i++) {
const btn = this.buttons[i];
const bg = btn.bg;
const halfW = bg.width / 2;
const halfH = bg.height / 2;
if (
pointer.x >= bg.x - halfW &&
pointer.x <= bg.x + halfW &&
pointer.y >= bg.y - halfH &&
pointer.y <= bg.y + halfH
) {
this.selectedIndex = i;
this.updateSelection();
this.confirmSelection();
return;
}
}
});
```
鼠标点击的碰撞检测是手动写的——没有用 Phaser 的 `setInteractive()`,因为按钮是 `Container`,而 Phaser 的交互系统对 Container 的支持在 3.90 版本里还不稳定。手动检测 `pointer.x` 和 `pointer.y` 是否在按钮的包围盒内,是最可靠的方式。
## 选中态切换
```ts
private updateSelection(): void {
this.buttons.forEach((btn, i) => {
const selected = i === this.selectedIndex;
btn.bg.setFillStyle(selected ? COLORS.NEON_YELLOW : COLORS.DARK_LINE, selected ? 0.25 : 0.6);
btn.bg.setStrokeStyle(1, selected ? COLORS.NEON_YELLOW : COLORS.NEON_CYAN, selected ? 1 : 0.4);
btn.text.setColor(selected ? '#FCEE0A' : '#FFFFFF');
});
}
```
选中态的视觉变化有三层:
1. 背景填充色从暗线色变成霓虹黄
2. 描边从青色变成黄色,线宽从 0.4 变成 1
3. 文字颜色从白色变成霓虹黄
这三层变化叠加在一起,让选中态非常明显——玩家不需要"猜"哪个按钮被选中了。
## 场景切换:淡出 + 重启
```ts
private confirmSelection(): void {
switch (this.selectedIndex) {
case 0:
this.cameras.main.fadeOut(400, 10, 10, 15);
this.cameras.main.once('camerafadeoutcomplete', () => {
this.scene.start('GameScene');
});
break;
case 2:
this.cameras.main.fadeOut(400, 255, 0, 60);
this.cameras.main.once('camerafadeoutcomplete', () => {
this.scene.start('MenuScene');
});
break;
}
}
```
"开始游戏"用深蓝 (10,10,15) 淡出,"退出"用红/橙 (255,0,60) 淡出——颜色暗示了"危险"。"选项"目前是空的(`break;` 没有逻辑),这是 TODO 标记。
## 下一章预告
`MenuScene` 讲完了,但菜单里最"花哨"的部分是 `GlitchText` 组件——那个 88px 的 "NEON RUNNER" 标题是怎么做出 RGB 分离故障效果的?下一章我会拆解这个 105 行的组件。
> 菜单设计的核心原则:**视觉层次 > 功能密度**。菜单不是功能列表,是游戏体验的第一印象。
- 点赞
- 收藏
- 关注作者
评论(0)