Iron & Logic Nodes 邏輯節(jié)點(diǎn)可視化編程

## ?? 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 提供了一個(gè)高級(jí)抽象的節(jié)點(diǎn)層次結(jié)構(gòu),基于 iron.object.Object:
- **traits** 集合包含了 Blender 對(duì)象中 Armory Traits 列表中添加的所有 iron traits 類型;
- **parent** 引用含父節(jié)點(diǎn);
- **children** 集合包含了所有子節(jié)點(diǎn);
- **animation** 引用節(jié)點(diǎn)的動(dòng)畫對(duì)象;
- **constraints** 集合包含所有約束關(guān)系;
- **lods** 集合包含包含分級(jí)細(xì)節(jié)圖;
iron.Trait 類型的兩個(gè)屬性:
- **name**: String 指定 Trait 的名稱。
- **object**: 當(dāng)前 trait 所屬著的對(duì)象,trait's owner,附著在對(duì)象 Armory Traits 列表。
Armory 框架中的 CanvasScript 對(duì)象擴(kuò)展了 iron.Trait,包含屬性:
- **cnvName** 即創(chuàng)建 Canvas UI 時(shí)指定的名稱,如 MyCanvas。
- **canvas** 屬性為 `TCanvas` 類型,對(duì)應(yīng) MyCanvas.json 中的基本畫布數(shù)據(jù),如寬高、位置。
- **cui** 畫布對(duì)象對(duì)應(yīng)的 `Zui` 框架實(shí)例。
iron.Scene 類型的幾個(gè)屬性:
- **active**: Scene 靜態(tài)變量,引用當(dāng)前活動(dòng)場(chǎng)景。
- **global**: Object 靜態(tài)變量,引用全局對(duì)象。
- **root**: Object 引用場(chǎng)景的根節(jié)點(diǎn)。
- **sceneParent**: Object 引用父場(chǎng)景。
它們構(gòu)建出來(lái)的節(jié)點(diǎn)樹的基本結(jié)構(gòu)如下:
Armory 引擎在 armory.trait 空間下定義了大量擴(kuò)展類型,當(dāng)然,其中也包括,CanvasScript,WasmScript,DebugConsole 等等核心擴(kuò)展類型。
當(dāng)設(shè)置對(duì)象的屬性面板,并且將一個(gè) trait 附著在對(duì)象 Armory Traits 列表,那么這個(gè) trait 就有了相應(yīng)的歸屬者,而這個(gè)對(duì)象就稱為 trait's owner,通過(guò) trait.object 屬性引用,也就對(duì)應(yīng)邏輯節(jié)點(diǎn)中的**自引用節(jié)點(diǎn)** `Self Object`。
Blender 邏輯節(jié)點(diǎn)編輯器中所有節(jié)點(diǎn)功能都由 Armory Blender 插件代碼實(shí)現(xiàn),在游戲執(zhí)行時(shí)則使用Armory 框架的代碼,即對(duì)應(yīng) `arm.logicnode` 和 `armory.logicnode` 兩套代碼,前者屬于 Python 腳本,后者是 Haxe 腳本。
兩套代碼在類型命名上,基本可以保持一致的對(duì)應(yīng)關(guān)系,但也有例外,比如 Scene 邏輯節(jié)點(diǎn)分類下的: Add Object to Collection 相應(yīng)的 Haxe 類型是 AddObjectToGroupNode,并且標(biāo)明的分類也可能不一致。但是,類型名字按代碼約定,Python 腳本文件名和 bl_idname 字段內(nèi)容帶有 `LN_` 前綴,Haxe 代碼定義的類型則去掉前綴。
Armory 構(gòu)架、Iron Trait 事件回調(diào),以及相關(guān)屬性、程序與邏輯節(jié)點(diǎn)的對(duì)照關(guān)系摘要如下,為了制表方便使用,某些名稱可能會(huì)采用簡(jiǎn)略表達(dá),比如 OnAppStateNode 表示 OnApplicationStateNode:
大多數(shù)邏輯節(jié)點(diǎn)的作用都是數(shù)據(jù)處理,除了幾何事件處理邏輯節(jié)點(diǎn),還有就是 Logic Nodes 分類下有多個(gè)節(jié)點(diǎn)類型用來(lái)處理控制流,這些屬于是邏輯編程的核心節(jié)點(diǎn):
1. `BranchNode` 邏輯節(jié)點(diǎn)有一個(gè)布爾值輸入,按成立條件與否輸出 True 和 False 兩個(gè)控制流分支。
1. `SwitchNode` Value 輸入值和多個(gè) Case 進(jìn)行比較,等值時(shí)輸出對(duì)應(yīng)的 Case 或者 Default 控制流。
1. `MergeNode` 和前面的節(jié)點(diǎn)相反,用于合并控制流,有兩種執(zhí)行模式:
? ? - Once Per Input:簡(jiǎn)單轉(zhuǎn)發(fā),多了一個(gè)索引號(hào)輸出。如果一幀內(nèi)多個(gè)輸入控制流激活,激活相應(yīng)輸出。
? ? - Once Per Frame:如果一幀內(nèi)多個(gè)輸入控制流激活,只激活一次輸出控制流。
1. `InvertOutputNode` - 反轉(zhuǎn)控制流,如果輸入的狀態(tài)處于激活狀態(tài),那么輸出非激活狀態(tài);
2. `AlternateNode` - 變換控制流,可以有多個(gè)控制流輸出,每次變換地執(zhí)行其中一個(gè),按順序循環(huán)執(zhí)行;
3. `SequenceNode` - 序列控制流,按順序執(zhí)行輸出的控件流;
4. `LoopNode` - 循環(huán)控制流,F(xiàn)rom 與 To 指定循環(huán)次數(shù),執(zhí)行 Loop 端口,結(jié)束時(shí)再執(zhí)行 Done 端口;
`LoopBreakNode` 節(jié)點(diǎn)用來(lái)打斷 `LoopNode` 這樣的循環(huán)控制流,但是,和代碼中直接使用 break 關(guān)鍵字不同,邏輯樹中有它自身的機(jī)制打斷循環(huán)控制流,需要設(shè)置 `tree.loopBreak = true;`。`LoopContinueNode` 節(jié)點(diǎn)也類似,需要設(shè)置 `tree.loopContinue = true;`。
節(jié)點(diǎn)的使用是有條件,有些節(jié)點(diǎn)不能搭配在一起使用,因?yàn)闆]有作用。例如,Draw 分類下的所有節(jié)點(diǎn)都需要和 `OnRender2DNode` 配合使用,只有打開一個(gè) Canvas 2D 繪畫上下文對(duì)象才可以進(jìn)行繪畫。并且場(chǎng)景中需要在 Armory Scene Traits 或者 Armory Traits 列表中添加 Canvas UI 擴(kuò)展,這樣才會(huì)觸發(fā) On Render2D 節(jié)點(diǎn)事件。
類似地,如果在一個(gè) Draw 節(jié)點(diǎn)引用了其它數(shù)據(jù),并且數(shù)據(jù)來(lái)源自其它的事件流,比如 `Keyboard` 事件流中執(zhí)行到 `MergeNode`,其輸出的 Active Input Index 數(shù)據(jù)就不能直接輸入到 Draw 分類的節(jié)點(diǎn)使用??梢允褂米兞炕蚨紝?duì)象屬性集中保存起來(lái),再通過(guò)變量節(jié)點(diǎn),或者獲取屬性值輸出到 Draw 節(jié)點(diǎn)中使用。
使用 `SelectNode` 節(jié)點(diǎn)可以實(shí)現(xiàn)這樣的邏輯,它不屬于控制流處理節(jié)點(diǎn),是數(shù)據(jù)選擇節(jié)點(diǎn),From Input 執(zhí)行模式下,其節(jié)點(diǎn)當(dāng)前輸出的 Input 值會(huì)保持直到改變輸出值,所以可以連接到其它事件流的節(jié)點(diǎn)上。
`SelectNode` 節(jié)點(diǎn)不屬于控制流處理節(jié)點(diǎn),它是數(shù)據(jù)選擇節(jié)點(diǎn),有兩種執(zhí)行模式,并且當(dāng)前輸出的 Input 值會(huì)保持直到改變輸出值:
1. From Index 直接指定要輸出的 Value 索引號(hào);
2. From Input 直接通過(guò)控制流選擇輸出,Input 和 Value 一一對(duì)應(yīng)成組;
`CaseIndexNode` 節(jié)點(diǎn)有一個(gè) Compare 和多個(gè) Value 輸入進(jìn)行比較,如果比較到相等值則輸出索引號(hào),否則輸出 null:
和時(shí)間關(guān)聯(lián)的邏輯節(jié)點(diǎn):
1. `OnTimerNode` 事件節(jié)點(diǎn),可以設(shè)置 duration 和 repeat,使用 Time.delta 作為觸發(fā)因素;
2. `TimerNode` 多功能定時(shí)器節(jié)點(diǎn),增加事件流輸入:Start、Pause、Stop 和輸出控件流:Out、Done;
3. `SleepNode` 多功能 Tween 計(jì)時(shí)器,到時(shí)間就觸發(fā)輸出控件流;
4. `PulseNode` 邏輯節(jié)點(diǎn),基于 Time.time() 和 tree.notifyOnUpdate(update) 的循環(huán)定時(shí)器,
? ? 提供 Start 和 Stop 兩個(gè)控件流輸入,分別用于啟用、停止定時(shí)器;
有些邏輯是無(wú)法通過(guò)節(jié)點(diǎn)直接表達(dá)的,需要迂回實(shí)現(xiàn)。比如,On Render2D 事件流中,希望在空格鍵按下的時(shí)候才繪圖指定內(nèi)容,這就不直接將兩種事件鏈接在一起,而需要將鍵盤事件保存到一個(gè)狀態(tài)變量中。然后,在繪畫邏輯中檢查狀態(tài)變量并執(zhí)行相應(yīng)的任務(wù)。
變量或字符串節(jié)點(diǎn)保存數(shù)據(jù),與使用對(duì)象的屬性集保存數(shù)據(jù)是兩種不同的方法,Variable vs. Properties。使用變量或字符串節(jié)點(diǎn),數(shù)據(jù)直接保存在節(jié)點(diǎn)上。比如,`IntegerNode` 定義一個(gè) value 屬性用來(lái)保存數(shù)據(jù),`StringNode` 也采用相同的方法。而對(duì)象屬性集合的方式,則是在 owner 對(duì)象 properties 集合中保存,通過(guò)它可以在多個(gè)邏輯樹之間共享狀態(tài)數(shù)據(jù)。
邏輯節(jié)點(diǎn)中與屬性數(shù)據(jù)讀寫有關(guān)的節(jié)點(diǎn):
1. `GetObjectProperty` 和 `SetObjectProperty` 讀寫對(duì)象的 Properties 屬性集合;
2. `GlobalObjectNode` 節(jié)點(diǎn)在 iron.Scene.global.properties 屬性集合讀寫數(shù)據(jù):
3. 各種 Variable Node 直接在邏輯節(jié)點(diǎn)對(duì)象上的 value 屬性保存數(shù)據(jù),例如 `FloatNode`;
4. `GetHaxePropertyNode` 和 `SetHaxePropertyNode` 直接讀寫 Haxe 對(duì)象的屬性:
邏輯節(jié)點(diǎn)樹變量**Tree Variables**是最新添加的功能,它的目標(biāo)很簡(jiǎn)單,但是實(shí)現(xiàn)代碼感覺很混亂。使用變量節(jié)點(diǎn)時(shí),如果需要在多個(gè)位置使用同一個(gè)變量,一般就是拉一條條的線進(jìn)行連接,在大量節(jié)點(diǎn)的場(chǎng)合下顯得混亂。Tree Variables 就是解決這個(gè)問(wèn)題的,在一個(gè)節(jié)點(diǎn)樹內(nèi),可以用多個(gè)變量節(jié)點(diǎn)引用同一個(gè)變量,這就是節(jié)點(diǎn)樹變量的用途。通過(guò)柵欄面板 Armory - Tree Variables 操作,可以將現(xiàn)有變量節(jié)點(diǎn)提升為節(jié)點(diǎn)樹變量,也可以解除變成一般變量節(jié)點(diǎn),或者將變量引用賦給其它同類型變量節(jié)點(diǎn)。實(shí)現(xiàn)代碼參考 `ArmLogicVariableNodeMixin(ArmLogicTreeNode)`。節(jié)點(diǎn)樹變量的側(cè)欄面板界面實(shí)現(xiàn): `ARM_PT_Variables(bpy.types.Panel)`。
New logic tree variable system https://github.com/armory3d/armory/pull/2439
邏輯節(jié)點(diǎn)有很多需要設(shè)置 Object 屬性,默認(rèn)留空表示使用 **owner** 對(duì)象,在邏輯樹執(zhí)行時(shí)會(huì)自動(dòng)配置好。比如 `SetObjectProperty` 節(jié)點(diǎn)沒有設(shè)置 Object 屬性,使用默認(rèn)值,那么在生成的邏輯樹類型定義中就會(huì)包含類似以下的代碼。`ObjectNode` 相當(dāng)是一個(gè)代理,它的 `get()` 方法檢查到使用了默認(rèn)值,就會(huì)返回 tree.object,也即是 owner 對(duì)象:
為了在邏輯節(jié)點(diǎn)與 Haxe 腳本之間交換數(shù)據(jù),使用 `GlobalObjectNode` 節(jié)點(diǎn)的屬性,對(duì)應(yīng)腳本對(duì)象:
? ? iron.Scene.global.properties
Set Haxe Property 和 Get Haxe Property 也用于屬性的讀寫,但是使用 Reflection API,輸入對(duì)象是一個(gè) Dynamic 類型。這兩個(gè)節(jié)點(diǎn)就是直接對(duì) Haxe 對(duì)象屬性的讀寫,而不是 Properties 集合中的屬性。可以用它們來(lái)設(shè)置 Iron Object 的旋轉(zhuǎn),先用 `GetHaxePropertyNode` 獲取transform 屬性,再使用 `SetHaxePropertyNode` 設(shè)置轉(zhuǎn)換矩陣的 rot 屬性完成旋轉(zhuǎn)操作:
與代碼或表達(dá)式編寫有關(guān)的節(jié)點(diǎn):
1. `ScriptNode` 和 `ExpressionNode` 可以用來(lái)執(zhí)行 Haxe 腳本,但需要 Haxe Script 類庫(kù)支持;
2. `MathExpressionNode` 輸入 2 ~ 10 個(gè)值和一個(gè)算式,內(nèi)置 `Formula` 解釋器執(zhí)行算式;
`MathExpressionNode` 不依賴外部模塊,內(nèi)置數(shù)學(xué)算式語(yǔ)法分析工具,但只支持常用的計(jì)算相關(guān)的功能,+ - * / ^ % 以及 abs ln sin cos tan cot asin acos atan atan2 log max min 常用函數(shù),只使用浮點(diǎn)數(shù)據(jù)類型。 其中 % 表示求余運(yùn)算,可以用它截取浮點(diǎn)的小數(shù):1.12345 - 1.12345 % 0.001。但是謹(jǐn)慎使用它,可能因?yàn)檩斎霐?shù)據(jù)錯(cuò)誤導(dǎo)數(shù)據(jù)流致相關(guān)節(jié)點(diǎn)中斷執(zhí)行,以下就是 `Formula` 解析出錯(cuò)一例。
`ScriptNode` 和 `ExpressionNode` 可以用來(lái)執(zhí)行 Haxe 腳本,需要 Haxe Script 類庫(kù)支持,它實(shí)現(xiàn)了 Haxe language 的一個(gè)子集,提供了 `Parser` 和 `Interp`。雖然 Haxe 到處是表達(dá)式,但是這兩個(gè)節(jié)點(diǎn)中不能使用受限的功能,`ExpressionNode` 就不能使用 Std.random(10) 或 trace(this) 等等。只能是純計(jì)算的表達(dá)式,例如,以下這句就可以,沒有外部引用不會(huì)產(chǎn)生異常:
? ? var a = 1; trace(1 + a); a++;
注意,編寫代碼或表達(dá)式時(shí)雙引號(hào)的使用,因?yàn)橐嬷皇呛?jiǎn)單地將內(nèi)容內(nèi)嵌到生成的邏輯節(jié)點(diǎn)樹類定義文件中,如果直接編寫 "99 + 1" 這樣的表達(dá)式,就會(huì)因?yàn)殡p引號(hào)配對(duì)導(dǎo)致語(yǔ)言錯(cuò)誤。
表達(dá)式節(jié)點(diǎn) `ExpressionNode` 可以用來(lái)執(zhí)行一些語(yǔ)句,執(zhí)行結(jié)果通過(guò)節(jié)點(diǎn)的 Result 端口輸出。因?yàn)?Armory 引擎使用 Haxe 腳本代碼,可以通過(guò)修改 armsdk 中的代碼來(lái)改變 Armory 的行為,比如在 ScriptNode 代碼文件中添加調(diào)試代碼到 `run(from: Int)` 方法,看看是否已經(jīng)啟用了hscript 模塊。默認(rèn)狀態(tài)沒有啟用 hscript,總是輸出 null,并且也不會(huì)提示。添加以下代碼就可以在缺失相關(guān)模塊功能支持時(shí),提示用戶安裝和啟用 hscript 模塊:
安裝 hscript 模塊,然后再設(shè)置 Armory Project - Modules - Append Khafile 添加構(gòu)建腳本片段,通過(guò) addLibrary() 引入模塊,并且定義相應(yīng)的符號(hào)定義,啟用 hscript 腳本執(zhí)行功能。設(shè)置不一定會(huì)立即生效,需要 Render - Armory Player - Clear 清理掉緩存文件。
邏輯節(jié)點(diǎn)中有三各和函數(shù)定義、調(diào)用相關(guān)的節(jié)點(diǎn):
1. `CallFunctionNode` 調(diào)用函數(shù)節(jié)點(diǎn),調(diào)用 Trait/Any 端口中指定對(duì)象中定義的 Function。
2. `FunctionNode` 定義可重用的函數(shù)供 [Call Function] 節(jié)點(diǎn)調(diào)用,需要與函數(shù)輸出節(jié)點(diǎn)配合使用。
3. `FunctionOutputNode` 給指定的 `FunctionNode` 節(jié)點(diǎn)設(shè)置返回值。
在生成的 `LogicTree` 類定義中,有兩個(gè)專用的 Map 類型的屬性用來(lái)管理函數(shù)節(jié)點(diǎn)的連接配置等等信息。`FunctionNode` 節(jié)點(diǎn)定義的函數(shù),比如 MyFun 就會(huì)對(duì)應(yīng)生成代碼文件中的一個(gè)同名的函數(shù):
`CallFunctionNode` 調(diào)用函數(shù)節(jié)點(diǎn)通過(guò) [Reflect] API 去調(diào)用 Trait/Any 端口中指定的對(duì)象中定義的函數(shù)。調(diào)用函數(shù)節(jié)點(diǎn)首先檢查對(duì)象是否存在 Function 輸入端口指定名稱的函數(shù),如果存在就發(fā)起調(diào)用,并且將輸入的參數(shù)傳遞給待調(diào)用的函數(shù)。函數(shù)返回值暫存于 result 變量中,等等待下游節(jié)點(diǎn)來(lái)獲取取。如果`FunctionNode` 不與 `FunctionOutputNode` 節(jié)點(diǎn)相連,那么調(diào)用函數(shù)時(shí),函數(shù)輸出值就不能通過(guò)儲(chǔ)到 functionOutputNodes 這個(gè)映射變量?jī)?nèi)的 FunctionOutputNode.result 獲取。由于邏輯節(jié)點(diǎn)的設(shè)計(jì)需要,當(dāng)然不能直接在 `FunctionNode` 節(jié)點(diǎn)中實(shí)現(xiàn)數(shù)據(jù)保存的邏輯。因?yàn)樾枰趦蓚€(gè)節(jié)點(diǎn)之間提供可以連接的機(jī)會(huì),才使用得函數(shù)節(jié)點(diǎn)有被添加功能定義的可能。
以下是一個(gè)什么也不干的 MyFunc,調(diào)用時(shí)傳入什么參數(shù),就返回什么值:


另外,因?yàn)檎{(diào)用函數(shù)節(jié)點(diǎn)是直接在 Trait/Any 端口中指定的對(duì)象中查找相關(guān)的函數(shù),所以這個(gè)端口就應(yīng)該輸入一個(gè) `Self Trait`。通常是 LogicTree,因?yàn)檫壿嫻?jié)點(diǎn)的方法會(huì)在它的生成代碼中定義。