8. 游戏UI
2026/4/14大约 6 分钟
游戏UI
开放世界游戏需要大量 UI(用户界面)来帮玩家管理帕鲁、物品、基地和地图。好的 UI 就像一个好管家——你需要什么信息,它就给你什么信息,不碍事又随时可用。
本章你将学到
- 帕鲁图鉴界面(收集进度、3D 预览)
- 物品栏和快捷栏的设计
- 建造菜单(分类选择、旋转预览)
- 状态面板(生命/饥饿/体力)
- 大地图和传送点
UI 层次结构
HUD 界面(始终显示)
HUD 就是游戏画面上始终显示的那些小元素——血条、饥饿条、快捷栏、小地图。
状态面板
状态面板放在屏幕左上角,显示玩家的三个核心数值:
| 状态 | 显示方式 | 颜色 | 位置 |
|---|---|---|---|
| 生命值(HP) | 红色条 | #FF4444 | 左上角第1条 |
| 饥饿值 | 绿色条 | #44FF44 | 左上角第2条 |
| 体力值 | 黄色条 | #FFFF44 | 左上角第3条 |
快捷栏
快捷栏放在屏幕底部,有 9 个格子,对应键盘 1~9 键:
| 格子 | 推荐放什么 |
|---|---|
| 1 | 武器 |
| 2 | 帕鲁球 |
| 3 | 治疗药水 |
| 4 | 食物 |
| 5 | 采集工具 |
| 6~9 | 自定义 |
HUD 更新代码
C#
// HUD.cs
// HUD界面控制器
using Godot;
public partial class HUD : CanvasLayer
{
private ProgressBar _hpBar;
private ProgressBar _hungerBar;
private ProgressBar _staminaBar;
private Label _hpLabel;
private Label _hungerLabel;
public override void _Ready()
{
_hpBar = GetNode<ProgressBar>("%HPBar");
_hungerBar = GetNode<ProgressBar>("%HungerBar");
_staminaBar = GetNode<ProgressBar>("%StaminaBar");
_hpLabel = GetNode<Label>("%HPLabel");
_hungerLabel = GetNode<Label>("%HungerLabel");
}
// 更新所有状态条
public void UpdateStatus(float hp, float maxHp, float hunger, float maxHunger,
float stamina, float maxStamina)
{
_hpBar.MaxValue = maxHp;
_hpBar.Value = hp;
_hpLabel.Text = $"{(int)hp}/{(int)maxHp}";
_hungerBar.MaxValue = maxHunger;
_hungerBar.Value = hunger;
_hungerLabel.Text = $"{(int)hunger}/{(int)maxHunger}";
_staminaBar.MaxValue = maxStamina;
_staminaBar.Value = stamina;
}
}GDScript
# hud.gd
# HUD界面控制器
extends CanvasLayer
@onready var hp_bar: ProgressBar = %HPBar
@onready var hunger_bar: ProgressBar = %HungerBar
@onready var stamina_bar: ProgressBar = %StaminaBar
@onready var hp_label: Label = %HPLabel
@onready var hunger_label: Label = %HungerLabel
## 更新所有状态条
func update_status(hp: float, max_hp: float, hunger: float, max_hunger: float,
stamina: float, max_stamina: float) -> void:
hp_bar.max_value = max_hp
hp_bar.value = hp
hp_label.text = "%d/%d" % [int(hp), int(max_hp)]
hunger_bar.max_value = max_hunger
hunger_bar.value = hunger
hunger_label.text = "%d/%d" % [int(hunger), int(max_hunger)]
stamina_bar.max_value = max_stamina
stamina_bar.value = stamina帕鲁图鉴界面
帕鲁图鉴就像一本"收集册"——记录你遇到过的所有帕鲁,还能 3D 预览它们。
图鉴界面布局
┌─────────────────────────────────────────┐
│ 帕鲁图鉴 收集: 15/50 │
├──────────────┬──────────────────────────┤
│ │ │
│ [帕鲁列表] │ 3D 预览区域 │
│ #001 火兔 │ ┌────────────┐ │
│ #002 水蛇 │ │ │ │
│ #003 草蝶 │ │ 3D模型 │ │
│ #004 铁鼹 │ │ 可旋转 │ │
│ #005 ??? │ │ │ │
│ #006 ??? │ └────────────┘ │
│ ... │ │
│ │ 名字: 火兔 │
│ │ 属性: 火 │
│ │ 稀有度: 普通 │
│ │ 描述: 喜欢晒太阳的小兔子 │
│ │ 已捕获: 3只 │
├──────────────┴──────────────────────────┤
│ [属性] [技能] [栖息地] │
└─────────────────────────────────────────┘图鉴数据管理
C#
// PalDexManager.cs
// 帕鲁图鉴管理器
using Godot;
using System.Collections.Generic;
public partial class PalDexManager : Node
{
// 图鉴条目:帕鲁ID -> 是否已发现
private Dictionary<int, bool> _discoveredPals = new();
// 已捕获数量:帕鲁ID -> 捕获数量
private Dictionary<int, int> _capturedCount = new();
// 总帕鲁种类数
private int _totalPalSpecies = 50;
// 发现一只新帕鲁
public void Discover(int palId)
{
if (!_discoveredPals.ContainsKey(palId))
{
_discoveredPals[palId] = true;
GD.Print($"发现新帕鲁!图鉴更新: #{palId}");
}
}
// 捕获一只帕鲁
public void Capture(int palId)
{
Discover(palId); // 捕获也算发现
if (!_capturedCount.ContainsKey(palId))
_capturedCount[palId] = 0;
_capturedCount[palId]++;
}
// 获取收集进度
public float GetCompletionRate()
{
return (float)_discoveredPals.Count / _totalPalSpecies * 100f;
}
// 检查是否已发现某只帕鲁
public bool IsDiscovered(int palId) => _discoveredPals.ContainsKey(palId);
}GDScript
# pal_dex_manager.gd
# 帕鲁图鉴管理器
extends Node
# 图鉴条目:帕鲁ID -> 是否已发现
var _discovered_pals: Dictionary = {}
# 已捕获数量:帕鲁ID -> 捕获数量
var _captured_count: Dictionary = {}
# 总帕鲁种类数
var _total_pal_species: int = 50
## 发现一只新帕鲁
func discover(pal_id: int) -> void:
if not _discovered_pals.has(pal_id):
_discovered_pals[pal_id] = true
print("发现新帕鲁!图鉴更新: #%d" % pal_id)
## 捕获一只帕鲁
func capture(pal_id: int) -> void:
discover(pal_id) # 捕获也算发现
if not _captured_count.has(pal_id):
_captured_count[pal_id] = 0
_captured_count[pal_id] += 1
## 获取收集进度
func get_completion_rate() -> float:
return float(_discovered_pals.size()) / _total_pal_species * 100.0
## 检查是否已发现某只帕鲁
func is_discovered(pal_id: int) -> bool:
return _discovered_pals.has(pal_id)物品栏界面
物品栏就是玩家的"背包"——所有捡到的东西都放在这里。
物品栏设计
| 设计要点 | 说明 |
|---|---|
| 网格布局 | 4x8 = 32 个格子 |
| 堆叠上限 | 同类物品可堆叠,最多 99 个 |
| 拖拽操作 | 鼠标拖拽移动物品 |
| 分类筛选 | 全部/材料/装备/食物/帕鲁球 |
| 快捷移动 | 右键点击物品自动使用或装备 |
物品数据结构
// 每个物品格子的数据
public class InventorySlot
{
public string ItemId; // 物品ID
public int Count; // 数量
public int MaxStack = 99; // 最大堆叠
public bool IsEmpty => Count <= 0;
}建造菜单
建造菜单是一个分类的建筑列表,让玩家快速选择要建造的东西。
菜单分类
| 分类 | 包含的建筑 |
|---|---|
| 基础 | 木墙、木地板、木门、楼梯 |
| 设施 | 工作台、储物箱、帕鲁小屋 |
| 生产 | 采石场、伐木场、农田、烹饪台 |
| 防御 | 围墙、陷阱、箭塔 |
| 装饰 | 火把、旗帜、路灯 |
建造菜单交互
- 按 B 键打开建造菜单
- 用分类标签筛选建筑类型
- 点击建筑图标进入建造预览模式
- 在 3D 世界中预览建筑位置(半透明)
- 左键确认放置,右键取消
大地图界面
大地图显示整个世界,可以查看群系分布、传送点和标记。
大地图功能
| 功能 | 说明 |
|---|---|
| 缩放 | 鼠标滚轮缩放地图 |
| 拖拽 | 鼠标右键拖动地图 |
| 传送点 | 金色图标,点击可传送 |
| 群系着色 | 不同颜色表示不同群系 |
| 玩家位置 | 白色箭头标记当前位置 |
| 自定义标记 | 玩家可以在地图上做标记 |
常见问题
Q:UI 应该用什么节点来搭建?
在 Godot 中,UI 使用 Control 节点系列。常见节点:
Panel:背景面板Label:文字TextureRect:图片ProgressBar:进度条GridContainer:网格布局VBoxContainer/HBoxContainer:垂直/水平布局MarginContainer:边距容器
Q:帕鲁图鉴的 3D 预览怎么做?
用 SubViewport 节点创建一个独立的小场景。在这个场景里放一个摄像机和一个帕鲁模型。把这个 SubViewport 的内容显示在 UI 的 TextureRect 里。玩家可以用鼠标拖动来旋转 3D 模型。
Q:物品栏的拖拽怎么实现?
Godot 4 提供了内置的拖拽系统。核心步骤:
- 在
_GetDragData中创建拖拽数据和预览图标 - 在
_CanDropData中检查目标格子是否能放下 - 在
_DropData中执行实际的物品移动
Q:怎么让 UI 不挡住游戏操作?
使用 CanvasLayer 的 Layer 属性控制 UI 的层级。HUD 放在底层(Layer 0),菜单放在高层(Layer 1)。打开菜单时设置 GetTree().Paused = true 暂停游戏,关闭时恢复。但要注意某些 UI(比如快捷栏)不应该暂停游戏。
下一步
UI 做好了,接下来添加 音效与特效——让你的游戏"听起来"和"看起来"都更棒。
