Proxyos Weekly 012
Laurence-042
- 2 minutes read - 240 wordsTL;DR 概览
ANORA的设计花费了比预期更多的时间,目前还未达到目标
本期目标
- 完成逻辑图系统,考虑用TypeScript+Vue/React之类的搞一个ANORA原型,然后通过wry集成进游戏,通过wry的事件系统进行操作
- Node框架
- 分层Port
- Connection折叠/展开
- 逻辑图计算
- 设计
进展速记(Changelog)
新增:
变更:
修复:
主要进展内容
ANORA的设计难题
虽然四天前我说“毕竟这些东西当时我用了好几个月内的工作间隙。但我觉得有善用claude应该能及时完成”,但实际上比我想象得更难,仅仅是设计就几乎花掉了这一期所有时间
设计文档中的致命问题
我第一天写了ANORA的设计文档。我以为已经很全面了,毕竟我是基于之前已经实现的AAW的经验,覆盖了各种实体的定义、执行逻辑、功能细节、项目结构等等
但是当我用claude检查的时候,我发现了很多致命问题
问题1:推还是拉?
在ANORA的前身AAW里,逻辑图里的顺序是“拉”的,即执行器只记录节点间的依赖关系,当某个节点所依赖的节点都执行完成后,就把数据从其依赖的节点的出Port上拉到这个节点的入Port上供这个节点使用
ANORA为了给节点更多的自由度,使节点可以根据当前入Port输入状态决定自己是否可以执行(为了提供现代编程语言的可选参数特性),这就必须使用“推”的,即一个节点执行完成后会把它出Port上的数据推到下一个节点的入Port上,然后节点才能做出判断
但这产生了一个问题:ANORA需要和AAW一样支持Port的嵌套。AAW在拉的模式下可以先拉外层Port再拉里层Port来保证里层Port的数据优先级最高,以此支持入Port数据模板之类的写法。但ANORA的推的模式就无法支持这个特性了
问题2:要不要支持Port的嵌套?
遇到上述问题后,我首先想的就是“问题的前提条件是否是无法改变的”,也就是是否一定要支持Port的嵌套。直接用get/set节点行不行
思考后我觉得是必须要支持的。因为我设计时就想好了这个项目的演示用后端——API录制回放。是的,还是AAW。但不是因为有个垃圾底层系统需要我包装,而是因为这个任务同时涉及了强时序、复杂数据流、异步调用这三个节点编程系统能发挥最大作用但又最难同时满足的场景,它会成为ANORA的试金石
而为了适配复杂数据流,“从默认数据模板中改一个数据”的操作必须要足够简便,最多只能用独立的参数节点提供默认数据模板,不能通过额外的节点数据合并。毕竟合并数据如果不使用嵌套Port,就必须通过某种更程序员的方式指定属性路径了,这显然和ANORA的设计目的相悖。
所以问题1必须要解决。但在思考解决方案的时候,我遇到了另一个更严重的问题
问题3:同步还是异步?
在ANORA的前身AAW里,逻辑图的执行是全顺序的,即使在一个批次里,节点的执行也是顺序的。当时这个设计是因为我没有很多时间实现一个复杂高效的系统,所以当时我就想只要在性能可以接受的情况下debug尽可能方便就行。但是ANORA自然不能这样,所以在我最初的设计中执行是全异步的。这使效率最大化,而且不管是用在什么任务中都足以胜任。
只能说理想是美好的。在全异步的模式下,我想不到一个可以同时满足易用、直观、可维护的循环逻辑执行方案。我参考了UE、ComfyUI等流行的节点编程系统,但它们要么就是直接封装编程语言里的while为节点,要么就是根本不支持流程控制。
问题4:要不要null?
这反而是最小的问题了,这玩意又不是给经验丰富开发者用的,拒绝null那就得引入更多保护措施,反而导致其上手门槛更多。一番折腾后我才明白了这个道理,然后释然地添加了null
解决方案?
显然,问题2和问题4都解决了,关键是1和3。
最后,我想到了这么一个设计:让流程控制节点可以一次激活多次执行
而这个方案不仅解决了两个问题,还让ANORA彻底甩开了AAW到达了全新层面。
ANORA的思想
更复杂的节点
之前我一直把节点当成函数调用的抽象,这使我陷入了误区——调用函数的语句天生就没有分支!
虽然在AAW里通过“以数据为核心,节点只有其需求的数据都输入后才能运行,运行后可以选择性地在特定出边输出数据”的方式实现了比UE更易用分支写法,但是其循环逻辑仍然十分粗糙、难以编写——它产生了环
而有环的图天生就是不直观的
但当我不再把节点当成一个函数,而是以更自然的方式进行比喻时,一切迎刃而解了
节点就像一个自动加工机:
| 概念 | 比喻 |
|---|---|
| inPorts | 原料入口,默认看到原料足够就开始工作,把成品放到 outPorts |
| outPorts | 成品出口 |
| inControlPorts | 模式设置面板,可改变工作模式在多次启动中使用不同逻辑 |
| outControlPorts | 状态显示面板,显示当前工作状态/进度 |
| inExecPort | 电源插座。未接线=内置电源,有原料就加工;接线=外部供电,线没电就不动 |
通过ControlPorts,节点可以支持远比AAW中更复杂的操作,同时让逻辑图尽可能无环
好奇怎么在没有环的情况下用有向图表示循环?这就是例子
DistributeNode (分配)
接受一个数组,然后在接下来的数次迭代中依次输出每个元素,相当于 for-each:
- outControlPort
index:输出当前索引 - outControlPort
finish:输出最后一个元素时同步激活,表示迭代完成 - outExecPort:每输出一个元素都会激活一次
激活条件:还有元素待输出 || 默认条件(需要重写 isReadyToActivate,可调 super 复用)
特殊行为:
- 在一个迭代中激活后,即便 in 端没有任何 Port 被写入,下一个迭代也会激活并输出数组中的元素
- 例:第 x 个迭代用长度为 y 的数组 arr 激活,则第 x+i(i<y)个迭代中总是会激活并输出 arr[i]
输出期间收到新数组:丢弃新输入,继续当前输出
- 例:第 x 个迭代用长度为 y 的数组 arr 激活,第 x+y-1 个迭代即使 inPort 被输入新数组,也只会丢掉输入继续输出 arr[y-1]
AggregateNode (聚集)
inPort 接受任意数据,有两种激活模式(需要重写 isReadyToActivate):
| 激活条件 | 行为 |
|---|---|
inControlPort aggregate 被写入 | 将 inPort 数据加进缓存数组(null 也缓存) |
| inExecPort 被写入 | 输出缓存数组,然后清空缓存 |
特点:两种激活模式都不是由 inPorts 的数据触发的
用途:往往和分配器共同使用,实现 filter、map、reduce 之类的操作
两难自解!
这个系统显然是需要保留的,其使用无环图表示循环逻辑的特性不论是教学、模仿神经活动还是实际使用都会大幅降低逻辑图的理解、调试、维护成本
而这个系统必须有良好的时序控制,否则DistributeNode和AggregateNode没法配合
因此AAW的迭代设计是必要的,即每次迭代执行一批可执行节点,然后下个迭代执行受上一迭代影响、可以执行的下一批节点。每次迭代只会往前走最多一个节点,这样才能避免DistributeNode只异步发俩数据,AggregateNode却异步激活三次的情况
因此,问题3解决:同步或者异步?或!
在迭代间使用同步,迭代内同一批执行的节点异步。这样既可以让异步节点之间不必彼此等待,又可以控制时序。而且这样用户调试起来也更方便(可以设置迭代间延迟,让用户看程序一步步执行)
再看问题1:如何在推数据的模式下提供嵌套Port赋值特性?控制好时序就行
ANORA提供了种特殊节点:ForwardNode (中继)
接受所有数据类型并原样输出,可使用 context 指定"直通"模式:
| 模式 | 行为 |
|---|---|
| 非直通 | 默认。走正常迭代流程 |
| 直通 | 填写入 Port 时立刻执行并填写后面的入 Port,不等迭代 |
直通机制详解:
通常一个迭代的流程是:当前节点执行 → 将执行结果填到出 Port → 将出 Port 数据推到下一个节点的入 Port
Executor 在推完数据后,还会检查目标入 Port 是不是直通 Forward 的:
- 如果是,Executor 会立刻执行这个直通 Forward
- 然后再将其出 Port 的数据继续往后推
- 直到没有任何直通 Forward 的入 Port 被推数据
用途:整理图结构、作为 MergeGate 保证后续节点在同一迭代中执行、缓存数据值、延迟迭代以控制执行时序
限制:两个直通模式的 Forward 不允许组成环(Graph 中需要检查)
逻辑图可以同时存在一对多和多对一:
| 情况 | 处理方式 |
|---|---|
| 一对多 | 一个出 Port 的值可以推到所有与其相连的入 Port |
| 多对一 | 同一迭代中执行完成的一批节点出 Port 同时连接下一节点的父入 Port 和子入 Port 时,先给父入 Port 赋值再给子入 Port,可以用非直通中继节点来保证父子入 Port 相连的出 Port 对应的节点在同一迭代中执行 |
| 多对一 | 同一迭代中执行完成的一批节点出 Port 同时连接同一个入 Port 时,覆盖顺序不确定,报告警 |
瓶颈与问题清单
- 暂无
下期计划(Next)
- 完成逻辑图系统,考虑用TypeScript+Vue/React之类的搞一个ANORA原型,然后通过wry集成进游戏,通过wry的事件系统进行操作
- Node框架
- 分层Port
- Connection折叠/展开
- 逻辑图计算
- 简而言之就是之前的AWW的所有除了定制节点之外的基础计算和显示逻辑,时间有点紧,毕竟这些东西当时我用了好几个月内的工作间隙。但我觉得有善用claude应该能及时完成——Again
试玩版
预计第一个可玩版本将在第二章的第一个涉及外部编程的游戏内容完成后推出