BPVM 小食包 #13 - DAG 调度器:排序混沌 | ebp BPVM 小食包 #13 - DAG 调度器:排序混沌 | ebp BPVM 小食包 #13 - DAG 调度器:排序混沌 | ebp
文章

BPVM 小食包 #13 - DAG 调度器:排序混沌

你的蓝图节点可以以复杂的方式连接,但它们必须按顺序执行。DAG 调度器将你的节点网络转变为线性执行列表。

BPVM 小食包 #13 - DAG 调度器:排序混沌

本文内容基于Unreal Engine 5.6.0

BPVM 小食包 - 蓝图知识快速投喂!是蓝图到字节码系列的一部分。

执行顺序问题

看看你的蓝图图表。节点以各种方式连接。但 CPU 一次只能做一件事

谁先?谁接着?这就是调度器的工作!

什么是 DAG?

DAG = 有向无环图 (Directed Acyclic Graph)

  • 有向 (Directed): 箭头指向一个方向(数据向前流动)
  • 无环 (Acyclic): 没有循环(不能绕圈)
  • 图 (Graph): 由边连接的节点

你的蓝图就是一个 DAG(如果它能编译)!

拓扑排序

调度器使用拓扑排序来排序节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CreateExecutionSchedule(Nodes, LinearExecutionList)
{
    // 拓扑排序算法
    while (NodesLeft) {
        // 找到没有依赖的节点
        Node = FindNodeWithNoDependencies();

        // 添加到执行列表
        LinearExecutionList.Add(Node);

        // 从图中移除
        RemoveNode(Node);
    }
}

就像穿衣服 - 袜子在鞋子前,衬衫在领带前!

可视化示例

你的图表:

1
2
3
A → B → D
    ↓
    C → E

调度后:

1
线性顺序: A → B → C → D → E

调度器找到了依赖关系被尊重的唯一有效顺序!

检测循环

如果你不小心创建了一个循环怎么办?

1
2
3
4
5
6
7
8
// 循环依赖!
A  B  C  A

// 调度器检测到:
if (NodesLeft && NoDependencyFreeNodes) {
    Error("在图中检测到循环!");
    // 准确显示哪些节点形成循环
}

调度器在循环发生之前防止无限循环!

数据依赖

调度器跟踪两种类型的连接:

1
2
3
4
5
// 执行引脚(白色箭头)
BeginPlay  PrintString  SetVariable

// 数据引脚(彩色线)
GetVariable  Add  SetVariable

两者都创建影响排序的依赖关系!

纯节点是特殊的

纯节点(没有执行引脚)按需求调度:

1
2
3
4
5
6
7
// 你的图表
[Exec]  PrintString(GetRandomFloat() + 10)

// 调度顺序
1. GetRandomFloat()  // 首先计算(Print 需要)
2. Add(result, 10)   // 然后加
3. PrintString()     // 最后打印

纯节点在需要它们的输出时及时运行!

调度算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LinearExecutionList = [];
DependencyCount = {};

// 为每个节点计数依赖
for (Node in Nodes) {
    DependencyCount[Node] = CountIncomingEdges(Node);
}

// 处理没有依赖的节点
Queue = GetNodesWithZeroDependencies();

while (!Queue.Empty()) {
    Node = Queue.Pop();
    LinearExecutionList.Add(Node);

    // 减少连接节点的依赖计数
    for (ConnectedNode in Node.Outputs) {
        DependencyCount[ConnectedNode]--;
        if (DependencyCount[ConnectedNode] == 0) {
            Queue.Push(ConnectedNode);
        }
    }
}

真实世界调度

复杂图表:

1
2
3
4
BeginPlay → GetActor → IsValid → Branch
                ↓                    ↓
           GetLocation          [True] SetLocation
                                [False] PrintError

调度顺序:

  1. BeginPlay
  2. GetActor
  3. IsValid
  4. Branch
  5. GetLocation(即使未使用)
  6. SetLocation 或 PrintError

所有内容在需要之前就准备好了!

为什么线性很重要

虚拟机不能很好地处理分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对虚拟机不好(分支)
if (Condition) {
    Path A 节点...
} else {
    Path B 节点...
}

// 对虚拟机好(带跳转的线性)
CheckCondition
JumpIfFalse Label_B
Path A 节点...
Jump Label_End
Label_B:
Path B 节点...
Label_End:

跳转的线性执行比真正的分支更快!

调度错误

当调度失败时:

1
2
3
4
// 错误类型
"检测到循环"  你有 ABA 循环
"孤立节点"  节点没有连接到任何东西
"多个入口点"  两个 BeginPlays?

调度器在编译时捕获这些,而不是运行时!

快速要点

  • DAG 调度器将你的节点网络转变为操作线
  • 使用拓扑排序来尊重所有依赖关系
  • 在循环导致无限循环之前检测循环
  • 纯节点按需及时调度
  • 为虚拟机创建一个 LinearExecutionList
  • 使分支图表成为带跳转的线性

从混沌到秩序

你美丽、蔓延的节点图可能看起来像有组织的混沌,但 DAG 调度器将其转换为虚拟机可以逐步执行的完美有序列表。它是使可视化脚本真正工作的无名英雄!

想要更多细节?

完整的调度算法:

下一篇:后端如何将语句转换为字节码!


🍿 BPVM 小食包系列

本文由作者按照 CC BY 4.0 进行授权