8. 游戏界面
2026/4/14大约 4 分钟
8. 超级玛丽奥——游戏界面
简介
游戏界面(UI)就是在游戏画面上叠加显示的信息。超级玛丽奥的 UI 非常简洁——顶部的黑色半透明条显示"马里奥"、"金币"、"关卡名"、"时间"四个信息。
┌────────────────────────────────────────────┐
│ MARIO ☓x00 WORLD TIME │
│ 000000 ☓00 1-1 400 │
├────────────────────────────────────────────┤
│ │
│ 游戏画面区域 │
│ │
│ │
└────────────────────────────────────────────┘经典超级玛丽奥的 HUD 只有四个信息区域:
| 区域 | 内容 | 说明 |
|---|---|---|
| MARIO | 分数 | 当前得分,6位数显示 |
| 金币图标 | 金币数量 | 当前金币/100(100金币=1UP) |
| WORLD | 关卡名 | 如 "1-1"、"1-2" |
| TIME | 剩余时间 | 倒计时,归零则死亡 |
HUD 场景
场景结构
HUD (CanvasLayer) # CanvasLayer 保证UI在画面最上层
├── TopBar (PanelContainer) # 顶部黑色半透明背景
│ └── HBoxContainer # 水平布局
│ ├── VBoxContainer # MARIO 区域
│ │ ├── Label ("MARIO")
│ │ └── Label ("000000") # 分数
│ │
│ ├── VBoxContainer # 金币区域
│ │ ├── HBoxContainer
│ │ │ ├── TextureRect (金币图标)
│ │ │ └── Label ("x00")
│ │
│ ├── VBoxContainer # WORLD 区域
│ │ ├── Label ("WORLD")
│ │ └── Label (" 1-1")
│ │
│ └── VBoxContainer # TIME 区域
│ ├── Label ("TIME")
│ └── Label (" 400")
│
├── LivesDisplay (HBoxContainer) # 生命数显示(左上角,游戏开始时短暂显示)
│ └── TextureRect (玛丽奥图标) x N
│
└── LevelTitle (CenterContainer) # 关卡标题(进入关卡时显示)
└── Label ("WORLD 1-1")HUD 脚本
// HUD.cs - 游戏抬头显示
using Godot;
public partial class HUD : CanvasLayer
{
// ========== 节点引用 ==========
[Export] public Label ScoreLabel { get; set; }
[Export] public Label CoinLabel { get; set; }
[Export] public Label WorldLabel { get; set; }
[Export] public Label TimeLabel { get; set; }
[Export] public Label LivesLabel { get; set; }
[Export] public Control LevelTitle { get; set; }
[Export] public Control LivesDisplay { get; set; }
// ========== 初始化 ==========
public override void _Ready()
{
// 连接信号
GameManager.Instance.ScoreChanged += OnScoreChanged;
GameManager.Instance.CoinsChanged += OnCoinsChanged;
GameManager.Instance.TimeChanged += OnTimeChanged;
GameManager.Instance.LivesChanged += OnLivesChanged;
GameManager.Instance.StateChanged += OnStateChanged;
// 初始化显示
UpdateScore(GameManager.Instance.Score);
UpdateCoins(GameManager.Instance.Coins);
UpdateTime(GameManager.Instance.TimeLeft);
UpdateWorld(GameManager.Instance.CurrentLevel);
// 显示关卡标题
ShowLevelTitle();
// 隐藏生命数(短暂显示后隐藏)
ShowLivesDisplay();
}
// ========== 更新方法 ==========
private void OnScoreChanged(int score) => UpdateScore(score);
private void UpdateScore(int score)
{
ScoreLabel.Text = score.ToString("D6");
}
private void OnCoinsChanged(int coins) => UpdateCoins(coins);
private void UpdateCoins(int coins)
{
CoinLabel.Text = $"x{coins:D2}";
}
private void OnTimeChanged(int time) => UpdateTime(time);
private void UpdateTime(int time)
{
TimeLabel.Text = time.ToString("D3");
// 时间不多时变红色警告
if (time <= 100)
{
TimeLabel.Modulate = new Color(1, 0.3f, 0.3f);
}
else
{
TimeLabel.Modulate = new Color(1, 1, 1);
}
}
private void UpdateWorld(int level)
{
WorldLabel.Text = $" {level}-1";
}
private void OnLivesChanged(int lives)
{
LivesLabel.Text = $"x{lives}";
}
// ========== 关卡标题 ==========
private void ShowLevelTitle()
{
LevelTitle.Visible = true;
// 3秒后隐藏标题
var timer = GetTree().CreateTimer(3.0);
timer.Timeout += () => LevelTitle.Visible = false;
}
// ========== 生命数显示 ==========
private void ShowLivesDisplay()
{
LivesDisplay.Visible = true;
// 2秒后隐藏
var timer = GetTree().CreateTimer(2.0);
timer.Timeout += () => LivesDisplay.Visible = false;
}
// ========== 状态变化 ==========
private void OnStateChanged(int state)
{
var s = (GameManager.GameState)state;
if (s == GameManager.GameState.Paused)
{
ShowPauseOverlay();
}
else
{
HidePauseOverlay();
}
}
// ========== 暂停覆盖层 ==========
private Control _pauseOverlay;
private void ShowPauseOverlay()
{
if (_pauseOverlay != null) return;
_pauseOverlay = new Control
{
Name = "PauseOverlay"
};
_pauseOverlay.SetAnchorsPreset(Control.LayoutPreset.FullRect);
var panel = new PanelContainer
{
AnchorLeft = 0.3f,
AnchorTop = 0.3f,
AnchorRight = 0.7f,
AnchorBottom = 0.7f
};
var vbox = new VBoxContainer
{
Alignment = BoxContainer.AlignmentMode.Center
};
var titleLabel = new Label
{
Text = "暂停",
HorizontalAlignment = HorizontalAlignment.Center,
ThemeOverrideFontSizes = { { "font_size", 32 } }
};
var resumeButton = new Button { Text = "继续游戏" };
var restartButton = new Button { Text = "重新开始" };
var quitButton = new Button { Text = "退出" };
resumeButton.Pressed += () => GameManager.Instance.TogglePause();
restartButton.Pressed += () =>
{
GameManager.Instance.TogglePause();
GameManager.Instance.StartNewGame();
};
quitButton.Pressed += () =>
{
GameManager.Instance.TogglePause();
GameManager.Instance.CurrentState = GameManager.GameState.MainMenu;
};
vbox.AddChild(titleLabel);
vbox.AddChild(resumeButton);
vbox.AddChild(restartButton);
vbox.AddChild(quitButton);
panel.AddChild(vbox);
_pauseOverlay.AddChild(panel);
AddChild(_pauseOverlay);
}
private void HidePauseOverlay()
{
if (_pauseOverlay != null)
{
_pauseOverlay.QueueFree();
_pauseOverlay = null;
}
}
// ESC 暂停
public override void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventKey key && key.Pressed && key.Keycode == Key.Escape)
{
if (GameManager.Instance.CurrentState == GameManager.GameState.Playing ||
GameManager.Instance.CurrentState == GameManager.GameState.Paused)
{
GameManager.Instance.TogglePause();
}
}
}
}# hud.gd - 游戏抬头显示
extends CanvasLayer
# ========== 节点引用 ==========
@export var score_label: Label
@export var coin_label: Label
@export var world_label: Label
@export var time_label: Label
@export var lives_label: Label
@export var level_title: Control
@export var lives_display: Control
# ========== 初始化 ==========
func _ready() -> void:
# 连接信号
GameManager.score_changed.connect(on_score_changed)
GameManager.coins_changed.connect(on_coins_changed)
GameManager.time_changed.connect(on_time_changed)
GameManager.lives_changed.connect(on_lives_changed)
GameManager.state_changed.connect(on_state_changed)
# 初始化显示
update_score(GameManager.score)
update_coins(GameManager.coins)
update_time(GameManager.time_left)
update_world(GameManager.current_level)
# 显示关卡标题
show_level_title()
# 显示生命数
show_lives_display()
# ========== 更新方法 ==========
func on_score_changed(score: int) -> void:
update_score(score)
func update_score(score: int) -> void:
score_label.text = "%06d" % score
func on_coins_changed(coins: int) -> void:
update_coins(coins)
func update_coins(coins: int) -> void:
coin_label.text = "x%02d" % coins
func on_time_changed(time: int) -> void:
update_time(time)
func update_time(time: int) -> void:
time_label.text = "%03d" % time
# 时间不多时变红色
if time <= 100:
time_label.modulate = Color(1, 0.3, 0.3)
else:
time_label.modulate = Color(1, 1, 1)
func update_world(level: int) -> void:
world_label.text = " %d-1" % level
func on_lives_changed(lives: int) -> void:
lives_label.text = "x%d" % lives
# ========== 关卡标题 ==========
func show_level_title() -> void:
level_title.visible = true
var timer = get_tree().create_timer(3.0)
timer.timeout.connect(func(): level_title.visible = false)
# ========== 生命数显示 ==========
func show_lives_display() -> void:
lives_display.visible = true
var timer = get_tree().create_timer(2.0)
timer.timeout.connect(func(): lives_display.visible = false)
# ========== 状态变化 ==========
func on_state_changed(state: int) -> void:
if state == GameManager.GameState.PAUSED:
show_pause_overlay()
else:
hide_pause_overlay()
# ========== 暂停覆盖层 ==========
var _pause_overlay: Control = null
func show_pause_overlay() -> void:
if _pause_overlay != null:
return
_pause_overlay = Control.new()
_pause_overlay.name = "PauseOverlay"
_pause_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
var panel = PanelContainer.new()
panel.anchor_left = 0.3
panel.anchor_top = 0.3
panel.anchor_right = 0.7
panel.anchor_bottom = 0.7
var vbox = VBoxContainer.new()
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
var title_label = Label.new()
title_label.text = "暂停"
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_label.add_theme_font_size_override("font_size", 32)
var resume_button = Button.new()
resume_button.text = "继续游戏"
var restart_button = Button.new()
restart_button.text = "重新开始"
var quit_button = Button.new()
quit_button.text = "退出"
resume_button.pressed.connect(GameManager.toggle_pause)
restart_button.pressed.connect(func():
GameManager.toggle_pause()
GameManager.start_new_game()
)
quit_button.pressed.connect(func():
GameManager.toggle_pause()
GameManager.current_state = GameManager.GameState.MAIN_MENU
)
vbox.add_child(title_label)
vbox.add_child(resume_button)
vbox.add_child(restart_button)
vbox.add_child(quit_button)
panel.add_child(vbox)
_pause_overlay.add_child(panel)
add_child(_pause_overlay)
func hide_pause_overlay() -> void:
if _pause_overlay != null:
_pause_overlay.queue_free()
_pause_overlay = null
## ESC 暂停
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
if GameManager.current_state == GameManager.GameState.PLAYING or \
GameManager.current_state == GameManager.GameState.PAUSED:
GameManager.toggle_pause()游戏开始画面
游戏开始时显示关卡标题和生命数:
┌────────────────────────────────────────┐
│ MARIO ☓x00 WORLD TIME │
│ 000000 ☓00 1-1 400 │
├────────────────────────────────────────┤
│ │
│ WORLD 1-1 │
│ │
│ 🏃 x 3 (3条命的图标) │
│ │
└────────────────────────────────────────┘游戏结束画面
游戏结束时的界面:
// GameOverUI.cs - 游戏结束界面
public partial class GameOverUI : Control
{
[Export] public Label FinalScoreLabel { get; set; }
[Export] public Control GameOverPanel { get; set; }
public override void _Ready()
{
Visible = false;
GameManager.Instance.StateChanged += OnStateChanged;
}
private void OnStateChanged(int state)
{
if (state == (int)GameManager.GameState.GameOver)
{
FinalScoreLabel.Text = $"最终得分: {GameManager.Instance.Score}";
Visible = true;
// 自动回到主菜单
var timer = GetTree().CreateTimer(5.0);
timer.Timeout += () =>
{
GameManager.Instance.CurrentState = GameManager.GameState.MainMenu;
};
}
else
{
Visible = false;
}
}
private void OnRetryButtonPressed()
{
GameManager.Instance.StartNewGame();
}
private void OnMenuButtonPressed()
{
GameManager.Instance.CurrentState = GameManager.GameState.MainMenu;
}
}# game_over_ui.gd - 游戏结束界面
extends Control
@export var final_score_label: Label
@export var game_over_panel: Control
func _ready() -> void:
visible = false
GameManager.state_changed.connect(on_state_changed)
func on_state_changed(state: int) -> void:
if state == GameManager.GameState.GAME_OVER:
final_score_label.text = "最终得分: %d" % GameManager.score
visible = true
# 自动回到主菜单
var timer = get_tree().create_timer(5.0)
timer.timeout.connect(func():
GameManager.current_state = GameManager.GameState.MAIN_MENU
)
else:
visible = false
func _on_retry_button_pressed() -> void:
GameManager.start_new_game()
func _on_menu_button_pressed() -> void:
GameManager.current_state = GameManager.GameState.MAIN_MENU下一章预告
游戏界面完成了!HUD 显示分数、金币、时间和关卡信息,暂停菜单也能正常工作。下一章我们将添加音效和视觉特效——跳跃、踩怪、吃道具的声音和动画。
