今日报丨Iron & Logic Nodes 逻辑节点可视化编程

来源:哔哩哔哩时间:2023-04-26 04:04:43

## Iron & Logic Nodes


【资料图】

Iron - 3D Engine Core https://github.com/armory3d/iron

Iron examples https://github.com/armory3d/iron_examples/

Iron wiki https://github.com/armory3d/iron/wiki

Krafix shader compiler https://github.com/Kode/krafix

* [Find objects in the scene](https://github.com/armory3d/armory/wiki/Find-objects-in-the-scene)

Iron 为 Armory 提供了一个高级抽象的节点层次结构,基于 iron.object.Object:

- **traits** 集合包含了 Blender 对象中 Armory Traits 列表中添加的所有 iron traits 类型;

- **parent** 引用含父节点;

- **children** 集合包含了所有子节点;

- **animation** 引用节点的动画对象;

- **constraints** 集合包含所有约束关系;

- **lods** 集合包含包含分级细节图;

iron.Trait 类型的两个属性:

- **name**: String 指定 Trait 的名称。

- **object**: 当前 trait 所属着的对象,trait's owner,附着在对象 Armory Traits 列表。

Armory 框架中的 CanvasScript 对象扩展了 iron.Trait,包含属性:

- **cnvName** 即创建 Canvas UI 时指定的名称,如 MyCanvas。

- **canvas** 属性为 `TCanvas` 类型,对应 MyCanvas.json 中的基本画布数据,如宽高、位置。

- **cui** 画布对象对应的 `Zui` 框架实例。

iron.Scene 类型的几个属性:

- **active**: Scene 静态变量,引用当前活动场景。

- **global**: Object 静态变量,引用全局对象。

- **root**: Object 引用场景的根节点。

- **sceneParent**: Object 引用父场景。

它们构建出来的节点树的基本结构如下:

Armory 引擎在 armory.trait 空间下定义了大量扩展类型,当然,其中也包括,CanvasScript,WasmScript,DebugConsole 等等核心扩展类型。

当设置对象的属性面板,并且将一个 trait 附着在对象 Armory Traits 列表,那么这个 trait 就有了相应的归属者,而这个对象就称为 trait's owner,通过 trait.object属性引用,也就对应逻辑节点中的**自引用节点** `Self Object`。

Blender 逻辑节点编辑器中所有节点功能都由 Armory Blender 插件代码实现,在游戏执行时则使用Armory 框架的代码,即对应 `arm.logicnode` 和 `armory.logicnode` 两套代码,前者属于 Python 脚本,后者是 Haxe 脚本。

两套代码在类型命名上,基本可以保持一致的对应关系,但也有例外,比如 Scene 逻辑节点分类下的: Add Object to Collection 相应的 Haxe 类型是 AddObjectToGroupNode,并且标明的分类也可能不一致。但是,类型名字按代码约定,Python 脚本文件名和 bl_idname字段内容带有 `LN_` 前缀,Haxe 代码定义的类型则去掉前缀。

Armory 构架、Iron Trait 事件回调,以及相关属性、程序与逻辑节点的对照关系摘要如下,为了制表方便使用,某些名称可能会采用简略表达,比如 OnAppStateNode表示 OnApplicationStateNode

大多数逻辑节点的作用都是数据处理,除了几何事件处理逻辑节点,还有就是 Logic Nodes 分类下有多个节点类型用来处理控制流,这些属于是逻辑编程的核心节点:

1. `BranchNode` 逻辑节点有一个布尔值输入,按成立条件与否输出 True 和 False 两个控制流分支。

1. `SwitchNode` Value 输入值和多个 Case 进行比较,等值时输出对应的 Case 或者 Default 控制流。

1. `MergeNode` 和前面的节点相反,用于合并控制流,有两种执行模式:

- Once Per Input:简单转发,多了一个索引号输出。如果一帧内多个输入控制流激活,激活相应输出。

- Once Per Frame:如果一帧内多个输入控制流激活,只激活一次输出控制流。

1. `InvertOutputNode` - 反转控制流,如果输入的状态处于激活状态,那么输出非激活状态;

2. `AlternateNode` - 变换控制流,可以有多个控制流输出,每次变换地执行其中一个,按顺序循环执行;

3. `SequenceNode` - 序列控制流,按顺序执行输出的控件流;

4. `LoopNode` - 循环控制流,From 与 To 指定循环次数,执行 Loop 端口,结束时再执行 Done 端口;

`LoopBreakNode` 节点用来打断 `LoopNode` 这样的循环控制流,但是,和代码中直接使用 break关键字不同,逻辑树中有它自身的机制打断循环控制流,需要设置 `tree.loopBreak = true;`。`LoopContinueNode` 节点也类似,需要设置 `tree.loopContinue = true;`。

节点的使用是有条件,有些节点不能搭配在一起使用,因为没有作用。例如,Draw 分类下的所有节点都需要和 `OnRender2DNode` 配合使用,只有打开一个 Canvas 2D 绘画上下文对象才可以进行绘画。并且场景中需要在 Armory Scene Traits 或者 Armory Traits 列表中添加 Canvas UI 扩展,这样才会触发 On Render2D节点事件。

类似地,如果在一个 Draw 节点引用了其它数据,并且数据来源自其它的事件流,比如 `Keyboard` 事件流中执行到 `MergeNode`,其输出的 Active Input Index 数据就不能直接输入到 Draw 分类的节点使用。可以使用变量或都对象属性集中保存起来,再通过变量节点,或者获取属性值输出到 Draw 节点中使用。

使用 `SelectNode` 节点可以实现这样的逻辑,它不属于控制流处理节点,是数据选择节点,From Input执行模式下,其节点当前输出的 Input 值会保持直到改变输出值,所以可以连接到其它事件流的节点上。

`SelectNode` 节点不属于控制流处理节点,它是数据选择节点,有两种执行模式,并且当前输出的 Input 值会保持直到改变输出值:

1. From Index直接指定要输出的 Value 索引号;

2. From Input直接通过控制流选择输出,Input 和 Value 一一对应成组;

`CaseIndexNode` 节点有一个 Compare 和多个 Value 输入进行比较,如果比较到相等值则输出索引号,否则输出 null:

和时间关联的逻辑节点:

1. `OnTimerNode` 事件节点,可以设置 duration 和 repeat,使用 Time.delta 作为触发因素;

2. `TimerNode` 多功能定时器节点,增加事件流输入:Start、Pause、Stop 和输出控件流:Out、Done;

3. `SleepNode` 多功能 Tween 计时器,到时间就触发输出控件流;

4. `PulseNode` 逻辑节点,基于 Time.time() 和 tree.notifyOnUpdate(update) 的循环定时器,

提供 Start 和 Stop 两个控件流输入,分别用于启用、停止定时器;

有些逻辑是无法通过节点直接表达的,需要迂回实现。比如,On Render2D事件流中,希望在空格键按下的时候才绘图指定内容,这就不直接将两种事件链接在一起,而需要将键盘事件保存到一个状态变量中。然后,在绘画逻辑中检查状态变量并执行相应的任务。

变量或字符串节点保存数据,与使用对象的属性集保存数据是两种不同的方法,Variable vs. Properties。使用变量或字符串节点,数据直接保存在节点上。比如,`IntegerNode` 定义一个 value 属性用来保存数据,`StringNode` 也采用相同的方法。而对象属性集合的方式,则是在 owner 对象 properties 集合中保存,通过它可以在多个逻辑树之间共享状态数据。

逻辑节点中与属性数据读写有关的节点:

1. `GetObjectProperty` 和 `SetObjectProperty` 读写对象的 Properties 属性集合;

2. `GlobalObjectNode` 节点在 iron.Scene.global.properties 属性集合读写数据:

3. 各种 Variable Node 直接在逻辑节点对象上的 value 属性保存数据,例如 `FloatNode`;

4. `GetHaxePropertyNode` 和 `SetHaxePropertyNode` 直接读写 Haxe 对象的属性:

逻辑节点树变量**Tree Variables**是最新添加的功能,它的目标很简单,但是实现代码感觉很混乱。使用变量节点时,如果需要在多个位置使用同一个变量,一般就是拉一条条的线进行连接,在大量节点的场合下显得混乱。Tree Variables 就是解决这个问题的,在一个节点树内,可以用多个变量节点引用同一个变量,这就是节点树变量的用途。通过栅栏面板 Armory - Tree Variables 操作,可以将现有变量节点提升为节点树变量,也可以解除变成一般变量节点,或者将变量引用赋给其它同类型变量节点。实现代码参考 `ArmLogicVariableNodeMixin(ArmLogicTreeNode)`。节点树变量的侧栏面板界面实现: `ARM_PT_Variables(bpy.types.Panel)`。

New logic tree variable system https://github.com/armory3d/armory/pull/2439

逻辑节点有很多需要设置 Object 属性,默认留空表示使用 **owner** 对象,在逻辑树执行时会自动配置好。比如 `SetObjectProperty` 节点没有设置 Object 属性,使用默认值,那么在生成的逻辑树类型定义中就会包含类似以下的代码。`ObjectNode` 相当是一个代理,它的 `get()` 方法检查到使用了默认值,就会返回 tree.object,也即是 owner 对象:

为了在逻辑节点与 Haxe 脚本之间交换数据,使用 `GlobalObjectNode` 节点的属性,对应脚本对象:

iron.Scene.global.properties

Set Haxe Property 和 Get Haxe Property 也用于属性的读写,但是使用 Reflection API,输入对象是一个 Dynamic 类型。这两个节点就是直接对 Haxe 对象属性的读写,而不是 Properties 集合中的属性。可以用它们来设置 Iron Object 的旋转,先用 `GetHaxePropertyNode` 获取transform属性,再使用 `SetHaxePropertyNode` 设置转换矩阵的 rot 属性完成旋转操作:

与代码或表达式编写有关的节点:

1. `ScriptNode` 和 `ExpressionNode` 可以用来执行 Haxe 脚本,但需要 Haxe Script 类库支持;

2. `MathExpressionNode` 输入 2 ~ 10 个值和一个算式,内置 `Formula` 解释器执行算式;

`MathExpressionNode` 不依赖外部模块,内置数学算式语法分析工具,但只支持常用的计算相关的功能,+ - * / ^ % 以及 abs ln sin cos tan cot asin acos atan atan2 log max min 常用函数,只使用浮点数据类型。 其中 % 表示求余运算,可以用它截取浮点的小数:1.12345 - 1.12345 % 0.001。但是谨慎使用它,可能因为输入数据错误导数据流致相关节点中断执行,以下就是 `Formula` 解析出错一例。

`ScriptNode` 和 `ExpressionNode` 可以用来执行 Haxe 脚本,需要 Haxe Script 类库支持,它实现了 Haxe language 的一个子集,提供了 `Parser` 和 `Interp`。虽然 Haxe 到处是表达式,但是这两个节点中不能使用受限的功能,`ExpressionNode` 就不能使用 Std.random(10) trace(this) 等等。只能是纯计算的表达式,例如,以下这句就可以,没有外部引用不会产生异常:

var a = 1; trace(1 + a); a++;

注意,编写代码或表达式时双引号的使用,因为引擎只是简单地将内容内嵌到生成的逻辑节点树类定义文件中,如果直接编写 "99 + 1" 这样的表达式,就会因为双引号配对导致语言错误。

表达式节点 `ExpressionNode` 可以用来执行一些语句,执行结果通过节点的 Result 端口输出。因为 Armory 引擎使用 Haxe 脚本代码,可以通过修改 armsdk 中的代码来改变 Armory 的行为,比如在 ScriptNode 代码文件中添加调试代码到 `run(from: Int)` 方法,看看是否已经启用了hscript 模块。默认状态没有启用 hscript,总是输出 null,并且也不会提示。添加以下代码就可以在缺失相关模块功能支持时,提示用户安装和启用 hscript 模块:

安装 hscript 模块,然后再设置 Armory Project - Modules - Append Khafile 添加构建脚本片段,通过 addLibrary() 引入模块,并且定义相应的符号定义,启用 hscript 脚本执行功能。设置不一定会立即生效,需要 Render - Armory Player - Clear 清理掉缓存文件。

逻辑节点中有三各和函数定义、调用相关的节点:

1. `CallFunctionNode` 调用函数节点,调用 Trait/Any 端口中指定对象中定义的 Function。

2. `FunctionNode` 定义可重用的函数供 [Call Function] 节点调用,需要与函数输出节点配合使用。

3. `FunctionOutputNode` 给指定的 `FunctionNode` 节点设置返回值。

在生成的 `LogicTree` 类定义中,有两个专用的 Map 类型的属性用来管理函数节点的连接配置等等信息。`FunctionNode` 节点定义的函数,比如 MyFun 就会对应生成代码文件中的一个同名的函数:

`CallFunctionNode` 调用函数节点通过 [Reflect] API 去调用 Trait/Any 端口中指定的对象中定义的函数。调用函数节点首先检查对象是否存在 Function 输入端口指定名称的函数,如果存在就发起调用,并且将输入的参数传递给待调用的函数。函数返回值暂存于 result 变量中,等等待下游节点来获取取。如果`FunctionNode` 不与 `FunctionOutputNode` 节点相连,那么调用函数时,函数输出值就不能通过储到 functionOutputNodes这个映射变量内的 FunctionOutputNode.result 获取。由于逻辑节点的设计需要,当然不能直接在 `FunctionNode` 节点中实现数据保存的逻辑。因为需要在两个节点之间提供可以连接的机会,才使用得函数节点有被添加功能定义的可能。

以下是一个什么也不干的 MyFunc,调用时传入什么参数,就返回什么值:

另外,因为调用函数节点是直接在 Trait/Any 端口中指定的对象中查找相关的函数,所以这个端口就应该输入一个 `Self Trait`。通常是 LogicTree,因为逻辑节点的方法会在它的生成代码中定义。

标签:

图文推荐

热门文字

标签

精彩赏析