8. 游戏界面
2026/4/14大约 8 分钟
开心消消乐——游戏界面
什么是游戏界面?
游戏界面(UI,User Interface)就是玩家和游戏之间的"对话窗口"。通过界面,玩家可以看到自己的分数、剩余步数,也可以看到过关提示、失败弹窗等信息。
你可以把游戏界面想象成一个仪表盘——就像汽车仪表盘显示速度、油量一样,游戏界面显示分数、步数、目标等关键信息。
界面组成
| 组件 | 说明 | 位置 |
|---|---|---|
| HUD | 顶部信息栏,显示分数和步数 | 顶部 |
| 过关弹窗 | 达到目标分数时弹出 | 屏幕中央 |
| 失败弹窗 | 步数用完时弹出 | 屏幕中央 |
| 关卡选择 | 选择要玩的关卡 | 主菜单 |
HUD(顶部信息栏)
HUD 是 Head-Up Display 的缩写,最早是战斗机上用的术语,意思是"把信息投射在视野上方,不用低头看仪表"。在游戏中,HUD 就是始终显示在屏幕上的关键信息。
HUD 布局
┌──────────────────────────────┐
│ 分数: 1250 步数: 18/30 │ ← HUD
│ 目标: 2000 │
├──────────────────────────────┤
│ │
│ 8x8 棋盘区域 │
│ │
│ │
└──────────────────────────────┘HUD 场景
创建一个 HUD 场景,包含分数、步数和进度条:
C
using Godot;
/// <summary>
/// 游戏顶部信息栏 (HUD)
/// 显示分数、步数、目标分数和进度条
/// </summary>
public partial class HUD : Control
{
/// <summary>分数标签</summary>
[Export] public Label ScoreLabel { get; set; }
/// <summary>步数标签</summary>
[Export] public Label MovesLabel { get; set; }
/// <summary>目标标签</summary>
[Export] public Label TargetLabel { get; set; }
/// <summary>关卡标签</summary>
[Export] public Label LevelLabel { get; set; }
/// <summary>进度条</summary>
[Export] public ProgressBar ScoreProgress { get; set; }
/// <summary>分数动画标签(飘字效果)</summary>
[Export] public Label ScoreAnimationLabel { get; set; }
/// <summary>GameManager 引用</summary>
private GameManager _gameManager;
/// <summary>LevelManager 引用</summary>
private LevelManager _levelManager;
public override void _Ready()
{
_gameManager = GetNode<GameManager>("/root/Main/GameManager");
_levelManager = GetNode<LevelManager>("/root/Main/LevelManager");
// 连接信号
_gameManager.ScoreChanged += OnScoreChanged;
_gameManager.MovesChanged += OnMovesChanged;
_levelManager.LevelDataChanged += OnLevelChanged;
// 初始更新
UpdateHUD();
}
/// <summary>
/// 分数变化时的回调
/// </summary>
private void OnScoreChanged(int newScore)
{
ScoreLabel.Text = $"分数: {newScore}";
// 更新进度条
var level = _levelManager.GetCurrentLevel();
if (level != null)
{
float progress = Mathf.Min((float)newScore / level.ThreeStarScore, 1.0f);
ScoreProgress.Value = progress;
}
// 显示分数飘字动画
ShowScoreAnimation(newScore);
}
/// <summary>
/// 步数变化时的回调
/// </summary>
private void OnMovesChanged(int movesLeft)
{
MovesLabel.Text = $"步数: {movesLeft}";
// 步数不足5步时变红色提醒
if (movesLeft <= 5)
{
MovesLabel.Modulate = new Color("FF4444");
}
else
{
MovesLabel.Modulate = Colors.White;
}
}
/// <summary>
/// 关卡变化时的回调
/// </summary>
private void OnLevelChanged(LevelData data)
{
LevelLabel.Text = $"第 {data.LevelNumber} 关";
TargetLabel.Text = $"目标: {data.ThreeStarScore}";
ScoreProgress.MaxValue = data.ThreeStarScore;
ScoreProgress.Value = 0;
UpdateHUD();
}
/// <summary>
/// 手动更新 HUD 显示
/// </summary>
public void UpdateHUD()
{
ScoreLabel.Text = $"分数: {_gameManager.GetScore()}";
MovesLabel.Text = $"步数: {_gameManager.GetMovesLeft()}";
}
/// <summary>
/// 显示分数增加的飘字动画
/// </summary>
private async void ShowScoreAnimation(int score)
{
ScoreAnimationLabel.Text = $"+{score}";
ScoreAnimationLabel.Modulate = new Color("FFDD44");
ScoreAnimationLabel.Visible = true;
// 向上飘并淡出
Tween tween = CreateTween();
tween.TweenProperty(ScoreAnimationLabel, "position:y",
ScoreAnimationLabel.Position.Y - 30, 0.8f);
tween.Parallel().TweenProperty(ScoreAnimationLabel, "modulate:a",
0f, 0.8f);
await ToSignal(tween, Tween.SignalName.Finished);
ScoreAnimationLabel.Visible = false;
ScoreAnimationLabel.Modulate = new Color("FFDD44");
}
}GDScript
extends Control
## 分数标签
@export var score_label: Label
## 步数标签
@export var moves_label: Label
## 目标标签
@export var target_label: Label
## 关卡标签
@export var level_label: Label
## 进度条
@export var score_progress: ProgressBar
## 分数动画标签(飘字效果)
@export var score_animation_label: Label
## GameManager 引用
var _game_manager: GameManager
## LevelManager 引用
var _level_manager: LevelManager
func _ready() -> void:
_game_manager = get_node("/root/Main/GameManager")
_level_manager = get_node("/root/Main/LevelManager")
# 连接信号
_game_manager.score_changed.connect(_on_score_changed)
_game_manager.moves_changed.connect(_on_moves_changed)
_level_manager.level_data_changed.connect(_on_level_changed)
# 初始更新
update_hud()
## 分数变化时的回调
func _on_score_changed(new_score: int) -> void:
score_label.text = "分数: %d" % new_score
# 更新进度条
var level := _level_manager.get_current_level()
if level != null:
var progress: float = minf(float(new_score) / float(level.three_star_score), 1.0)
score_progress.value = progress
# 显示分数飘字动画
_show_score_animation(new_score)
## 步数变化时的回调
func _on_moves_changed(moves_left: int) -> void:
moves_label.text = "步数: %d" % moves_left
# 步数不足5步时变红色提醒
if moves_left <= 5:
moves_label.modulate = Color("FF4444")
else:
moves_label.modulate = Color.WHITE
## 关卡变化时的回调
func _on_level_changed(data) -> void:
level_label.text = "第 %d 关" % data.level_number
target_label.text = "目标: %d" % data.three_star_score
score_progress.max_value = data.three_star_score
score_progress.value = 0
update_hud()
## 手动更新 HUD 显示
func update_hud() -> void:
score_label.text = "分数: %d" % _game_manager.get_score()
moves_label.text = "步数: %d" % _game_manager.get_moves_left()
## 显示分数增加的飘字动画
func _show_score_animation(score: int) -> void:
score_animation_label.text = "+%d" % score
score_animation_label.modulate = Color("FFDD44")
score_animation_label.visible = true
# 向上飘并淡出
var tween := create_tween()
tween.tween_property(score_animation_label, "position:y",
score_animation_label.position.y - 30, 0.8)
tween.parallel().tween_property(score_animation_label, "modulate:a",
0.0, 0.8)
await tween.finished
score_animation_label.visible = false
score_animation_label.modulate = Color("FFDD44")过关弹窗
当玩家达到目标分数时,弹出一个漂亮的过关界面,显示获得的星星和分数。
过关弹窗布局
┌────────────────────┐
│ │
│ ★ ★ ☆ │ ← 星级评价
│ │
│ 分数: 2150 │ ← 最终分数
│ 目标: 2000 │
│ │
│ ┌──────┐ ┌──────┐ │
│ │ 下一关 │ │ 重玩 │ │ ← 按钮
│ └──────┘ └──────┘ │
└────────────────────┘C
using Godot;
/// <summary>
/// 过关弹窗
/// 达到目标分数时显示
/// </summary>
public partial class WinPopup : Control
{
/// <summary>背景遮罩</summary>
[Export] public ColorRect Overlay { get; set; }
/// <summary>弹窗容器</summary>
[Export] public PanelContainer PopupPanel { get; set; }
/// <summary>关卡标题</summary>
[Export] public Label TitleLabel { get; set; }
/// <summary>分数标签</summary>
[Export] public Label ScoreLabel { get; set; }
/// <summary>三颗星星的 TextureRect</summary>
[Export] public TextureRect Star1 { get; set; }
[Export] public TextureRect Star2 { get; set; }
[Export] public TextureRect Star3 { get; set; }
/// <summary>"下一关"按钮</summary>
[Export] public Button NextButton { get; set; }
/// <summary>"重玩"按钮</summary>
[Export] public Button ReplayButton { get; set; }
/// <summary>"返回"按钮</summary>
[Export] public Button HomeButton { get; set; }
// 信号
[Signal] public delegate void NextLevelPressedEventHandler();
[Signal] public delegate void ReplayPressedEventHandler();
[Signal] public delegate void HomePressedEventHandler();
public override void _Ready()
{
// 默认隐藏
Visible = false;
// 连接按钮信号
NextButton.Pressed += () => EmitSignal(SignalName.NextLevelPressed);
ReplayButton.Pressed += () => EmitSignal(SignalName.ReplayPressed);
HomeButton.Pressed += () => EmitSignal(SignalName.HomePressed);
}
/// <summary>
/// 显示过关弹窗
/// </summary>
/// <param name="level">关卡数据</param>
/// <param name="score">最终分数</param>
/// <param name="stars">获得的星级</param>
public async void Show(LevelData level, int score, int stars)
{
Visible = true;
// 设置文字
TitleLabel.Text = $"第 {level.LevelNumber} 关 完成!";
ScoreLabel.Text = $"分数: {score} / {level.ThreeStarScore}";
// 先把所有星星设为灰色(未点亮)
SetStarState(Star1, false);
SetStarState(Star2, false);
SetStarState(Star3, false);
// 依次点亮星星(带动画)
await ToSignal(GetTree().CreateTimer(0.3), SceneTree.TimerSignalName.Timeout);
if (stars >= 1) SetStarState(Star1, true);
await ToSignal(GetTree().CreateTimer(0.3), SceneTree.TimerSignalName.Timeout);
if (stars >= 2) SetStarState(Star2, true);
await ToSignal(GetTree().CreateTimer(0.3), SceneTree.TimerSignalName.Timeout);
if (stars >= 3) SetStarState(Star3, true);
}
/// <summary>
/// 设置星星的点亮状态
/// </summary>
private void SetStarState(TextureRect star, bool lit)
{
star.Modulate = lit ? new Color("FFDD44") : new Color("666666");
if (lit)
{
// 点亮时的缩放弹跳动画
Tween tween = CreateTween();
star.Scale = Vector2.One * 1.5f;
tween.TweenProperty(star, "scale", Vector2.One, 0.3f)
.SetTrans(Tween.TransitionType.Bounce)
.SetEase(Tween.EaseType.Out);
}
}
/// <summary>
/// 隐藏弹窗
/// </summary>
public void Hide()
{
Visible = false;
}
}GDScript
extends Control
## 背景遮罩
@export var overlay: ColorRect
## 弹窗容器
@export var popup_panel: PanelContainer
## 关卡标题
@export var title_label: Label
## 分数标签
@export var score_label: Label
## 三颗星星
@export var star1: TextureRect
@export var star2: TextureRect
@export var star3: TextureRect
## 按钮
@export var next_button: Button
@export var replay_button: Button
@export var home_button: Button
## 信号
signal next_level_pressed()
signal replay_pressed()
signal home_pressed()
func _ready() -> void:
# 默认隐藏
visible = false
# 连接按钮信号
next_button.pressed.connect(func(): next_level_pressed.emit())
replay_button.pressed.connect(func(): replay_pressed.emit())
home_button.pressed.connect(func(): home_pressed.emit())
## 显示过关弹窗
## level: 关卡数据, score: 最终分数, stars: 获得的星级
func show_win(level: LevelData, score: int, stars: int) -> void:
visible = true
# 设置文字
title_label.text = "第 %d 关 完成!" % level.level_number
score_label.text = "分数: %d / %d" % [score, level.three_star_score]
# 先把所有星星设为灰色(未点亮)
_set_star_state(star1, false)
_set_star_state(star2, false)
_set_star_state(star3, false)
# 依次点亮星星(带动画)
await get_tree().create_timer(0.3).timeout
if stars >= 1:
_set_star_state(star1, true)
await get_tree().create_timer(0.3).timeout
if stars >= 2:
_set_star_state(star2, true)
await get_tree().create_timer(0.3).timeout
if stars >= 3:
_set_star_state(star3, true)
## 设置星星的点亮状态
func _set_star_state(star: TextureRect, lit: bool) -> void:
star.modulate = Color("FFDD44") if lit else Color("666666")
if lit:
# 点亮时的缩放弹跳动画
star.scale = Vector2.ONE * 1.5
var tween := create_tween()
tween.tween_property(star, "scale", Vector2.ONE, 0.3) \
.set_trans(Tween.TransitionType.BOUNCE) \
.set_ease(Tween.EaseType.OUT)
## 隐藏弹窗
func hide_popup() -> void:
visible = false失败弹窗
当步数用完但没达到目标分数时,显示失败弹窗。
C
/// <summary>
/// 失败弹窗
/// 步数用完时显示
/// </summary>
public partial class LosePopup : Control
{
[Export] public Label TitleLabel { get; set; }
[Export] public Label ScoreLabel { get; set; }
[Export] public Label TargetLabel { get; set; }
[Export] public Button RetryButton { get; set; }
[Export] public Button HomeButton { get; set; }
[Signal] public delegate void RetryPressedEventHandler();
[Signal] public delegate void HomePressedEventHandler();
public override void _Ready()
{
Visible = false;
RetryButton.Pressed += () => EmitSignal(SignalName.RetryPressed);
HomeButton.Pressed += () => EmitSignal(SignalName.HomePressed);
}
/// <summary>
/// 显示失败弹窗
/// </summary>
public void Show(int score, int targetScore)
{
Visible = true;
TitleLabel.Text = "挑战失败";
ScoreLabel.Text = $"获得: {score} 分";
TargetLabel.Text = $"目标: {targetScore} 分";
}
public void Hide()
{
Visible = false;
}
}GDScript
extends Control
@export var title_label: Label
@export var score_label: Label
@export var target_label: Label
@export var retry_button: Button
@export var home_button: Button
signal retry_pressed()
signal home_pressed()
func _ready() -> void:
visible = false
retry_button.pressed.connect(func(): retry_pressed.emit())
home_button.pressed.connect(func(): home_pressed.emit())
## 显示失败弹窗
func show_lose(score: int, target_score: int) -> void:
visible = true
title_label.text = "挑战失败"
score_label.text = "获得: %d 分" % score
target_label.text = "目标: %d 分" % target_score
func hide_popup() -> void:
visible = false关卡选择界面
关卡选择界面让玩家可以选择要玩的关卡。已解锁的关卡可以点击进入,未解锁的关卡显示锁图标。
关卡选择布局
┌──────────────────────────┐
│ 选择关卡 │
│ │
│ [1] [2] [3] [4] [5] │ ← 关卡按钮
│ ★ ★ ★ ★ ★ │ ← 星级显示
│ │
│ [6] [7] [8] [9] [10] │
│ ★ ★ ★ ★ 🔒 │ ← 未解锁显示锁
│ │
│ [11] [12] [13] ... │
│ 🔒 🔒 🔒 ... │
└──────────────────────────┘C
using Godot;
using System;
/// <summary>
/// 关卡选择界面
/// </summary>
public partial class LevelSelect : Control
{
/// <summary>关卡按钮容器(GridContainer)</summary>
[Export] public GridContainer LevelGrid { get; set; }
/// <summary>关卡按钮场景</summary>
[Export] public PackedScene LevelButtonScene { get; set; }
/// <summary>返回按钮</summary>
[Export] public Button BackButton { get; set; }
/// <summary>关卡管理器引用</summary>
private LevelManager _levelManager;
[Signal] public delegate void LevelSelectedEventHandler(int levelNumber);
[Signal] public delegate void BackPressedEventHandler();
public override void _Ready()
{
Visible = false;
_levelManager = GetNode<LevelManager>("/root/Main/LevelManager");
BackButton.Pressed += () => EmitSignal(SignalName.BackPressed);
GenerateLevelButtons();
}
/// <summary>
/// 生成关卡按钮
/// </summary>
private void GenerateLevelButtons()
{
// 清空现有按钮
foreach (Node child in LevelGrid.GetChildren())
{
child.QueueFree();
}
// 生成所有关卡按钮
int totalLevels = 50;
for (int i = 1; i <= totalLevels; i++)
{
var button = LevelButtonScene.Instantiate<Button>();
button.Text = i.ToString();
bool isUnlocked = i <= _levelManager._maxUnlockedLevel;
if (isUnlocked)
{
int levelNum = i;
button.Pressed += () => EmitSignal(SignalName.LevelSelected, levelNum);
button.Disabled = false;
}
else
{
button.Text = "🔒";
button.Disabled = true;
}
LevelGrid.AddChild(button);
}
}
}GDScript
extends Control
## 关卡按钮容器(GridContainer)
@export var level_grid: GridContainer
## 关卡按钮场景
@export var level_button_scene: PackedScene
## 返回按钮
@export var back_button: Button
## 关卡管理器引用
var _level_manager: LevelManager
signal level_selected(level_number: int)
signal back_pressed()
func _ready() -> void:
visible = false
_level_manager = get_node("/root/Main/LevelManager")
back_button.pressed.connect(func(): back_pressed.emit())
_generate_level_buttons()
## 生成关卡按钮
func _generate_level_buttons() -> void:
# 清空现有按钮
for child in level_grid.get_children():
child.queue_free()
# 生成所有关卡按钮
var total_levels: int = 50
for i in range(1, total_levels + 1):
var button: Button = level_button_scene.instantiate()
button.text = str(i)
var is_unlocked: bool = i <= _level_manager._max_unlocked_level
if is_unlocked:
var level_num: int = i
button.pressed.connect(func(): level_selected.emit(level_num))
button.disabled = false
else:
button.text = "🔒"
button.disabled = true
level_grid.add_child(button)本章小结
| 完成项 | 说明 |
|---|---|
| HUD | 顶部信息栏,实时显示分数、步数、进度条 |
| 步数提醒 | 剩余5步以下时步数变红 |
| 过关弹窗 | 显示星级评价,星星依次点亮动画 |
| 失败弹窗 | 显示当前分数和目标分数 |
| 关卡选择 | 网格布局,已解锁/未解锁状态 |
| 飘字动画 | 分数增加时显示 "+xxx" 飘字 |
现在游戏已经有了完整的界面系统。下一章,我们将添加音效与特效,让游戏"听起来"和"看起来"都更加精彩。
