UE4运行时交互工具框架
在本文中,我將介紹很多內容。我提前為太長的篇幅道歉。但是,這篇文章的主題本質上是“如何使用虛幻引擎構建 3D 工具”,這是一個很大的話題。在本文結束時,我將介紹交互式工具框架,這是 Unreal Engine 4.26 中的一個系統,可以相對簡單地構建多種類型的交互式 3D 工具。我將專注于“在運行時”使用這個框架,即在構建的游戲中。但是,我們使用這個完全相同的框架在虛幻編輯器中構建 3D 建模工具套件。而且,其中許多工具都可以在運行時直接使用!在你的游戲中雕刻!它太酷了。
下面有一個 ToolsFramworkDemo 應用程序的短視頻和一些屏幕截圖 - 這是一個構建的可執行文件,不在 UE 編輯器中運行(盡管也可以)。該演示允許你創建一組網格,可以通過單擊進行選擇(通過 shift-click/ctrl-click 支持多選),并為活動選擇顯示 3D 變換 Gizmo。左側的一小組 UI 按鈕用于執行各種操作。Add Bunny按鈕將導入和附加一個兔子網格,Undo和Redo會按照你的預期進行。World按鈕在World 和 Local 坐標系之間切換 Gizmo:
其余按鈕啟動各種建模工具,它們與 UE 4.26 編輯器的建模模式中使用的工具實現完全相同。PolyExtrude是繪制多邊形工具,你可以在其中在 3D 工作平面上繪制一個封閉的多邊形(可以通過 ctrl 單擊重新定位),然后以交互方式設置擠出高度。PolyRevolve允許你在 3D 工作平面上繪制開放或封閉路徑 - 雙擊或關閉路徑到終點 - 然后編輯生成的旋轉曲面。Edit Polygons是編輯器中的 PolyEdit 工具,在這里您可以選擇面/邊/頂點并使用 3D gizmo 移動它們 — 請注意,各種 PolyEdit 子操作,如 Extrude 和 Inset,不會在 UI 中公開,但可以工作。
所有這些幾何圖形是在演示中創建的。選擇窗口并使用 GIZMO 旋轉。
Plane Cut使用工作平面切割網格,Boolean執行網格布爾運算(需要兩個選定對象)。Remesh重新對網格進行三角剖分(不幸的是,我無法輕松顯示網格線框)。Vertex Sculpt允許您對頂點位置進行基本的 3D 雕刻,而DynaSculpt 進行自適應拓撲雕刻,這就是我在屏幕截圖中展示的應用于 Bunny 的內容。最后,Accept和Cancel按鈕應用或放棄當前的工具結果(這只是一個預覽) - 我將在下面進一步解釋。
兔子長出一些新的部分
這不是一個功能齊全的 3D 建模工具,它只是一個基本的演示。一方面,沒有任何形式的保存或導出,不過,添加一個快速的 OBJ 導出并不難!不存在對分配材質的支持,您看到的材質是硬編碼的或由工具自動使用,例如動態網格雕刻中的平面著色。同樣,一個積極的 C++ 開發人員可以相對容易地添加類似的東西。2D 用戶界面是一個非常基本的 UMG 用戶界面。我假設這是一次性的,你將構建自己的 UI。再說一次,如果你想做一個非常簡單的特定領域的建模工具,比如一個用于清理醫學掃描的 3D 雕刻工具,你也許可以在稍加修改后擺脫這個 UI。
1、獲取并運行示例項目
在開始之前,本教程適用于 UE 4.26,你可以從Epic Games Launcher安裝它。本教程的項目位于 Github 上的 UnrealRuntimeToolsFrameworkDemo存儲庫(MIT 許可)。目前,該項目只能在 Windows 上運行,因為它依賴于MeshModelingToolset引擎插件,該插件目前僅適用于 Windows。讓該插件在 OSX/Linux 上工作主要是選擇性刪除的問題,但它需要引擎源代碼構建,這超出了本教程的范圍。
進入頂級文件夾后,右鍵單擊Windows 資源管理器中的ToolsFrameworkDemo.uproject ,然后從上下文菜單中選擇Generate Visual Studio project files 。這將生成ToolsFrameworkDemo.sln,你可以使用它來打開 Visual Studio。也可以直接在編輯器中打開 .uproject — 它會要求編譯,但可能需要參考 C++ 代碼才能真正了解該項目中發生的情況。
構建解決方案并啟動(按 F5),編輯器應打開到示例地圖中??梢允褂弥鞴ぞ邫谥械拇蟛シ虐粹o在 PIE 中測試該項目,或者單擊啟動按鈕來構建一個熟化的可執行文件。這將需要幾分鐘,之后構建的游戲將在單獨的窗口中彈出。如果它以這種方式啟動(我認為這是默認設置),可以點擊 Escape 退出全屏。在全屏模式下,你必須按Alt+F4退出,因為沒有菜單/UI。
2、概述
這篇文章太長了,需要一個目錄。以下是我要介紹的內容:
首先,我將解釋交互式工具框架(ITF) 作為一個概念的一些背景。它來自哪里,它試圖解決什么問題。隨意跳過這個 author-on-his-soapbox 部分,因為本文的其余部分不以任何方式依賴它。
接下來我將解釋 UE4 交互工具框架的主要部分。我們將從工具、工具構建器和工具管理器開始,并討論工具生命周期、接受/取消模型和基礎工具。輸入處理將在輸入行為系統、通過工具屬性集存儲的工具設置和工具操作中進行介紹。
接下來我將解釋Gizmos系統,用于實現視口內 3D 小部件,重點介紹上面剪輯/圖像中顯示的標準 UTransformGizmo 。
在 ITF 的最高級別,我們有Tools Context 和 ToolContext API,我將詳細介紹 ITF 的客戶端需要實現的 4 個不同的 API - IToolsContextQueriesAPI、IToolsContextTransactionsAPI、IToolsContextRenderAPI 和 IToolsContextAssetAPI。然后我們將介紹一些特定于網格編輯工具的細節,特別是Actor/Component Selections、FPrimitiveComponentTargets和FComponentTargetFactory。
到目前為止,一切都將與 UE4.26 附帶的 ITF 模塊有關。為了在運行時使用 ITF,我們將創建我們自己的運行時工具框架后端,其中包括一個基本的可選網格“場景對象”的 3D 場景、一個非常標準的 3D 應用程序變換 gizmo 系統以及 ToolsContext API 的實現 I上面提到的與這個運行時場景系統兼容的。本節主要解釋了我們必須添加到 ITF 以在運行時使用它的額外位,因此您需要閱讀前面的部分才能真正理解它。
接下來我將介紹一些特定于演示的材料,包括使演示工作所需的ToolsFrameworkDemo 項目設置、RuntimeGeometryUtils 更新,特別是對 USimpleDynamicMeshComponent 的碰撞支持,然后是一些關于在運行時使用建模模式工具的注釋,因為這通常需要一些膠水代碼才能使現有的網格編輯工具在游戲環境中發揮作用。
就是這樣!讓我們開始…
3、交互式工具框架 - 為什么
我不喜歡通過證明它的存在來開始一篇關于某事的文章的想法。但是,我想我需要。我花了很多年 - 基本上是我的整個職業生涯 - 構建 3D 創建/編輯工具。我的第一個系統是ShapeShop(它自 2008 年以來一直沒有更新,但仍然可以工作——這是 Windows 向后兼容性的證明!)。我還構建了 Meshmixer,它成為 Autodesk 產品,下載數百萬次,并被廣泛使用至今。通過Twitter搜索,我不斷驚訝于人們使用 Meshmixer 做的事情,很多數字牙醫!!。我還構建了其他從未出現過的全功能系統,例如我們稱之為手繪世界的 3D 透視草圖界面 ,是我在 Autodesk Research 構建的。之后,我幫助構建了一些醫療 3D 設計工具,例如Archform 牙齒矯正器規劃應用程序和NiaFit 小腿假肢插座設計工具(VR ),遺憾的是我在它有任何流行的希望之前就放棄了。
撇開自我祝賀不談,在過去 15 多年制作這些 3D 工具的過程中,我學到的是,制造一個巨大的混亂是非常容易的。我開始研究后來成為 Meshmixer 的東西,因為 Shapeshop 已經到了無法添加任何東西的地步。然而,Shapeshop 的某些部分形成了一個非常早期的“工具框架”,我將其提取并用作其他各種項目的基礎,甚至還有一些 Meshmixer(最終也變得非常脆弱!)。該代碼仍在我的網站上。當我離開 Autodesk 時,我回到了如何構建工具的這個問題,并創建了frame3Sharp 庫這使得在 C# 游戲引擎中構建運行時 3D 工具變得(相對)容易。這個框架圍繞上面提到的 Archform、NiaFit 和 Cotangent 應用程序發展起來,并一直為它們提供動力。但是,后來我加入了 Epic,并重新開始使用 C++!
所以,這就是 UE4 交互式工具框架的起源故事。使用這個框架,一個小團隊(6 人或更少的人,取決于月份)在 UE4 中構建了建模模式,它有 50 多個“工具”。有些非常簡單,例如使用選項復制事物的工具,有些則非常復雜,例如整個 3D 雕刻工具。但關鍵點是,工具代碼相對干凈且很大程度上獨立 - 幾乎所有工具都是一個獨立的 cpp/h 對。不是通過剪切和粘貼而獨立,而是獨立于這一點,我們盡可能地將“標準”工具功能移動到框架中,否則這些功能將不得不被復制。
3.1 讓我們談談框架
我在解釋交互式工具框架時遇到的一個挑戰是我沒有參考點來比較它。大多數 3D 內容創建工具在其代碼庫中都有一定程度的“工具框架”,但除非你嘗試向 Blender 添加功能,否則可能從未與這些東西進行過交互。所以,我不能試圖通過類比來解釋。并且這些工具并沒有真正努力提供類似的原型框架作為大寫-F 框架。所以很難把握。(PS:如果您認為您知道類似的Framework,請聯系并告訴我!)
但是,在其他類型的應用程序開發中,框架非常常見。例如,如果你想構建一個 Web 應用程序或移動應用程序,你幾乎肯定會使用一個定義明確的框架,如 Angular 或 React 或本月流行的任何東西(實際上有數百個)。這些框架傾向于將“小部件”等低級方面與視圖等高級概念混合在一起。我在這里關注視圖,因為這些框架中的絕大多數都是基于視圖的概念。通常,前提是你擁有數據,并且你希望將這些數據放入視圖中,并帶有一定數量的 UI,允許用戶探索和操作該數據。甚至還有一個標準術語,“模型-視圖-控制器”架構。XCode 界面生成器是我所知道的最好的例子,你實際上是在故事板上用戶將看到的視圖,并通過這些視圖之間的轉換來定義應用程序行為。我經常使用的每個手機應用程序都是這樣工作的。
提高復雜性,我們有像 Microsoft Word 或 Keynote 這樣的應用程序,它們與基于視圖的應用程序完全不同。在這些應用程序中,用戶將大部分時間花在單個視圖中,并且直接操作內容而不是抽象地與數據交互。但大部分操作都是以Commands的形式進行的,例如刪除文本或編輯Properties。例如,在 Word 中,當我不鍵入字母時,我通常要么將鼠標移動到命令按鈕上以便我可以單擊它——一個離散的操作——要么打開對話框并更改屬性。我不做的是花費大量時間使用連續的鼠標輸入(拖放和選擇是明顯的例外)。
現在考慮一個內容創建應用程序,如 Photoshop 或 Blender。同樣,作為用戶,您將大部分時間花在標準化視圖中,并且你直接操作的是內容而不是數據。仍然有大量具有屬性的命令和對話框。但是這些應用程序的許多用戶——尤其是在創意環境中——也花費大量時間非常小心地在按住其中一個按鈕的同時移動鼠標。此外,當他們這樣做時,應用程序通常處于特定模式,其中鼠標移動(通常與修改熱鍵結合使用)以特定模式的方式被捕獲和解釋。該模式允許應用程序在大量方式之間消除歧義,mouse-movement-with-button-held-down動作可以被解釋,本質上是為了將捕獲的鼠標輸入引導到正確的位置。這與命令根本不同,命令通常是無模式的,并且在輸入設備方面也是無狀態的。
除了模式之外,內容創建應用程序的一個標志是我將稱為Gizmos的東西,它們是附加的臨時交互式視覺元素,它們不是內容的一部分,但提供了一種(半無模式)操作內容的方式。例如,可以單擊拖動以調整矩形大小的矩形角上的小框或 V 形將是 Gizmo 的標準示例。這些通常被稱為小部件,但我認為使用這個術語會讓人感到困惑,因為它與按鈕和菜單小部件重疊,所以我將使用 Gizmos。
所以,現在我可以開始暗示交互式工具框架的用途了。在最基本的層面上,它提供了一種系統的方法來實現捕獲和響應用戶輸入的模態狀態,為了簡潔起見,我將其稱為交互工具或工具,以及實現 Gizmos(我將假定它本質上是空間本地化的上下文敏感模式,但我們可以將討論保存在 Twitter 上)。
3.2 為什么需要一個框架?
這是我被問過很多次的問題,主要是那些沒有嘗試構建復雜的基于工具的應用程序的人。簡短的回答是,減少(但遺憾的是沒有消除)你制造邪惡災難的機會。但我也會做一個長的回答。
關于基于工具的應用程序需要了解的重要一點是,一旦你為用戶提供以任何順序使用工具的選項,他們就會這樣做,這將使一切變得更加復雜。在基于視圖的應用程序中,用戶通常是“On Rails”,因為應用程序允許在 Y 之后而不是之前執行 X。當我啟動 Twitter 應用程序時,我不能直接跳轉到所有內容——我必須瀏覽一系列視圖。這允許應用程序的開發人員對應用程序狀態做出大量假設。特別是,盡管視圖可能會操作相同的底層 DataModel(幾乎總是某種形式的數據庫),但我永遠不必擔心區分一個視圖中的點擊與另一個視圖中的點擊。在某種意義上,意見是模式,在特定視圖的上下文中,通常只有命令,沒有工具。
因此,在基于視圖的應用程序中,談論工作流非常容易。創建基于視圖的應用程序的人往往會畫很多類似這樣的圖表:
這些圖可能是視圖本身,但更多時候它們是用戶通過應用程序所采取的步驟——如果你愿意的話,它們是用戶故事。它們并不總是嚴格線性的,可能存在分支和循環(Google Image Search for Workflow 有很多更復雜的示例)。但總是有明確的進入和退出點。用戶從一個任務開始,并通過工作流完成該任務。然后很自然地設計一個應用程序來提供用戶可以完成任務的工作流。我們可以通過 Workflow 有意義地談論 Progress,關聯的 Data 和 Application State 也構成了一種 Progress。隨著額外任務的添加,開發團隊的工作是提出一種設計,以允許有效地完成這些必要的工作流程。
內容創建/編輯應用程序的根本復雜性在于,這種方法根本不適用于它們。我認為最終的區別在于內容創建/編輯工具中沒有固有的進度概念。例如,作為 Powerpoint 用戶,我可以(而且確實!)花幾個小時重新組織我的幻燈片,調整圖像大小和對齊方式,稍微調整文本。在我看來,我可能對進度有一些模糊的概念,但這并沒有在應用程序中編碼。我的任務在應用程序之外。如果沒有明確的任務或進度衡量標準,就沒有工作流程!
我認為內容創建/編輯應用程序更有用的心智模型就像右邊的圖像。綠色中央集線器是這些應用程序中的默認狀態,通常你只是在其中查看你的內容。例如,在 Photoshop 中平移和縮放圖像,或在 Blender 中瀏覽 3D 場景。這是用戶花費大量時間的地方。藍色輻條是工具。我會去一個工具一段時間,但我總是回到中心。
因此,如果我要隨著時間的推移跟蹤我的狀態,那將是通過無數工具進出默認集線器的曲折路徑。沒有明確定義的順序,作為用戶,我通常可以按照我認為合適的任何順序自由使用工具。在一個縮影中,我們可能能夠找到定義明確的小型工作流來分析和優化,但在應用程序級別,工作流實際上是無限的。
看起來相對明顯的是,你需要在此處采用的架構方法與在視圖方法中的不同。通過以正確的方式瞇眼看它,人們可能會爭辯說每個工具基本上都是一個視圖,那么這里真正不同的是什么?根據我的經驗,不同之處在于我認為是Tool Sprawl。
如果你有明確定義的工作流程,那么很容易判斷什么是必要的,什么是不必要的。與所需工作流程無關的功能不僅會浪費設計和工程時間,而且最終會使工作流程變得比必要的復雜——這會使用戶體驗變得更糟!現代軟件開發的正統觀念非常關注這個前提——構建最小可行的產品,然后迭代、迭代、迭代以消除用戶的摩擦。
基于工具的應用程序根本不同,因為每增加一個工具都會增加應用程序的價值。如果我沒有使用特定工具,那么除了啟動該工具所需的附加工具欄按鈕帶來的小 UI 開銷之外,它的添加幾乎不會影響我。當然,學習新工具需要付出一些努力。但是,這種努力的回報是這個新工具現在可以與所有其他工具相結合!這導致了一種應用級網絡效應,其中每個新工具都是所有現有工具的力量倍增器。如果觀察幾乎所有主要的內容創建/編輯工具,這一點就會立即顯現出來,其中有無數的工具欄和工具欄菜單以及工具欄的嵌套選項卡,隱藏在其他工具欄后面。對局外人來說,這看起來很瘋狂,但對用戶來說,
許多來自面向工作流的軟件世界的人都驚恐地看著這些應用程序。我觀察到許多新項目,其中團隊開始嘗試構建一些“簡單”的東西,專注于“核心工作流程”,也許是為“新手用戶”繪制的,并且繪制了許多漂亮的線性工作流程圖。但現實情況是,新手用戶在掌握你的應用程序之前只是新手,然后他們會立即要求更多功能。因此,你將在這里和那里添加一個工具。幾年后,你將擁有一套龐大的工具,如果沒有系統的方法來組織它們,手上就會一團糟。
3.3 遏制傷害
混亂從何而來?據我所見,有幾種常見的惹麻煩的方法。首先是低估了手頭任務的復雜性。許多內容創建應用程序以“查看器”開始,其中所有應用程序邏輯(如 3D 相機控件)都直接在鼠標和 UI 按鈕處理程序中完成。然后隨著時間的推移,只需添加更多 if/else 分支或 switch case,就可以合并新的編輯功能。這種方法可以持續很長時間,而且我工作過的許多 3D 應用程序的核心仍然是這些殘留的代碼分支。但是你只是在挖掘一個更深的代碼洞并用代碼意大利面填充它。最終,將需要一些實際的軟件架構,并且需要進行痛苦的重構工作(隨后是多年的修復回歸,
即使有一定數量的“工具架構”,如何處理設備輸入也很棘手,而且往往最終導致混亂的架構鎖定。鑒于“工具”通常由設備輸入驅動,一個看似顯而易見的方法是直接為工具提供輸入事件處理程序,如 OnMouseUp/OnMouseMove/OnMouseDown 函數。這成為放置“做事”代碼的自然位置,例如在鼠標事件上,你可以直接在繪畫工具中應用畫筆印章。在用戶要求支持其他輸入設備(如觸摸、筆或 VR 控制器)之前,這似乎是無害的。怎么辦?只是將呼叫轉發給鼠標處理程序嗎?壓力或 3D 位置呢?然后是自動化,當用戶開始要求能夠為你的工具編寫腳本時。它不是。絕對不。真的,不要)。
將重要代碼放入輸入事件處理程序還會導致諸如標準事件處理模式的猖獗復制粘貼之類的事情,如果需要進行更改,這可能會很乏味。而且,昂貴的鼠標事件處理程序實際上會使您的應用程序感覺不如應有的響應,這是由于稱為鼠標事件優先級的東西。所以,你真的要小心處理工具架構的這一部分,因為看似標準的設計模式可能會引發一系列問題。
同時,如果工具架構定義過于嚴格,它可能成為擴展工具集的障礙,因為新的需求不“符合”初始設計的假設。如果許多工具都建立在初始架構之上,那么更改就變得棘手,然后聰明的工程師被迫想出變通辦法,現在你有兩個(或更多)工具架構。最大的挑戰之一就是如何在工具實現和框架之間劃分職責。
我不能聲稱交互式工具框架 (ITF) 會為你解決這些問題。最終,任何成功的軟件最終都會被早期的設計決策所困,在這些決策之上已經建造了高山,而改變路線只能付出巨大的代價。我可以整天給你講故事,關于我是如何對自己做到這一點的。我能說的是,在 UE4 中實現的 ITF 希望能從我過去的錯誤中受益。在過去的 2 年中,我們使用 ITF 在 UE4 編輯器中構建新工具的經驗(到目前為止)相對輕松,我們一直在尋找消除任何摩擦點的方法。
4、工具、工具構建器和工具管理器
如上所述,交互工具是應用程序的模態狀態,在此期間可以以特定方式捕獲和解釋設備輸入。在交互式工具框架 (ITF) 中,UInteractiveTool基類表示模態狀態,并具有你可能需要實現的非常小的 API 函數集。下面我總結了 psuedo-C++ 中的核心 UInteractiveTool API — 為簡潔起見,我省略了虛擬、常量、可選參數等內容。我們稍后會在一定程度上介紹其他 API 函數集,但這些是關鍵的。在::Setup()中初始化您的工具,并在::Shutdown()中進行任何最終確定和清理,這也是你執行“應用”操作之類的地方。EToolShutdownType與HasAccept()和CanAccept()函數有關,我將在下面詳細解釋。最后,工具將有機會渲染()并勾選每一幀。請注意,還有一個 ::Tick() 函數,但你應該重寫::OnTick()因為基類 ::Tick() 具有必須始終運行的關鍵功能。
UCLASS() class UInteractiveTool : public UObject, public IInputBehaviorSource {void Setup();void Shutdown(EToolShutdownType ShutdownType);void Render(IToolsContextRenderAPI* RenderAPI);void OnTick(float DeltaTime);bool HasAccept();bool CanAccept(); };UInteractiveTool 不是一個獨立的對象,你不能簡單地自己生成一個。為了使其發揮作用,必須調用 Setup/Render/Tick/Shutdown,并傳遞諸如IToolsContextRenderAPI之類的適當實現,從而允許工具繪制線條/等。我將在下面進一步解釋。但是現在你需要知道的是,要創建一個 Tool 實例,你需要從UInteractiveToolManager請求一個。要允許 ToolManager 構建任意類型,您需要向 ToolManager 注冊一個 <String, UInteractiveToolBuilder > 對。UInteractiveToolBuilder 是一個非常簡單的工廠模式基類,必須為每種工具類型實現:
UCLASS() class UInteractiveToolBuilder : public UObject {bool CanBuildTool(const FToolBuilderState& SceneState);UInteractiveTool* BuildTool(const FToolBuilderState& SceneState); };UInteractiveToolManager的主要 API總結如下。通常,你不需要實現自己的 ToolManager,基本實現功能齊全,應該完成使用工具所需的一切。但如有必要,你可以自由擴展子類中的各種功能。
下面的函數大致按照你調用它們的順序列出。RegisterToolType()將字符串標識符與 ToolBuilder 實現相關聯。然后應用程序使用SelectActiveToolType()設置一個活動的生成器,然后使用ActivateTool()創建一個新的 UInteractiveTool 實例。有 getter 可以訪問活動工具,但實際上很少有人經常調用。應用程序必須在每一幀調用 Render() 和 Tick() 函數,然后應用程序調用活動工具的相關函數。最后DeactiveTool()用于終止活動工具。
UCLASS() class UInteractiveToolManager : public UObject, public IToolContextTransactionProvider {void RegisterToolType(const FString& Identifier, UInteractiveToolBuilder* Builder);bool SelectActiveToolType(const FString& Identifier);bool ActivateTool();void Tick(float DeltaTime);void Render(IToolsContextRenderAPI* RenderAPI);void DeactivateTool(EToolShutdownType ShutdownType); };4.1 工具生命周期
在高層次上,工具的生命周期如下
- ToolBuilder 注冊到 ToolManager
- 一段時間后,用戶表示他們希望啟動工具(例如通過按鈕)
- UI 代碼集 Active ToolBuilder,請求工具激活
- ToolManager 檢查 ToolBuilder.CanBuildTool() = true,如果是,則調用 BuildTool() 創建新實例
- ToolManager 調用 Tool Setup()
- 直到 Tool 被停用,它是 Tick()'d 和 Render()'d 每一幀
- 用戶表示他們希望退出工具(例如通過按鈕、熱鍵等)
- ToolManager 使用適當的關閉類型調用 Tool Shutdown()
- 一段時間后,工具實例被垃圾收集
注意最后一步。工具是 UObject,因此你不能依賴 C++ 析構函數進行清理。你應該在 Shutdown() 實現中進行任何清理,例如銷毀臨時參與者。
4.2 EToolShutdownType 和接受/取消模型
工具可以以兩種不同的方式支持終止,具體取決于工具支持的交互類型。更復雜的替代方案是可以接受 — EToolShutdownType::Accept 或取消 EToolShutdownType::Cancel 的工具。這通常在工具的交互支持某種操作的實時預覽時使用,用戶可能希望放棄該操作。例如,將網格簡化算法應用于選定網格的工具可能具有用戶可能希望探索的一些參數,但如果探索不令人滿意,則用戶可能更愿意根本不應用簡化。在這種情況下,UI 可以提供按鈕來接受或取消活動工具,這會導致使用適當的 EToolShutdownType 值調用 ToolManager::DeactiveTool()。
第二個終止選項 - EToolShutdownType::Completed - 更簡單,因為它只是指示工具應該“退出”。這種類型的終止可用于處理沒有明確的“接受”或“取消”操作的情況,例如在簡單可視化數據的工具中,增量應用編輯操作的工具(例如基于點擊點生成對象),等等。
需要明確的是,你在使用 ITF 時不需要使用或支持接受/取消式工具。這樣做通常會導致更復雜的 UI。如果你在應用程序中支持 Undo,那么即使是具有 Accept 和 Cancel 選項的 Tools,也可以等效為 Complete-style Tools,如果用戶不滿意,也可以 Undo。但是,如果工具完成可能涉及冗長的計算或以某種方式具有破壞性,則支持接受/取消往往會帶來更好的用戶體驗。在 UE 編輯器的建模模式中,我們通常在編輯靜態網格體資源時使用 Accept/Cancel 正是出于這個原因。
你必須做出的另一個決定是如何處理工具的模態性質。通常,將用戶視為“處于”工具中是有用的,即處于特定的模態狀態。那么他們是如何“走出去”的呢?您可以要求用戶明確單擊接受/取消/完成按鈕以退出活動工具,這是最簡單和最明確的,但確實意味著需要單擊,并且用戶必須在心理上意識到并管理此狀態。或者,當用戶在工具工具欄/菜單/等中選擇另一個工具時(例如),你可以自動接受/取消/完成。然而,這引發了一個棘手的問題,即應該自動接受還是自動取消。這個問題沒有正確答案,你必須決定什么最適合你的特定環境 —雖然根據我的經驗,當一個人意外誤點擊時,自動取消可能會非常令人沮喪!
4.3 基礎工具
ITF 的主要目標之一是減少編寫工具所需的樣板代碼量,并提高一致性。幾個“工具模式”出現得如此頻繁,以至于我們在 ITF 的 /BaseTools/ 子文件夾中包含了它們的標準實現。基本工具通常包括一個或多個 InputBehaviors(見下文),其操作映射到您可以覆蓋和實現的虛擬功能。我將簡要介紹這些基本工具中的每一個,因為它們既是構建您自己的工具的有用方式,也是如何做事的示例代碼的良好來源:
USingleClickTool捕獲鼠標單擊輸入,如果IsHitByClick()函數返回有效點擊,則調用OnClicked()函數。您提供這兩個的實現。請注意,此處的FInputDeviceRay結構包括 2D 鼠標位置和 3D 射線。
class INTERACTIVETOOLSFRAMEWORK_API USingleClickTool : public UInteractiveTool {FInputRayHit IsHitByClick(const FInputDeviceRay& ClickPos);void OnClicked(const FInputDeviceRay& ClickPos); };UClickDragTool捕獲并轉發連續的鼠標輸入,而不是單擊。如果CanBeginClickDragSequence()返回 true —通常你會在此處進行命中測試,類似于 USingleClickTool,則將調用 OnClickPress() / OnClickDrag() / OnClickRelease(),類似于標準 OnMouseDown/Move/Up 事件模式。但是請注意,你必須在OnTerminateDragSequence()中處理序列中止但沒有釋放的情況。
class INTERACTIVETOOLSFRAMEWORK_API UClickDragTool : public UInteractiveTool {FInputRayHit CanBeginClickDragSequence(const FInputDeviceRay& PressPos);void OnClickPress(const FInputDeviceRay& PressPos);void OnClickDrag(const FInputDeviceRay& DragPos);void OnClickRelease(const FInputDeviceRay& ReleasePos);void OnTerminateDragSequence(); };UMeshSurfacePointTool與 UClickDragTool 相似之處在于它提供了單擊-拖動-釋放輸入處理模式。但是,UMesSurfacePointTool 假定它正在作用于一個目標 UPrimitiveComponent —它是如何獲取這個 Component 的將在下面解釋。下面HitTest()函數的默認實現將使用標準 LineTraces — 因此,如果足夠的話,你不必重寫此函數。UMeshSurfacePointTool 還支持懸停,并跟蹤 Shift 和 Ctrl 修飾鍵的狀態。對于簡單的“表面繪圖”類型工具,這是一個很好的起點,許多建模模式工具派生自 UMeshSurfacePointTool — 一個小提示:這個類也支持閱讀手寫筆壓力,但是在 UE4.26 手寫筆輸入是 Editor-Only。
附注:雖然命名為 UMeshSurfacePointTool,但其實并不需要Mesh,只需要一個支持LineTrace的UPrimitiveComponent
class INTERACTIVETOOLSFRAMEWORK_API UMeshSurfacePointTool : public UInteractiveTool {bool HitTest(const FRay& Ray, FHitResult& OutHit);void OnBeginDrag(const FRay& Ray);void OnUpdateDrag(const FRay& Ray);void OnEndDrag(const FRay& Ray);void OnBeginHover(const FInputDeviceRay& DevicePos);bool OnUpdateHover(const FInputDeviceRay& DevicePos);void OnEndHover(); };還有第四個基礎工具,UBaseBrushTool,它擴展了 UMeshSurfacePointTool,具有各種特定于基于畫筆的 3D 工具的功能,即表面繪畫筆刷、3D 雕刻工具等。這包括一組標準畫筆屬性、一個 3D 畫筆位置/大小/衰減指示器、“畫筆印記”跟蹤以及各種其他有用的位。如果你正在構建畫筆式工具,可能會發現這很有用。
4.4 FToolBuilder狀態
UInteractiveToolBuilder API 函數都采用 FToolBuilderState 參數。此結構的主要目的是提供選擇信息 - 它指示工具將或應該采取的行動。結構的關鍵字段如下所示。ToolManager 將構造一個 FToolBuilderState 并將其傳遞給 ToolBuilders,然后 ToolBuilders 將使用它來確定它們是否可以對 Selection 進行操作。在 UE4.26 ITF 實現中,Actor 和 Components 都可以傳遞,但也只能傳遞 Actor 和 Components。請注意,如果一個組件出現在 SelectedComponents 中,那么它的 Actor 將在 SelectedActors 中。包含這些 Actor 的 UWorld 也包括在內。
struct FToolBuilderState {UWorld* World;TArray<AActor*> SelectedActors;TArray<UActorComponent*> SelectedComponents; };在建模模式工具中,我們不直接對組件進行操作,我們將它們包裝在一個標準容器中,這樣我們就可以,例如,3D 雕刻具有容器實現的“任何”網格組件。這在很大程度上是我可以編寫本教程的原因,因為我可以讓這些工具編輯其他類型的網格,例如運行時網格。但是在構建自己的工具時,你可以隨意忽略 FToolBuilderState。你的 ToolBuilder 可以使用任何其他方式來查詢場景狀態,并且你的工具不限于作用于 Actor 或組件。
4.5 關于工具構建器
ITF 用戶經常提出的一個問題是 UInteractiveToolBuilder 是否必要。在最簡單的情況下,也就是最常見的情況下,你的 ToolBuilder 將是簡單的樣板代碼 —不幸的是,因為它是一個 UObject,這個樣板不能直接轉換為 C++ 模板。當人們開始重新利用現有的 UInteractiveTool 實現來解決不同的問題時,ToolBuilders 的實用程序就會出現。
例如,在 UE 編輯器中,我們有一個用于編輯網格多邊形組(實際上是多邊形)的工具,稱為 PolyEdit。我們還有一個非常相似的工具用于編輯網格三角形,稱為 TriEdit。在引擎蓋下,這些是相同的 UInteractiveTool 類。在 TriEdit 模式下,Setup() 函數將工具的各個方面配置為適合三角形。為了在 UI 中公開這兩種模式,我們使用了兩個獨立的 ToolBuilder,它們在創建的 Tool 實例被分配之后、Setup() 運行之前設置了一個“bIsTriangleMode”標志。
我當然不會聲稱這是一個優雅的解決方案。但是,這是權宜之計。根據我的經驗,隨著你的工具集不斷發展以處理新情況,這種情況總是會出現。通??梢酝ㄟ^一些自定義初始化、一些附加選項/屬性等來填充現有工具來解決新問題。在理想世界中,人們會重構工具以通過子類化或組合來實現這一點,但我們很少生活在理想世界中。因此,破解工具以完成第二項工作所需的一些難看的代碼可以放置在自定義 ToolBuilder 中,并(相對)封裝在其中。
使用 ToolManager 注冊 ToolBuilder 的基于字符串的系統可以允許你的 UI 級別(即按鈕處理程序等)啟動工具,而無需實際了解 Tool 類類型。這通常可以在構建 UI 時實現更清晰的關注點分離。例如,在我將在下面描述的 ToolsFrameworkDemo 中,工具是由 UMG 藍圖小部件啟動的,它們只是將字符串常量傳遞給 BP 函數——它們根本不了解工具系統。 然而,在生成工具之前需要設置一個“活動”構建器有點像退化的肢體,這些操作可能會在未來結合起來。
5、輸入行為系統
上面我說過“交互式工具是應用程序的模態狀態,在此期間可以以特定方式捕獲和解釋設備輸入”。但是 UInteractiveTool API 沒有任何鼠標輸入處理函數。這是因為輸入處理(大部分)與工具分離。輸入由工具創建并注冊到UInputRouter的UInputBehavior對象捕獲和解釋, UInputRouter “擁有”輸入設備并將輸入事件路由到適當的行為。
這種分離的原因是絕大多數輸入處理代碼都是剪切和粘貼的,在特定交互的實現方式上略有不同。例如考慮一個簡單的按鈕點擊交互。在一個常見的事件 API 中,您將擁有可以實現的 OnMouseDown()、OnMouseMove() 和 OnMouseUp() 等函數,假設你希望將這些事件映射到單個 OnClickEvent() 處理程序,以便按下按鈕-釋放動作。數量驚人的應用程序(尤其是 Web 應用程序)會觸發 OnMouseDown 中的點擊——這是錯誤的!但是,在 OnMouseUp 中盲目地觸發 OnClickEvent 也是錯誤的!這里的正確行為實際上是相當復雜的。在 OnMouseDown() 中,你必須對按鈕進行點擊測試,并開始捕獲鼠標輸入。在 OnMouseUp 中,你必須點擊測試按鈕再次,如果光標仍在點擊按鈕,則僅觸發 OnClickEvent。這允許取消點擊,并且是所有嚴肅的 UI 工具包如何實現它(試試看!)。
如果你甚至擁有數十個工具,那么實現所有這些處理代碼,特別是針對多個設備,將變得非常容易出錯。因此,出于這個原因,ITF 將這些小的輸入事件處理狀態機移動到 UInputBehavior 實現中,這些實現可以在許多工具之間共享。事實上,一些簡單的行為,如USingleClickInputBehavior、UClickDragBehavior和UHoverBehavior 可以處理大多數鼠標驅動交互的情況。然后,行為通過工具或 Gizmo 等可以實現的簡單接口將其提煉的事件轉發到目標對象。例如 USingleClickInputBehavior 可以作用于任何實現 IClickBehaviorTarget 的東西,它只有兩個函數 - IsHitByClick() 和 OnClicked()。請注意,由于 InputBehavior 不知道它作用于什么——“按鈕”可以是 2D 矩形或任意 3D 形狀——Target 接口必須提供命中測試功能。
InputBehavior 系統的另一個方面是工具不直接與 UInputRouter 對話。他們只提供他們希望激活的 UInputBehavior 的列表。UInteractiveTool API 添加的支持此功能如下所示。通常,在工具的 ::Setup() 實現中,會創建和配置一個或多個輸入行為,然后將其傳遞給 AddInputBehavior。然后,ITF 在必要時調用 GetInputBehaviors,將這些行為注冊到 UInputRouter。注意:目前 InputBehavior 集不能在工具期間動態更改,但是您可以配置您的 Behaviors 以根據您希望的任何標準忽略事件。
class UInteractiveTool : public UObject, public IInputBehaviorSource {// ...previous functions...void AddInputBehavior(UInputBehavior* Behavior);const UInputBehaviorSet* GetInputBehaviors(); };UInputRouter與UInteractiveToolManager的相似之處在于默認實現足以滿足大多數用途。InputRouter 的唯一工作是跟蹤所有活動的 InputBehavior 并調解捕獲的輸入設備。捕獲是工具中輸入處理的核心。當 MouseDown 事件進入 InputRouter 時,它會檢查所有已注冊的 Behaviors 以詢問它們是否要開始捕獲鼠標事件流。例如,如果您按下一個按鈕,該按鈕注冊的 USingleClickInputBehavior 將表明是的,它想要開始捕獲。一次只允許單個行為捕獲輸入,并且可能需要捕獲多個行為(彼此不了解) - 例如,與當前視圖重疊的 3D 對象。因此,每個 Behavior 返回一個 FInputCaptureRequest,指示“是”或“否”以及深度測試和優先級信息。UInputRouter 然后查看所有捕獲請求,并根據深度排序和優先級,選擇一個行為并告訴它捕獲將開始。然后 MouseMove 和 MouseRelease 事件僅傳遞給該行為,直到 Capture 終止(通常在 MouseRelease 上)。
實際上,在使用 ITF 時,你很少需要與 UInputRouter 交互。一旦建立了應用程序級鼠標事件和 InputRouter 之間的連接,你就不需要再次觸摸它了。該系統主要處理常見錯誤,例如由于捕獲出錯而導致鼠標處理“卡住”,因為 UInputRouter 最終控制鼠標捕獲,而不是單個行為或工具。在隨附的 ToolsFrameworkDemo 項目中,我已經實現了 UInputRouter 運行所需的一切。
基本的 UInputBehavior API 如下所示。FInputDeviceState是一個大型結構,包含給定事件/時間的所有輸入設備狀態,包括常用修飾鍵的狀態、鼠標按鈕狀態、鼠標位置等。與許多輸入事件的一個主要區別是還包括與輸入設備位置相關的 3D 世界空間射線。
UCLASS() class UInputBehavior : public UObject {FInputCapturePriority GetPriority();EInputDevices GetSupportedDevices();FInputCaptureRequest WantsCapture(const FInputDeviceState& InputState);FInputCaptureUpdate BeginCapture(const FInputDeviceState& InputState);FInputCaptureUpdate UpdateCapture(const FInputDeviceState& InputState);void ForceEndCapture(const FInputCaptureData& CaptureData);// ... hover support... }我在上面的 API 中省略了一些額外的參數,以簡化事情。特別是如果你實現自己的行為,你會發現幾乎到處都有一個 EInputCaptureSide 枚舉,主要作為默認的 EInputCaptureSide::Any。這是為了將來使用,以支持行為可能特定于任一手的 VR 控制器的情況。
但是,對于大多數應用程序,你可能會發現實際上不必實現自己的行為。一組標準行為,例如上面提到的那些,包含在 InteractiveToolFramework 模塊的 /BaseBehaviors/ 文件夾中。大多數標準行為都是從基類UAnyButtonInputBehavior 派生的,它允許它們使用任何鼠標按鈕,包括由 TFunction(可能是鍵盤鍵)定義的“自定義”按鈕!類似地,標準 BehaviorTarget 實現都派生自IModifierToggleBehaviorTarget,它允許在 Behavior 上配置任意修飾鍵并將其轉發到 Target,而無需子類化或修改 Behavior 代碼。
直接使用 UInputBehaviors
在上面的討論中,我重點討論了 UInteractiveTool 提供 UInputBehaviorSet 的情況。Gizmos 將類似地工作。但是,UInputRouter 本身根本不知道 Tools,完全可以單獨使用 InputBehavior 系統。在 ToolsFrameworkDemo 中,我在USceneObjectSelectionInteraction類中以這種方式實現了點擊選擇網格交互。這個類實現了 IInputBehaviorSource 和 IClickBehaviorTarget 本身,并且只屬于框架后端子系統。即使這不是絕對必要的 - 您可以直接使用 UInputRouter 注冊您自己創建的 UInputBehavior (但是請注意,由于我對 API 的疏忽,在 UE4.26 中您無法顯式注銷單個行為,您只能通過源注銷)。
5.1 非鼠標輸入設備
UE4.26 ITF 實現中當前未處理其他設備類型,但是 frame3Sharp 中此行為系統的先前迭代支持觸摸和 VR 控制器輸入,并且這些應該(最終)在 ITF 設計中類似地工作。一般的想法是只有 InputRouter 和 Behaviors 需要明確了解不同的輸入模式。IClickBehaviorTarget 實現應該與鼠標按鈕、手指點擊或 VR 控制器點擊類似地工作,但也不排除為特定于設備的交互(例如,來自兩指捏合、空間控制器手勢等)定制的額外行為目標. 工具可以為不同的設備類型注冊不同的行為,InputRouter 將負責處理哪些設備是活動的和可捕獲的。
目前,可以通過映射到鼠標事件來完成對其他設備類型的某種程度的處理。由于 InputRouter 不直接監聽輸入事件流,而是由 ITF 后端創建和轉發事件,這是做這種映射的自然場所,下面將解釋更多細節。
5.2 限制 - 捕獲中斷
在設計交互時需要注意的這個系統的一個重要限制是,框架尚不支持主動捕獲的“中斷”。當人們希望進行單擊或拖動的交互時,這種情況最常見,具體取決于鼠標是立即在同一位置釋放還是移動了某個閾值距離。在簡單的情況下,這可以通過UClickDragBehavior處理,由你的 IClickDragBehaviorTarget 實現做出決定。但是,如果單擊和拖動動作需要去到彼此不知道的非常不同的地方,這可能會很痛苦。支持這種交互的一種更簡潔的方法是允許一個 UInputBehavior “中斷”另一個 - 在這種情況下,當滿足先決條件(即足夠的鼠標移動)時,拖動以“中斷”單擊的活動捕獲。這是 ITF 未來可能會改進的一個領域。
6、工具屬性集
UInteractiveTool 還有一組我沒有介紹的 API 函數,用于管理一組附加的UInteractiveToolPropertySet對象。這是一個完全可選的系統,在某種程度上是為在 UE 編輯器中使用而量身定制的。對于運行時使用,它不太有效。本質上,UInteractiveToolPropertySet 用于存儲你的工具設置和選項。它們是具有 UProperties 的 UObject,在編輯器中,這些 UObject 可以添加到 Slate DetailsView 以在編輯器 UI 中自動公開這些屬性。
額外的 UInteractiveTool API 總結如下。一般在Tool ::Setup()函數中,會創建各種UInteractiveToolPropertySet子類并傳遞給AddToolPropertySource()。ITF 后端將使用 GetToolProperties() 函數初始化 DetailsView 面板,然后 Tool 可以使用 SetToolPropertySourceEnabled() 動態顯示和隱藏屬性集
class UInteractiveTool : public UObject, public IInputBehaviorSource {// ...previous functions... public:TArray<UObject*> GetToolProperties(); protected:void AddToolPropertySource(UObject* PropertyObject);void AddToolPropertySource(UInteractiveToolPropertySet* PropertySet);bool SetToolPropertySourceEnabled(UInteractiveToolPropertySet* PropertySet, bool bEnabled); };在 UE 編輯器中,可以使用元標記來標記 UProperties 以控制生成的 UI 小部件 - 例如滑塊范圍、有效整數值以及基于其他屬性的值啟用/禁用小部件。建模模式中的大部分 UI 都是以這種方式工作的。
不幸的是,UProperty 元標記在運行時不可用,并且 UMG 小部件不支持 DetailsView 面板。結果,ToolPropertySet 系統變得不那么引人注目了。不過,它仍然提供了一些有用的功能。一方面,屬性集支持使用屬性集的 SaveProperties() 和 RestoreProperties() 函數跨工具調用保存和恢復其設置。您只需在 Tool Shutdown() 中設置的每個屬性上調用 SaveProperties(),并在 ::Setup() 中調用 RestoreProperties()。
第二個有用的功能是 WatchProperty() 函數,它允許響應 PropertySet 值的更改而無需任何類型的更改通知。這對于 UObject 是必要的,因為 C++ 代碼可以直接更改 UObject 上的 UProperty,這不會導致發送任何類型的更改通知。因此,可靠檢測此類更改的唯一方法是通過輪詢。是的,投票。這并不理想,但請考慮 (1) 工具必須具有有限數量的用戶可以處理的屬性,以及 (2) 一次只有一個工具處于活動狀態。為了讓您不必為 ::OnTick() 中的每個屬性實現存儲值比較,您可以使用以下模式添加觀察者:
MyPropertySet->WatchProperty( MyPropertySet->bBooleanProp, [this](bool bNewValue) { // handle change! } );
在 UE4.26 中,有一些額外的警告(閱讀:錯誤)必須解決,請參閱下文了解更多詳細信息。
7、工具操作
最后,UInteractiveTool API 的最后一個主要部分是對Tool Actions的支持。這些在建模模式工具集中沒有廣泛使用,除了實現熱鍵功能。但是,工具操作與熱鍵沒有特別的關系。它們允許工具公開可以通過整數標識符調用的“動作”(即無參數函數)。Tool 構造并返回一個FInteractiveToolActionSet,然后更高級別的客戶端代碼可以枚舉這些操作,并使用下面定義的ExecuteAction函數執行它們。
class UInteractiveTool : public UObject, public IInputBehaviorSource {// ...previous functions... public:FInteractiveToolActionSet* GetActionSet();void ExecuteAction(int32 ActionID); protected:void RegisterActions(FInteractiveToolActionSet& ActionSet); };下面的示例代碼顯示了兩個正在注冊的工具操作。請注意,盡管FInteractiveToolAction包含熱鍵和修飾符,但這些只是對更高級別客戶端的建議。UE 編輯器查詢操作的工具,然后將建議的熱鍵注冊為編輯器熱鍵,這允許用戶重新映射它們。UE在運行時沒有任何類似的熱鍵系統,您需要自己手動映射這些熱鍵
void UDynamicMeshSculptTool::RegisterActions(FInteractiveToolActionSet& ActionSet) {ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 61,TEXT("SculptDecreaseSpeed"),LOCTEXT("SculptDecreaseSpeed", "Decrease Speed"),LOCTEXT("SculptDecreaseSpeedTooltip", "Decrease Brush Speed"),EModifierKey::None, EKeys::W,[this]() { DecreaseBrushSpeedAction(); });ActionSet.RegisterAction(this, (int32)EStandardToolActions::ToggleWireframe,TEXT("ToggleWireframe"),LOCTEXT("ToggleWireframe", "Toggle Wireframe"),LOCTEXT("ToggleWireframeTooltip", "Toggle visibility of wireframe overlay"),EModifierKey::Alt, EKeys::W,[this]() { ViewProperties->bShowWireframe = !ViewProperties->bShowWireframe; }); }最終,每個 ToolAction 有效負載都存儲為 TFunction<void()>。如果你只是轉發到另一個 Tool 函數,比如上面的 DecreaseBrushSpeedAction() 調用,你不一定受益于 ToolAction 系統,根本不需要使用它。然而,由于當前工具暴露于藍圖的限制,ToolActions(因為它們可以通過一個簡單的整數調用)可能是一種將工具功能暴露給 BP 的有效方法,而無需編寫許多包裝函數。
8、小玩意兒
正如我所提到的,“Gizmo”是指我們在 2D 和 3D 內容創建/編輯應用程序中使用的那些在視口內點擊的小東西,可以讓你有效地操縱視覺元素或對象的參數。例如,如果您使用過任何 3D 工具,那么你幾乎肯定使用過標準的平移/旋轉/縮放 Gizmo。與工具一樣,Gizmo 捕獲用戶輸入,但不是完整的 Modal 狀態,Gizmo 通常是瞬態的,即 Gizmo 可以來來去去,并且你可以同時激活多個 Gizmo,它們僅在你單擊時捕獲輸入“開”他們(“開”的意思可能有點模糊)。正因為如此,Gizmo 通常需要一些特定的可視化表示,以允許用戶指示他們何時想要“使用”Gizmo,但從概念上講,你也可以擁有基于熱鍵或應用程序狀態(例如復選框)執行此操作的 Gizmo。
在 Interactive Tools Framework 中,Gizmo 被實現為UInteractiveGizmo的子類,它與 UInteractiveTool 非常相似:
UCLASS() class UInteractiveGizmo : public UObject, public IInputBehaviorSource {void Setup();void Shutdown();void Render(IToolsContextRenderAPI* RenderAPI);void Tick(float DeltaTime);void AddInputBehavior(UInputBehavior* Behavior);const UInputBehaviorSet* GetInputBehaviors(); }同樣,Gizmo 實例由UInteractiveGizmoManager管理,使用通過字符串注冊的UInteractiveGizmoBuilder工廠。Gizmo 使用相同的 UInputBehavior 設置,并且由 ITF 每幀進行類似渲染和勾選。
在這個高層次上,UInteractiveGizmo 只是一個骨架,要實現自定義 Gizmo,你必須自己做很多工作。與工具不同,提供“基礎”小玩意兒更具挑戰性,因為它具有視覺表示方面。特別是,標準的 InputBehaviors 將要求你能夠對 Gizmo 進行光線投射命中測試,因此不能只在 Render() 函數中繪制任意幾何圖形。也就是說,ITF 確實提供了一個非常靈活的標準 Translate-Rotate-Scale Gizmo 實現,可以重新利用它來解決許多問題。
8.1 標準 UTransformGizmo
如果 ITF 不包含標準的平移-旋轉-縮放 (TRS) Gizmos,那么將 ITF 稱為構建 3D 工具的框架將是非常有問題的。目前在 UE4.26 中可用的是一個名為UTransformGizmo的組合 TRS Gizmo(右側屏幕截圖) ,它支持軸和平面平移(軸線和中心人字形)、軸旋轉(圓)、統一比例(中心框)、軸比例(外軸括號)和平面刻度(外人字形)。這些子 Gizmo 可以單獨配置,因此你可以(例如)通過將某些枚舉值傳遞給 Gizmo 構建器來創建僅具有 XY 平面平移和 Z 旋轉的 UTransformGizmo 實例。
這個 TRS Gizmo 不是一個單一的整體 Gizmo,它是由一組可以重新用于許多其他用途的部件組成的。這個子系統足夠復雜,值得單獨寫一篇文章,但總而言之,我上面提到的 UTransformGizmo 的每個元素實際上都是一個單獨的 UInteractiveGizmo(所以,是的,你可以有嵌套/分層 Gizmo,你可以繼承 UTransformGizmo 來添加額外的自定義控件)。例如,軸平移子 Gizmo(繪制為紅/綠/藍線段)是UAxisPositionGizmo的實例,旋轉圓是UAxisAngleGizmo。
像 UAxisPositionGizmo 這樣的子 Gizmo 并沒有顯式地繪制上圖中的線條。相反,它們連接到提供視覺表示和命中測試的任意 UPrimitiveComponent。因此,如果你愿意,可以使用任何 UStaticMesh。默認情況下,UTransformGizmo 生成自定義 Gizmo 特定的 UPrimitiveComponents,在線條的情況下,它是一個UGizmoArrowComponent。這些 GizmoComponents 提供了一些細節,如恒定的屏幕空間尺寸、懸停支持等。但是你絕對不必使用它們,并且 Gizmo 外觀可以完全根據你的目的進行定制(未來以 Gizmo 為重點的文章的主題!)。
因此,UAxisPositionGizmo 實際上只是“根據鼠標輸入沿線指定位置”這一抽象概念的實現。3D 線、線位置到抽象參數的映射(默認情況下為 3D 世界位置)以及狀態變化信息都通過 UInterfaces 實現,因此可以根據需要進行自定義。視覺表示只是為了通知用戶,并為捕獲鼠標的 InputBehavior 提供命中目標。這允許以最小的難度集成任意捕捉或參數約束等功能。
但是,這都是旁白。實際上,要使用 UTransformGizmo,你只需使用以下調用之一從 GizmoManager 請求一個:
class UInteractiveGizmoManager {// ... UTransformGizmo* Create3AxisTransformGizmo(void* Owner);UTransformGizmo* CreateCustomTransformGizmo(ETransformGizmoSubElements Elements, void* Owner); }然后創建一個UTransformProxy實例并將其設置為 Gizmo 的目標。Gizmo 現在將具有完整功能,你可以在 3D 場景中移動它,并通過 UTransformProxy::OnTransformChanged 委托響應變換更改??梢允褂酶鞣N其他委托,例如開始/結束轉換交互?;谶@些委托,你可以變換場景中的對象、更新對象的參數等。
稍微復雜一點的用法是,如果你希望 UTransformProxy 直接移動一個或多個 UPrimitiveComponent,即實現幾乎每個 3D 設計應用程序都有的普通“選擇對象并使用 gizmo 移動它們”類型的界面。在這種情況下,可以將組件添加為代理的目標。Gizmo 仍然作用于 UTransformProxy,并且 Proxy 將單個變換重新映射到對象集上的相對變換。
UTransformGizmo 不必為工具所有。在 ToolsFrameworkDemo 中,USceneObjectTransformInteraction類監視運行時對象場景中的選擇變化,如果存在活動選擇,則生成合適的新 UTransformGizmo。代碼只有幾行:
TransformProxy = NewObject<UTransformProxy>(this); for (URuntimeMeshSceneObject* SceneObject : SelectedObjects) {TransformProxy->AddComponent(SO->GetMeshComponent()); }TransformGizmo = GizmoManager->CreateCustomTransformGizmo(ETransformGizmoSubElements::TranslateRotateUniformScale, this); TransformGizmo->SetActiveTarget(TransformProxy);在這種情況下,我將傳遞ETransformGizmoSubElements::TranslateRotateUniformScale以創建沒有非均勻縮放子元素的 TRS gizmo。要銷毀 Gizmo,代碼只需調用 DestroyAllGizmosByOwner,傳遞創建期間使用的相同 void* 指針:
GizmoManager->DestroyAllGizmosByOwner(this);UTransformGizmo 自動發出必要的撤消/重做信息,這將在下面進一步討論。因此,只要使用中的 ITF 后端支持撤消/重做,Gizmo 轉換也將支持。
8.2 本地與全球坐標系
UTransformGizmo 支持局部和全局坐標系。默認情況下,它從 ITF 后端請求當前的本地/全局設置。在 UE 編輯器中,其控制方式與默認 UE 編輯器 Gizmo 相同,方法是在主視口頂部使用相同的世界/本地切換。你也可以覆蓋此行為,請參閱 UTransformGizmoBuilder 標頭中的注釋。
一個警告,不過。UE4 僅支持組件的局部坐標系中的非均勻縮放變換。這是因為在大多數情況下,不能將具有非均勻縮放的兩個單獨的 FTransform 組合成一個 FTransform。因此,在全局模式下,TRS Gizmo 將不會顯示非均勻縮放手柄(軸括號和外角 V 形)。默認的 UE 編輯器 Gizmo 具有相同的限制,但通過僅允許在縮放 Gizmo 中使用本地坐標系(不與平移和旋轉 Gizmo 組合)來處理它。
9、工具上下文和 ToolContext API
在這一點上,我們有 Tools 和 ToolManager,還有 Gizmos 和 GizmoManager,但誰來管理 Manager?為什么,當然是上下文。UInteractiveToolsContext是交互工具框架的最頂層。它本質上是工具和 Gizmo 所在的“宇宙”,并且還擁有 InputRouter。默認情況下,你可以簡單地使用此類,這就是我在 ToolsFrameworkDemo 中所做的。在 ITF 的 UE 編輯器使用中,有一些子類可以調解 ITF 和更高級別的編輯器構造(如 FEdMode)之間的通信(例如,參見UEdModeInteractiveToolsContext)。
ToolsContext 還為 Managers 和 InputRouter 提供了各種 API 的實現,這些 API 提供了“類似編輯器”的功能。這些 API 的目的本質上是提供“編輯器”的抽象,這使我們能夠防止 ITF 具有顯式的虛幻編輯器依賴項。在上面的文字中,我多次提到“ITF 后端”——這就是我所指的。
如果仍然不清楚我所說的“編輯器的抽象”是什么意思,也許可以舉個例子。我還沒有提到任何關于對象選擇的內容。這是因為選定對象的概念在很大程度上超出了 ITF 的范圍。當 ToolManager 去構造一個新工具時,它會傳遞一個選定的 Actor 和組件的列表。但是它通過詢問工具上下文來獲得這個列表。而且工具上下文也不知道。工具上下文需要通過IToolsContextQueriesAPI詢問創建它的應用程序。這個周圍的應用程序必須創建 IToolsContextQueriesAPI 的實現并將其傳遞給構造時的 ToolsContext。
ITF 無法以通用方式解決“對象選擇的工作原理”,因為這高度依賴于您的應用程序。在 ToolsFrameworkDemo 中,我實現了一個基本的網格對象和選擇列表機制,其行為類似于大多數 DCC 工具。虛幻編輯器在主視口中有一個類似的系統。但是,在資產編輯器中,只有一個對象,根本沒有選擇。所以 Asset Editors 中使用的 IToolsContextQueriesAPI 是不同的。如果你在游戲環境中使用 ITF,您可能會對什么是“選擇”,甚至什么是“對象”有一個非常不同的概念。
因此,我們使用 ToolContext API 的目標是需要最少的函數集,以允許工具在“類似編輯器的容器”中工作。隨著需要查詢編輯器容器的新情況的出現,這些 API 隨著時間的推移而增長。它們在文件 ToolContextInterfaces.h 中定義并總結如下
9.1 IToolsContextQueriesAPI
該 API 提供了從 Editor 容器中查詢狀態信息的功能。最關鍵的是GetCurrentSelectionState(),ToolManager 將使用它來確定要傳遞給 ToolBuilder 的選定參與者和組件。在使用 ITF 時,你可能需要對此進行自定義實現。許多工具和 TRS Gizmo 也需要GetCurrentViewState()才能正常工作,因為它提供 3D 相機/視圖信息。然而,ToolsFrameworkDemo 中的示例實現可能足以滿足任何運行時使用,即標準全屏單一 3D 視圖。這里的其他函數可以有簡單的實現,只返回一個默認值。
class IToolsContextQueriesAPI {void GetCurrentSelectionState(FToolBuilderState& StateOut);void GetCurrentViewState(FViewCameraState& StateOut);EToolContextCoordinateSystem GetCurrentCoordinateSystem();bool ExecuteSceneSnapQuery(const FSceneSnapQueryRequest& Request, TArray<FSceneSnapQueryResult>& Results );UMaterialInterface* GetStandardMaterial(EStandardToolContextMaterials MaterialType); }9.2 IToolsContextTransactionsAPI
IToolsContextTransactionsAPI主要用于將數據發送回編輯器容器。DisplayMessage()由工具調用,其中包含各種用戶信息消息、錯誤和狀態消息等。如果愿意,可以忽略這些。PostInvalidation()用于指示需要重繪,在引擎以最大/固定幀速率持續重繪的運行時上下文中,通??梢院雎赃@一點。RequestSelectionChange()是某些工具提供的提示,通常在它們創建新對象時提供,可以忽略。
class IToolsContextTransactionsAPI {void DisplayMessage(const FText& Message, EToolMessageLevel Level);void PostInvalidation();bool RequestSelectionChange(const FSelectedOjectsChangeList& SelectionChange);void BeginUndoTransaction(const FText& Description);void AppendChange(UObject* TargetObject, TUniquePtr<FToolCommandChange> Change, const FText& Description);void EndUndoTransaction(); }追加更改()由想要發出 FCommandChange 記錄 — 實際上是 FToolCommandChange 子類 — 的工具調用,這是 ITF 撤消/重做方法的核心組件。為了理解為什么這個設計是這樣的,我必須解釋一下撤銷/重做是如何在 UE 編輯器中工作的。
編輯器不使用命令對象/模式方法來撤消/重做,這通常是大多數 3D 內容創建/編輯工具執行此操作的方式。相反,編輯器使用事務系統。打開事務后,對任何即將被修改的對象調用 UObject::Modify(),這會保存所有 UObject 當前 UProperty 值的副本。當 Transaction 關閉時,比較修改對象的 UProperties,并序列化任何更改。這個系統真的是對像 UObjects 這樣的東西的唯一方法,可以通過 UProperties 擁有任意用戶定義的數據。
然而,眾所周知,事務系統在處理大型復雜數據結構(如網格)時表現不佳。例如,將任意部分更改存儲為一個巨大的網格作為事務將涉及預先制作完整副本,然后搜索和編碼對復雜網格數據結構(本質上是非結構化圖)的更改。這是一個非常困難(閱讀:慢)的計算問題。類似地,一個簡單的 3D 平移將修改每個頂點,需要一個 Transaction 中所有位置數據的完整副本,但在 Change 中可以僅存儲為平移向量和一些關于應用什么操作的信息,然后搜索并編碼對復雜網格數據結構(本質上是非結構化圖)的更改。這是一個非常困難(閱讀:慢)的計算問題。
因此,在構建 ITF 時,我們添加了對在 UE 編輯器事務中嵌入 FCommandChange 對象的支持。這有點雜亂無章,但通常有效,并且有用的副作用是這些 FCommandChanges 也可以在不存在 UE 編輯器事務系統的運行時使用。當用戶與工具交互時,我們的大多數建模模式工具都會不斷調用 AppendChange(),而 Gizmo 也這樣做。因此,我們可以構建一個基本的撤消/重做歷史系統,只需按照它們進入的順序存儲這些更改,然后在撤消/重做列表中后退/前進,在每個 FToolCommandChange 對象上調用 Revert()/Apply() .
BeginUndoTransaction()和EndUndoTransaction()是相關函數,它們標記應分組的一組變更記錄的開始和結束 - 通常 AppendChange() 將在其間被調用一次或多次。為了提供正確的用戶體驗 - 即單個撤消/重做熱鍵/命令一次處理所有更改 - ToolsFrameworkDemo 有一個非常基本的系統,用于存儲一組 FCommandChanges。
IToolsContextRenderAPI
此 API 被傳遞給 UInteractiveTool::Render() 和 UInteractiveGizmo::Render() 以提供常見渲染任務所需的信息。GetPrimitiveDrawInterface()返回抽象FPrimitiveDrawInterface API 的實現,這是一個標準的 UE 接口,提供線和點繪制功能(通??s寫為 PDI)。各種工具使用 PDI 來繪制基本的線反饋,例如在繪制多邊形工具中繪制的當前多邊形的邊緣。但是請注意,運行時的 PDI 線圖與編輯器中的 PDI 線圖不同 - 它的質量較低,無法繪制編輯器可以繪制的隱藏線。
GetCameraState()、GetSceneView() 和 GetViewInteractionState() 返回有關當前視圖的信息。這些在編輯器中很重要,因為用戶可能有多個可見的 3D 視口(例如,在 4-up 視圖中),并且工具必須在每個視口中正確繪制。在運行時,通常只有一個相機/視圖,您應該可以使用 ToolsFramworkDemo 中的基本實現。但是,如果您想實現多個視圖,則需要在此 API 中正確提供它們。
class IToolsContextRenderAPI {FPrimitiveDrawInterface* GetPrimitiveDrawInterface();FViewCameraState GetCameraState();const FSceneView* GetSceneView();EViewInteractionState GetViewInteractionState(); }9.3 IToolsContextAssetAPI
ITooslContextAssetAPI 可用于發出新對象。這是一個可選的 API,我只列出了下面的頂級函數,API 還包含其他一些特定于 UE 編輯器的函數。這是最難抽象的部分,因為它需要一些關于“對象”是什么的固有假設。但是,它也不是您必須在自己的工具中使用的東西。GenerateStaticMeshActor( )編輯器建模工具使用該函數來生成新的靜態網格體資源/組件/Actor,例如在繪制多邊形工具中,使用拉伸多邊形(AssetConfig 參數的一部分)調用此函數來創建資源。此創建過程涉及查找位置(可能會產生對話框/等)、創建新包等。
class IToolsContextAssetAPI {AActor* GenerateStaticMeshActor(UWorld* TargetWorld,FTransform Transform,FString ObjectBaseName,FGeneratedStaticMeshAssetConfig&& AssetConfig);}在運行時,你不能創建資產,所以這個函數必須做“其他事情”。在 ToolsFrameworkDemo 中,我實現了 GenerateStaticMeshActor(),這樣一些建模模式工具(如 Draw Polygon Tool)就可以正常工作了。但是,它會發出完全不同的 Actor 類型。
10、組件選擇和PrimitiveComponentTargets
在上面的工具和工具構建器部分中,我描述了 FToolBuilderState,以及 ToolManager 如何構造一個選定的 Actor 和組件列表以傳遞給 ToolBuilder。如果你的工具應該作用于Actor或組件,可以將該選擇傳遞給新的工具實例。但是,如果你瀏覽建模模式工具代碼,會看到大多數工具都作用于稱為FPrimitiveComponentTarget的東西,它是在 ToolBuilders 中基于選定的 UPrimitiveComponents 創建的。我們有基類USingleSelectionTool和UMultiSelectionTool,大多數建模模式工具都派生自它們,它們包含這些選擇。
如果你是從頭開始構建自己的工具,這不是你需要做的事情。但是,如果你想利用建模模式工具,你需要理解它,所以我會解釋。FPrimitiveComponentTarget 的目的是為工具提供“可以編輯的網格”的抽象。這很有用,因為我們在 Unreal 中有許多不同的 Mesh 類型(您可能有自己的)。有 FMeshDescription(UStaticMesh 使用)、USkeletalMesh、FRawMesh、Cloth Meshes、Geometry Collections(即網格)等等。必須操縱低級網格數據結構的網格編輯工具本質上需要許多并行代碼路徑來支持其中的每一個。此外,在 Unreal 中更新網格的成本很高。正如我在之前的教程中所解釋的,當您在 UStaticMesh 中修改 FMeshDescription 時,“構建”步驟是重新生成渲染數據所必需的,這在大型網格上可能需要幾秒鐘。這在例如用戶期望即時反饋的 3D 雕刻工具中是不可接受的。
因此,通常建模模式工具不能直接編輯上面列出的任何 UE 組件網格格式。相反,ToolBuilder 將目標組件包裝在 FPrimitiveComponentTarget 實現中,該實現必須提供 API 來讀取和寫入其內部網格(無論格式如何)作為 FMeshDescription。這允許想要編輯網格以支持單一標準輸入/輸出格式的工具,但需要(潛在)網格轉換成本。在大多數建模模式工具中,我們隨后將該 FMeshDescription 轉換為 FDynamicMesh3 以進行實際編輯,并創建一個新的 USimpleDynamicMeshComponent 用于快速預覽,并且僅在 Tool Accept 上寫回更新的 FMeshDescription。但這被封裝在 Tool 內部,與 FPrimtiveComponentTarget 沒有真正的關系。
10.1 FComponentTargetFactory
我們需要允許交互式工具框架為它不知道的組件創建一個 FPrimitiveComponentTarget 子類包裝器 —因為許多組件是 ITF 不可見的插件的一部分。例如,UProceduralMeshComponent 或 USimpleDynamicMeshComponent。為此,我們提供了一個 FComponentTargetFactory 實現,它有兩個功能:
class INTERACTIVETOOLSFRAMEWORK_API FComponentTargetFactory { public:virtual bool CanBuild( UActorComponent* Candidate ) = 0;virtual TUniquePtr<FPrimitiveComponentTarget> Build( UPrimitiveComponent* PrimitiveComponent ) = 0; };這些一般都很簡單,舉個例子,見EditorComponentSourceFactory.cpp中的FStaticMeshComponentTargetFactory,它為UStaticMeshComponents構建了FStaticMeshComponentTarget實例。在這種情況下,FStaticMeshComponentTarget 也很簡單。我們將利用這個 API 來解決下面運行時使用的一些問題。
最后,一旦 FComponentTargetFactory 可用,全局函數AddComponentTargetFactory()用于注冊它。不幸的是,在 UE4.26 中,此函數將工廠存儲在全局靜態 TArray 中,該 TArray 對 ComponentSourceInterfaces.cpp 是私有的,因此無法以任何方式修改或操作。在啟動時,編輯器將注冊默認的 FStaticMeshComponentTargetFactory 以及處理 PMC 的 FProceduralMeshComponentTargetFactory。這兩個工廠都存在無法在運行時用于網格編輯工具的問題,因此,在改進此系統之前,我們不能使用 SMC 或 PMC 進行運行時網格編輯。我們將為 USimpleDynamicMeshComponent 創建一個新的 ComponentTarget(有關此網格組件類型的詳細信息,請參閱之前的教程)。
10.2 ToolBuilderUtil.h
如果你查看大多數工具的 ToolBuilders,會發現 CanBuildTool() 和 BuildTool() 實現通常調用ToolBuilderUtil命名空間中的靜態函數,以及函數CanMakeComponentTarget()和MakeComponentTarget()。后兩個函數通過已注冊的 ComponentTargetFactory 實例列表進行枚舉,以確定任何工廠是否可以處理特定的 UPrimitiveComponent 類型。ToolBuilderUtil 函數在很大程度上只是迭代FToolBuilderState中的選定組件(如上所述)并調用 lambda 謂詞(通常是上述函數之一)。
我將在這里重申,你不需要在自己的工具中使用 FPrimitiveComponentTarget 系統,甚至不需要在 FToolBuilderState 中使用。你可以在 ToolBuilders 中輕松查詢其他(全局)選擇系統,檢查目標組件類型的強制轉換,并將 UPrimitiveComponent* 或子類傳遞給您的工具。然而,正如我所提到的,建模模式工具以這種方式工作,它將成為我現在將描述的運行時網格編輯工具框架設計的重要驅動力。
11、運行時工具框架后端
為交互式工具框架創建運行時后端并不是那么復雜。我們要弄清楚的主要事情是:
- 如何收集鼠標輸入事件(即鼠標向下/移動/向上)并將此數據發送到UInputRouter
- 如何實現IToolsContextQueriesAPI和IToolsContextRenderAPI
- (可選)如何實現IToolsContextTransactionsAPI和IToolsContextAssetAPI
- 如何/何時 Render() 和 Tick() UInteractiveToolManager和UInteractiveGizmoManager
就是這樣。一旦這些事情完成(甚至跳過第 3 步),那么基本的工具和Gizmo (甚至UTransformGizmo)就會起作用。
在這個示例項目中,完成上述所有相關代碼都在RuntimeToolsSystem模塊中,分為四個子目錄:
- RuntimeToolsFramework\ - 包含核心工具框架實現
- MeshScene\ - 一個簡單的網格對象“場景圖”,這是我們的網格編輯工具將編輯的內容,以及一個基本的歷史(即撤消/重做)系統
- Interaction\ - 使用 UTransformGizmo 進行對象選擇和轉換的基本用戶界面交互,建立在 ITF 之上
- 工具\ - 幾個 MeshModelingToolset UInteractiveTools 和/或 Builders 的子類,需要讓它們在運行時正常運行
在高層次上,這里是所有東西的連接方式,用簡單的英語(希望這會讓下面的描述更容易理解)。自定義游戲模式AToolsFrameworkDemoGameModeBase在 Play 上初始化,這反過來又初始化了管理工具框架的URuntimeToolsFrameworkSubsystem和URuntimeMeshSceneSubsystem。后者管理一組URuntimeMeshSceneObject,它們是圍繞網格 Actor 和組件的包裝,可以通過單擊選擇并使用 UTransformGizmo 進行轉換。URuntimeToolsFrameworkSubsystem 初始化并擁有 UInteractiveToolsContext,以及各種幫助類,如USceneObjectSelectionInteraction(實現單擊選擇)、USceneObjectTransformInteraction(管理變換 Gizmo 狀態)和USceneHistoryManager(提供撤消/重做系統)。URuntimeToolsFrameworkSubsystem 還創建了一個UToolsContextRenderComponent,用于允許在工具和 Gizmo 中進行 PDI 渲染。在內部,URuntimeToolsFrameworkSubsystem 還定義了各種 API 實現,這些都完全包含在 cpp 文件中。最后一塊是 Game Mode 的默認 Pawn,它是由 GameMode 在 Play 上生成的AToolsContextActor 。這個 Actor 監聽各種輸入事件并將它們轉發到 URuntimeToolsFrameworkSubsystem。一種FSimpleDynamicMeshComponentTargetFactory也在 Play 上注冊,它允許在 URuntimeMeshSceneObject 中使用的 Mesh 組件由現有的建模模式工具進行編輯。
哇!由于它相對獨立于工具框架方面,讓我們從網格場景方面開始。
11.1 URuntimeMeshSceneSubsystem 和 MeshSceneObjects
此演示的目的是展示在運行時通過 ITF 選擇和編輯網格。可以想象,這樣做可以編輯任何 StaticMeshActor/Component,類似于建模模式在 UE 編輯器中的工作方式。但是,正如我在之前的教程中所建議的,如果你正在構建某種建模工具應用程序或游戲關卡編輯器,我認為你不希望直接使用 Actor 和組件構建所有內容。至少,你可能需要一種序列化“場景”的方法。而且可能希望在你的環境中擁有不可編輯的可見網格(即使只是 3D UI 元素)。我認為擁有一個代表可編輯世界的獨立數據模型是很有用的——一個“對象”的“場景”,不與特定的 Actor 或組件相關聯。反而,
所以,這就是我在這個演示中所做的。URuntimeMeshSceneObject是一個場景對象,在 UE 級別由ADynamicSDMCActor 表示,我在之前的教程中對此進行了描述。此 Actor 是RuntimeGeometryUtils插件的一部分。它生成/管理一個子網格 USimpleDynamicMeshComponent,可以在需要時進行更新。在這個項目中,我們不會使用我之前開發的任何藍圖編輯功能,而是使用工具進行編輯,并且僅使用 SDMC 作為顯示源網格的一種方式。
URuntimeMeshSceneSubsystem管理現有 URuntimeMeshSceneObjects 的集合,我將在此處(和代碼中)將其縮寫為“SO”。提供了生成新 SO、按 Actor 查找一個、刪除一個或多個 SO 以及管理一組選定 SO 的功能。此外,FindNearestHitObject() 可用于將光線投射到場景中,類似于 LineTrace(但只會命中 SO)。
URuntimeMeshSceneSubsystem 還擁有選中時分配給 SO 的材質以及默認材質。在這個演示中只有對材料的基線支持,所有創建的 SO 都分配了 DefaultMaterial(白色),并且在選擇時交換為 SelectedMaterial(橙色)。但是,SO 確實會跟蹤分配的材料,因此你可以相對輕松地擴展現有的材料。
11.2 USceneHistoryManager
對場景的更改 - 場景對象的創建、刪除和編輯、選擇更改、變換更改等 - 由USceneHistoryManager存儲。此類存儲FChangeHistoryTransaction結構的列表,其中存儲FChangeHistoryRecord的序列,它是一個元組(UObject*、FCommandChange、Text)。該系統大致近似于 UE 編輯器事務系統,但僅支持顯式 FCommandChange 對象,而在編輯器中,對 UObjects 的更改可以自動存儲在事務中。我在上面的 IToolsContextTransactionsAPI 部分中更詳細地描述了 FCommandChange。本質上,這些對象具有 Apply() 和 Revert() 函數,它們必須“重做”或“撤消”它們對任何修改的全局狀態的影響。
這里的使用模式是調用BeginTransaction(),然后AppendChange()一次或多次,然后是 EndTransaction()。IToolsContextTransactionsAPI 實現將為 ITF 組件執行此操作,并且諸如場景選擇更改之類的操作將直接執行此操作。Undo()函數回滾到之前的歷史狀態/事務,Redo ()函數向前滾動。通常的想法是將所有更改分組到單個事務中以用于單個高級用戶“操作”,因此不必多次撤消/重做即可“通過”復雜的狀態更改。為了簡化這一點,可以嵌套 BeginTransaction()/EndTransaction() 調用,這在需要調用多個單獨的函數并且每個函數都需要發出自己的事務時經常發生。與任何支持 Undo/Redo 的應用程序一樣,如果用戶執行 Undo 一次或多次,然后執行推送新事務/更改的操作,History 序列將被截斷。
11.3 AToolsContextActor
在虛幻引擎游戲中,玩家控制 Pawn Actor,而在第一人稱視角游戲中,場景是從 Pawn 的視點渲染的。在 ToolsFrameworkDemo 中,我們將實現一個名為AToolsContextActor的自定義 ADefaultPawn 子類來收集用戶輸入并將其轉發給 ITF。此外,此 Actor 將處理項目設置中定義的各種熱鍵輸入事件。最后,AToolsContextActor 是我實現標準鼠標右鍵飛行的地方(這是 ADefaultPawn 的標準行為,我只是將調用轉發給它)和 Maya 風格的 alt-mouse 相機控制的初始步驟(但是圍繞目標旋轉)點尚未實施)。
所有事件連接設置都在AToolsContextActor::SetupPlayerInputComponent()中完成。這是在Project Settings的Input部分中定義的熱鍵事件,以及硬編碼的按鈕 Action 和鼠標軸映射。大多數硬編碼映射——可識別為對UPlayerInput::AddEngineDefinedActionMapping()的調用——可以在項目設置中替換為可配置的映射。
此 Actor 由游戲模式在啟動時自動創建。我將在下面進一步描述這一點。
我將在這里只提到另一個選項,而不是讓 Pawn 將輸入轉發到 ITF 的 InputRouter,而是使用自定義 ViewportClient。ViewportClient 是“高于”Actor 和 Pawn 的級別,并且在某種程度上負責將原始設備輸入轉換為 Action 和 Axis Mappings。由于就 ITF 而言,我們的主要目標只是收集設備輸入并將其轉發給 ITF,因此自定義 ViewportClient 可能是更自然的地方。然而,這不是我在這個演示中的做法。
11.4 URuntimeToolsFramework子系統
Runtime ITF 后端的核心部分是URuntimeToolsFrameworkSubsystem。這個 UGameInstanceSubsystem(本質上是一個 Singleton)創建并初始化 UInteractiveToolsContext、所有必要的 IToolsContextAPI 實現、USceneHistoryManager、Selection 和 Transform Interactions,以及將在下面描述的幾個其他幫助對象。這一切都發生在::InitializeToolsContext()函數中。
子系統還具有用于啟動工具和管理活動工具的各種藍圖功能。這些是必要的,因為 ITF 當前未向藍圖公開。最后它做了一點鼠標狀態跟蹤,在::Tick()函數中,為光標位置構造一個世界空間射線(這是一個相對晦澀的代碼),然后將此信息轉發給 UInputRouter,以及勾選和渲染 ToolManager 和 GizmoManager。
如果這感覺有點像功能性的抓包,那么它就是。URuntimeToolsFrameworkSubsystem 基本上是 ITF 和我們的“編輯器”之間的“粘合劑”,在這種情況下它非常小。唯一需要注意的其他代碼是各種 API 實現,它們都在 .cpp 文件中定義,因為它們不是公共類。
FRuntimeToolsContextQueriesImpl是 IToolsContextQueriesAPI 的實現。此 API 為 ToolBuilders 提供 SelectionState,并支持對當前視圖狀態和坐標系狀態的查詢(詳情如下)。ExecuteSceneSnapQuery() 函數未實現,僅返回 false。但是,如果您想支持可選的變換 Gizmo 功能,例如網格捕捉或捕捉到其他幾何體,這將是開始的地方。
FRuntimeToolsContextTransactionImpl是 IToolsContextTransactionsAPI 的實現。在這里,我們只是將調用直接轉發到 USceneHistoryManager。目前我還沒有實現 RequestSelectionChange(),一些建模模式工具使用它來將選擇更改為新創建的對象,并且還忽略了 PostInvalidation() 調用,它們在 UE 編輯器中用于強制在非實時模式下刷新視口. 構建的游戲始終實時運行,因此這在標準游戲中不是必需的,但如果您正在構建一個不需要恒定 60fps 重繪的應用程序,并且已經實施了避免重繪的方案,則此調用可以為您提供提示強制重繪以查看實時工具更新/等。
FRuntimeToolsFrameworkRenderImpl是 IToolsContextRenderAPI 的實現。此 API 的主要目的是為工具和 Gizmos 提供 FPrimitiveDrawInterface 實現。這是在運行時使用建模模式工具時最有問題的部分之一,我將在下面關于 UToolsContextRenderComponent 的部分中描述它是如何實現的。否則,這里的函數只是轉發 RuntimeToolsFrameworkSubsystem 提供的信息。
最后是 FRuntimeToolsContextAssetImpl實現了 IToolsContextAssetAPI,在我們的 Runtime 案例中是非常有限的。此 API 中的許多功能旨在用于更復雜的編輯器使用,因為 UE 編輯器必須處理其中的 UPackage 和資產,可以執行諸如彈出內部資產創建對話框之類的操作,具有復雜的游戲資產路徑系統,等等。此 API 中的一些函數可能不應該是基本 API 的一部分,因為工具不會直接調用它們,而是調用使用這些函數的實用程序代碼。因此,我們只需要實現 Tools 調用的 GenerateStaticMeshActor() 函數來發射新對象(例如 DrawPolygon Tool,它可以繪制和擠出一個新的網格)。函數名稱顯然不合適,因為我們不想發出一個新的 AStaticMeshActor,而是一個新的 URuntimeMeshSceneObject。幸運的是,
就是這樣!當我提到“ITF 后端”或“類似編輯器的功能”時,我所指的就是這些。800 多行極其冗長的 C++,其中大部分是不同系統之間相對簡單的“粘合劑”。對于基本的 ITF 實現來說,甚至很多現有的部分都不是必需的,例如,如果您不想使用建模模式工具,則根本不需要 IToolsContextAssetAPI 實現。
11.5 USceneObjectSelectionInteraction 和 USceneObjectTransformInteraction
當我介紹 ITF 時,我將工具和 Gizmos 視為 ITF 的頂級“部分”,即實施用戶輸入的結構化處理(通過 InputBehaviors)、將動作應用于對象等的認可方法。但是,沒有嚴格的理由使用工具或 Gizmos 來實現所有用戶交互。為了證明這一點,我將“點擊選擇場景對象”交互實現為獨立類USceneObjectSelectionInteraction。
USceneObjectSelectionInteraction是IInputBehaviorSource的子類,所以它可以注冊到 UInputRouter,然后它的 UInputBehaviors 會被自動收集并允許捕獲鼠標輸入。USingleClickInputBehavior _實現了收集鼠標左鍵單擊,并支持 Shift+Click 和 Ctrl+Click 修飾鍵,以添加到選擇或切換選擇。IClickBehaviorTarget 實現函數只是確定動作應該指示什么狀態,并通過 URuntimeMeshSceneSubsystem API 函數將它們應用于場景。因此,整個點擊選擇交互只需要相對少量的代碼。如果你想實現額外的選擇交互,比如框選框選擇,這可以通過切換到 UClickDragBehavior/Target 并通過鼠標移動閾值確定用戶是否完成了點擊和拖動來相對容易地完成。
URuntimeToolsFrameworkSubsystem 只是在啟動時創建這個類的一個實例,將它注冊到 UInputRouter,這就是系統其余部分所知道的一切。當然可以將選擇實現為工具,盡管通常選擇是“默認”模式,并且當任何其他工具開始退出時切換出/進入默認工具需要一點小心。或者,它可以使用沒有場景內表示的 Gizmo 來完成,并且僅在支持選擇更改時始終可用。這可能是我的偏好,因為 Gizmo 獲得 Tick() 和 Render() 調用,這可能很有用(例如,可以通過 Render() 繪制選取框矩形)。
隨著選擇狀態的變化,3D 變換 Gizmo 會不斷更新 - 它在選定對象的原點之間移動,如果有多個選定對象則移動到共享原點,或者如果沒有選定對象則消失。此行為在USceneObjectTransformInteraction中實現,它同樣由 URuntimeToolsFrameworkSubsystem 創建。URuntimeMeshSceneSubsystem 的委托 OnSelectionModified 用于在修改場景選擇時啟動更新。生成的 UTransformGizmo 作用于 UTransformProxy,它被賦予當前選擇集。請注意,任何選擇更改都會生成一個新的 UTransformGizmo,而現有的 UTransformGizmo 會被銷毀。這有點重,可以對其進行優化以重用單個 Gizmo(各種建模模式工具就是這樣做的)。
最后一點是活動坐標系的管理。這主要在后臺處理,UTransformGizmo 將查詢可用的 IToolsContextQueriesAPI 以確定世界或局部坐標系。這可以是硬編碼的,但是為了支持兩者,我們需要在某個地方放置這個狀態。目前我已將它放在 URuntimeToolsFrameworkSubsystem 中,并暴露了一些 BP 函數以允許 UI 切換選項。
11.6 UToolsContextRenderComponent
我在上面提到過,IToolsContextRenderAPI 實現需要返回一個可用于繪制線和點的 FPrimitiveDrawInterface(或“PDI”),這有點問題。這是因為在 UE 編輯器中,承載 ITF 的編輯器模式具有自己的 PDI,可以簡單地傳遞給工具和 Gizmo。但是在運行時,這并不存在,我們可以訪問 PDI 實現的唯一地方是在渲染線程上運行的 UPrimitiveComponent 的渲染代碼中(哎呀!)。
如果這不完全有意義,那么基本上你需要了解的是,我們不能只是從 C++ 代碼中的任何地方“渲染”。我們只能在“內部”渲染組件,例如 UStaticMeshComponent 或 UProceduralMeshComponent。但是,我們的工具和 Gizmo 具有運行在游戲線程上的 ::Render() 函數,并且與任何組件相距甚遠。
所以,我所做的是制作一個自定義組件,稱為UToolsContextRenderComponent,它可以充當橋梁。這個組件有一個函數::GetPDIForView(),它返回一個自定義的 FPrimitiveDrawInterface 實現(準確地說是 FToolsContextRenderComponentPDI,盡管它隱藏在組件內部)。URuntimeToolsFrameworkSubsystem 每幀創建一個此 PDI 的實例以傳遞給工具和 Gizmo。PDI DrawLine() 和 DrawPoint() 實現不是試圖立即渲染,而是將每個函數調用的參數存儲在一個列表中。然后,組件 SceneProxy 獲取這些 Line 和 Point 參數集,并將它們傳遞給 FToolsContextRenderComponentSceneProxy::GetDynamicMeshElements() 實現內的標準 UPrimitiveComponent PDI(渲染器調用它來獲取每幀動態幾何圖形以進行繪制)。
該系統是功能性的,并且允許建模模式工具通常像在編輯器中一樣工作。然而,一個障礙是游戲和渲染線程并行運行。因此,如果什么都不做,我們可能會在工具和 Gizmo 完成繪制之前調用 GetDynamicMeshElements(),這會導致閃爍。目前我已經通過在 URuntimeToolsFrameworkSubsystem::Tick() 的末尾調用FlushRenderingCommands()來“修復”這個問題,這會強制渲染線程處理所有未完成的提交幾何圖形。但是,這可能無法完全解決問題。
另一個復雜之處在于,在 UE 編輯器中,PDI 線和點繪圖可以繪制“隱藏線”,即在正面幾何圖形后面帶有點畫圖案的線。這涉及將自定義深度/模板渲染與后處理通道結合使用。這在運行時也不存在。但是,在你自己的應用程序中,實際上有更多的能力來制作這些效果,因為你完全控制這些渲染系統,而在編輯器中,它們需要添加到任何游戲內效果的“頂部”因此必然受到更多限制。本文很好地概述了如何實現隱藏對象渲染,以及類似于 UE 編輯器的對象輪廓。
11.7 FSimpleDynamicMeshComponentTarget
正如我在 PrimitiveComponentTargets 部分中所描述的,為了允許在此演示中使用建模模式中的網格編輯工具,我們需要在我們要編輯的 UPrimitiveComponents 周圍提供一種“包裝器”。在這種情況下,這將是 USimpleDynamicMeshComponent。FSimpleDynamicMeshComponentTarget及其關聯的 Factory的代碼相對簡單。如果您深入研究,您可能會注意到,SDMC 中的 FDynamicMesh3 正在轉換為 FMeshDescription 以傳遞給工具,然后工具將其轉換回 FDynamicMesh3 進行編輯。這是當前設計的一個限制,該設計專注于靜態網格體。如果您正在構建自己的網格編輯工具,則無需進行此轉換,但要使用建模模式工具集,則不可避免。
請注意,對網格的更改(存儲在 ::CommitMesh() 中)在更改歷史記錄中保存為FMeshReplacementChange,其中存儲了兩個完整的網格副本。這對于大型網格來說并不理想,但是建模工具在內部創建的用于存儲預覽網格上的更改(例如在 3D 雕刻中)的網格“增量”當前不會“冒泡”。
最后,我將再次重申,由于 FPrimitiveComponentTarget 部分中描述的工廠注冊問題,無法在 UE4.26 的運行時使用建模模式工具集直接編輯 UStaticMeshComponent 或 UProceduralMeshComponent。雖然,由于在很大程度上只有 ToolBuilders 使用 FPrimitiveComponentTargetFactory 注冊表,您也許可以讓它們與直接創建替代 FPrimitiveComponentTarget 實現的自定義 ToolBuilders 一起使用。這不是我探索過的路線。
11.8 AToolsFrameworkDemoGameModeBase
教程項目的最終 C++ 代碼組件是AToolsFrameworkDemoGameModeBase。這是 AGameModeBase 的子類,我們將在編輯器中將其配置為默認游戲模式。本質上,這就是“啟動”我們的運行時工具框架的原因。請注意,這不是 RuntimeToolsFramework 模塊的一部分,而是基本游戲模塊,你無需在自己的應用程序中以這種方式初始化。例如,如果你想實現某種游戲內關卡設計/編輯工具,可能會將此代碼折疊到您現有的游戲模式中(或者可能會根據需要啟動一個新模式)。你也不需要使用游戲模式來執行此操作,盡管在這種情況下復雜的是默認 pawn AToolsContextActor,它可能也需要替換。
在這種游戲模式中很少發生。我們將其配置為 Tick,在 Tick() 函數中,我們 Tick() URuntimeToolsFrameworkSubsystem。否則所有動作都在AToolsFrameworkDemoGameModeBase::InitializeToolsSystem()中,我們在其中初始化 URuntimeMeshSceneSubsystem 和 URuntimeToolsFrameworkSubsystem,然后將可用工具集注冊到 ToolManager。所有這些代碼都可以(也許應該)從游戲模式本身中移出,并移到一些實用功能中。
12、ToolsFrameworkDemo 項目設置
如果你打算根據本教程設置自己的項目或進行更改,則需要了解涉及的各種資產和項目設置。下面的內容瀏覽器屏幕截圖顯示了主要資產。DefaultMap是我使用的關卡,它只包含地平面并在關卡藍圖中初始化 UMG 用戶界面(見下文)。
BP_ToolsContextActor是 AToolsContextActor 的藍圖子類,在游戲模式中被配置為默認 Pawn。在這個 BP Actor 中,我禁用了Add Default Movement Bindings設置,因為我在 Actor 中手動設置了這些綁定。DemoPlayerController是 AToolsFrameworkDemoPlayerController 的 Blueprint 子類,這再次存在只是為了在 BP 中配置一些設置,特別是我啟用了Show Mouse Cursor以便繪制標準 Windows 光標(這是人們在 3D 工具中可能期望的)并禁用 Touch事件。最后DemoGameMode是我們AToolsFrameworkDemoGameModeBase的 BP 子類C++ 類,在這里我們配置游戲模式以生成我們的 DemoPlayerController 和 BP_ToolsContextActor,而不是默認值。
最后在Project Settings對話框中,我將Default GameMode配置為我們的DemoGameMode藍圖,并將DefaultMap配置為 Editor 和 Game 啟動圖。我還在Input部分添加了各種操作,我在 AToolsContextActor 的描述中顯示了上面這些設置的屏幕截圖。最后在Packaging部分,我添加了兩條到 Materials 的路徑到Additional Asset Directories to Cook部分。這對于強制將這些材料包含在構建的游戲可執行文件中是必要的,因為關卡中的任何資產都沒有特別引用它們。
13、RuntimeGeometryUtils 更新
在我之前的教程中,我一直在 RuntimeGeometryUtils 插件中積累各種 Runtime 網格生成功能。為了實現本教程,我做了一個重要的補充,URuntimeDynamicMeshComponent。這是 USimpleDynamicMeshComponent (SDMC) 的子類,增加了對碰撞和物理的支持。如果您還記得之前的教程,建模模式工具使用 USimpleDynamicMeshComponent 來支持在編輯期間實時預覽網格。在這種情況下,SDMC 針對原始渲染性能的快速更新進行了優化,并且由于它僅用于“預覽”,因此不需要對碰撞或物理的支持。
但是,我們也一直在使用 SDMC 作為渲染運行時生成的幾何圖形的一種方式。在許多方面它與 UProceduralMeshComponent (PMC) 非常相似,但是 PMC 的一個顯著優勢是它支持碰撞幾何,這意味著它可以與 UE 光線投射/線跟蹤系統以及物理/碰撞正常工作系統。事實證明,支持這一點相對簡單,所以我創建了 URuntimeDynamicMeshComponent 子類。SDMC的這個變種,我想我們可以稱之為RDMC,支持簡單和復雜的碰撞,還有一個函數SetSimpleCollisionGeometry()可用,它可以采用任意簡單的碰撞幾何(甚至 PMC 也不支持)。但是請注意,目前不支持異步物理烹飪。這不是要添加的主要內容,但我沒有這樣做。
我已將ADynamicSDMCActor中的組件類型切換為這個新組件,因為其他方面的功能是相同的,但現在基礎 Actor 上的碰撞選項的工作方式與它們在 PMC 變體上的工作方式相同。最終結果是以前的教程演示,如兔子槍和程序世界,應該與 SDMC 以及 PMC 一起使用。這將為將來更有趣(或高性能)的運行時程序網格工具打開大門。
14、在運行時使用 ModelingMode 工具
這花費了相當長的時間,但我們現在可以在運行時游戲的 MeshModelingToolset 中公開現有的網格編輯工具,并使用它們來編輯選定的 URuntimeMeshSceneObject。從概念上講,這個“正常工作”并添加工具工作的基本能力只需要在AToolsFrameworkDemoGameModeBase::RegisterTools()中注冊一個 ToolBuilder ,然后添加一些方式(熱鍵、UMG 按鈕等)以通過URuntimeToolsFrameworkSubsystem 啟動它: :BeginToolByName()。這適用于許多工具,例如 PlaneCutTool 和 EditMeshPolygonsTool 開箱即用。
但是,并非所有工具都能立即發揮作用。與全局 ToolTargetTargetFactory 系統類似,各種當時可能看起來微不足道的小設計決策可能會阻止工具在構建的游戲中工作。通常,通過一些實驗,可以在基本工具的子類中使用少量代碼來解決這些問題。我已經在幾種情況下這樣做了,我將解釋這些,以便如果你嘗試公開其他工具,你可能會有一個嘗試做什么的策略。如果你發現自己陷入困境,請在評論中發布有關該工具不起作用的信息,我會盡力提供幫助。
請注意,要創建 Tool 子類,你還需要創建一個新的 ToolBuilder 來啟動該子類。通常,這意味著子類化基礎 Builder 并覆蓋創建工具的函數,無論是基礎 ::BuildTool() 還是基礎 Builder 的調用 NewObject 的函數(這些通常更容易處理)。
在某些情況下,默認工具設置是有問題的。例如,URemeshTool默認啟用僅編輯器的線框渲染。因此,有必要重寫 Setup() 函數,調用基本的 Setup(),然后禁用此標志(不幸的是,目前在 Builder 中沒有辦法這樣做,因為 Builder 沒有機會接觸分配新實例后的工具)。
創建新對象的工具,例如UDrawPolygonTool,通常在未經修改的情況下無法在運行時工作。在許多情況下,發出新對象的代碼是#ifdef 出來的,而是用 check() 代替。但是,我們可以將這些工具子類化并替換 Shutdown() 函數或工具的內部函數,以實現新對象的創建(通常來自工具生成的 FDynamicMesh3)。URuntimeDrawPolygonTool::EmitCurrentPolygon()是為 UDrawPolygonTool 執行此操作的示例,而URuntimeMeshBooleanTool::Shutdown()為 UCSGMeshesTool 執行此操作的示例。在后一種情況下,覆蓋執行基本工具代碼的子集,因為我只支持替換第一個選定的輸入對象。
這是我遇到的兩個主要問題。第三個復雜因素是許多現有工具,尤其是舊工具,不使用 WatchProperty() 系統來檢測其 UInteractiveToolPropertySet 設置對象的值何時被修改。它們不依賴于輪詢,而是依賴于僅編輯器的回調,這在構建的游戲中不會發生。因此,如果你以編程方式更改這些 PropertSet 的設置,工具將不會更新以反映它們的值而無需輕推。不過,我已經將這些“輕推”與一種將工具設置公開給藍圖的方式相結合,我現在將對此進行解釋。
14.1 藍圖公開的 ToolPropertySets
4.26 中工具框架的一個主要限制是,盡管它是由 UObject 構建的,但它們都沒有暴露給藍圖。因此,你不能輕易地做一件微不足道的事情,例如將 UMG UI 連接到活動工具,以直接更改工具設置。但是,如果我們對現有工具進行子類化,我們可以將子類標記為UCLASS(BlueprintType),然后將活動工具(通過URuntimeToolsFrameworkSubsystem::GetActiveTool()訪問)轉換為該類型。類似地,我們可以定義一個新的 UInteractiveToolPropertySet,也就是 UCLASS(BlueprintType),并公開標記為 BlueprintReadWrite 的新 UProperties 以使它們可以從 BP 訪問。
為了包含這個新的 Property Set,我們將繼承 Tool ::Setup()函數,調用基類::Setup(),然后創建并注冊我們的新 PropertySet。對于每個屬性,我們將添加一個 WatchProperty() 調用,將更改從我們的新 PropertySet 轉發到基本工具設置,然后在必要時調用一個函數來啟動重新計算或更新(例如 URuntimeMeshBooleanTool 必須調用Preview->InvalidateResult () )。
一個復雜的問題是枚舉值設置,它在編輯器中會自動生成下拉列表,但是這對于 UMG 是不可能的。因此,在這些情況下,我使用整數 UProperties 并將整數映射到自己的枚舉。因此,例如,這里是 UDrawPolygonTool 的 URuntimeDrawPolygonTool 的所有 PropertySet 相關代碼(我省略了上面提到的 EmitCurrentPolygon() 覆蓋和 new ToolBuilder)。這是一種剪切和粘貼模式,我可以在我的所有工具覆蓋中重復使用它來為我的 UMG UI 公開工具屬性。
UENUM(BlueprintType) enum class ERuntimeDrawPolygonType : uint8 {Freehand = 0, Circle = 1, Square = 2, Rectangle = 3, RoundedRectangle = 4, HoleyCircle = 5 };UCLASS(BlueprintType) class RUNTIMETOOLSSYSTEM_API URuntimeDrawPolygonToolProperties : public UInteractiveToolPropertySet {GENERATED_BODY() public:UPROPERTY(BlueprintReadWrite)int SelectedPolygonType; };UCLASS(BlueprintType) class RUNTIMETOOLSSYSTEM_API URuntimeDrawPolygonTool : public UDrawPolygonTool {GENERATED_BODY() public:virtual void Setup() override;UPROPERTY(BlueprintReadOnly)URuntimeDrawPolygonToolProperties* RuntimeProperties; };void URuntimeDrawPolygonTool::Setup() {UDrawPolygonTool::Setup();// mirror properties we want to expose at runtime RuntimeProperties = NewObject<URuntimeDrawPolygonToolProperties>(this);RuntimeProperties->SelectedPolygonType = (int)PolygonProperties->PolygonType;RuntimeProperties->WatchProperty(RuntimeProperties->SelectedPolygonType,[this](int NewType) { PolygonProperties->PolygonType = (EDrawPolygonDrawMode)NewType; });AddToolPropertySource(RuntimeProperties); }14.2 ToolPropertySet Keepalive Hack
我在嘗試讓 MeshModelingToolset 工具在構建的游戲中工作時遇到的一個主要問題是,事實證明它們使用 UObjects 做了一些……非法的事情。這真的很麻煩,但我會簡要解釋一下,以防它與您相關。我之前提到 UInteractiveToolPropertySet 用于在幾乎所有工具中以結構化的方式公開“工具設置”。像這樣的系統的一個理想屬性是能夠保存工具??調用之間的設置狀態。為此,我們可以只保留屬性集本身的一個實例,但我們需要將它保留在某個地方。
各種編輯器系統通過在其他一些 UObject 的 CDO 中保存指向已保存設置 UObject 的指針來執行此操作 - 每個 UObject 都有一個 CDO(類默認對象),它就像一個用于構造附加實例的“模板”。CDO 是全球性的,因此這是放置東西的好地方。然而,在編輯器中,CDO 將阻止這個 UObject 被垃圾收集(GC’d),但在運行時,它不會!事實上,在運行時,垃圾收集器會進行安全檢查以確定這是否尚未完成,如果它檢測到這一點,就會終止游戲(!)。這需要在 UE 的未來版本中修復,但要讓這個演示在二進制 4.26 版本中運行,我們需要一個解決方法。
首先,我必須通過在 URuntimeToolsFrameworkSubsystem::InitializeToolsContext() 中設置全局GShouldVerifyGCAssumptions = false來禁用 GC 安全檢查。這可以防止硬殺,但是當工具嘗試訪問它們并假定它們仍然存在時,保存的 PropertySet 仍將被垃圾收集并導致崩潰。因此,在 URuntimeToolsFrameworkSubsystem::OnToolStarted() 事件處理程序中,調用了 AddAllPropertySetKeepalives() 函數,該函數遍歷新工具的所有已注冊 PropertySet UObjects 的 CDO,并將這些“保存的設置”UObjects 添加到將防止他們被GC’d。
這是…一個嚴重的黑客攻擊。但功能齊全,似乎沒有任何有問題的副作用。但我打算在未來解決底層架構問題。
14.3 用戶界面
本教程的重點是演示交互式工具框架和網格建模工具集的運行時使用,而不是實際構建功能運行時建模工具。然而,為了真正能夠啟動和使用演示工具,我必須構建一個最小的 UMG 用戶界面。我不是 UMG 的專家(這是我第一次使用它)所以這可能不是最好的方法。但是,它有效。在/ToolUI子文件夾中,您將找到幾個 UI 小部件資產。
ToolTestUI是主用戶界面,位于左上角,右下角有一個圖像。我在教程開始時描述了各種工具按鈕。Accept、Cancel和Complete按鈕根據活動工具狀態動態更新可見性和啟用性,此邏輯位于藍圖中。撤消和重做按照您的預期進行,并且“世界”按鈕可在任何活動 Gizmo 的“世界”和“本地”幀之間切換。此 UI 由關卡藍圖在 BeginPlay 上生成,位于右下方。
還有幾個每個工具的 UI 面板顯示工具設置。這些每個工具的 UI 面板在它們啟動工具后由 ToolUI 按鈕生成,請參閱 ToolUI 藍圖,它非常簡單。我只為一些工具添加了這些設置面板,并且只公開了一些設置。添加設置的工作并不多,但有點乏味,而且由于這是一個教程,我不太關心公開所有可能的選項。下面的截圖來自DrawPolygonToolUI,顯示游戲內面板(左)和 UI 藍圖(右)。本質上,在初始化時,Active Tool 被轉換為正確的類型,我們提取 RuntimeProperties 屬性集,然后初始化所有 UI 小部件(在這種情況下只有一個)。然后在小部件事件更新時,我們將新值轉發到屬性集。不涉及火箭科學。
15、結束語
我曾有很多人詢問 UE Editor Modeling Mode Tools 和 Gizmos 是否可以在運行時使用,我的回答一直是“嗯,這很復雜,但可能”。我希望這個示例項目和文章能回答這個問題!這絕對是可能的,在 GeometryProcessing 庫和 MeshModelingToolset 工具和組件之間,UE4.26中提供了大量可用于構建交互式 3D 內容創建應用程序的功能,從基本的“放置和移動對象”工具到從字面上看,一個功能齊全的 3D 網格雕刻應用程序。你真正需要做的就是設計和實現 UI。
根據我過去構建的設計工具,我可以肯定地說,當前的建模模式工具可能并不完全是您自己的應用程序所需要的。它們是一個不錯的起點,但我認為它們提供的實際上是關于如何實現不同交互和行為的參考指南。你想要一個可以使用 Gizmo 移動的 3D 工作平面嗎?查看UConstructionPlaneMechanic以及它是如何在各種工具中使用的。在該平面上繪制和編輯 2D 多邊形怎么樣?請參閱UDrawAndRevolveTool中的UCurveControlPointsMechanic用法。用于在網格上繪制最短邊路徑的界面?USeamSculptTool這樣做。想要制作一個運行一些第三方幾何處理代碼的工具,具有設置和實時預覽以及為您預先計算的各種有用的東西?只是子類UBaseMeshProcessingTool。需要在工具期間在后臺線程中運行昂貴的操作,以便您的 UI 不會鎖定?UMeshOpPreviewWithBackgroundCompute和TGenericDataBackgroundCompute實現模式,URemeshMeshTool 等工具展示了如何使用它。
我可以繼續,很長一段時間。建模模式中有超過 50 種工具,它們可以做各種各樣的事情,遠遠超過我可能有時間解釋的。但是,如果您可以在 UE 編輯器中找到與您想要的內容相近的內容,則基本上可以復制 Tool .cpp 和 .h,重命名類型,然后開始根據您的目的對其進行自定義。
所以,玩得開心!
原文鏈接:UE4運行時交互工具框架 — BimAnt
總結
以上是生活随笔為你收集整理的UE4运行时交互工具框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软件负载均衡
- 下一篇: 更改文件和目录(及子目录)的拥有者