> 功能都实现了,为什么改一行崩三处?本篇讲两件事:Control 节点体系怎么搭 UI,以及游戏级别的架构(自动加载、信号总线、依赖注入)怎么防腐化。
一、Control 体系:锚点、容器与主题
UI 全家是 Control 基础类的子孙,与 2D 游戏物件(Node2D)分属两族。三个核心概念:
- 锚点(Anchor)与边距(Offset):Control 的布局由「四角锚在父矩形的哪里 + 距锚点偏移多少」决定。全屏 UI 锚四角,血条锚左上角……编辑器顶栏的锚点预设(居中、全矩形、两列布局)覆盖 90% 场景。
- 容器(Container):让布局自动排。
HBoxContainer/VBoxContainer排队、GridContainer网格、MarginContainer加边距、CenterContainer居中、ScrollContainer滚动。手动摆位与容器混用是 UI 错乱的第一原因——容器会重排子节点,你的手动 position 会被它吃掉。容器内要微调位置怎么办?4.7 给了正解:offset_transform属性(translate/rotate/scale),不影响容器布局,语义类似 CSS 的transform,做按钮滑入、缩放淡出正合适;它还带开关控制变换是否影响鼠标命中。 - 主题(Theme):一个
.tres资源统管全项目 UI 皮肤(字体、颜色、九宫格图)。项目设置里设为默认主题,或按节点局部覆盖。4.6 的新编辑器主题「Modern」本身就是官方对主题系统的自信展示。
RichTextLabel(BBCode 富文本,4.7 支持图片随字号缩放 [img height=1em])、Tree(4.7 拖放带落点指示)、PopupMenu(4.7 可加搜索栏过滤)、TabContainer(拖资源时悬停即切页)。调布局时的好帮手是 4.6 的 MarginContainer 可视化——选中即在视口里画出边距区。UI 里还有个反直觉的点:Control 也有 _gui_input(event),点击/悬停/滚轮不走 _input 走它,配合 mouse_filter(Stop/Pass/Ignore)三态控制事件穿透。
二、自动加载(Autoload):全局单例的正路
需求:金币数、存档管理、音效播放器……任何「跨场景活着的对象」。Godot 的答案不是静态类,是 Autoload:项目设置 → 自动加载,把一个场景/脚本注册为树根常驻子节点。
# game.gd —— 注册为 Autoload "Game"
signal coin_changed(total: int)
var coins := 0
func add_coin(v: int = 1) -> void:
coins += v
coin_changed.emit(coins)
任何脚本里直接 Game.add_coin(),任何 UI 里 Game.coin_changed.connect(...)。它与场景树同生共死,切场景不销毁,get_tree().paused 时可选择是否暂停(自动加载设置里的 Process Mode)。
克制原则:Autoload 超过五个就该警惕——它本质是全局变量,滥用会重新发明「所有代码耦合所有代码」。
三、信号总线:把「谁通知谁」变成「谁订阅谁」
传统写法:敌人在死亡时找到 UI 节点、找到存档管理器、找到音效管理器——调用方持有所有被调方引用,链条越拉越长。
信号总线的写法:定义一个 Events Autoload 只装信号,各方只跟它说话:
# events.gd —— Autoload "Events"
signal enemy_died(enemy: Node2D)
signal player_damaged(hp: int)
# 敌人只管喊
Events.enemy_died.emit(self)
# UI / 音效 / 存档 各自订阅,互不相识
Events.enemy_died.connect(ui.show_kill_feed)
Events.enemy_died.connect(sfx.play_death_sound)
依赖方向从「网状」变「星形」:新增一个「击杀计数成就系统」=写一个新订阅者,不改任何旧代码。这是 Godot 官方文档「使用信号解耦」一节的精神,也是 MVC 在游戏语境的对应物。
四、场景与脚本的组织:目录即架构
推荐的中型项目布局(官方演示项目同款思路):
res://
├─ autoload/ # Game.gd, Events.gd, SaveManager.gd
├─ scenes/
│ ├─ main/
│ ├─ player/ # player.tscn + player.gd 同名同目录
│ ├─ enemies/
│ └─ ui/
├─ assets/ # 美术、音频
├─ resources/ # .tres 数据(武器表、关卡参数)
└─ shaders/
三条纪律:
- 场景与脚本同名同目录(player.tscn 旁就是 player.gd),移动即整体移动;
- 数据驱动:武器、关卡这类「一堆同类配置」用自定义
Resource(@export的类做成 Weapon.tres 文件),策划改表不改码; - 场景切换统一走一个函数(如
Game.goto_scene("res://scenes/main/level.tscn")),散落各处的change_scene_to_file是调试灾难。
五、暂停与流程控制
游戏暂停不是停 process 那么简单——get_tree().paused = true 后:
- 默认所有节点跟着停(Process Mode 继承);
- 设
PROCESS_MODE_ALWAYS的节点照常跑(暂停菜单、Tween 需要的 UI); PROCESS_MODE_DISABLED彻底停,WHEN_PAUSED只在暂停时跑。
PROCESS_MODE_ALWAYS + get_tree().paused,三行框架。六、本篇心法
- 布局交给容器,装饰交给 offset_transform,皮肤交给 Theme;
- 全局状态用 Autoload,但保持五个以内;
- 事件走信号总线,依赖星形化;
- 目录结构即架构,数据用 Resource,切换走单点。
*本篇配图:UI 锚点与信号总线。*