同一帧里几百个节点的 _process 总有个先后。默认顺序是节点在场景树里的挂接次序,靠 process_priority(默认 0)可以插队。这个字段看起来只是个"优先级数字",但它背后是 SceneTree 处理组的有序插入维护,而且 4.2 之后还和线程组(process thread group)耦合在一起。这篇把排序规则和踩坑点讲清楚。
处理组:SceneTree 的有序队列
前两篇说过,开 set_process(true) 的节点会被登记进 SceneTree 的处理组。4.x 的结构(scene/main/scene_tree.h 的 ProcessGroup,示意):
struct ProcessGroup {
LocalVector<Node *> nodelist; // 按 process_priority 有序
uint32_t tree_version = 0;
// ... 自增下标、遍历时增删用的缓存
};
有序性在登记时维持。Node::_add_to_process_thread_group() 里做的是按优先级找插入位置(node.cpp,有删减):
int order = data.process_priority;
int pos = process_group->nodelist.size();
// 从尾往前找第一个优先级 <= order 的位置,插在它后面
while (pos > 0 && process_group->nodelist[pos - 1]->data.process_priority <= order) {
pos--;
}
process_group->nodelist.insert(pos, this);
这个插入规则决定了同优先级的行为:<= 意味着后插入的同优先级节点排在已有节点后面——同优先级按登记先后稳定排序,先 set_process(true) 的先跑。对绝大多数节点来说"登记先后"就是场景树里 add_child 的次序,也就是你在编辑器里看到的兄弟节点顺序。
每帧 SceneTree 遍历 nodelist,从头调到尾,优先级小的在前。排序只发生在登记/改优先级/线程组变更时,每帧是纯线性遍历,没有每帧排序开销——这是它和"每帧 sort"方案的关键区别。
负优先级插队
process_priority 是 int,可以是负数。想让自己的 _process 抢在所有人前面,设成 INT32_MIN 级别的负数就行;反过来设大正数殿后。典型用法:
- 输入处理最先:管理输入状态的节点设小优先级,保证 AI、动画读的输入是这一帧最新的;
- 相机跟随最后:相机在所有移动逻辑之后更新,读到的位置才是本帧定稿的,否则相机对快速目标会抖一帧;
- 调试 HUD 最后:显示的帧率、状态读的是本帧汇总值。
有一个容易误会的地方:这个顺序只保证 _process 的调用次序,不保证完整数据依赖。A 优先级比 B 高,A 的 _process 先跑,A 里读到 B 的状态是 B 上一帧的。如果 A 强依赖 B 本帧的输出,优先级解决不了,得把逻辑合并进一个节点,或者用 B 跑完发信号的方式显式编排。
线程组:4.2 的新维度
4.2 引入 process_thread_group(Node 属性,inherit/main_thread/sub_thread),节点按这个标志分进不同的处理组。inherit 是默认值,沿父链继承。这套机制的目的:把"纯计算、不碰场景树"的 _process(比如大量 agent 的状态机推进)挪到子线程组并行跑,主线程组保持轻量。
它和优先级的交互规则要记牢:不同线程组之间的相对顺序没有任何保证。主线程组全部跑完之前、之中还是之后跑子线程组,取决于引擎的调度,跨组不能用优先级排序。所以"相机必须最后"这类需求,前提是所有相关节点在同一个线程组里——默认全在主线程组,一般不用操心;一旦开了子线程组,就要接受它和主线程的调用顺序是并发关系。
子线程组的 _process 里访问其他节点是不安全的(和主线程并发),这也是它被文档反复警告的原因。我实测过把寻路agent 挪进子线程组:CPU 降了 40%,但每几百帧偶发一次状态竞争——最后收敛到"子线程组只碰自己的数据,结果写进原子变量,主线程组读"的纪律。
改动优先级的成本
set_process_priority() 不是免费赋值:它要先从处理组摘除(O(n) 定位)再按新优先级插回(O(n) 扫描)。每帧改优先级就是把 $O(n)$ 的插入变成每帧开销,n 是处理组规模。正确姿势是静态设好,运行期不动;确实要动态排序的场景(比如回合制按速度排行动条),用"把节点存进数组自己 sort"代替改 process_priority——那个 sort 你还能控制稳定性和时机,引擎的处理组不是为这个设计的。
还有个小坑:process_priority 同时作用于 _process 和 _physics_process(还有内部处理),没有分开的优先级。想让物理回调和渲染回调排不同次序,做不到,只能拆节点。
顺序一览图
最后总结成三条规则:同组内,优先级小的先跑,同优先级先进先跑;动态改优先级每次 $O(n)$,别放进帧循环;跨线程组的并发关系由调度器说了算,优先级管不着。记住这三条,process_priority 就是个可预测的工具,而不是玄学。
本文代码引自 Godot 4.2.2-stable 的 scene/main/node.cpp、scene/main/scene_tree.h,均有删减;线程组部分参考 4.2 引入的 Node.process_thread_group 机制。