Proxyos Weekly 024
Laurence-042
- 2 minutes read - 402 wordsTL;DR 概览
本期目标
- 完成第一章打磨
- 第五节
- 第六节
- 过渡到第二章
- 完成第一章测试
- 第五节
- 第六节
- 过渡到第二章
进展速记(Changelog)
本期假设 / 预期
我当时以为世界是怎样的? 这个预期中,哪一条被证伪 / 被削弱 / 被确认?
- 第一章核心 bug 已修复,主要打磨内容是调整文本和图表现。考虑到加上本期就是一整周了,应该能搞定文本和图表现。音效、视觉效果等发 demo 前统一处理
- 实际上在打磨到需要更复杂输入数据的第五节时,发现 ParamNode 大小不能很好地显示其内容,于是临时加了功能
- 问题大了去了!详情见
主要进展内容/本期关键判断点一节的 ANORA 相关段落
本期确定性变化
哪些东西现在「更确定」或「被明确否定」了? “确认 X 不可行” “删掉 Y 抽象” “意识到 Z 是伪问题”
新增:
- ANORA 添加节点 resize 功能
变更:
- 优化第一章 Python 脚本中一些不太合适的抽象,使整体叙事和呈现风格更加一致
- ANORA 处于回放模式时不再允许节点修改 context
- ANORA 执行逻辑大修
修复:
删除:
主要进展内容/本期关键判断点
我做出了哪些「如果错了也要付代价」的判断?
剧情打磨
我完成了第一章剩下的文案/交互打磨,并将最新的逻辑整理成了单独的剧本。
通过分析剧本,识别出了一些逻辑问题,并进一步优化了第一章文案。
这下第一章的内容部分算是彻底打磨完成了。接下来只剩把 5、6 两节涉及的 ANORA 回放给录制出来就可以了……吗?
完全不是,出大篓子了!
ANORA 执行逻辑大修
我在 Proxyos Weekly 012 里曾经提过我是怎么设计 ANORA 的,并在那里面确定了“推数据,而不是拉数据”的核心思想。
同时,我也提到了其更高的自由度、必要时使用环来实现复杂逻辑的特性。
其实 ANORA 的总体思想没啥问题:
- 图灵完备的同时保持直观
- 使用数据流为核心思想
- 极佳的可扩展性
- 让节点具备更高的自由度
- 可以自由添加各种节点而不必编辑源代码
- 纯前端完成节点调度
这三个是 ANORA 立项之初就定下的死规矩,因为如果没有这些,那它就只是一个另一个 UE Blueprint 那种披着流程图外衣的常规编程语言了,而不是一个更加直观的通用流程图计算框架
虽然当时我就说“有环的图是不直观的”,所以引入了带 ControlPorts 的流程控制节点
但我没说的是,我希望一个图在底层就是图灵完备的,即使全使用 2 入 2 出的节点都能实现分支和循环,而不是必须依赖特殊的节点才能实现循环。
我觉得只有这样才算一个合格的架构,而我也有能力设计这样的架构
但实现时我傻逼了
要说为啥傻逼,我们得从头说起
我之前是怎么想的
最初版本
最开始,我使用了如下节点概念
| 概念 | 比喻 |
|---|---|
| inPorts | 原料入口,默认看到原料足够就开始工作,把成品放到 outPorts |
| outPorts | 成品出口 |
| inControlPorts | 模式设置面板,可改变工作模式在多次启动中使用不同逻辑 |
| outControlPorts | 状态显示面板,显示当前工作状态/进度 |
| inDependsOnPort | 电源插座。未接线=内置电源,有原料就加工;接线=外部供电,线没电就不动 |
默认情况下节点当且仅当所有被连接的入 Port 有数据时辉表示自己可以被执行
而执行器的逻辑如下
- 初始化
- 查询所有节点的可运行状态
- 执行已准备好(Activation-Ready)的节点
- 检查停止请求:检查当前用户是否要求停止或暂停
- 并行执行:
- 节点的执行函数
activate必定是异步的 - 使用类似
await Promise.allSettled同时启动当前迭代中所有准备好的节点并等待完成 - 同一个迭代中节点的执行顺序并不确定
- 支持取消操作,终止时当前所有未完成的节点视为执行失败
- 节点的执行函数
- 执行后处理:
- 清空执行后节点的入 Port(避免下个迭代再次被激活)
- 统一检查这些执行后节点的准备状态(主要用于
DistributeNode这类在激活后的多个迭代都会保持 READY 的节点) - 从执行后节点的所有出 Port 里取出数据填入其连接的另一节点的入 Port
- 即使值为 null 或 ContainerPort 内的子 Port 为 null 也要填入
- 特殊处理直通节点:如果目标节点是
directThrough=true的 ForwardNode,立即执行并继续传播 - 查询其他受影响节点的准备状态
看着没啥问题?如果你这么觉得,那就说明你和我一样想得太少。
如果这个图有环,那么就会在 A->B->A 的结构中,A 第一次就没法执行——A 在等待 B 执行,而 B 在等待 A 执行
优化版本
在发现了上述问题后,我立刻进行了调整,为节点新增了 inActivateOnPort
| 概念 | 比喻 |
|---|---|
| inActivateOnPort | 遥控启动按钮。可以远程再次启动机器,但机器首次启动不需要等这个按钮 |
现在除了之前的激活条件外,只要 inActivateOnPort 被激活,节点直接会表示自己可以执行
并调整了后处理逻辑
- 执行后处理:
- 清空执行后节点的入 Port(避免下个迭代再次被激活)
- 统一检查这些执行后节点的准备状态(主要用于
DistributeNode这类在激活后的多个迭代都会保持 READY 的节点) - 从执行后节点的所有出 Port 里取出数据填入其连接的另一节点的入 Port
- 即使值为 null 或 ContainerPort 内的子 Port 为 null 也要填入
- 特殊处理 activateOn:当数据被推到某节点的
inActivateOnPort时,Executor 会从上游出 Port 再次将数据推到该节点的入 Port (因为推式传递会在节点执行后清空入 Port,而 activateOn 触发的再次激活需要这些数据) - 特殊处理直通节点:如果目标节点是
directThrough=true的 ForwardNode,立即执行并继续传播 - 查询其他受影响节点的准备状态
然后我测试了一下工作正常后,就心满意足地去继续 ProxyOS 的开发,把 ANORA 暂时放下了。
但它真的没问题吗?
我在打磨第一章第五节的时候,里面提到了一个使用自增计数器的循环结构演示。
然后我就发现了新问题:自增计数器的缓存节点同时接受两个输入,一个是默认的 0 的参数节点输入,一个是将其加 1 的加法节点输出。
多对一,怎么能确定从哪个前置节点推呢?或者更本质的说——从哪个前置节点拉呢?
推式数据流的设计本身就是考虑了多对一场景才确定的,但我却因为“会从上游出 Port 再次将数据推到该节点的入 Port”这样的文字游戏把自己骗过去了,误以为这是推——但实际上这是拉。
显然,我这期又在折腾这玩意了
现在是什么样的
inActivateOnPort 确实是必要的,否则没法解决环。但它的含义发生了些变化
| 概念 | 比喻 |
|---|---|
| inActivateOnPort | 遥控启动按钮。可以远程再次启动机器,用上次剩下的原料再加工一次 |
不应使用拉数据,而总是要推数据。因此就没必要再清除原本的入 Port 里的数据了,而是直接区分新旧数据。默认所有数据为新数据时激活,但 inActivateOnPort 激活时,只要都有数据就可以激活(可以新旧混杂甚至没有任何新数据)
- 执行后处理:
- 统一检查这些执行后节点的准备状态(主要用于
DistributeNode这类在激活后的多个迭代都会保持 READY 的节点) - 从执行后节点的所有出 Port 里取出数据填入其连接的另一节点的入 Port
- 即使值为 null 或 ContainerPort 内的子 Port 为 null 也要填入
- 写入会使目标入 Port 的
_version递增,标记为"新数据" - 传播完成后清空 outPort,防止下次迭代时重复传播
- 特殊处理直通节点:如果目标节点是
directThrough=true的 ForwardNode,立即执行并继续传播 - 查询其他受影响节点的准备状态
- 统一检查这些执行后节点的准备状态(主要用于
现在,整个逻辑都是“推”的,而且性能没有什么下降,只是每个节点多了个 number
它还能优化吗?
看到这里,你可能会寻思为啥要用 _version,因为乍看这个方案本质上和“执行后不清入 port,而是给节点加个 executed 标记,让节点默认有 executed 就不执行,activateOn 会清除 executed”似乎没啥区别
但实际上不然,考虑一个简单的 A->B->A 场景
| _version | _executed |
|---|---|
A 除了 activateOn 没有输入依赖,A 执行,推数据给 B,B 记录数据_version = 1 | A 除了 activateOn 没有输入依赖,A 执行,推数据给 B |
| B 的所有数据都是最新的版本 1,B 执行 | B 的所有数据都被满足,B 执行,B 记录执行状态_executed = true |
A 被 activateOn 输入再次触发执行,推新数据给 B,B 记录数据_version = 2 | A 被 activateOn 输入再次触发执行,推新数据给 B |
| B 的所有数据都是最新的版本 2,B 执行 | B 的 isReadyToActivate 检查。.._executed = true,B 不执行 |
也就是说,使用 executed 在节点 A 上的表现和使用、_version 是一致的,但在 A 后面的 B 上表现不同。
我也在考虑它还有没有啥优化空间,但目前我没想到更好的方案
瓶颈与问题清单
哪些问题还没解,但也许我已经知道“它们不是什么”?
下期计划(Next)
下期(3 天)我想缓一缓(做 1 休 2),这期脑力消耗有点大了,实际上之前提到的 ANORA 逻辑大修实际走的弯路比写出来的只多不少
- 完成第一章测试
- 第五节
- 第六节
- 过渡到第二章
- 修 ANORA
- 单步调试功能需要做对应调整
- 似乎有个偶现的无法重复录制的问题
- 添加边选中逻辑
试玩版
预计第一个可玩版本将在第二章的第一个涉及外部编程的游戏内容完成后推出