这个分类的文章都假设读者愿意翻源码,但"翻源码"最大的障碍不是代码量,是没有地图——四十万行引擎代码摆在面前,不知道从哪个文件进、看了有什么用。这篇导览把我读 4.x 源码时建立起来的整体结构压缩成一页:引擎分成哪两层、一帧的节拍谁在打、脚本在哪个位置接入,以及每一层对应的深读文章。读完再去开 godot/ 仓库,方向感会好很多。
一切结构的根:节点层与服务器层二分
Godot 4 最重要的架构事实只有一条:你打交道的节点不做实事,实事全在服务器(Server)里。Node3D 不渲染、RigidBody3D 不算物理、AudioStreamPlayer 不解码——它们只是句柄的持有者。节点创建时向对应服务器申请一个 RID(资源句柄),之后的每个操作都是"拿 RID 调服务器接口":
MeshInstance3D ──RID──> RenderingServer
RigidBody3D ──RID──> PhysicsServer3D
Camera3D ──RID──> RenderingServer / CameraServer
这个二分解决的是三个问题。解耦:场景树的生灭和渲染/物理资源的生命周期分离,queue_free 一个节点不必立刻销毁 GPU 资源。可替换:PhysicsServer3D 是注册式接口,4.4 把 Jolt 整个换进来靠的就是这层(《Jolt 接入 Godot 4.4 始末》讲透了替换过程)。线程边界:服务器可以跑在独立线程上,节点层的主线程只发命令不收结果。
RID 本身也值得一个解释:它不是裸指针,是"槽位索引 + 代际计数"的句柄。资源释放后槽位复用、代际递增,过期句柄只会被校验拒绝而不是悬垂崩溃。多线程渲染下,这是不用给每个资源上锁的前提。《RenderingDevice 抽象层设计》里有一节专门讲这个 ABA 防护。
一帧的节拍器:Main::iteration()
引擎的主循环不在引擎核心,而在各平台入口的 run() 里,循环体只有一件事:反复调 Main::iteration()。一次调用里发生的事按顺序是:
- 累加器结算物理帧——攒够几个固定步长(默认 $\frac{1}{60}$ 秒)就连跑几个
physics_process,上限 8 步,超了丢弃,防止死亡螺旋; - 跑一次
SceneTree::process()——网络 poll、process_frame信号、消息队列 flush、逐节点_process、帧尾统一析构queue_free的节点; RenderingServersync + draw,画面出去。
物理步长固定不是洁癖,是数值积分的要求:显式欧拉积分 $x_{t+1} = x_t + v \cdot \Delta t$ 的误差依赖 $\Delta t$ 取值,步长飘了同一段抛物线能积出两个落点。累加器的职责就是把不均匀的渲染帧"兑换"成固定步长的物理帧,让长期平均逼近设定频率。完整的推导和源码走读在《SceneTree 的 MainLoop 循环》。
节点生命周期:两个方向相反的递归
场景树里最容易踩坑的时序问题,源码里其实写得很直白。add_child 之后节点收到两组通知,递归方向相反:
_enter_tree自顶向下:父先子后,因为它维护的是上下文继承(节点从父节点抄 viewport、深度等环境信息);_ready自底向上:子先父后,因为它维护的是依赖初始化(父的初始化几乎总是依赖子已就绪)。
记住"父的 _ready 里子节点必然 ready,父的 _enter_tree 里子节点还没影"这两条,九成的初始化顺序 bug 就不会写出来了。另外你从不手写 set_process(true)——NOTIFICATION_READY 里引擎检测到脚本覆写了 _process 会自动注册;反过来删掉 _process 函数,节点下帧起就从每帧轮询列表里消失。链路细节见《Node 的 _ready 与 _process 调用链源码分析》。
全局结构一张图
把上面几块拼起来,加上脚本运行时和资源系统,是这张全景:
层间依赖只向下:脚本调节点、节点调服务器,服务器不知道节点的存在。这个单向依赖是"物理引擎能整个换掉"和"渲染线程能独立跑"的共同基础。
跨层的三个公共件
图里最下面一行不是某一层的内部设计,而是每层都在用的公共机制,值得单独记住:
- RID:所有服务器资源的引用形式,句柄化换来多线程安全和可替换性;
- StringName:字符串驻留表,引擎里所有"按名字"的接口(方法、信号、属性、组)比较时都退化为指针比较,反射系统能跑得快全靠它垫底;
- MessageQueue:
call_deferred和CONNECT_DEFERRED信号的共同落点,每帧在固定时机 flush,是"本帧晚点执行"语义的实现者。
这个分类的阅读路径
本分类的文章按"先会用再翻源码"的顺序组织,建议路径:
- 本文(地图)→《SceneTree 的 MainLoop 循环》→《Node 的 _ready 与 _process 调用链》:先建立一帧和一棵树的直觉;
- 《Object 基类与 ClassDB》:理解反射与信号槽,之后看任何
_bind_methods()都不再是黑盒; - 按兴趣选子系统下沉:渲染走《RenderingDevice 抽象层设计》,物理走《Jolt 接入 Godot 4.4 始末》,脚本走《GDScript 编译管线》,资源走《Resource 的引用计数与 ResourceCache》,网络走《MultiplayerSynchronizer 的属性同步》。
每篇都标注了基于哪个版本、翻了哪些文件,源码引用保留文件路径方便对照。读源码的方法论只有一条:带着一个具体问题进去(比如"_ready 为什么先调子节点的"),找到答案就出来,不要试图顺序读完——引擎代码是按机制组织的,不是按阅读顺序组织的。