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)
實現(xiàn)自定義的邏輯節(jié)點:將邏輯節(jié)點實現(xiàn)的 Python 腳本文件放到 Armory 源代碼中,或者項目中的Libraries 目錄下。本文內(nèi)容涉及 Blender Python API,但又不完全是 Blender 插件開發(fā),因為主要內(nèi)容是 Armory 邏輯節(jié)點的框架。

Blender 節(jié)點編程接口參考 SocketDeclarationBuilder、NodeDeclarationBuilder,以及節(jié)點編輯器源代碼空間 space_node:
Armory 邏輯節(jié)點及編輯器 API 定義在以下各個文件:
閱讀 Logic Nodes 代碼時應(yīng)該注意區(qū)分,設(shè)計階段的邏輯節(jié)點實現(xiàn)代碼,以及邏輯節(jié)點運行階段的代碼。前者基于 Blender Python API 接口開發(fā),后者用 Haxe 語言開發(fā),Armory 引擎運行時執(zhí)行以復(fù)現(xiàn)邏輯節(jié)點的可視化設(shè)計的邏輯狀態(tài)。
以下是 arm_nodes.py 文件定義的邏輯節(jié)點類型,屬于設(shè)計階段的功能,調(diào)用 Blender API 實現(xiàn)邏輯節(jié)點編輯器的節(jié)點功能,頂級基類是 Blender `Node`,邏輯節(jié)點功能父類是 `ArmLogicTreeNode`:
以下是 nodes_logic.py 文件定義的邏輯節(jié)點編輯器功能,設(shè)計階段的功能,調(diào)用 Blender API 實現(xiàn)邏輯節(jié)點編輯器的節(jié)點樹,頂級基類是 Blender `NodeTree`,邏輯節(jié)點樹功能父類是 `ArmLogicTree`:
面包屑導(dǎo)航(Breadcrumb)是邏輯編輯器左上角顯示當前邏輯樹與子樹路徑關(guān)系的提示性工具,添加節(jié)點組并編輯它就會進入下一子級節(jié)點樹,UI 組件的當前上下文對象可獲取路徑:context.space_data.path。
節(jié)點輸入、輸出端口的 UI 列表對應(yīng) `ARM_UL_interface_sockets`,這個列表維護的是 Blender 界面繪圖之用的 UI 數(shù)據(jù),即這個列表有什么數(shù)據(jù),邏輯編輯器 UI 界面上就會繪畫相應(yīng)的視覺元素,端口的圓點、節(jié)點之間的連線,這些都是視覺元素。節(jié)點交互行為基本由 `ArmLogicTreeNode` 類型實現(xiàn)。
節(jié)點樹中有變量節(jié)點,它可以像代碼中的變量一樣存儲數(shù)據(jù),只不過這些數(shù)據(jù)是以節(jié)點這種數(shù)據(jù)結(jié)構(gòu)保存,`ArmLogicVariableNodeMixin` 就是實現(xiàn)這個機制的類型。變量節(jié)點還可以提升為 tree variable 節(jié)點樹變量,即多個節(jié)點引用同一個數(shù)據(jù),這樣就可以在設(shè)計邏輯節(jié)點時,避免總是從同一個節(jié)點引線連接到其它位置非常遠的節(jié)點上,從而可以得到更整潔的節(jié)點布局。
有些邏輯節(jié)點可以有多個輸入、輸出端口,比如在 LN_select.py 文件中定義的 `SelectNode` 節(jié)點,它有兩種執(zhí)行模式,可以擁有多個控制流、數(shù)據(jù)流輸入端口:
1. From Index 有一個 Index 輸入,和多個 Value 輸入,前者指定要輸出的 Value 端口索引號;
2. From Input 模式下 Input 控件流和 Value 數(shù)據(jù)流輸入成對出現(xiàn),控制流激活時輸出相應(yīng) Value;
邏輯節(jié)點設(shè)計了版本機制,實現(xiàn)升級替換功能,`ARM_OT_ReplaceNodesOperator` 提供手動替換功能。
Blender 節(jié)點編輯器是一個基礎(chǔ)功能接口,Armory 基于些設(shè)計了邏輯節(jié)點樹編輯器,ArmLogicTree 代表 Blender 中的邏輯節(jié)點編輯器的用戶界面,配合各種 UI 組件,構(gòu)建一個完整的操作界面。
Add Node 菜單提供的功能由 `ARM_MT_NodeAddOverride` 實現(xiàn),它覆蓋 Blender 默認的節(jié)點菜單,用 arm_nodes.category_items 登記的邏輯節(jié)點定義填充菜單,并按分區(qū)添加 separator 分隔線。點擊菜單項就觸發(fā) ARM_OT_AddNodeOverride `invoke()` -> `bpy.ops.node.add_node()`,Blender 界面中的菜單項和按鈕是同一種對象類型 `Operator`。
所有登記的邏輯節(jié)點分類,會在菜單中產(chǎn)生相應(yīng)的分類菜單類條目,名稱前綴 `ARM_MT_`。
如側(cè)欄面板的在線文檔功能,Armory - Armory Logic Node 對應(yīng)? `ARM_PT_LogicNodePanel`,其中提供三個用于打開在線文檔的按鈕,對應(yīng)類型為 `Operator`:
1. ArmOpenNodeHaxeSource 對應(yīng) **Open Node Haxe Source** 按鈕;
2. ArmOpenNodePythonSource 對應(yīng) **Open Node Python Source** 按鈕;
3. ArmOpenNodeWikiEntry 對應(yīng) **Open Node Wiki Entry** 按鈕;
它們的功能就是調(diào)用 webbrowser.open() 方法打開對應(yīng)的在線文檔資源。
側(cè)欄面板 Armory Logic Node - Node Development 對應(yīng) `ARM_PT_NodeDevelopment`,提供當前選中節(jié)點的信息:
1. 節(jié)點所屬分類 **Category** (arm_nodes.eval_node_category(node))
2. 節(jié)點所屬分區(qū) **Section** (node.arm_section)
3. 節(jié)點的版本號 **Specific** Version (node.arm_version)
4. 節(jié)點類定義版本 **Class Version** (node.__class__.arm_version)
5. 節(jié)點是否處于棄用狀態(tài) **Is Deprecated** (node.arm_is_obsolete)
6. 是否是變量節(jié)點 **Is Variable Node**? (isinstance(node, arm_nodes.ArmLogicVariableNodeMixin))
7. 邏輯 ID 編號 **Logic ID** (node.arm_logic_id)
邏輯節(jié)點編輯器中所有節(jié)點目錄都是按分類、分區(qū)二級目錄進行管理,在整個邏輯目錄上劃分了 7 個區(qū)和多個分類,比如 Event 分類和 Input 分類就歸屬于 Basic 分區(qū)。再有,Input 分類下的所有節(jié)點劃分為 6 個分區(qū),但它們?nèi)紝傩杂?Input 分類菜單下顯示,每個分區(qū)對應(yīng)菜單上的分隔線。
Armory 默認的邏輯節(jié)點分類、分區(qū)與圖標設(shè)置,圖標參考 Blender?bpy.types.Node(bpy_struct):
* https://wiki.blender.org/wiki/Source/Interface/Icons
Haxe 邏輯節(jié)點數(shù)據(jù)文件中,或者邏輯節(jié)點連接端口 API 文件中,都可以找到節(jié)點端口的類型定義。在使用邏輯節(jié)點編輯器時,只需要知道事件流與數(shù)據(jù)流的區(qū)分即夠用了,事件流對應(yīng) **ArmNodeSocketAction**,數(shù)據(jù)流,由于數(shù)據(jù)類型不同,有不同的端口,對應(yīng)不同顏色的圓點。
在編寫節(jié)點類型實現(xiàn)代碼,調(diào)用 `add_output()` 和 `add_input()` 中添加端口時,socket_type 參數(shù)需要使用字符串指定端口類型,盡管這種操作不太恰當,但是 Armory 就是這樣做的。
Bledner 本身支持節(jié)點編輯器,提供端口的基類 `bpy.types.NodeSocket`,Armory 在此基礎(chǔ)上派生出 `ArmCustomSocket`,作為所有邏輯節(jié)點端口的基類型,定義在文件:**arm_sockets.py**。
在大型類庫中,通常用單個腳本文件定義一個類型,多個文件構(gòu)成一個包,不同分類的節(jié)點類型保存在子目錄。每個邏輯節(jié)點腳本名稱前綴 `LN_` 并且類型的 [`bl_idname`] 字段**必須**前綴 `LN` 后跟一個名稱,這個名稱用來定義 Haxe 類型,注意去掉了前綴。由于 haxe 默認 private 成員訪問方式,所以,如果存在邏輯節(jié)點使用的 `property0` - `property9` 這樣的屬性,就需要使用 public 聲明。
參考 LN_select.py 和 SelectNode.hx 定義的 `SelectNode` 節(jié)點如何使用 `property0` 屬性來設(shè)置執(zhí)行模式,F(xiàn)rom Input 模式下,輸出的是緩存到 value 變量的數(shù)據(jù),這使得它可以“跨事件流”向其它事件傳遞數(shù)據(jù):
邏輯節(jié)點初始化腳本的 `init_nodes()` 方法分將所有節(jié)點模塊加載到 Blender 環(huán)境,并添加到邏輯編輯器的菜單中。
注意,Libraries/your_lib/blender.py 這個腳本文件和路徑是 Armory 約定的,當檢測到擴展庫存在這個腳本就會執(zhí)行 `importlib.import_module('blender')` 加載它,并打印信息到控制臺。這個腳本是 Python 語言規(guī)范中的一個模塊,也相當于一個插件,因為 Blender 集成了解釋器,腳本直接可以導(dǎo)入 Blender bpy 模塊。在打開 .blend 文件時,Bledner 首先打開的是默認配置文件,然后才真正打開用戶文件,Armory 會通過檢測 bpy.data.filepath 來避免二次執(zhí)行。
? ? Armory: Loaded Python library LogicNodes
Logic Node API 參考:
`run(from: Int): Void` 方法會在邏輯節(jié)點接收到活動事件流時執(zhí)行,它的調(diào)用者是上游節(jié)點的 `runOutput(i)` 函數(shù),參數(shù) `from` 指明了當前節(jié)點的尋一個輸入端口,是它連接的上游節(jié)點,參數(shù) `i` 是上游節(jié)點輸出端口的索引號,是它連接當前執(zhí)行中的節(jié)點。
`get(from: Int): Dynamic` 方法用來獲取數(shù)據(jù)流的數(shù)據(jù),邏輯節(jié)點是之間是低耦合的,即節(jié)點之間可以可以任意連接,即使將控制流連接到數(shù)據(jù)流也不會產(chǎn)生錯誤,當然這并不是正確用法。這個 `get()` 方法是程序邏輯解耦功能的一部分,它供下游節(jié)點調(diào)用來獲取端口的輸入數(shù)據(jù)。節(jié)點進入運行狀態(tài)時,通過 `runOutput(i: Int)` 調(diào)用相應(yīng)端口號所鏈接的下游節(jié)點 `run()` 方法,端口傳遞的數(shù)據(jù)由下游節(jié)點來調(diào)用自身的 `get()` 方法獲取,即下游節(jié)點調(diào)用`input.get(i)`。這個過程就是事件流的流向,通過節(jié)點的事件控件流的鏈接,整個邏輯樹就可以按設(shè)計的功能運行。
官方文檔中,用 impulse socket 表示控制流端口,non-impulse (data) 表示數(shù)據(jù)流端口。
以下是自定義邏輯節(jié)點 SwitchCameraNode 的代碼以及目錄結(jié)構(gòu),在大型擴展庫中,blender.py 應(yīng)該是一個入口腳本,各種節(jié)點的定義應(yīng)該放在獨立腳本文件中,Python 以單個文件為模塊,并且可以設(shè)置初始化模塊 `__init__.py` 文件,當然 Python 模塊也不一定必需是腳本文件,也可以是 C/C++ 實現(xiàn)擴展模塊。
考慮到邏輯節(jié)點的連接具有高度靈活性,防止用戶直接因為使用字符串指定相機而不是指定對象,代碼就需要考慮兩種情況,當設(shè)置的參數(shù)不為字符串時就使用 `cast(camera, CameraObject)` 進行安全轉(zhuǎn)型。
因為自定義邏輯節(jié)點使用 armory.logicnode 包空間,可以直接引用 LogicNode 等類型。
Python 腳本實現(xiàn)的是邏輯節(jié)點設(shè)計階段的功能,Armory 引擎編譯時,會生成 `LogicTree` 根據(jù)邏輯節(jié)點樹的設(shè)計的功能狀態(tài),邏輯樹代碼生成器 arm\make_logic.py 會生成相應(yīng)的 `LogicTree` 子類。以下是一個自動生成的邏輯節(jié)點樹:使用兩個 `Keyboard` 節(jié)點觸發(fā)兩個`SwitchCameraNode` 實現(xiàn)相機鏡頭的切換功能。Haxe 語言的標注 `@:access` 意思是使被標注的類具有訪問私有成員的權(quán)力,注意使用全路徑。另一個 `@:keep` 標注意思是避免 DCE (Dead Code Elimination) 功能清理無用代碼。
代碼生成器并不是做編譯的工作,它只是按照邏輯節(jié)點的固有功能進行一個轉(zhuǎn)化操作,比如,生成的類型命名為 `SwitchCameraNode`,這是因為在 Python 代碼中,將 `LNSwitchCameraNode` 這種約定的字符串指定給 bl_idname 屬性,代碼生成器將它的前綴 LN 去掉,再寫入生成的 Haxe 腳本文件。