Armory3D: Custom Logic Node - Switch Camera


🐥 Custom Logic Node
* Logic Nodes
* [Introduction](https://github.com/armory3d/armory/wiki/Introduction-to-Logic-Nodes)
* Engine Development
* [Logic Nodes](https://github.com/armory3d/armory/wiki/logicnodes)
实现自定义的逻辑节点:将逻辑节点实现的 Python 脚本文件放到 Armory 源代码中,或者项目中的Libraries 目录下。本文内容涉及 Blender Python API,但又不完全是 Blender 插件开发,因为主要内容是 Armory 逻辑节点的框架。

Blender 节点编程接口参考 SocketDeclarationBuilder、NodeDeclarationBuilder,以及节点编辑器源代码空间 space_node:
Armory 逻辑节点及编辑器 API 定义在以下各个文件:
阅读 Logic Nodes 代码时应该注意区分,设计阶段的逻辑节点实现代码,以及逻辑节点运行阶段的代码。前者基于 Blender Python API 接口开发,后者用 Haxe 语言开发,Armory 引擎运行时执行以复现逻辑节点的可视化设计的逻辑状态。
以下是 arm_nodes.py 文件定义的逻辑节点类型,属于设计阶段的功能,调用 Blender API 实现逻辑节点编辑器的节点功能,顶级基类是 Blender `Node`,逻辑节点功能父类是 `ArmLogicTreeNode`:
以下是 nodes_logic.py 文件定义的逻辑节点编辑器功能,设计阶段的功能,调用 Blender API 实现逻辑节点编辑器的节点树,顶级基类是 Blender `NodeTree`,逻辑节点树功能父类是 `ArmLogicTree`:
面包屑导航(Breadcrumb)是逻辑编辑器左上角显示当前逻辑树与子树路径关系的提示性工具,添加节点组并编辑它就会进入下一子级节点树,UI 组件的当前上下文对象可获取路径:context.space_data.path。
节点输入、输出端口的 UI 列表对应 `ARM_UL_interface_sockets`,这个列表维护的是 Blender 界面绘图之用的 UI 数据,即这个列表有什么数据,逻辑编辑器 UI 界面上就会绘画相应的视觉元素,端口的圆点、节点之间的连线,这些都是视觉元素。节点交互行为基本由 `ArmLogicTreeNode` 类型实现。
节点树中有变量节点,它可以像代码中的变量一样存储数据,只不过这些数据是以节点这种数据结构保存,`ArmLogicVariableNodeMixin` 就是实现这个机制的类型。变量节点还可以提升为 tree variable 节点树变量,即多个节点引用同一个数据,这样就可以在设计逻辑节点时,避免总是从同一个节点引线连接到其它位置非常远的节点上,从而可以得到更整洁的节点布局。
有些逻辑节点可以有多个输入、输出端口,比如在 LN_select.py 文件中定义的 `SelectNode` 节点,它有两种执行模式,可以拥有多个控制流、数据流输入端口:
1. From Index 有一个 Index 输入,和多个 Value 输入,前者指定要输出的 Value 端口索引号;
2. From Input 模式下 Input 控件流和 Value 数据流输入成对出现,控制流激活时输出相应 Value;
逻辑节点设计了版本机制,实现升级替换功能,`ARM_OT_ReplaceNodesOperator` 提供手动替换功能。
Blender 节点编辑器是一个基础功能接口,Armory 基于些设计了逻辑节点树编辑器,ArmLogicTree 代表 Blender 中的逻辑节点编辑器的用户界面,配合各种 UI 组件,构建一个完整的操作界面。
Add Node 菜单提供的功能由 `ARM_MT_NodeAddOverride` 实现,它覆盖 Blender 默认的节点菜单,用 arm_nodes.category_items 登记的逻辑节点定义填充菜单,并按分区添加 separator 分隔线。点击菜单项就触发 ARM_OT_AddNodeOverride `invoke()` -> `bpy.ops.node.add_node()`,Blender 界面中的菜单项和按钮是同一种对象类型 `Operator`。
所有登记的逻辑节点分类,会在菜单中产生相应的分类菜单类条目,名称前缀 `ARM_MT_`。
如侧栏面板的在线文档功能,Armory - Armory Logic Node 对应 `ARM_PT_LogicNodePanel`,其中提供三个用于打开在线文档的按钮,对应类型为 `Operator`:
1. ArmOpenNodeHaxeSource 对应 **Open Node Haxe Source** 按钮;
2. ArmOpenNodePythonSource 对应 **Open Node Python Source** 按钮;
3. ArmOpenNodeWikiEntry 对应 **Open Node Wiki Entry** 按钮;
它们的功能就是调用 webbrowser.open() 方法打开对应的在线文档资源。
侧栏面板 Armory Logic Node - Node Development 对应 `ARM_PT_NodeDevelopment`,提供当前选中节点的信息:
1. 节点所属分类 **Category** (arm_nodes.eval_node_category(node))
2. 节点所属分区 **Section** (node.arm_section)
3. 节点的版本号 **Specific** Version (node.arm_version)
4. 节点类定义版本 **Class Version** (node.__class__.arm_version)
5. 节点是否处于弃用状态 **Is Deprecated** (node.arm_is_obsolete)
6. 是否是变量节点 **Is Variable Node** (isinstance(node, arm_nodes.ArmLogicVariableNodeMixin))
7. 逻辑 ID 编号 **Logic ID** (node.arm_logic_id)
逻辑节点编辑器中所有节点目录都是按分类、分区二级目录进行管理,在整个逻辑目录上划分了 7 个区和多个分类,比如 Event 分类和 Input 分类就归属于 Basic 分区。再有,Input 分类下的所有节点划分为 6 个分区,但它们全都属性于 Input 分类菜单下显示,每个分区对应菜单上的分隔线。
Armory 默认的逻辑节点分类、分区与图标设置,图标参考 Blender bpy.types.Node(bpy_struct):
* https://wiki.blender.org/wiki/Source/Interface/Icons
Haxe 逻辑节点数据文件中,或者逻辑节点连接端口 API 文件中,都可以找到节点端口的类型定义。在使用逻辑节点编辑器时,只需要知道事件流与数据流的区分即够用了,事件流对应 **ArmNodeSocketAction**,数据流,由于数据类型不同,有不同的端口,对应不同颜色的圆点。
在编写节点类型实现代码,调用 `add_output()` 和 `add_input()` 中添加端口时,socket_type 参数需要使用字符串指定端口类型,尽管这种操作不太恰当,但是 Armory 就是这样做的。
Bledner 本身支持节点编辑器,提供端口的基类 `bpy.types.NodeSocket`,Armory 在此基础上派生出 `ArmCustomSocket`,作为所有逻辑节点端口的基类型,定义在文件:**arm_sockets.py**。
在大型类库中,通常用单个脚本文件定义一个类型,多个文件构成一个包,不同分类的节点类型保存在子目录。每个逻辑节点脚本名称前缀 `LN_` 并且类型的 [`bl_idname`] 字段**必须**前缀 `LN` 后跟一个名称,这个名称用来定义 Haxe 类型,注意去掉了前缀。由于 haxe 默认 private 成员访问方式,所以,如果存在逻辑节点使用的 `property0` - `property9` 这样的属性,就需要使用 public 声明。
参考 LN_select.py 和 SelectNode.hx 定义的 `SelectNode` 节点如何使用 `property0` 属性来设置执行模式,From Input 模式下,输出的是缓存到 value 变量的数据,这使得它可以“跨事件流”向其它事件传递数据:
逻辑节点初始化脚本的 `init_nodes()` 方法分将所有节点模块加载到 Blender 环境,并添加到逻辑编辑器的菜单中。
注意,Libraries/your_lib/blender.py 这个脚本文件和路径是 Armory 约定的,当检测到扩展库存在这个脚本就会执行 `importlib.import_module('blender')` 加载它,并打印信息到控制台。这个脚本是 Python 语言规范中的一个模块,也相当于一个插件,因为 Blender 集成了解释器,脚本直接可以导入 Blender bpy 模块。在打开 .blend 文件时,Bledner 首先打开的是默认配置文件,然后才真正打开用户文件,Armory 会通过检测 bpy.data.filepath 来避免二次执行。
Armory: Loaded Python library LogicNodes
Logic Node API 参考:
`run(from: Int): Void` 方法会在逻辑节点接收到活动事件流时执行,它的调用者是上游节点的 `runOutput(i)` 函数,参数 `from` 指明了当前节点的寻一个输入端口,是它连接的上游节点,参数 `i` 是上游节点输出端口的索引号,是它连接当前执行中的节点。
`get(from: Int): Dynamic` 方法用来获取数据流的数据,逻辑节点是之间是低耦合的,即节点之间可以可以任意连接,即使将控制流连接到数据流也不会产生错误,当然这并不是正确用法。这个 `get()` 方法是程序逻辑解耦功能的一部分,它供下游节点调用来获取端口的输入数据。节点进入运行状态时,通过 `runOutput(i: Int)` 调用相应端口号所链接的下游节点 `run()` 方法,端口传递的数据由下游节点来调用自身的 `get()` 方法获取,即下游节点调用`input.get(i)`。这个过程就是事件流的流向,通过节点的事件控件流的链接,整个逻辑树就可以按设计的功能运行。
官方文档中,用 impulse socket 表示控制流端口,non-impulse (data) 表示数据流端口。
以下是自定义逻辑节点 SwitchCameraNode 的代码以及目录结构,在大型扩展库中,blender.py 应该是一个入口脚本,各种节点的定义应该放在独立脚本文件中,Python 以单个文件为模块,并且可以设置初始化模块 `__init__.py` 文件,当然 Python 模块也不一定必需是脚本文件,也可以是 C/C++ 实现扩展模块。
考虑到逻辑节点的连接具有高度灵活性,防止用户直接因为使用字符串指定相机而不是指定对象,代码就需要考虑两种情况,当设置的参数不为字符串时就使用 `cast(camera, CameraObject)` 进行安全转型。
因为自定义逻辑节点使用 armory.logicnode 包空间,可以直接引用 LogicNode 等类型。
Python 脚本实现的是逻辑节点设计阶段的功能,Armory 引擎编译时,会生成 `LogicTree` 根据逻辑节点树的设计的功能状态,逻辑树代码生成器 arm\make_logic.py 会生成相应的 `LogicTree` 子类。以下是一个自动生成的逻辑节点树:使用两个 `Keyboard` 节点触发两个`SwitchCameraNode` 实现相机镜头的切换功能。Haxe 语言的标注 `@:access` 意思是使被标注的类具有访问私有成员的权力,注意使用全路径。另一个 `@:keep` 标注意思是避免 DCE (Dead Code Elimination) 功能清理无用代码。
代码生成器并不是做编译的工作,它只是按照逻辑节点的固有功能进行一个转化操作,比如,生成的类型命名为 `SwitchCameraNode`,这是因为在 Python 代码中,将 `LNSwitchCameraNode` 这种约定的字符串指定给 bl_idname 属性,代码生成器将它的前缀 LN 去掉,再写入生成的 Haxe 脚本文件。