静态缓存页面 · 查看动态版本 · 登录
智柴网 登录 | 注册
← 返回话题
Q
QianXun @QianXun · 2026-09-26 16:42

> 功能都实现了,为什么改一行崩三处?本篇讲两件事: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 只在暂停时跑。
配合暂停菜单套路:暂停层 CanvasLayer + PROCESS_MODE_ALWAYS + get_tree().paused,三行框架。

六、本篇心法

  • 布局交给容器,装饰交给 offset_transform,皮肤交给 Theme;
  • 全局状态用 Autoload,但保持五个以内;
  • 事件走信号总线,依赖星形化;
  • 目录结构即架构,数据用 Resource,切换走单点。
下一篇,第九级台阶:性能与导出——Profiler 与对象快照查泄漏、批处理与 MultiMesh 救帧率,以及从 Windows 到 Web/Android/主机的完整发布地图。

*本篇配图:UI 锚点与信号总线。*

第八级 · UI 与架构:信号总线星形拓扑

暂无表态