本节摘要:所有节点共享一个祖宗类,其下分出画布节点、三维节点、控制节点三大分支,加上叶子型的功能节点构成完整家族。每个节点从实例化到销毁要经历一段固定的生命周期,理解这段时序是解释大半"玄学故障"的钥匙。本节讲清继承体系、生命周期回调与树内寻址三件事。
先看一张图,再说话。下面是节点家族的骨架——记住它的收益极高,因为你从此能推断任何陌生节点的行为边界。

图里最值得咀嚼的是三大分支的划分逻辑。画布节点分支活在二维世界,坐标以像素计,精灵、瓦片、二维粒子都长在这里;三维分支活在米制世界,带三维变换,网格、灯光、三维相机在此安家;控制分支专为界面而生,不问世界坐标,只问锚点与容器,按钮、标签、进度条属于这一支。
三条世界线不共坐标却共祖宗,这带来一个容易踩的认知坑:父子节点可以跨分支——把一个界面按钮拖成精灵的子节点完全合法,但按钮不会"跟着"精灵在世界里移动,因为它压根不认世界坐标。规则一句话:坐标系统沿分支传播,跨分支挂靠时空间关系断裂。实践中界面归界面、世界归世界,根节点选对分支,是最便宜的结构保险。
功能型节点(计时器、音频播放器、碰撞体)没有视觉,直接挂在基类之下,像器官一样吸附到三大分支的节点上。你随手新建的根节点通常是基类本身——纯逻辑容器,不带任何空间属性。
节点从出生到死亡要过六道门,每道门对应一个可以覆写的回调。理解它们的触发时机,等于拿到调试结构问题的时序坐标系。
extends Node func _init() -> void: pass # 第一道:构造。刚出生,还没进树,拿不到兄弟与父节点 func _enter_tree() -> void: pass # 第二道:进树。有了父节点,但自己的子节点可能还没进来 func _ready() -> void: pass # 第三道:就绪。本节点及其全部子节点已进树,做初始化的最佳时机 func _process(delta: float) -> void: pass # 第四道起:每帧被调用,视觉类逻辑住这里 func _exit_tree() -> void: pass # 第五道:整棵子树即将离开树,做清理与断开连接 func _notification(what: int) -> void: pass # 旁路:收系统通知,如暂停变化、翻译切换等
时序里最有用的一条推论:父节点的就绪晚于子节点。树是自底向上"就绪"的——叶子先就绪,根最后就绪。所以父节点在就绪回调里可以放心访问所有子节点;反过来,子节点的就绪回调里访问父节点是安全的(父已进树),但访问父节点身上"在父的就绪回调里才初始化的成员"会拿到空值。这个细节解释了海量"明明有这个变量却是空"的报警。
⚠️ 常见坑:在初始化回调里遍历兄弟节点。初始化时兄弟可能尚未进树,遍历结果残缺。需要感知兄弟的地方,把逻辑挪到就绪回调,或改用信号由兄弟主动汇报。
树大了,"找到对方"本身成为一门技能。三种主要方式各有适用面。
相对路径从自己出发:get_node("Sprite") 拿子节点,get_node("../Enemy") 拿父亲的子节点。写起来直观,但树一重构路径就断,适合近距离取用。
绝对路径从根出发:get_node("/root/Main/Enemy")。路径最长也最脆,除了调试期临时取用,不推荐写进正式代码。
唯一名节点是重构友好的方案:在编辑器里把某节点标记为场景内唯一,之后用百分号语法直达,%Camera。改名移动都不受影响,只要唯一性还在。默认导出的场景模板里,相机与关键组件常用这种方式引用。
@onready var sprite: Sprite2D = $Sprite # 相对路径的简写 @onready var camera: Camera2D = %Camera # 唯一名直达,重构安全 @onready var label := get_node("../HUD/ScoreLabel") # 跨层相对路径,谨慎使用
就绪注解把"取得节点"推迟到就绪回调时点执行,恰好与生命周期配合:成员在声明处拿到已进树的引用。要强调的是它只保证时序,不保证对方存在——路径写错照样得到空引用,随后崩溃。空引用的防与治,放在 3.4 排错实录细说。
💡 一条纪律:节点的"可用"分三层——构造后能用自身成员,进树后能用父节点,就绪后能用整棵子树。写任何初始化代码前,先问自己需要的资源在哪一层就位。
三大分支与生命周期之外,还有一个高频使用的组织工具值得单独一讲:节点组。把节点加入命名的小组(比如"敌人"组、"可交互"组),之后任何脚本都能按组名取回全部成员,或对整组批量调用方法。
组的定位与唯一名不同:唯一名是"场景内精确找一个人",组是"全树广播找一群人"。典型用法:关卡管理器对"敌人"组批量下达强化指令;存档系统遍历"可保存"组收集状态;音效管理器对"静音目标"组批量处理。组的增删可以随时进行,节点销毁时自动退组——这层自动清理让组比手工维护的列表安全得多。
# 组的三种常见用法 add_to_group(&"enemies") # 入组:通常写在就绪回调 var enemies := get_tree().get_nodes_in_group(&"enemies") # 取整组 for e in enemies: e.apply_buff(&"rage") # 批量下达 get_tree().call_group(&"enemies", &"apply_buff", &"rage") # 一行等价写法
⚠️ 常见坑:把组当长期缓存用——入组后把取回的列表存成成员变量反复用。节点会销毁、会退组,缓存的列表很快过期。组的正确用法是每次用每次取,把它当成实时查询而不是快照。
下一节把镜头拉远:单棵树如何组织成完整游戏——场景切换、跨场景通信与全局单例。