8. 游戏界面
2026/4/14大约 6 分钟
8. 伏魔记——游戏界面
8.1 什么是游戏UI?
想象你走进一家餐厅。墙上挂着菜单,桌上摆着筷子,收银台前排着队——这些都是餐厅的"界面"(UI)。没有菜单你不知道吃什么,没有筷子你不知道怎么吃。
游戏UI(User Interface,用户界面)就是玩家和游戏之间的"桥梁"。RPG的UI包括:
| UI元素 | 说明 | 出现时机 |
|---|---|---|
| 主菜单 | 开始游戏、继续、退出 | 游戏启动 |
| 游戏内HUD | 角色HP/MP条、金币、小地图 | 探索时始终显示 |
| 对话框 | NPC说话时的文字框 | 和NPC对话时 |
| 游戏菜单 | 背包、状态、任务、存档 | 按菜单键 |
| 战斗UI | 行动选择、技能列表、敌人信息 | 战斗中 |
| 商店UI | 买卖物品 | 和商人对话时 |
8.2 主菜单
主菜单是玩家启动游戏后看到的第一个画面。
┌──────────────────────────────────────────┐
│ │
│ ╔══════════╗ │
│ ║ 伏 魔 记 ║ │
│ ╚══════════╝ │
│ │
│ ┌────────────────┐ │
│ │ 新 游 戏 │ │
│ ├────────────────┤ │
│ │ 继 续 游 戏 │ │
│ ├────────────────┤ │
│ │ 退 出 游 戏 │ │
│ └────────────────┘ │
│ │
│ v1.0.0 衍宇科技 │
└──────────────────────────────────────────┘C
using Godot;
/// <summary>
/// 主菜单控制器
/// </summary>
public partial class MainMenu : Control
{
private Button _newGameButton;
private Button _continueButton;
private Button _quitButton;
public override void _Ready()
{
_newGameButton = GetNode<Button>("%NewGameButton");
_continueButton = GetNode<Button>("%ContinueButton");
_quitButton = GetNode<Button>("%QuitButton");
_newGameButton.Pressed += OnNewGamePressed;
_continueButton.Pressed += OnContinuePressed;
_quitButton.Pressed += OnQuitPressed;
// 检查是否有存档,决定"继续"按钮是否可用
_continueButton.Disabled = !HasSaveData();
}
/// <summary>
/// 新游戏
/// </summary>
private void OnNewGamePressed()
{
// 初始化玩家数据
var playerStats = CharacterStats.CreateDefault("少年侠客");
GameManager.Instance.AddPartyMember(playerStats);
// 给初始物品
var inventory = GetNode<InventoryManager>("/root/Main/InventoryManager");
inventory.AddItem("wooden_sword", 1);
inventory.AddItem("cloth_armor", 1);
inventory.AddItem("hp_potion_s", 3);
// 切换到城镇场景
GameManager.Instance.GameState = RpgGameState.TownExplore;
var sceneManager = GetNode<SceneManager>("/root/Main/SceneManager");
sceneManager.ChangeScene("res://scenes/maps/town_main.tscn");
}
/// <summary>
/// 继续游戏
/// </summary>
private void OnContinuePressed()
{
// 加载存档
// ...
GameManager.Instance.GameState = RpgGameState.TownExplore;
}
/// <summary>
/// 退出游戏
/// </summary>
private void OnQuitPressed()
{
GetTree().Quit();
}
/// <summary>
/// 检查是否有存档数据
/// </summary>
private bool HasSaveData()
{
// 检查存档目录是否存在文件
return DirAccess.DirExistsAbsolute("user://saves/");
}
}GDScript
extends Control
## 主菜单控制器
@onready var _new_game_button: Button = %NewGameButton
@onready var _continue_button: Button = %ContinueButton
@onready var _quit_button: Button = %QuitButton
func _ready() -> void:
_new_game_button.pressed.connect(_on_new_game_pressed)
_continue_button.pressed.connect(_on_continue_pressed)
_quit_button.pressed.connect(_on_quit_pressed)
# 检查是否有存档
_continue_button.disabled = not _has_save_data()
## 新游戏
func _on_new_game_pressed() -> void:
# 初始化玩家数据
var player_stats = CharacterStats.create_default("少年侠客")
GameManager.add_party_member(player_stats)
# 给初始物品
var inventory = get_node("/root/Main/InventoryManager")
inventory.add_item("wooden_sword", 1)
inventory.add_item("cloth_armor", 1)
inventory.add_item("hp_potion_s", 3)
# 切换到城镇场景
GameManager.game_state = RpgGameState.TOWN_EXPLORE
var scene_manager = get_node("/root/Main/SceneManager")
scene_manager.change_scene("res://scenes/maps/town_main.tscn")
## 继续游戏
func _on_continue_pressed() -> void:
GameManager.game_state = RpgGameState.TOWN_EXPLORE
## 退出游戏
func _on_quit_pressed() -> void:
get_tree().quit()
## 检查是否有存档数据
func _has_save_data() -> bool:
return DirAccess.dir_exists_absolute("user://saves/")8.3 游戏内HUD
HUD(Head-Up Display)是始终显示在屏幕上的信息条,让玩家随时看到角色的状态。
┌──────────────────────────────────────────────┐
│ HP: ████████░░ 80/100 MP: ████░░░░ 12/30 │
│ LV.5 少年侠客 金币: 350 │
│ │
│ (游戏画面) │
│ │
│ │
│ │
│ [菜单] ← 按M打开 │
└──────────────────────────────────────────────┘C
using Godot;
/// <summary>
/// 游戏内HUD——显示角色状态信息
/// </summary>
public partial class GameHUD : CanvasLayer
{
// UI元素引用
private ProgressBar _hpBar;
private ProgressBar _mpBar;
private Label _hpLabel;
private Label _mpLabel;
private Label _levelLabel;
private Label _nameLabel;
private Label _goldLabel;
public override void _Ready()
{
_hpBar = GetNode<ProgressBar>("%HPBar");
_mpBar = GetNode<ProgressBar>("%MPBar");
_hpLabel = GetNode<Label>("%HPLabel");
_mpLabel = GetNode<Label>("%MPLabel");
_levelLabel = GetNode<Label>("%LevelLabel");
_nameLabel = GetNode<Label>("%NameLabel");
_goldLabel = GetNode<Label>("%GoldLabel");
// 连接信号
GameManager.Instance.GoldChanged += OnGoldChanged;
// 初始更新
UpdateHUD();
}
public override void _Process(double delta)
{
// 每帧更新角色状态(如果有变化)
UpdateHUD();
}
/// <summary>
/// 更新HUD显示
/// </summary>
private void UpdateHUD()
{
var party = GameManager.Instance.Party;
if (party.Count == 0) return;
var player = party[0];
// 更新HP条
float hpPercent = (float)player.CurrentHp / player.MaxHp * 100;
_hpBar.Value = hpPercent;
_hpLabel.Text = $"{player.CurrentHp}/{player.MaxHp}";
// 更新MP条
float mpPercent = (float)player.CurrentMp / player.MaxMp * 100;
_mpBar.Value = mpPercent;
_mpLabel.Text = $"{player.CurrentMp}/{player.MaxMp}";
// 更新角色信息
_levelLabel.Text = $"LV.{player.Level}";
_nameLabel.Text = player.Name;
_goldLabel.Text = $"金币: {GameManager.Instance.Gold}";
}
private void OnGoldChanged(int newGold)
{
_goldLabel.Text = $"金币: {newGold}";
}
}GDScript
extends CanvasLayer
## 游戏内HUD——显示角色状态信息
@onready var _hp_bar: ProgressBar = %HPBar
@onready var _mp_bar: ProgressBar = %MPBar
@onready var _hp_label: Label = %HPLabel
@onready var _mp_label: Label = %MPLabel
@onready var _level_label: Label = %LevelLabel
@onready var _name_label: Label = %NameLabel
@onready var _gold_label: Label = %GoldLabel
func _ready() -> void:
# 连接信号
GameManager.gold_changed.connect(_on_gold_changed)
_update_hud()
func _process(delta: float) -> void:
_update_hud()
## 更新HUD显示
func _update_hud() -> void:
var party = GameManager._party
if party.size() == 0:
return
var player = party[0]
# 更新HP条
var hp_percent: float = float(player.current_hp) / player.max_hp * 100
_hp_bar.value = hp_percent
_hp_label.text = "%d/%d" % [player.current_hp, player.max_hp]
# 更新MP条
var mp_percent: float = float(player.current_mp) / player.max_mp * 100
_mp_bar.value = mp_percent
_mp_label.text = "%d/%d" % [player.current_mp, player.max_mp]
# 更新角色信息
_level_label.text = "LV.%d" % player.level
_name_label.text = player.name
_gold_label.text = "金币: %d" % GameManager.gold
func _on_gold_changed(new_gold: int) -> void:
_gold_label.text = "金币: %d" % new_gold8.4 游戏内菜单
按菜单键(Esc或M)打开游戏内菜单,包含四个主要功能:
┌──────────────────────────────────────────┐
│ 游 戏 菜 单 │
│ │
│ ┌──────────┬──────────┬──────────┐ │
│ │ 状 态 │ 背 包 │ 任 务 │ │
│ └──────────┴──────────┴──────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ [状态面板内容] │ │
│ │ │ │
│ │ 少年侠客 LV.5 │ │
│ │ HP: 80/100 │ │
│ │ MP: 12/30 │ │
│ │ 攻击: 24 防御: 15 │ │
│ │ 速度: 13 │ │
│ │ 经验: 30/80 │ │
│ │ │ │
│ │ 装备: 铁剑 / 铁甲 │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌──────┬──────┐ │
│ │ 存档 │ 返回 │ │
│ └──────┴──────┘ │
└──────────────────────────────────────────┘C
using Godot;
/// <summary>
/// 游戏内菜单控制器
/// </summary>
public partial class GameMenu : Control
{
// 菜单选项卡
private Button _statusTab;
private Button _inventoryTab;
private Button _questTab;
private Button _saveTab;
// 面板容器
private Control _statusPanel;
private Control _inventoryPanel;
private Control _questPanel;
public override void _Ready()
{
Visible = false;
_statusTab = GetNode<Button>("%StatusTab");
_inventoryTab = GetNode<Button>("%InventoryTab");
_questTab = GetNode<Button>("%QuestTab");
_saveTab = GetNode<Button>("%SaveTab");
_statusPanel = GetNode<Control>("%StatusPanel");
_inventoryPanel = GetNode<Control>("%InventoryPanel");
_questPanel = GetNode<Control>("%QuestPanel");
_statusTab.Pressed += () => SwitchTab("status");
_inventoryTab.Pressed += () => SwitchTab("inventory");
_questTab.Pressed += () => SwitchTab("quest");
_saveTab.Pressed += OnSavePressed;
}
public override void _UnhandledInput(InputEvent @event)
{
// 按菜单键打开/关闭
if (@event.IsActionPressed("menu"))
{
if (Visible)
CloseMenu();
else if (GameManager.Instance.GameState == RpgGameState.TownExplore)
OpenMenu();
}
// 按取消键关闭
if (@event.IsActionPressed("cancel") && Visible)
{
CloseMenu();
}
}
/// <summary>
/// 打开菜单
/// </summary>
public void OpenMenu()
{
Visible = true;
GameManager.Instance.GameState = RpgGameState.GameMenu;
SwitchTab("status"); // 默认显示状态面板
}
/// <summary>
/// 关闭菜单
/// </summary>
public void CloseMenu()
{
Visible = false;
GameManager.Instance.GameState = RpgGameState.TownExplore;
}
/// <summary>
/// 切换选项卡
/// </summary>
private void SwitchTab(string tabName)
{
_statusPanel.Visible = (tabName == "status");
_inventoryPanel.Visible = (tabName == "inventory");
_questPanel.Visible = (tabName == "quest");
// 更新选项卡按钮的高亮状态
_statusTab.Modulate = tabName == "status"
? Colors.White : new Color(0.5f, 0.5f, 0.5f);
_inventoryTab.Modulate = tabName == "inventory"
? Colors.White : new Color(0.5f, 0.5f, 0.5f);
_questTab.Modulate = tabName == "quest"
? Colors.White : new Color(0.5f, 0.5f, 0.5f);
}
/// <summary>
/// 存档
/// </summary>
private void OnSavePressed()
{
var saveData = GameManager.Instance.GetSaveData();
// 保存到文件...
GD.Print("游戏已保存!");
}
}GDScript
extends Control
## 游戏内菜单控制器
@onready var _status_tab: Button = %StatusTab
@onready var _inventory_tab: Button = %InventoryTab
@onready var _quest_tab: Button = %QuestTab
@onready var _save_tab: Button = %SaveTab
@onready var _status_panel: Control = %StatusPanel
@onready var _inventory_panel: Control = %InventoryPanel
@onready var _quest_panel: Control = %QuestPanel
func _ready() -> void:
visible = false
_status_tab.pressed.connect(func(): switch_tab("status"))
_inventory_tab.pressed.connect(func(): switch_tab("inventory"))
_quest_tab.pressed.connect(func(): switch_tab("quest"))
_save_tab.pressed.connect(_on_save_pressed)
func _unhandled_input(event: InputEvent) -> void:
# 按菜单键打开/关闭
if event.is_action_pressed("menu"):
if visible:
close_menu()
elif GameManager.game_state == RpgGameState.TOWN_EXPLORE:
open_menu()
# 按取消键关闭
if event.is_action_pressed("cancel") and visible:
close_menu()
## 打开菜单
func open_menu() -> void:
visible = true
GameManager.game_state = RpgGameState.GAME_MENU
switch_tab("status")
## 关闭菜单
func close_menu() -> void:
visible = false
GameManager.game_state = RpgGameState.TOWN_EXPLORE
## 切换选项卡
func switch_tab(tab_name: String) -> void:
_status_panel.visible = (tab_name == "status")
_inventory_panel.visible = (tab_name == "inventory")
_quest_panel.visible = (tab_name == "quest")
var dimmed_color = Color(0.5, 0.5, 0.5)
_status_tab.modulate = Color.WHITE if tab_name == "status" else dimmed_color
_inventory_tab.modulate = Color.WHITE if tab_name == "inventory" else dimmed_color
_quest_tab.modulate = Color.WHITE if tab_name == "quest" else dimmed_color
## 存档
func _on_save_pressed() -> void:
var save_data = GameManager.get_save_data()
print("游戏已保存!")8.5 对话框
对话框在上一章已经详细介绍过,这里补充UI布局的实现要点:
| UI组件 | 类型 | 说明 |
|---|---|---|
| 背景面板 | PanelContainer | 半透明黑色背景 |
| 头像 | TextureRect | 显示说话者的头像 |
| 说话者名 | Label | 显示NPC的名字 |
| 对话文字 | RichTextLabel | 支持文字颜色和效果 |
| 选项容器 | VBoxContainer | 放置选项按钮 |
| 继续提示 | Label | 显示"按确认键继续" |
8.6 战斗UI
战斗UI是战斗系统的交互界面,需要在合适的时候显示/隐藏各个面板。
┌──────────────────────────────────────────────────┐
│ [妖怪A] HP:████░░ 30/50 [妖怪B] HP:██████ 60/60│ ← 敌方信息条
│ │
│ (战斗动画区域) │
│ │
│ ───── 行动顺序: 主角 → 妖怪A → 妖怪B → 同伴 ────│
│ │
│ (我方动画区域) │
│ │
│ [主角] HP:████████ 80/80 [同伴] HP:████░░ 40/60│ ← 我方信息条
│ │
│ ┌──────┬──────┬──────┬──────┐ │
│ │ 攻击 │ 技能 │ 物品 │ 防御 │ │ ← 行动菜单
│ └──────┴──────┴──────┴──────┘ │
└──────────────────────────────────────────────────┘8.7 本章小结
| 知识点 | 说明 |
|---|---|
| 主菜单 | 新游戏、继续、退出三个选项 |
| 游戏HUD | 实时显示HP/MP条、等级、金币 |
| 游戏内菜单 | 状态/背包/任务/存档四个选项卡 |
| 对话框 | 半透明面板+打字机效果+选项按钮 |
| 战斗UI | 敌我信息条+行动菜单+行动顺序显示 |
下一章我们将添加音效与特效,让游戏"活"起来。
