1926 字
10 分钟
Godot C# 零基础教程 07:UI 与游戏流程

教程一 · 第 7 章:UI 与游戏流程#

Godot 的 UI 系统(Control 节点体系)功能极强,也有自己的一套规则。本章做完整的游戏外壳:主菜单 → 关卡 → 暂停 → 胜利结算 → 设置(音量)。

7.1 Control 体系心智模型#

所有 UI 节点都继承 Control,它和 Node2D 最大的不同:拥有锚点(Anchor)、容器(Container)、焦点(Focus)三大布局机制。

常用节点:

节点用途
Label文字
Button按钮(子类 CheckButton、TextureButton)
LineEdit / TextEdit单行输入 / 多行文本
ProgressBar / TextureProgressBar血条、进度条
ColorRect / TextureRect色块 / 图片
Panel / PanelContainer背景板
HBoxContainer / VBoxContainer横向/纵向自动排列子节点
GridContainer网格排列(背包、关卡选择)
MarginContainer / CenterContainer留白 / 居中
Control纯容器/自定义绘制基类

第一规则:UI 放在 CanvasLayer 下。 UI 是屏幕空间的东西,不该跟着游戏世界和摄像机移动。HUD、菜单各放一个 CanvasLayer(Layer 值越大越靠上,暂停菜单可以盖过 HUD)。

7.2 布局:锚点与容器#

锚点(Anchor):选中 Control,检查器 Layout 下拉有现成预设:Top Wide(顶部横条)、Center、Full Rect(铺满)等。锚点决定”父节点尺寸变化时我怎么贴”。

容器(Container):子 Control 的位置和尺寸由容器自动管理,你手动拖会被覆盖。

  • VBoxContainer:子节点从上到下排,加 Separation 控制间距
  • HBoxContainer:从左到右
  • MarginContainer:四周留白(Theme Override Constants 调边距)
  • CenterContainer:唯一子节点居中

经典菜单结构(布局稳、自适应分辨率):

MainMenu(Control,Layout = Full Rect)
└── CenterContainer(Full Rect)
└── VBoxContainer
├── Label "我的游戏"(大字号)
├── Button "开始游戏"
├── Button "设置"
└── Button "退出"

按钮宽度统一:给 VBox 下的 Button 设置 Custom Minimum Size,或在 VBoxContainer 里它们默认等宽拉伸。

7.3 主菜单脚本#

菜单场景 scenes/ui/main_menu.tscn,根 Control 挂脚本:

using Godot;
public partial class MainMenu : Control
{
public override void _Ready()
{
GetNode<Button>("CenterContainer/VBox/StartButton").Pressed += OnStart;
GetNode<Button>("CenterContainer/VBox/SettingsButton").Pressed += OnSettings;
GetNode<Button>("CenterContainer/VBox/QuitButton").Pressed += OnQuit;
}
private void OnStart()
{
GetTree().ChangeSceneToFile("res://scenes/level1.tscn");
}
private void OnSettings()
{
GetNode<Control>("SettingsPanel").Visible = true;
}
private void OnQuit()
{
GetTree().Quit();
// 注意:编辑器里运行时 Quit 不会关编辑器窗口,这是正常的(发布版才退出游戏)
}
}

让主菜单成为项目主场景(项目设置 → 应用 → 运行 → 主场景)。游戏从菜单启动,而不是直接冲进关卡。

7.4 HUD:血条与分数#

HUD(CanvasLayer)
└── MarginContainer(Top Wide,四周 16px 边距)
└── HBoxContainer
├── Label "HP"
├── TextureProgressBar(或 ProgressBar)命名 HpBar,Min=0 Max=100
└── Label 命名 CoinLabel

订阅玩家信号(第 5 章模式):

public partial class HUD : CanvasLayer
{
private ProgressBar _hpBar = null!;
private Label _coinLabel = null!;
public override void _Ready()
{
_hpBar = GetNode<ProgressBar>("MarginContainer/HBox/HpBar");
_coinLabel = GetNode<Label>("MarginContainer/HBox/CoinLabel");
var player = GetTree().GetFirstNodeInGroup("player");
if (player is Player p)
{
p.HealthChanged += hp => _hpBar.Value = hp;
}
if (GameManager.Instance != null)
GameManager.Instance.CoinsChanged += c => _coinLabel.Text = $"金币 {c}";
}
}

字符串插值 $"金币 {c}" 是 C# 6 语法,教程二第 3 章细讲,UI 文本几乎全用它。

血条想更花哨:TextureProgressBar 三张图(底、填充、高光);或者用 Tween 让数字滚动:

CreateTween()
.TweenProperty(_hpBar, "value", (double)hp, 0.25f)
.SetTrans(Tween.TransitionType.Quad)
.SetEase(Tween.EaseType.Out);

7.5 Tween:让 UI 动起来#

Tween(补间)= “在 X 秒内把属性从当前值平滑变到目标值”,UI 动画 90% 靠它,不用碰 AnimationPlayer:

// 面板淡入(需要面板 Modulate 初始 alpha=0)
panel.Modulate = panel.Modulate with { A = 0f };
panel.Show();
CreateTween().TweenProperty(panel, "modulate:a", 1f, 0.2f);
// 伤害数字飘起淡出
var label = ...;
var tween = CreateTween();
tween.SetParallel(true);
tween.TweenProperty(label, "position:y", label.Position.Y - 60f, 0.8f);
tween.TweenProperty(label, "modulate:a", 0f, 0.8f);
tween.Chain().TweenCallback(Callable.From(label.QueueFree));
  • SetParallel(true) 后续动画同时播放;Chain() 切回顺序
  • modulate:a 这种”属性:子属性”路径可以只动画透明度
  • TweenCallback 在动画里插函数调用
  • 结束节点释放:tween.TweenCallback(Callable.From(() => QueueFree()))

7.6 暂停菜单#

暂停的本质是停掉 SceneTree 的处理,但菜单自己还能动:

public partial class PauseMenu : CanvasLayer
{
public override void _Ready() => Visible = false;
public override void _UnhandledInput(InputEvent @event)
{
if (Input.IsActionJustPressed("pause")) // 自己加个输入动作绑 Esc
Toggle();
}
private void Toggle()
{
bool paused = !GetTree().Paused;
GetTree().Paused = paused;
Visible = paused;
}
}

关键配置:PauseMenu 根节点(CanvasLayer/Control)检查器 Process → Mode 设为 Always,否则树暂停后它自己也收不到输入、按钮点不动。游戏世界节点保持默认 Inherit/Pausable,暂停时它们全部冻结。

按钮:

  • 继续:GetTree().Paused = false; Visible = false;
  • 回主菜单:先 GetTree().Paused = false; 再 ChangeScene(暂停状态切场景会让新场景也冻结,踩过的人都记得)
  • 退出:Quit()

焦点与手柄:暂停菜单打开时设一个默认按钮焦点:

GetNode<Button>("Panel/ResumeButton").GrabFocus();

用方向键/手柄在按钮间移动是 Control 内置能力,零代码,但要检查 Theme 里有 Focus 样式否则看不出选中了谁。

7.7 游戏流程状态机#

主菜单、游玩、暂停、胜利、死亡,流程切换最好集中管理。建 Autoload SceneManager:

public partial class SceneManager : Node
{
[Signal] public delegate void GameStateChangedEventHandler(StringName state);
public StringName State { get; private set; } = "boot";
public void SetState(StringName newState)
{
State = newState;
EmitSignal(SignalName.GameStateChanged, newState);
GetTree().Paused = newState is "paused" or "gameover";
}
public async void ChangeScene(string path)
{
SetState("loading");
// 这里可以弹一个过场画面
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
GetTree().Paused = false;
GetTree().ChangeSceneToFile(path);
}
}

is "paused" or "gameover" 是 C# 9 的模式匹配(教程二第 2 章)。

为什么要显式状态:否则”玩家死了但暂停菜单开着""胜利结算动画还没完就重开”这类组合 bug 层出不穷。状态集中一处,各系统订阅状态信号自行反应。

简单项目不用搞这么正式——菜单和关卡各一个场景、一个暂停层就够。知道有这个台阶,需要时再上。

7.8 设置界面:音量、全屏、按键#

音频总线(Audio Bus)#

Godot 音量不是直接调播放器,而是调总线(底部面板 Audio:Master、SFX、Music…):

// 线性滑块 0..1 转分贝
private float LinearToDb(float v) => v <= 0.001f ? -80f : Mathf.LinearToDb(v);
public void OnMasterSliderChanged(float v)
=> AudioServer.SetBusVolumeDb(AudioServer.GetBusIndex("Master"), LinearToDb(v));

Slider 的 ValueChanged 信号连过来。设置值存存档(第 6 章 SaveData 加 MasterVolume 字段),启动时读回设置总线。

全屏/分辨率#

DisplayServer.WindowSetMode(
fullscreen ? DisplayServer.WindowMode.Fullscreen : DisplayServer.WindowMode.Windowed);

项目设置里把窗口 Stretch Mode 设为 canvas_items + Aspect expand,做自适应最省心;UI 用锚点+容器铺满,16:9 和 21:9 都不会崩。

按键重绑定#

// 监听下一个按键输入并替换动作绑定
public override async void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventKey key && key.Pressed && !key.Echo)
{
InputMap.ActionEraseEvents("jump");
InputMap.ActionAddEvent("jump", new InputEventKey { PhysicalKeycode = key.PhysicalKeycode });
// 存存档时遍历 InputMap 保存
}
}

7.9 场景过渡(Loading/淡入淡出)#

做一个 Autoload TransitionLayer(CanvasLayer,ProcessMode=Always):

public partial class TransitionLayer : CanvasLayer
{
private ColorRect _fade = null!;
public override void _Ready()
{
_fade = new ColorRect { Color = new Color(0,0,0,0), MouseFilter = Control.MouseFilterEnum.Ignore };
_fade.SetAnchorsPreset(Control.LayoutPreset.FullRect);
AddChild(_fade);
}
public async Task FadeToScene(string path, float time = 0.3f)
{
_fade.MouseFilter = Control.MouseFilterEnum.Stop; // 挡住误操作
await CreateTween().TweenProperty(_fade, "color:a", 1f, time).ToSignal(Tween.SignalName.Finished);
GetTree().ChangeSceneToFile(path);
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
await CreateTween().TweenProperty(_fade, "color:a", 0f, time).ToSignal(Tween.SignalName.Finished);
_fade.MouseFilter = Control.MouseFilterEnum.Ignore;
}
}

调用:await TransitionLayer.Instance.FadeToScene("res://scenes/level2.tscn");(普通 async 方法用 Task,await 它的节点方法可以正常等待)。

大关卡加载卡顿明显时,用 ResourceInteractiveLoader 分帧加载显示进度条,等项目真有需求再查。

7.10 本章检查清单#

做完游戏外壳,逐项确认:

  • 启动进主菜单,而不是直接进关卡
  • 菜单按钮全部有响应,退出在发布版可用
  • HUD 用信号更新,不在 _Process 里刷文本
  • Esc 暂停时世界冻结、菜单可操作、再按恢复
  • 暂停中切场景不会冻住新场景
  • 音量设置存盘,重启后保留
  • 分辨率变化时 UI 不错位(拖窗口测试)
  • 键盘能操作菜单(GrabFocus,为以后手柄铺路)

第 8 章讲”果汁感”:音效、粒子、屏幕震动,以及性能分析与调试;第 9 章导出发布。


《Godot C# 零基础》系列导航

⬅️ 上一篇:Godot C# 零基础教程 06:Resource 数据驱动、动态实例化与存档 📑 返回系列总目录(第 01 篇) ➡️ 下一篇:Godot C# 零基础教程 08:音效、粒子、屏幕震动与调试

Godot C# 零基础教程 07:UI 与游戏流程
https://021028.xyz/posts/default/167/
作者
021028
发布于
2026-10-14
许可协议
CC BY-NC-SA 4.0