8. 游戏UI
2026/4/14大约 5 分钟
休闲益智——游戏UI
想象你走进一家餐厅,菜单上没有图片、没有价格、服务员也不理你——你肯定会掉头就走。游戏也一样:如果玩家不知道自己得了多少分、还剩几步、目标是什么,很快就会失去兴趣。
游戏 UI 就是玩家的"信息窗口"——让玩家随时知道"我玩得怎么样了"。
本章你将学到
- 设计三消游戏的界面布局
- 创建 HUD(抬头显示)系统
- 实现过关/失败弹窗
- 连击和分数飘字效果
- 星级评价动画
界面布局
三消游戏的 UI 布局通常是这样的:
┌──────────────────────────┐
│ 关卡 5 分数: 1200 │ ← 顶部信息栏
│ ┌──────────────────┐ │
│ │ │ │
│ │ 8x8 棋盘 │ │ ← 游戏主区域
│ │ │ │
│ └──────────────────┘ │
│ 目标: ★★★☆☆ 3500/5000 │ ← 目标进度条
│ 剩余步数: 25/30 │ ← 步数显示
└──────────────────────────┘HUD 场景结构
HUD(Head-Up Display)是覆盖在游戏画面上的信息层,用 CanvasLayer 确保它始终显示在最前面。
HUD (CanvasLayer) ← UI 层
├── TopBar (HBoxContainer) ← 顶部信息栏
│ ├── LevelLabel (Label) ← "关卡 5"
│ ├── ScoreLabel (Label) ← "分数: 1200"
│ └── MovesLabel (Label) ← "步数: 25/30"
├── TargetPanel (PanelContainer) ← 目标进度面板
│ ├── TargetLabel (Label) ← "目标分数"
│ ├── ProgressBar ← 进度条
│ └── StarsContainer (HBoxContainer) ← 星级显示
├── ComboLabel (Label) ← 连击显示(默认隐藏)
└── ResultPopup (Control) ← 过关/失败弹窗(默认隐藏)HUD 代码实现
C
using Godot;
/// <summary>
/// HUD 控制器 —— 管理游戏内所有 UI 元素的更新。
/// 就像体育场的大屏幕:实时显示比分、时间等信息。
/// </summary>
public partial class HUD : CanvasLayer
{
[Export] public Label LevelLabel { get; set; }
[Export] public Label ScoreLabel { get; set; }
[Export] public Label MovesLabel { get; set; }
[Export] public Label ComboLabel { get; set; }
[Export] public ProgressBar TargetBar { get; set; }
[Export] public Control ResultPopup { get; set; }
public override void _Ready()
{
// 监听游戏管理器的信号,实时更新 UI
if (GameManager.Instance != null)
{
GameManager.Instance.Connect(
GameManager.SignalName.ScoreChanged,
new Callable(this, MethodName.OnScoreChanged));
GameManager.Instance.Connect(
GameManager.SignalName.MovesChanged,
new Callable(this, MethodName.OnMovesChanged));
GameManager.Instance.Connect(
GameManager.SignalName.StateChanged,
new Callable(this, MethodName.OnStateChanged));
}
ComboLabel.Visible = false;
ResultPopup.Visible = false;
}
/// <summary>分数变化时更新显示</summary>
private void OnScoreChanged(int score)
{
ScoreLabel.Text = $"分数: {score}";
}
/// <summary>步数变化时更新显示</summary>
private void OnMovesChanged(int moves)
{
MovesLabel.Text = $"步数: {moves}";
// 步数不多时变红色,给玩家紧迫感
MovesLabel.Modulate = moves <= 5
? new Color("#FF4444")
: new Color("#FFFFFF");
}
/// <summary>游戏状态变化时处理</summary>
private void OnStateChanged(GameManager.GameState newState)
{
if (newState == GameManager.GameState.LevelComplete)
{
ShowResult(true);
}
else if (newState == GameManager.GameState.GameOver)
{
ShowResult(false);
}
}
/// <summary>显示连击(带放大动画)</summary>
public void ShowCombo(int combo)
{
if (combo < 2) return;
ComboLabel.Text = $"连击 x{combo}!";
ComboLabel.Visible = true;
// 先缩小再弹大的动画效果
var tween = CreateTween();
ComboLabel.Scale = Vector2.One * 0.5f;
tween.TweenProperty(ComboLabel, "scale",
Vector2.One * 1.5f, 0.2f)
.SetTrans(Tween.TransitionType.Back);
tween.TweenProperty(ComboLabel, "scale",
Vector2.One, 0.3f);
tween.TweenCallback(
new Callable(this, MethodName.HideCombo))
.SetDelay(1.5f);
}
private void HideCombo()
{
ComboLabel.Visible = false;
}
/// <summary>更新目标进度条</summary>
public void UpdateTargetProgress(float progress)
{
TargetBar.Value = Mathf.Min(progress * 100.0, 100.0);
}
/// <summary>显示过关或失败弹窗</summary>
private void ShowResult(bool won)
{
ResultPopup.Visible = true;
// 具体的弹窗内容在 ResultPopup 脚本中处理
}
}GDScript
extends CanvasLayer
## HUD 控制器 —— 管理游戏内所有 UI 元素的更新
@onready var level_label: Label = $TopBar/LevelLabel
@onready var score_label: Label = $TopBar/ScoreLabel
@onready var moves_label: Label = $TopBar/MovesLabel
@onready var combo_label: Label = $ComboLabel
@onready var target_bar: ProgressBar = $TargetPanel/ProgressBar
@onready var result_popup: Control = $ResultPopup
func _ready() -> void:
# 监听游戏管理器的信号
if GameManager.instance:
GameManager.instance.score_changed.connect(_on_score_changed)
GameManager.instance.moves_changed.connect(_on_moves_changed)
GameManager.instance.state_changed.connect(_on_state_changed)
combo_label.visible = false
result_popup.visible = false
## 分数变化时更新显示
func _on_score_changed(score: int) -> void:
score_label.text = "分数: %d" % score
## 步数变化时更新显示
func _on_moves_changed(moves: int) -> void:
moves_label.text = "步数: %d" % moves
moves_label.modulate = Color("#FF4444") if moves <= 5 else Color("#FFFFFF")
## 游戏状态变化时处理
func _on_state_changed(new_state: int) -> void:
if new_state == GameManager.GameState.LEVEL_COMPLETE:
_show_result(true)
elif new_state == GameManager.GameState.GAME_OVER:
_show_result(false)
## 显示连击(带放大动画)
func show_combo(combo: int) -> void:
if combo < 2:
return
combo_label.text = "连击 x%d!" % combo
combo_label.visible = true
var tween = create_tween()
combo_label.scale = Vector2.ONE * 0.5
tween.tween_property(combo_label, "scale", Vector2.ONE * 1.5, 0.2)
tween.set_trans(Tween.TRANS_BACK)
tween.tween_property(combo_label, "scale", Vector2.ONE, 0.3)
tween.tween_callback(_hide_combo).set_delay(1.5)
func _hide_combo() -> void:
combo_label.visible = false
## 更新目标进度条
func update_target_progress(progress: float) -> void:
target_bar.value = minf(progress * 100.0, 100.0)
## 显示过关或失败弹窗
func _show_result(won: bool) -> void:
result_popup.visible = true过关/失败弹窗
关卡结束时弹出的界面。过关时显示星级和分数,失败时显示"再试一次"按钮。
C
/// <summary>
/// 过关/失败弹窗 —— 关卡结束时的结算界面。
/// 过关时:显示分数、星级、下一关按钮。
/// 失败时:显示"差一点"的鼓励语、重试按钮。
/// </summary>
public partial class ResultPopup : Control
{
[Export] public Label TitleLabel { get; set; }
[Export] public Label ScoreLabel { get; set; }
[Export] public Label StarsLabel { get; set; }
[Export] public Button NextLevelBtn { get; set; }
[Export] public Button RetryBtn { get; set; }
[Signal] public delegate void NextLevelEventHandler();
[Signal] public delegate void RetryEventHandler();
public void ShowResult(bool won, int score, int stars)
{
if (won)
{
TitleLabel.Text = "关卡通过!";
TitleLabel.Modulate = new Color("#FFD700");
NextLevelBtn.Visible = true;
// 显示星级(用文字模拟星星图标)
StarsLabel.Text = "★".Repeat(stars) + "☆".Repeat(3 - stars);
}
else
{
TitleLabel.Text = "差一点就过了!";
TitleLabel.Modulate = new Color("#FF6666");
NextLevelBtn.Visible = false;
StarsLabel.Text = "";
}
ScoreLabel.Text = $"分数: {score}";
Visible = true;
// 弹出动画:从缩小到正常大小
Scale = Vector2.One * 0.5f;
var tween = CreateTween();
tween.TweenProperty(this, "scale", Vector2.One, 0.3f)
.SetTrans(Tween.TransitionType.Back);
}
private void OnNextLevelPressed()
{
EmitSignal(SignalName.NextLevel);
}
private void OnRetryPressed()
{
EmitSignal(SignalName.Retry);
}
}GDScript
extends Control
## 过关/失败弹窗
@onready var title_label: Label = $Panel/TitleLabel
@onready var score_label: Label = $Panel/ScoreLabel
@onready var stars_label: Label = $Panel/StarsLabel
@onready var next_level_btn: Button = $Panel/NextLevelBtn
@onready var retry_btn: Button = $Panel/RetryBtn
signal next_level
signal retry
func show_result(won: bool, score: int, stars: int) -> void:
if won:
title_label.text = "关卡通过!"
title_label.modulate = Color("#FFD700")
next_level_btn.visible = true
stars_label.text = "★".repeat(stars) + "☆".repeat(3 - stars)
else:
title_label.text = "差一点就过了!"
title_label.modulate = Color("#FF6666")
next_level_btn.visible = false
stars_label.text = ""
score_label.text = "分数: %d" % score
visible = true
# 弹出动画
scale = Vector2.ONE * 0.5
var tween = create_tween()
tween.tween_property(self, "scale", Vector2.ONE, 0.3)
tween.set_trans(Tween.TRANS_BACK)
func _on_next_level_pressed() -> void:
next_level.emit()
func _on_retry_pressed() -> void:
retry.emit()UI 设计要点
| 要点 | 说明 | 例子 |
|---|---|---|
| 信息层次 | 最重要的信息放最上面 | 分数和步数在顶部,一目了然 |
| 颜色编码 | 用颜色传达信息 | 步数少于5时变红色 |
| 动画反馈 | 数值变化时有动画 | 分数增加时数字跳动 |
| 不遮挡棋盘 | UI 不要挡住游戏区域 | 信息栏在棋盘上方和下方 |
| 字体大小 | 关键数字用大字体 | 步数和分数要足够大 |
下一章预告
UI 做好了,但游戏还是"哑巴"——消除方块没有声音,过关没有庆祝音效。下一章我们将添加音效和视觉特效,让游戏"有声有色"。
