發(fā)布時(shí)間:2024-04-09 15:31:52 瀏覽量:198次
本文為Epic Games 的Ryan Schmidt對UE4內(nèi)置的Runtime Interactive Tools Framework的起源、設(shè)計(jì)思路和使用方法的全面介紹。
在本文中,我將介紹很多內(nèi)容。我提前為太長的篇幅道歉。但是,這篇文章的主題本質(zhì)上是“如何使用虛幻引擎構(gòu)建 3D 工具”,這是一個(gè)很大的話題。在本文結(jié)束時(shí),我將介紹交互式工具框架,這是 Unreal Engine 4.26 中的一個(gè)系統(tǒng),可以相對簡單地構(gòu)建多種類型的交互式 3D 工具。我將專注于“在運(yùn)行時(shí)”使用這個(gè)框架,即在構(gòu)建的游戲中。但是,我們使用這個(gè)完全相同的框架在虛幻編輯器中構(gòu)建 3D 建模工具套件。而且,其中許多工具都可以在運(yùn)行時(shí)直接使用!在你的游戲中雕刻!它太酷了。
下面有一個(gè) ToolsFramworkDemo 應(yīng)用程序的短視頻和一些屏幕截圖 - 這是一個(gè)構(gòu)建的可執(zhí)行文件,不在 UE 編輯器中運(yùn)行(盡管也可以)。該演示允許你創(chuàng)建一組網(wǎng)格,可以通過單擊進(jìn)行選擇(通過 shift-click/ctrl-click 支持多選),并為活動(dòng)選擇顯示 3D 變換 Gizmo。左側(cè)的一小組 UI 按鈕用于執(zhí)行各種操作。Add Bunny按鈕將導(dǎo)入和附加一個(gè)兔子網(wǎng)格,Undo和Redo會(huì)按照你的預(yù)期進(jìn)行。World按鈕在World 和 Local 坐標(biāo)系之間切換 Gizmo:
其余按鈕啟動(dòng)各種建模工具,它們與 UE 4.26 編輯器的建模模式中使用的工具實(shí)現(xiàn)完全相同。PolyExtrude是繪制多邊形工具,你可以在其中在 3D 工作平面上繪制一個(gè)封閉的多邊形(可以通過 ctrl 單擊重新定位),然后以交互方式設(shè)置擠出高度。PolyRevolve允許你在 3D 工作平面上繪制開放或封閉路徑 - 雙擊或關(guān)閉路徑到終點(diǎn) - 然后編輯生成的旋轉(zhuǎn)曲面。Edit Polygons是編輯器中的 PolyEdit 工具,在這里您可以選擇面/邊/頂點(diǎn)并使用 3D gizmo 移動(dòng)它們 — 請注意,各種 PolyEdit 子操作,如 Extrude 和 Inset,不會(huì)在 UI 中公開,但可以工作。
所有這些幾何圖形是在演示中創(chuàng)建的。選擇窗口并使用 GIZMO 旋轉(zhuǎn)。
Plane Cut使用工作平面切割網(wǎng)格,Boolean執(zhí)行網(wǎng)格布爾運(yùn)算(需要兩個(gè)選定對象)。Remesh重新對網(wǎng)格進(jìn)行三角剖分(不幸的是,我無法輕松顯示網(wǎng)格線框)。Vertex Sculpt允許您對頂點(diǎn)位置進(jìn)行基本的 3D 雕刻,而DynaSculpt 進(jìn)行自適應(yīng)拓?fù)涞窨蹋@就是我在屏幕截圖中展示的應(yīng)用于 Bunny 的內(nèi)容。最后,Accept和Cancel按鈕應(yīng)用或放棄當(dāng)前的工具結(jié)果(這只是一個(gè)預(yù)覽) - 我將在下面進(jìn)一步解釋。
兔子長出一些新的部分
這不是一個(gè)功能齊全的 3D 建模工具,它只是一個(gè)基本的演示。一方面,沒有任何形式的保存或?qū)С?,不過,添加一個(gè)快速的 OBJ 導(dǎo)出并不難!不存在對分配材質(zhì)的支持,您看到的材質(zhì)是硬編碼的或由工具自動(dòng)使用,例如動(dòng)態(tài)網(wǎng)格雕刻中的平面著色。同樣,一個(gè)積極的 C++ 開發(fā)人員可以相對容易地添加類似的東西。2D 用戶界面是一個(gè)非?;镜?UMG 用戶界面。我假設(shè)這是一次性的,你將構(gòu)建自己的 UI。再說一次,如果你想做一個(gè)非常簡單的特定領(lǐng)域的建模工具,比如一個(gè)用于清理醫(yī)學(xué)掃描的 3D 雕刻工具,你也許可以在稍加修改后擺脫這個(gè) UI。
在開始之前,本教程適用于 UE 4.26,你可以從Epic Games Launcher安裝它。本教程的項(xiàng)目位于 Github 上的
UnrealRuntimeToolsFrameworkDemo存儲(chǔ)庫(MIT 許可)。目前,該項(xiàng)目只能在 Windows 上運(yùn)行,因?yàn)樗蕾囉?/span>MeshModelingToolset引擎插件,該插件目前僅適用于 Windows。讓該插件在 OSX/Linux 上工作主要是選擇性刪除的問題,但它需要引擎源代碼構(gòu)建,這超出了本教程的范圍。
進(jìn)入頂級文件夾后,右鍵單擊Windows 資源管理器中的
ToolsFrameworkDemo.uproject ,然后從上下文菜單中選擇Generate Visual Studio project files 。這將生成ToolsFrameworkDemo.sln,你可以使用它來打開 Visual Studio。也可以直接在編輯器中打開 .uproject — 它會(huì)要求編譯,但可能需要參考 C++ 代碼才能真正了解該項(xiàng)目中發(fā)生的情況。
構(gòu)建解決方案并啟動(dòng)(按 F5),編輯器應(yīng)打開到示例地圖中。可以使用主工具欄中的大播放按鈕在 PIE 中測試該項(xiàng)目,或者單擊啟動(dòng)按鈕來構(gòu)建一個(gè)熟化的可執(zhí)行文件。這將需要幾分鐘,之后構(gòu)建的游戲?qū)⒃趩为?dú)的窗口中彈出。如果它以這種方式啟動(dòng)(我認(rèn)為這是默認(rèn)設(shè)置),可以點(diǎn)擊 Escape 退出全屏。在全屏模式下,你必須按Alt+F4退出,因?yàn)闆]有菜單/UI。
這篇文章太長了,需要一個(gè)目錄。以下是我要介紹的內(nèi)容:
首先,我將解釋交互式工具框架(ITF) 作為一個(gè)概念的一些背景。它來自哪里,它試圖解決什么問題。隨意跳過這個(gè) author-on-his-soapbox 部分,因?yàn)楸疚牡钠溆嗖糠植灰匀魏畏绞揭蕾囁?/span>
接下來我將解釋 UE4 交互工具框架的主要部分。我們將從工具、工具構(gòu)建器和工具管理器開始,并討論工具生命周期、接受/取消模型和基礎(chǔ)工具。輸入處理將在輸入行為系統(tǒng)、通過工具屬性集存儲(chǔ)的工具設(shè)置和工具操作中進(jìn)行介紹。
接下來我將解釋Gizmos系統(tǒng),用于實(shí)現(xiàn)視口內(nèi) 3D 小部件,重點(diǎn)介紹上面剪輯/圖像中顯示的標(biāo)準(zhǔn) UTransformGizmo 。
在 ITF 的最高級別,我們有Tools Context 和 ToolContext API,我將詳細(xì)介紹 ITF 的客戶端需要實(shí)現(xiàn)的 4 個(gè)不同的 API - IToolsContextQueriesAPI、
IToolsContextTransactionsAPI、IToolsContextRenderAPI 和 IToolsContextAssetAPI。然后我們將介紹一些特定于網(wǎng)格編輯工具的細(xì)節(jié),特別是Actor/Component Selections、
FPrimitiveComponentTargets和FComponentTargetFactory。
到目前為止,一切都將與 UE4.26 附帶的 ITF 模塊有關(guān)。為了在運(yùn)行時(shí)使用 ITF,我們將創(chuàng)建我們自己的運(yùn)行時(shí)工具框架后端,其中包括一個(gè)基本的可選網(wǎng)格“場景對象”的 3D 場景、一個(gè)非常標(biāo)準(zhǔn)的 3D 應(yīng)用程序變換 gizmo 系統(tǒng)以及 ToolsContext API 的實(shí)現(xiàn) I上面提到的與這個(gè)運(yùn)行時(shí)場景系統(tǒng)兼容的。本節(jié)主要解釋了我們必須添加到 ITF 以在運(yùn)行時(shí)使用它的額外位,因此您需要閱讀前面的部分才能真正理解它。
接下來我將介紹一些特定于演示的材料,包括使演示工作所需的ToolsFrameworkDemo 項(xiàng)目設(shè)置、RuntimeGeometryUtils 更新,特別是對
USimpleDynamicMeshComponent 的碰撞支持,然后是一些關(guān)于在運(yùn)行時(shí)使用建模模式工具的注釋,因?yàn)檫@通常需要一些膠水代碼才能使現(xiàn)有的網(wǎng)格編輯工具在游戲環(huán)境中發(fā)揮作用。
就是這樣!讓我們開始…
我不喜歡通過證明它的存在來開始一篇關(guān)于某事的文章的想法。但是,我想我需要。我花了很多年 - 基本上是我的整個(gè)職業(yè)生涯 - 構(gòu)建 3D 創(chuàng)建/編輯工具。我的第一個(gè)系統(tǒng)是ShapeShop(它自 2008 年以來一直沒有更新,但仍然可以工作——這是 Windows 向后兼容性的證明!)。我還構(gòu)建了 Meshmixer,它成為 Autodesk 產(chǎn)品,下載數(shù)百萬次,并被廣泛使用至今。通過Twitter搜索,我不斷驚訝于人們使用 Meshmixer 做的事情,很多數(shù)字牙醫(yī)??!。我還構(gòu)建了其他從未出現(xiàn)過的全功能系統(tǒng),例如我們稱之為手繪世界的 3D 透視草圖界面 ,是我在 Autodesk Research 構(gòu)建的。之后,我?guī)椭鷺?gòu)建了一些醫(yī)療 3D 設(shè)計(jì)工具,例如Archform 牙齒矯正器規(guī)劃應(yīng)用程序和NiaFit 小腿假肢插座設(shè)計(jì)工具(VR ),遺憾的是我在它有任何流行的希望之前就放棄了。
撇開自我祝賀不談,在過去 15 多年制作這些 3D 工具的過程中,我學(xué)到的是,制造一個(gè)巨大的混亂是非常容易的。我開始研究后來成為 Meshmixer 的東西,因?yàn)?Shapeshop 已經(jīng)到了無法添加任何東西的地步。然而,Shapeshop 的某些部分形成了一個(gè)非常早期的“工具框架”,我將其提取并用作其他各種項(xiàng)目的基礎(chǔ),甚至還有一些 Meshmixer(最終也變得非常脆弱?。?。該代碼仍在我的網(wǎng)站上。當(dāng)我離開 Autodesk 時(shí),我回到了如何構(gòu)建工具的這個(gè)問題,并創(chuàng)建了frame3Sharp 庫這使得在 C# 游戲引擎中構(gòu)建運(yùn)行時(shí) 3D 工具變得(相對)容易。這個(gè)框架圍繞上面提到的 Archform、NiaFit 和 Cotangent 應(yīng)用程序發(fā)展起來,并一直為它們提供動(dòng)力。但是,后來我加入了 Epic,并重新開始使用 C++!
所以,這就是 UE4 交互式工具框架的起源故事。使用這個(gè)框架,一個(gè)小團(tuán)隊(duì)(6 人或更少的人,取決于月份)在 UE4 中構(gòu)建了建模模式,它有 50 多個(gè)“工具”。有些非常簡單,例如使用選項(xiàng)復(fù)制事物的工具,有些則非常復(fù)雜,例如整個(gè) 3D 雕刻工具。但關(guān)鍵點(diǎn)是,工具代碼相對干凈且很大程度上獨(dú)立 - 幾乎所有工具都是一個(gè)獨(dú)立的 cpp/h 對。不是通過剪切和粘貼而獨(dú)立,而是獨(dú)立于這一點(diǎn),我們盡可能地將“標(biāo)準(zhǔn)”工具功能移動(dòng)到框架中,否則這些功能將不得不被復(fù)制。
我在解釋交互式工具框架時(shí)遇到的一個(gè)挑戰(zhàn)是我沒有參考點(diǎn)來比較它。大多數(shù) 3D 內(nèi)容創(chuàng)建工具在其代碼庫中都有一定程度的“工具框架”,但除非你嘗試向 Blender 添加功能,否則可能從未與這些東西進(jìn)行過交互。所以,我不能試圖通過類比來解釋。并且這些工具并沒有真正努力提供類似的原型框架作為大寫-F 框架。所以很難把握。(PS:如果您認(rèn)為您知道類似的Framework,請聯(lián)系并告訴我?。?/span>
但是,在其他類型的應(yīng)用程序開發(fā)中,框架非常常見。例如,如果你想構(gòu)建一個(gè) Web 應(yīng)用程序或移動(dòng)應(yīng)用程序,你幾乎肯定會(huì)使用一個(gè)定義明確的框架,如 Angular 或 React 或本月流行的任何東西(實(shí)際上有數(shù)百個(gè))。這些框架傾向于將“小部件”等低級方面與視圖等高級概念混合在一起。我在這里關(guān)注視圖,因?yàn)檫@些框架中的絕大多數(shù)都是基于視圖的概念。通常,前提是你擁有數(shù)據(jù),并且你希望將這些數(shù)據(jù)放入視圖中,并帶有一定數(shù)量的 UI,允許用戶探索和操作該數(shù)據(jù)。甚至還有一個(gè)標(biāo)準(zhǔn)術(shù)語,“模型-視圖-控制器”架構(gòu)。XCode 界面生成器是我所知道的最好的例子,你實(shí)際上是在故事板上用戶將看到的視圖,并通過這些視圖之間的轉(zhuǎn)換來定義應(yīng)用程序行為。我經(jīng)常使用的每個(gè)手機(jī)應(yīng)用程序都是這樣工作的。
提高復(fù)雜性,我們有像 Microsoft Word 或 Keynote 這樣的應(yīng)用程序,它們與基于視圖的應(yīng)用程序完全不同。在這些應(yīng)用程序中,用戶將大部分時(shí)間花在單個(gè)視圖中,并且直接操作內(nèi)容而不是抽象地與數(shù)據(jù)交互。但大部分操作都是以Commands的形式進(jìn)行的,例如刪除文本或編輯Properties。例如,在 Word 中,當(dāng)我不鍵入字母時(shí),我通常要么將鼠標(biāo)移動(dòng)到命令按鈕上以便我可以單擊它——一個(gè)離散的操作——要么打開對話框并更改屬性。我不做的是花費(fèi)大量時(shí)間使用連續(xù)的鼠標(biāo)輸入(拖放和選擇是明顯的例外)。
現(xiàn)在考慮一個(gè)內(nèi)容創(chuàng)建應(yīng)用程序,如 Photoshop 或 Blender。同樣,作為用戶,您將大部分時(shí)間花在標(biāo)準(zhǔn)化視圖中,并且你直接操作的是內(nèi)容而不是數(shù)據(jù)。仍然有大量具有屬性的命令和對話框。但是這些應(yīng)用程序的許多用戶——尤其是在創(chuàng)意環(huán)境中——也花費(fèi)大量時(shí)間非常小心地在按住其中一個(gè)按鈕的同時(shí)移動(dòng)鼠標(biāo)。此外,當(dāng)他們這樣做時(shí),應(yīng)用程序通常處于特定模式,其中鼠標(biāo)移動(dòng)(通常與修改熱鍵結(jié)合使用)以特定模式的方式被捕獲和解釋。該模式允許應(yīng)用程序在大量方式之間消除歧義,
mouse-movement-with-button-held-down動(dòng)作可以被解釋,本質(zhì)上是為了將捕獲的鼠標(biāo)輸入引導(dǎo)到正確的位置。這與命令根本不同,命令通常是無模式的,并且在輸入設(shè)備方面也是無狀態(tài)的。
除了模式之外,內(nèi)容創(chuàng)建應(yīng)用程序的一個(gè)標(biāo)志是我將稱為Gizmos的東西,它們是附加的臨時(shí)交互式視覺元素,它們不是內(nèi)容的一部分,但提供了一種(半無模式)操作內(nèi)容的方式。例如,可以單擊拖動(dòng)以調(diào)整矩形大小的矩形角上的小框或 V 形將是 Gizmo 的標(biāo)準(zhǔn)示例。這些通常被稱為小部件,但我認(rèn)為使用這個(gè)術(shù)語會(huì)讓人感到困惑,因?yàn)樗c按鈕和菜單小部件重疊,所以我將使用 Gizmos。
所以,現(xiàn)在我可以開始暗示交互式工具框架的用途了。在最基本的層面上,它提供了一種系統(tǒng)的方法來實(shí)現(xiàn)捕獲和響應(yīng)用戶輸入的模態(tài)狀態(tài),為了簡潔起見,我將其稱為交互工具或工具,以及實(shí)現(xiàn) Gizmos(我將假定它本質(zhì)上是空間本地化的上下文敏感模式,但我們可以將討論保存在 Twitter 上)。
這是我被問過很多次的問題,主要是那些沒有嘗試構(gòu)建復(fù)雜的基于工具的應(yīng)用程序的人。簡短的回答是,減少(但遺憾的是沒有消除)你制造邪惡災(zāi)難的機(jī)會(huì)。但我也會(huì)做一個(gè)長的回答。
關(guān)于基于工具的應(yīng)用程序需要了解的重要一點(diǎn)是,一旦你為用戶提供以任何順序使用工具的選項(xiàng),他們就會(huì)這樣做,這將使一切變得更加復(fù)雜。在基于視圖的應(yīng)用程序中,用戶通常是“On Rails”,因?yàn)閼?yīng)用程序允許在 Y 之后而不是之前執(zhí)行 X。當(dāng)我啟動(dòng) Twitter 應(yīng)用程序時(shí),我不能直接跳轉(zhuǎn)到所有內(nèi)容——我必須瀏覽一系列視圖。這允許應(yīng)用程序的開發(fā)人員對應(yīng)用程序狀態(tài)做出大量假設(shè)。特別是,盡管視圖可能會(huì)操作相同的底層 DataModel(幾乎總是某種形式的數(shù)據(jù)庫),但我永遠(yuǎn)不必?fù)?dān)心區(qū)分一個(gè)視圖中的點(diǎn)擊與另一個(gè)視圖中的點(diǎn)擊。在某種意義上,意見是模式,在特定視圖的上下文中,通常只有命令,沒有工具。
因此,在基于視圖的應(yīng)用程序中,談?wù)摴ぷ髁鞣浅H菀?。?chuàng)建基于視圖的應(yīng)用程序的人往往會(huì)畫很多類似這樣的圖表:
這些圖可能是視圖本身,但更多時(shí)候它們是用戶通過應(yīng)用程序所采取的步驟——如果你愿意的話,它們是用戶故事。它們并不總是嚴(yán)格線性的,可能存在分支和循環(huán)(Google Image Search for Workflow 有很多更復(fù)雜的示例)。但總是有明確的進(jìn)入和退出點(diǎn)。用戶從一個(gè)任務(wù)開始,并通過工作流完成該任務(wù)。然后很自然地設(shè)計(jì)一個(gè)應(yīng)用程序來提供用戶可以完成任務(wù)的工作流。我們可以通過 Workflow 有意義地談?wù)?Progress,關(guān)聯(lián)的 Data 和 Application State 也構(gòu)成了一種 Progress。隨著額外任務(wù)的添加,開發(fā)團(tuán)隊(duì)的工作是提出一種設(shè)計(jì),以允許有效地完成這些必要的工作流程。
內(nèi)容創(chuàng)建/編輯應(yīng)用程序的根本復(fù)雜性在于,這種方法根本不適用于它們。我認(rèn)為最終的區(qū)別在于內(nèi)容創(chuàng)建/編輯工具中沒有固有的進(jìn)度概念。例如,作為 Powerpoint 用戶,我可以(而且確實(shí)?。┗◣讉€(gè)小時(shí)重新組織我的幻燈片,調(diào)整圖像大小和對齊方式,稍微調(diào)整文本。在我看來,我可能對進(jìn)度有一些模糊的概念,但這并沒有在應(yīng)用程序中編碼。我的任務(wù)在應(yīng)用程序之外。如果沒有明確的任務(wù)或進(jìn)度衡量標(biāo)準(zhǔn),就沒有工作流程!
我認(rèn)為內(nèi)容創(chuàng)建/編輯應(yīng)用程序更有用的心智模型就像右邊的圖像。綠色中央集線器是這些應(yīng)用程序中的默認(rèn)狀態(tài),通常你只是在其中查看你的內(nèi)容。例如,在 Photoshop 中平移和縮放圖像,或在 Blender 中瀏覽 3D 場景。這是用戶花費(fèi)大量時(shí)間的地方。藍(lán)色輻條是工具。我會(huì)去一個(gè)工具一段時(shí)間,但我總是回到中心。
因此,如果我要隨著時(shí)間的推移跟蹤我的狀態(tài),那將是通過無數(shù)工具進(jìn)出默認(rèn)集線器的曲折路徑。沒有明確定義的順序,作為用戶,我通??梢园凑瘴艺J(rèn)為合適的任何順序自由使用工具。在一個(gè)縮影中,我們可能能夠找到定義明確的小型工作流來分析和優(yōu)化,但在應(yīng)用程序級別,工作流實(shí)際上是無限的。
看起來相對明顯的是,你需要在此處采用的架構(gòu)方法與在視圖方法中的不同。通過以正確的方式瞇眼看它,人們可能會(huì)爭辯說每個(gè)工具基本上都是一個(gè)視圖,那么這里真正不同的是什么?根據(jù)我的經(jīng)驗(yàn),不同之處在于我認(rèn)為是Tool Sprawl。
如果你有明確定義的工作流程,那么很容易判斷什么是必要的,什么是不必要的。與所需工作流程無關(guān)的功能不僅會(huì)浪費(fèi)設(shè)計(jì)和工程時(shí)間,而且最終會(huì)使工作流程變得比必要的復(fù)雜——這會(huì)使用戶體驗(yàn)變得更糟!現(xiàn)代軟件開發(fā)的正統(tǒng)觀念非常關(guān)注這個(gè)前提——構(gòu)建最小可行的產(chǎn)品,然后迭代、迭代、迭代以消除用戶的摩擦。
基于工具的應(yīng)用程序根本不同,因?yàn)槊吭黾右粋€(gè)工具都會(huì)增加應(yīng)用程序的價(jià)值。如果我沒有使用特定工具,那么除了啟動(dòng)該工具所需的附加工具欄按鈕帶來的小 UI 開銷之外,它的添加幾乎不會(huì)影響我。當(dāng)然,學(xué)習(xí)新工具需要付出一些努力。但是,這種努力的回報(bào)是這個(gè)新工具現(xiàn)在可以與所有其他工具相結(jié)合!這導(dǎo)致了一種應(yīng)用級網(wǎng)絡(luò)效應(yīng),其中每個(gè)新工具都是所有現(xiàn)有工具的力量倍增器。如果觀察幾乎所有主要的內(nèi)容創(chuàng)建/編輯工具,這一點(diǎn)就會(huì)立即顯現(xiàn)出來,其中有無數(shù)的工具欄和工具欄菜單以及工具欄的嵌套選項(xiàng)卡,隱藏在其他工具欄后面。對局外人來說,這看起來很瘋狂,但對用戶來說,
許多來自面向工作流的軟件世界的人都驚恐地看著這些應(yīng)用程序。我觀察到許多新項(xiàng)目,其中團(tuán)隊(duì)開始嘗試構(gòu)建一些“簡單”的東西,專注于“核心工作流程”,也許是為“新手用戶”繪制的,并且繪制了許多漂亮的線性工作流程圖。但現(xiàn)實(shí)情況是,新手用戶在掌握你的應(yīng)用程序之前只是新手,然后他們會(huì)立即要求更多功能。因此,你將在這里和那里添加一個(gè)工具。幾年后,你將擁有一套龐大的工具,如果沒有系統(tǒng)的方法來組織它們,手上就會(huì)一團(tuán)糟。
混亂從何而來?據(jù)我所見,有幾種常見的惹麻煩的方法。首先是低估了手頭任務(wù)的復(fù)雜性。許多內(nèi)容創(chuàng)建應(yīng)用程序以“查看器”開始,其中所有應(yīng)用程序邏輯(如 3D 相機(jī)控件)都直接在鼠標(biāo)和 UI 按鈕處理程序中完成。然后隨著時(shí)間的推移,只需添加更多 if/else 分支或 switch case,就可以合并新的編輯功能。這種方法可以持續(xù)很長時(shí)間,而且我工作過的許多 3D 應(yīng)用程序的核心仍然是這些殘留的代碼分支。但是你只是在挖掘一個(gè)更深的代碼洞并用代碼意大利面填充它。最終,將需要一些實(shí)際的軟件架構(gòu),并且需要進(jìn)行痛苦的重構(gòu)工作(隨后是多年的修復(fù)回歸,
即使有一定數(shù)量的“工具架構(gòu)”,如何處理設(shè)備輸入也很棘手,而且往往最終導(dǎo)致混亂的架構(gòu)鎖定。鑒于“工具”通常由設(shè)備輸入驅(qū)動(dòng),一個(gè)看似顯而易見的方法是直接為工具提供輸入事件處理程序,如
OnMouseUp/OnMouseMove/OnMouseDown 函數(shù)。這成為放置“做事”代碼的自然位置,例如在鼠標(biāo)事件上,你可以直接在繪畫工具中應(yīng)用畫筆印章。在用戶要求支持其他輸入設(shè)備(如觸摸、筆或 VR 控制器)之前,這似乎是無害的。怎么辦?只是將呼叫轉(zhuǎn)發(fā)給鼠標(biāo)處理程序嗎?壓力或 3D 位置呢?然后是自動(dòng)化,當(dāng)用戶開始要求能夠?yàn)槟愕墓ぞ呔帉懩_本時(shí)。它不是。絕對不。真的,不要)。
將重要代碼放入輸入事件處理程序還會(huì)導(dǎo)致諸如標(biāo)準(zhǔn)事件處理模式的猖獗復(fù)制粘貼之類的事情,如果需要進(jìn)行更改,這可能會(huì)很乏味。而且,昂貴的鼠標(biāo)事件處理程序?qū)嶋H上會(huì)使您的應(yīng)用程序感覺不如應(yīng)有的響應(yīng),這是由于稱為鼠標(biāo)事件優(yōu)先級的東西。所以,你真的要小心處理工具架構(gòu)的這一部分,因?yàn)榭此茦?biāo)準(zhǔn)的設(shè)計(jì)模式可能會(huì)引發(fā)一系列問題。
同時(shí),如果工具架構(gòu)定義過于嚴(yán)格,它可能成為擴(kuò)展工具集的障礙,因?yàn)樾碌男枨蟛弧胺稀背跏荚O(shè)計(jì)的假設(shè)。如果許多工具都建立在初始架構(gòu)之上,那么更改就變得棘手,然后聰明的工程師被迫想出變通辦法,現(xiàn)在你有兩個(gè)(或更多)工具架構(gòu)。最大的挑戰(zhàn)之一就是如何在工具實(shí)現(xiàn)和框架之間劃分職責(zé)。
我不能聲稱交互式工具框架 (ITF) 會(huì)為你解決這些問題。最終,任何成功的軟件最終都會(huì)被早期的設(shè)計(jì)決策所困,在這些決策之上已經(jīng)建造了高山,而改變路線只能付出巨大的代價(jià)。我可以整天給你講故事,關(guān)于我是如何對自己做到這一點(diǎn)的。我能說的是,在 UE4 中實(shí)現(xiàn)的 ITF 希望能從我過去的錯(cuò)誤中受益。在過去的 2 年中,我們使用 ITF 在 UE4 編輯器中構(gòu)建新工具的經(jīng)驗(yàn)(到目前為止)相對輕松,我們一直在尋找消除任何摩擦點(diǎn)的方法。
如上所述,交互工具是應(yīng)用程序的模態(tài)狀態(tài),在此期間可以以特定方式捕獲和解釋設(shè)備輸入。在交互式工具框架 (ITF) 中,UInteractiveTool基類表示模態(tài)狀態(tài),并具有你可能需要實(shí)現(xiàn)的非常小的 API 函數(shù)集。下面我總結(jié)了 psuedo-C++ 中的核心 UInteractiveTool API — 為簡潔起見,我省略了虛擬、常量、可選參數(shù)等內(nèi)容。我們稍后會(huì)在一定程度上介紹其他 API 函數(shù)集,但這些是關(guān)鍵的。在::Setup()中初始化您的工具,并在::Shutdown()中進(jìn)行任何最終確定和清理,這也是你執(zhí)行“應(yīng)用”操作之類的地方。EToolShutdownType與HasAccept()和CanAccept()函數(shù)有關(guān),我將在下面詳細(xì)解釋。最后,工具將有機(jī)會(huì)渲染()并勾選每一幀。請注意,還有一個(gè) ::Tick() 函數(shù),但你應(yīng)該重寫::OnTick()因?yàn)榛?::Tick() 具有必須始終運(yùn)行的關(guān)鍵功能。
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 不是一個(gè)獨(dú)立的對象,你不能簡單地自己生成一個(gè)。為了使其發(fā)揮作用,必須調(diào)用
Setup/Render/Tick/Shutdown,并傳遞諸如IToolsContextRenderAPI之類的適當(dāng)實(shí)現(xiàn),從而允許工具繪制線條/等。我將在下面進(jìn)一步解釋。但是現(xiàn)在你需要知道的是,要?jiǎng)?chuàng)建一個(gè) Tool 實(shí)例,你需要從UInteractiveToolManager請求一個(gè)。要允許 ToolManager 構(gòu)建任意類型,您需要向 ToolManager 注冊一個(gè) <String, UInteractiveToolBuilder > 對。UInteractiveToolBuilder 是一個(gè)非常簡單的工廠模式基類,必須為每種工具類型實(shí)現(xiàn):
UCLASS()
class UInteractiveToolBuilder : public UObject
{
bool CanBuildTool(const FToolBuilderState& SceneState);
UInteractiveTool* BuildTool(const FToolBuilderState& SceneState);
};
UInteractiveToolManager的主要 API總結(jié)如下。通常,你不需要實(shí)現(xiàn)自己的 ToolManager,基本實(shí)現(xiàn)功能齊全,應(yīng)該完成使用工具所需的一切。但如有必要,你可以自由擴(kuò)展子類中的各種功能。
下面的函數(shù)大致按照你調(diào)用它們的順序列出。RegisterToolType()將字符串標(biāo)識(shí)符與 ToolBuilder 實(shí)現(xiàn)相關(guān)聯(lián)。然后應(yīng)用程序使用SelectActiveToolType()設(shè)置一個(gè)活動(dòng)的生成器,然后使用ActivateTool()創(chuàng)建一個(gè)新的 UInteractiveTool 實(shí)例。有 getter 可以訪問活動(dòng)工具,但實(shí)際上很少有人經(jīng)常調(diào)用。應(yīng)用程序必須在每一幀調(diào)用 Render() 和 Tick() 函數(shù),然后應(yīng)用程序調(diào)用活動(dòng)工具的相關(guān)函數(shù)。最后DeactiveTool()用于終止活動(dòng)工具。
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);
};
在高層次上,工具的生命周期如下
注意最后一步。工具是 UObject,因此你不能依賴 C++ 析構(gòu)函數(shù)進(jìn)行清理。你應(yīng)該在 Shutdown() 實(shí)現(xiàn)中進(jìn)行任何清理,例如銷毀臨時(shí)參與者。
工具可以以兩種不同的方式支持終止,具體取決于工具支持的交互類型。更復(fù)雜的替代方案是可以接受 — EToolShutdownType::Accept 或取消 EToolShutdownType::Cancel 的工具。這通常在工具的交互支持某種操作的實(shí)時(shí)預(yù)覽時(shí)使用,用戶可能希望放棄該操作。例如,將網(wǎng)格簡化算法應(yīng)用于選定網(wǎng)格的工具可能具有用戶可能希望探索的一些參數(shù),但如果探索不令人滿意,則用戶可能更愿意根本不應(yīng)用簡化。在這種情況下,UI 可以提供按鈕來接受或取消活動(dòng)工具,這會(huì)導(dǎo)致使用適當(dāng)?shù)?EToolShutdownType 值調(diào)用 ToolManager::DeactiveTool()。
第二個(gè)終止選項(xiàng) -
EToolShutdownType::Completed - 更簡單,因?yàn)樗皇侵甘竟ぞ邞?yīng)該“退出”。這種類型的終止可用于處理沒有明確的“接受”或“取消”操作的情況,例如在簡單可視化數(shù)據(jù)的工具中,增量應(yīng)用編輯操作的工具(例如基于點(diǎn)擊點(diǎn)生成對象),等等。
需要明確的是,你在使用 ITF 時(shí)不需要使用或支持接受/取消式工具。這樣做通常會(huì)導(dǎo)致更復(fù)雜的 UI。如果你在應(yīng)用程序中支持 Undo,那么即使是具有 Accept 和 Cancel 選項(xiàng)的 Tools,也可以等效為 Complete-style Tools,如果用戶不滿意,也可以 Undo。但是,如果工具完成可能涉及冗長的計(jì)算或以某種方式具有破壞性,則支持接受/取消往往會(huì)帶來更好的用戶體驗(yàn)。在 UE 編輯器的建模模式中,我們通常在編輯靜態(tài)網(wǎng)格體資源時(shí)使用 Accept/Cancel 正是出于這個(gè)原因。
你必須做出的另一個(gè)決定是如何處理工具的模態(tài)性質(zhì)。通常,將用戶視為“處于”工具中是有用的,即處于特定的模態(tài)狀態(tài)。那么他們是如何“走出去”的呢?您可以要求用戶明確單擊接受/取消/完成按鈕以退出活動(dòng)工具,這是最簡單和最明確的,但確實(shí)意味著需要單擊,并且用戶必須在心理上意識(shí)到并管理此狀態(tài)?;蛘?,當(dāng)用戶在工具工具欄/菜單/等中選擇另一個(gè)工具時(shí)(例如),你可以自動(dòng)接受/取消/完成。然而,這引發(fā)了一個(gè)棘手的問題,即應(yīng)該自動(dòng)接受還是自動(dòng)取消。這個(gè)問題沒有正確答案,你必須決定什么最適合你的特定環(huán)境 —雖然根據(jù)我的經(jīng)驗(yàn),當(dāng)一個(gè)人意外誤點(diǎn)擊時(shí),自動(dòng)取消可能會(huì)非常令人沮喪!
ITF 的主要目標(biāo)之一是減少編寫工具所需的樣板代碼量,并提高一致性。幾個(gè)“工具模式”出現(xiàn)得如此頻繁,以至于我們在 ITF 的 /BaseTools/ 子文件夾中包含了它們的標(biāo)準(zhǔn)實(shí)現(xiàn)。基本工具通常包括一個(gè)或多個(gè) InputBehaviors(見下文),其操作映射到您可以覆蓋和實(shí)現(xiàn)的虛擬功能。我將簡要介紹這些基本工具中的每一個(gè),因?yàn)樗鼈兗仁菢?gòu)建您自己的工具的有用方式,也是如何做事的示例代碼的良好來源:
USingleClickTool捕獲鼠標(biāo)單擊輸入,如果IsHitByClick()函數(shù)返回有效點(diǎn)擊,則調(diào)用OnClicked()函數(shù)。您提供這兩個(gè)的實(shí)現(xiàn)。請注意,此處的FInputDeviceRay結(jié)構(gòu)包括 2D 鼠標(biāo)位置和 3D 射線。
class INTERACTIVETOOLSFRAMEWORK_API USingleClickTool : public UInteractiveTool
{
FInputRayHit IsHitByClick(const FInputDeviceRay& ClickPos);
void OnClicked(const FInputDeviceRay& ClickPos);
};
UClickDragTool捕獲并轉(zhuǎn)發(fā)連續(xù)的鼠標(biāo)輸入,而不是單擊。如果CanBeginClickDragSequence()返回 true —通常你會(huì)在此處進(jìn)行命中測試,類似于 USingleClickTool,則將調(diào)用 OnClickPress() / OnClickDrag() / OnClickRelease(),類似于標(biāo)準(zhǔn) 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 相似之處在于它提供了單擊-拖動(dòng)-釋放輸入處理模式。但是,UMesSurfacePointTool 假定它正在作用于一個(gè)目標(biāo) UPrimitiveComponent —它是如何獲取這個(gè) Component 的將在下面解釋。下面HitTest()函數(shù)的默認(rèn)實(shí)現(xiàn)將使用標(biāo)準(zhǔn) LineTraces — 因此,如果足夠的話,你不必重寫此函數(shù)。UMeshSurfacePointTool 還支持懸停,并跟蹤 Shift 和 Ctrl 修飾鍵的狀態(tài)。對于簡單的“表面繪圖”類型工具,這是一個(gè)很好的起點(diǎn),許多建模模式工具派生自 UMeshSurfacePointTool — 一個(gè)小提示:這個(gè)類也支持閱讀手寫筆壓力,但是在 UE4.26 手寫筆輸入是 Editor-Only。
附注:雖然命名為 UMeshSurfacePointTool,但其實(shí)并不需要Mesh,只需要一個(gè)支持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();
};
還有第四個(gè)基礎(chǔ)工具,UBaseBrushTool,它擴(kuò)展了 UMeshSurfacePointTool,具有各種特定于基于畫筆的 3D 工具的功能,即表面繪畫筆刷、3D 雕刻工具等。這包括一組標(biāo)準(zhǔn)畫筆屬性、一個(gè) 3D 畫筆位置/大小/衰減指示器、“畫筆印記”跟蹤以及各種其他有用的位。如果你正在構(gòu)建畫筆式工具,可能會(huì)發(fā)現(xiàn)這很有用。
UInteractiveToolBuilder API 函數(shù)都采用 FToolBuilderState 參數(shù)。此結(jié)構(gòu)的主要目的是提供選擇信息 - 它指示工具將或應(yīng)該采取的行動(dòng)。結(jié)構(gòu)的關(guān)鍵字段如下所示。ToolManager 將構(gòu)造一個(gè) FToolBuilderState 并將其傳遞給 ToolBuilders,然后 ToolBuilders 將使用它來確定它們是否可以對 Selection 進(jìn)行操作。在 UE4.26 ITF 實(shí)現(xiàn)中,Actor 和 Components 都可以傳遞,但也只能傳遞 Actor 和 Components。請注意,如果一個(gè)組件出現(xiàn)在 SelectedComponents 中,那么它的 Actor 將在 SelectedActors 中。包含這些 Actor 的 UWorld 也包括在內(nèi)。
struct FToolBuilderState
{
UWorld* World;
TArray<AActor*> SelectedActors;
TArray<UActorComponent*> SelectedComponents;
};
在建模模式工具中,我們不直接對組件進(jìn)行操作,我們將它們包裝在一個(gè)標(biāo)準(zhǔn)容器中,這樣我們就可以,例如,3D 雕刻具有容器實(shí)現(xiàn)的“任何”網(wǎng)格組件。這在很大程度上是我可以編寫本教程的原因,因?yàn)槲铱梢宰屵@些工具編輯其他類型的網(wǎng)格,例如運(yùn)行時(shí)網(wǎng)格。但是在構(gòu)建自己的工具時(shí),你可以隨意忽略 FToolBuilderState。你的 ToolBuilder 可以使用任何其他方式來查詢場景狀態(tài),并且你的工具不限于作用于 Actor 或組件。
ITF 用戶經(jīng)常提出的一個(gè)問題是 UInteractiveToolBuilder 是否必要。在最簡單的情況下,也就是最常見的情況下,你的 ToolBuilder 將是簡單的樣板代碼 —不幸的是,因?yàn)樗且粋€(gè) UObject,這個(gè)樣板不能直接轉(zhuǎn)換為 C++ 模板。當(dāng)人們開始重新利用現(xiàn)有的 UInteractiveTool 實(shí)現(xiàn)來解決不同的問題時(shí),ToolBuilders 的實(shí)用程序就會(huì)出現(xiàn)。
例如,在 UE 編輯器中,我們有一個(gè)用于編輯網(wǎng)格多邊形組(實(shí)際上是多邊形)的工具,稱為 PolyEdit。我們還有一個(gè)非常相似的工具用于編輯網(wǎng)格三角形,稱為 TriEdit。在引擎蓋下,這些是相同的 UInteractiveTool 類。在 TriEdit 模式下,Setup() 函數(shù)將工具的各個(gè)方面配置為適合三角形。為了在 UI 中公開這兩種模式,我們使用了兩個(gè)獨(dú)立的 ToolBuilder,它們在創(chuàng)建的 Tool 實(shí)例被分配之后、Setup() 運(yùn)行之前設(shè)置了一個(gè)“bIsTriangleMode”標(biāo)志。
我當(dāng)然不會(huì)聲稱這是一個(gè)優(yōu)雅的解決方案。但是,這是權(quán)宜之計(jì)。根據(jù)我的經(jīng)驗(yàn),隨著你的工具集不斷發(fā)展以處理新情況,這種情況總是會(huì)出現(xiàn)。通常可以通過一些自定義初始化、一些附加選項(xiàng)/屬性等來填充現(xiàn)有工具來解決新問題。在理想世界中,人們會(huì)重構(gòu)工具以通過子類化或組合來實(shí)現(xiàn)這一點(diǎn),但我們很少生活在理想世界中。因此,破解工具以完成第二項(xiàng)工作所需的一些難看的代碼可以放置在自定義 ToolBuilder 中,并(相對)封裝在其中。
使用 ToolManager 注冊 ToolBuilder 的基于字符串的系統(tǒng)可以允許你的 UI 級別(即按鈕處理程序等)啟動(dòng)工具,而無需實(shí)際了解 Tool 類類型。這通??梢栽跇?gòu)建 UI 時(shí)實(shí)現(xiàn)更清晰的關(guān)注點(diǎn)分離。例如,在我將在下面描述的 ToolsFrameworkDemo 中,工具是由 UMG 藍(lán)圖小部件啟動(dòng)的,它們只是將字符串常量傳遞給 BP 函數(shù)——它們根本不了解工具系統(tǒng)。 然而,在生成工具之前需要設(shè)置一個(gè)“活動(dòng)”構(gòu)建器有點(diǎn)像退化的肢體,這些操作可能會(huì)在未來結(jié)合起來。
上面我說過“交互式工具是應(yīng)用程序的模態(tài)狀態(tài),在此期間可以以特定方式捕獲和解釋設(shè)備輸入”。但是 UInteractiveTool API 沒有任何鼠標(biāo)輸入處理函數(shù)。這是因?yàn)檩斎胩幚恚ù蟛糠郑┡c工具分離。輸入由工具創(chuàng)建并注冊到UInputRouter的UInputBehavior對象捕獲和解釋, UInputRouter “擁有”輸入設(shè)備并將輸入事件路由到適當(dāng)?shù)男袨椤?/span>
這種分離的原因是絕大多數(shù)輸入處理代碼都是剪切和粘貼的,在特定交互的實(shí)現(xiàn)方式上略有不同。例如考慮一個(gè)簡單的按鈕點(diǎn)擊交互。在一個(gè)常見的事件 API 中,您將擁有可以實(shí)現(xiàn)的 OnMouseDown()、OnMouseMove() 和 OnMouseUp() 等函數(shù),假設(shè)你希望將這些事件映射到單個(gè) OnClickEvent() 處理程序,以便按下按鈕-釋放動(dòng)作。數(shù)量驚人的應(yīng)用程序(尤其是 Web 應(yīng)用程序)會(huì)觸發(fā) OnMouseDown 中的點(diǎn)擊——這是錯(cuò)誤的!但是,在 OnMouseUp 中盲目地觸發(fā) OnClickEvent 也是錯(cuò)誤的!這里的正確行為實(shí)際上是相當(dāng)復(fù)雜的。在 OnMouseDown() 中,你必須對按鈕進(jìn)行點(diǎn)擊測試,并開始捕獲鼠標(biāo)輸入。在 OnMouseUp 中,你必須點(diǎn)擊測試按鈕再次,如果光標(biāo)仍在點(diǎn)擊按鈕,則僅觸發(fā) OnClickEvent。這允許取消點(diǎn)擊,并且是所有嚴(yán)肅的 UI 工具包如何實(shí)現(xiàn)它(試試看?。?。
如果你甚至擁有數(shù)十個(gè)工具,那么實(shí)現(xiàn)所有這些處理代碼,特別是針對多個(gè)設(shè)備,將變得非常容易出錯(cuò)。因此,出于這個(gè)原因,ITF 將這些小的輸入事件處理狀態(tài)機(jī)移動(dòng)到 UInputBehavior 實(shí)現(xiàn)中,這些實(shí)現(xiàn)可以在許多工具之間共享。事實(shí)上,一些簡單的行為,如USingleClickInputBehavior、UClickDragBehavior和UHoverBehavior 可以處理大多數(shù)鼠標(biāo)驅(qū)動(dòng)交互的情況。然后,行為通過工具或 Gizmo 等可以實(shí)現(xiàn)的簡單接口將其提煉的事件轉(zhuǎn)發(fā)到目標(biāo)對象。例如 USingleClickInputBehavior 可以作用于任何實(shí)現(xiàn) IClickBehaviorTarget 的東西,它只有兩個(gè)函數(shù) - IsHitByClick() 和 OnClicked()。請注意,由于 InputBehavior 不知道它作用于什么——“按鈕”可以是 2D 矩形或任意 3D 形狀——Target 接口必須提供命中測試功能。
InputBehavior 系統(tǒng)的另一個(gè)方面是工具不直接與 UInputRouter 對話。他們只提供他們希望激活的 UInputBehavior 的列表。UInteractiveTool API 添加的支持此功能如下所示。通常,在工具的 ::Setup() 實(shí)現(xiàn)中,會(huì)創(chuàng)建和配置一個(gè)或多個(gè)輸入行為,然后將其傳遞給 AddInputBehavior。然后,ITF 在必要時(shí)調(diào)用 GetInputBehaviors,將這些行為注冊到 UInputRouter。注意:目前 InputBehavior 集不能在工具期間動(dòng)態(tài)更改,但是您可以配置您的 Behaviors 以根據(jù)您希望的任何標(biāo)準(zhǔn)忽略事件。
class UInteractiveTool : public UObject, public IInputBehaviorSource
{
// ...previous functions...
void AddInputBehavior(UInputBehavior* Behavior);
const UInputBehaviorSet* GetInputBehaviors();
};
UInputRouter與UInteractiveToolManager的相似之處在于默認(rèn)實(shí)現(xiàn)足以滿足大多數(shù)用途。InputRouter 的唯一工作是跟蹤所有活動(dòng)的 InputBehavior 并調(diào)解捕獲的輸入設(shè)備。捕獲是工具中輸入處理的核心。當(dāng) MouseDown 事件進(jìn)入 InputRouter 時(shí),它會(huì)檢查所有已注冊的 Behaviors 以詢問它們是否要開始捕獲鼠標(biāo)事件流。例如,如果您按下一個(gè)按鈕,該按鈕注冊的 USingleClickInputBehavior 將表明是的,它想要開始捕獲。一次只允許單個(gè)行為捕獲輸入,并且可能需要捕獲多個(gè)行為(彼此不了解) - 例如,與當(dāng)前視圖重疊的 3D 對象。因此,每個(gè) Behavior 返回一個(gè) FInputCaptureRequest,指示“是”或“否”以及深度測試和優(yōu)先級信息。UInputRouter 然后查看所有捕獲請求,并根據(jù)深度排序和優(yōu)先級,選擇一個(gè)行為并告訴它捕獲將開始。然后 MouseMove 和 MouseRelease 事件僅傳遞給該行為,直到 Capture 終止(通常在 MouseRelease 上)。
實(shí)際上,在使用 ITF 時(shí),你很少需要與 UInputRouter 交互。一旦建立了應(yīng)用程序級鼠標(biāo)事件和 InputRouter 之間的連接,你就不需要再次觸摸它了。該系統(tǒng)主要處理常見錯(cuò)誤,例如由于捕獲出錯(cuò)而導(dǎo)致鼠標(biāo)處理“卡住”,因?yàn)?UInputRouter 最終控制鼠標(biāo)捕獲,而不是單個(gè)行為或工具。在隨附的 ToolsFrameworkDemo 項(xiàng)目中,我已經(jīng)實(shí)現(xiàn)了 UInputRouter 運(yùn)行所需的一切。
基本的 UInputBehavior API 如下所示。FInputDeviceState是一個(gè)大型結(jié)構(gòu),包含給定事件/時(shí)間的所有輸入設(shè)備狀態(tài),包括常用修飾鍵的狀態(tài)、鼠標(biāo)按鈕狀態(tài)、鼠標(biāo)位置等。與許多輸入事件的一個(gè)主要區(qū)別是還包括與輸入設(shè)備位置相關(guān)的 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 中省略了一些額外的參數(shù),以簡化事情。特別是如果你實(shí)現(xiàn)自己的行為,你會(huì)發(fā)現(xiàn)幾乎到處都有一個(gè) EInputCaptureSide 枚舉,主要作為默認(rèn)的 EInputCaptureSide::Any。這是為了將來使用,以支持行為可能特定于任一手的 VR 控制器的情況。
但是,對于大多數(shù)應(yīng)用程序,你可能會(huì)發(fā)現(xiàn)實(shí)際上不必實(shí)現(xiàn)自己的行為。一組標(biāo)準(zhǔn)行為,例如上面提到的那些,包含在 InteractiveToolFramework 模塊的 /BaseBehaviors/ 文件夾中。大多數(shù)標(biāo)準(zhǔn)行為都是從基類UAnyButtonInputBehavior 派生的,它允許它們使用任何鼠標(biāo)按鈕,包括由 TFunction(可能是鍵盤鍵)定義的“自定義”按鈕!類似地,標(biāo)準(zhǔn) BehaviorTarget 實(shí)現(xiàn)都派生自
IModifierToggleBehaviorTarget,它允許在 Behavior 上配置任意修飾鍵并將其轉(zhuǎn)發(fā)到 Target,而無需子類化或修改 Behavior 代碼。
直接使用 UInputBehaviors
在上面的討論中,我重點(diǎn)討論了 UInteractiveTool 提供 UInputBehaviorSet 的情況。Gizmos 將類似地工作。但是,UInputRouter 本身根本不知道 Tools,完全可以單獨(dú)使用 InputBehavior 系統(tǒng)。在 ToolsFrameworkDemo 中,我在
USceneObjectSelectionInteraction類中以這種方式實(shí)現(xiàn)了點(diǎn)擊選擇網(wǎng)格交互。這個(gè)類實(shí)現(xiàn)了 IInputBehaviorSource 和 IClickBehaviorTarget 本身,并且只屬于框架后端子系統(tǒng)。即使這不是絕對必要的 - 您可以直接使用 UInputRouter 注冊您自己創(chuàng)建的 UInputBehavior (但是請注意,由于我對 API 的疏忽,在 UE4.26 中您無法顯式注銷單個(gè)行為,您只能通過源注銷)。
UE4.26 ITF 實(shí)現(xiàn)中當(dāng)前未處理其他設(shè)備類型,但是 frame3Sharp 中此行為系統(tǒng)的先前迭代支持觸摸和 VR 控制器輸入,并且這些應(yīng)該(最終)在 ITF 設(shè)計(jì)中類似地工作。一般的想法是只有 InputRouter 和 Behaviors 需要明確了解不同的輸入模式。IClickBehaviorTarget 實(shí)現(xiàn)應(yīng)該與鼠標(biāo)按鈕、手指點(diǎn)擊或 VR 控制器點(diǎn)擊類似地工作,但也不排除為特定于設(shè)備的交互(例如,來自兩指捏合、空間控制器手勢等)定制的額外行為目標(biāo). 工具可以為不同的設(shè)備類型注冊不同的行為,InputRouter 將負(fù)責(zé)處理哪些設(shè)備是活動(dòng)的和可捕獲的。
目前,可以通過映射到鼠標(biāo)事件來完成對其他設(shè)備類型的某種程度的處理。由于 InputRouter 不直接監(jiān)聽輸入事件流,而是由 ITF 后端創(chuàng)建和轉(zhuǎn)發(fā)事件,這是做這種映射的自然場所,下面將解釋更多細(xì)節(jié)。
在設(shè)計(jì)交互時(shí)需要注意的這個(gè)系統(tǒng)的一個(gè)重要限制是,框架尚不支持主動(dòng)捕獲的“中斷”。當(dāng)人們希望進(jìn)行單擊或拖動(dòng)的交互時(shí),這種情況最常見,具體取決于鼠標(biāo)是立即在同一位置釋放還是移動(dòng)了某個(gè)閾值距離。在簡單的情況下,這可以通過UClickDragBehavior處理,由你的 IClickDragBehaviorTarget 實(shí)現(xiàn)做出決定。但是,如果單擊和拖動(dòng)動(dòng)作需要去到彼此不知道的非常不同的地方,這可能會(huì)很痛苦。支持這種交互的一種更簡潔的方法是允許一個(gè) UInputBehavior “中斷”另一個(gè) - 在這種情況下,當(dāng)滿足先決條件(即足夠的鼠標(biāo)移動(dòng))時(shí),拖動(dòng)以“中斷”單擊的活動(dòng)捕獲。這是 ITF 未來可能會(huì)改進(jìn)的一個(gè)領(lǐng)域。
UInteractiveTool 還有一組我沒有介紹的 API 函數(shù),用于管理一組附加的
UInteractiveToolPropertySet對象。這是一個(gè)完全可選的系統(tǒng),在某種程度上是為在 UE 編輯器中使用而量身定制的。對于運(yùn)行時(shí)使用,它不太有效。本質(zhì)上,
UInteractiveToolPropertySet 用于存儲(chǔ)你的工具設(shè)置和選項(xiàng)。它們是具有 UProperties 的 UObject,在編輯器中,這些 UObject 可以添加到 Slate DetailsView 以在編輯器 UI 中自動(dòng)公開這些屬性。
額外的 UInteractiveTool API 總結(jié)如下。一般在Tool ::Setup()函數(shù)中,會(huì)創(chuàng)建各種
UInteractiveToolPropertySet子類并傳遞給AddToolPropertySource()。ITF 后端將使用 GetToolProperties() 函數(shù)初始化 DetailsView 面板,然后 Tool 可以使用
SetToolPropertySourceEnabled() 動(dòng)態(tài)顯示和隱藏屬性集
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 編輯器中,可以使用元標(biāo)記來標(biāo)記 UProperties 以控制生成的 UI 小部件 - 例如滑塊范圍、有效整數(shù)值以及基于其他屬性的值啟用/禁用小部件。建模模式中的大部分 UI 都是以這種方式工作的。
不幸的是,UProperty 元標(biāo)記在運(yùn)行時(shí)不可用,并且 UMG 小部件不支持 DetailsView 面板。結(jié)果,ToolPropertySet 系統(tǒng)變得不那么引人注目了。不過,它仍然提供了一些有用的功能。一方面,屬性集支持使用屬性集的 SaveProperties() 和 RestoreProperties() 函數(shù)跨工具調(diào)用保存和恢復(fù)其設(shè)置。您只需在 Tool Shutdown() 中設(shè)置的每個(gè)屬性上調(diào)用 SaveProperties(),并在 ::Setup() 中調(diào)用 RestoreProperties()。
第二個(gè)有用的功能是 WatchProperty() 函數(shù),它允許響應(yīng) PropertySet 值的更改而無需任何類型的更改通知。這對于 UObject 是必要的,因?yàn)?C++ 代碼可以直接更改 UObject 上的 UProperty,這不會(huì)導(dǎo)致發(fā)送任何類型的更改通知。因此,可靠檢測此類更改的唯一方法是通過輪詢。是的,投票。這并不理想,但請考慮 (1) 工具必須具有有限數(shù)量的用戶可以處理的屬性,以及 (2) 一次只有一個(gè)工具處于活動(dòng)狀態(tài)。為了讓您不必為 ::OnTick() 中的每個(gè)屬性實(shí)現(xiàn)存儲(chǔ)值比較,您可以使用以下模式添加觀察者:
MyPropertySet->WatchProperty( MyPropertySet->bBooleanProp, [this](bool bNewValue) { // handle change! } );
在 UE4.26 中,有一些額外的警告(閱讀:錯(cuò)誤)必須解決,請參閱下文了解更多詳細(xì)信息。
最后,UInteractiveTool API 的最后一個(gè)主要部分是對Tool Actions的支持。這些在建模模式工具集中沒有廣泛使用,除了實(shí)現(xiàn)熱鍵功能。但是,工具操作與熱鍵沒有特別的關(guān)系。它們允許工具公開可以通過整數(shù)標(biāo)識(shí)符調(diào)用的“動(dòng)作”(即無參數(shù)函數(shù))。Tool 構(gòu)造并返回一個(gè)FInteractiveToolActionSet,然后更高級別的客戶端代碼可以枚舉這些操作,并使用下面定義的ExecuteAction函數(shù)執(zhí)行它們。
class UInteractiveTool : public UObject, public IInputBehaviorSource
{
// ...previous functions...
public:
FInteractiveToolActionSet* GetActionSet();
void ExecuteAction(int32 ActionID);
protected:
void RegisterActions(FInteractiveToolActionSet& ActionSet);
};
下面的示例代碼顯示了兩個(gè)正在注冊的工具操作。請注意,盡管FInteractiveToolAction包含熱鍵和修飾符,但這些只是對更高級別客戶端的建議。UE 編輯器查詢操作的工具,然后將建議的熱鍵注冊為編輯器熱鍵,這允許用戶重新映射它們。UE在運(yùn)行時(shí)沒有任何類似的熱鍵系統(tǒng),您需要自己手動(dòng)映射這些熱鍵
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; });
}
最終,每個(gè) ToolAction 有效負(fù)載都存儲(chǔ)為 TFunction<void()>。如果你只是轉(zhuǎn)發(fā)到另一個(gè) Tool 函數(shù),比如上面的 DecreaseBrushSpeedAction() 調(diào)用,你不一定受益于 ToolAction 系統(tǒng),根本不需要使用它。然而,由于當(dāng)前工具暴露于藍(lán)圖的限制,ToolActions(因?yàn)樗鼈兛梢酝ㄟ^一個(gè)簡單的整數(shù)調(diào)用)可能是一種將工具功能暴露給 BP 的有效方法,而無需編寫許多包裝函數(shù)。
正如我所提到的,“Gizmo”是指我們在 2D 和 3D 內(nèi)容創(chuàng)建/編輯應(yīng)用程序中使用的那些在視口內(nèi)點(diǎn)擊的小東西,可以讓你有效地操縱視覺元素或?qū)ο蟮膮?shù)。例如,如果您使用過任何 3D 工具,那么你幾乎肯定使用過標(biāo)準(zhǔn)的平移/旋轉(zhuǎn)/縮放 Gizmo。與工具一樣,Gizmo 捕獲用戶輸入,但不是完整的 Modal 狀態(tài),Gizmo 通常是瞬態(tài)的,即 Gizmo 可以來來去去,并且你可以同時(shí)激活多個(gè) Gizmo,它們僅在你單擊時(shí)捕獲輸入“開”他們(“開”的意思可能有點(diǎn)模糊)。正因?yàn)槿绱?,Gizmo 通常需要一些特定的可視化表示,以允許用戶指示他們何時(shí)想要“使用”Gizmo,但從概念上講,你也可以擁有基于熱鍵或應(yīng)用程序狀態(tài)(例如復(fù)選框)執(zhí)行此操作的 Gizmo。
在 Interactive Tools Framework 中,Gizmo 被實(shí)現(xiàn)為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 實(shí)例由UInteractiveGizmoManager管理,使用通過字符串注冊的UInteractiveGizmoBuilder工廠。Gizmo 使用相同的 UInputBehavior 設(shè)置,并且由 ITF 每幀進(jìn)行類似渲染和勾選。
在這個(gè)高層次上,UInteractiveGizmo 只是一個(gè)骨架,要實(shí)現(xiàn)自定義 Gizmo,你必須自己做很多工作。與工具不同,提供“基礎(chǔ)”小玩意兒更具挑戰(zhàn)性,因?yàn)樗哂幸曈X表示方面。特別是,標(biāo)準(zhǔn)的 InputBehaviors 將要求你能夠?qū)?Gizmo 進(jìn)行光線投射命中測試,因此不能只在 Render() 函數(shù)中繪制任意幾何圖形。也就是說,ITF 確實(shí)提供了一個(gè)非常靈活的標(biāo)準(zhǔn) Translate-Rotate-Scale Gizmo 實(shí)現(xiàn),可以重新利用它來解決許多問題。
如果 ITF 不包含標(biāo)準(zhǔn)的平移-旋轉(zhuǎn)-縮放 (TRS) Gizmos,那么將 ITF 稱為構(gòu)建 3D 工具的框架將是非常有問題的。目前在 UE4.26 中可用的是一個(gè)名為UTransformGizmo的組合 TRS Gizmo(右側(cè)屏幕截圖) ,它支持軸和平面平移(軸線和中心人字形)、軸旋轉(zhuǎn)(圓)、統(tǒng)一比例(中心框)、軸比例(外軸括號(hào))和平面刻度(外人字形)。這些子 Gizmo 可以單獨(dú)配置,因此你可以(例如)通過將某些枚舉值傳遞給 Gizmo 構(gòu)建器來創(chuàng)建僅具有 XY 平面平移和 Z 旋轉(zhuǎn)的 UTransformGizmo 實(shí)例。
這個(gè) TRS Gizmo 不是一個(gè)單一的整體 Gizmo,它是由一組可以重新用于許多其他用途的部件組成的。這個(gè)子系統(tǒng)足夠復(fù)雜,值得單獨(dú)寫一篇文章,但總而言之,我上面提到的 UTransformGizmo 的每個(gè)元素實(shí)際上都是一個(gè)單獨(dú)的 UInteractiveGizmo(所以,是的,你可以有嵌套/分層 Gizmo,你可以繼承 UTransformGizmo 來添加額外的自定義控件)。例如,軸平移子 Gizmo(繪制為紅/綠/藍(lán)線段)是UAxisPositionGizmo的實(shí)例,旋轉(zhuǎn)圓是UAxisAngleGizmo。
像 UAxisPositionGizmo 這樣的子 Gizmo 并沒有顯式地繪制上圖中的線條。相反,它們連接到提供視覺表示和命中測試的任意 UPrimitiveComponent。因此,如果你愿意,可以使用任何 UStaticMesh。默認(rèn)情況下,UTransformGizmo 生成自定義 Gizmo 特定的 UPrimitiveComponents,在線條的情況下,它是一個(gè)UGizmoArrowComponent。這些 GizmoComponents 提供了一些細(xì)節(jié),如恒定的屏幕空間尺寸、懸停支持等。但是你絕對不必使用它們,并且 Gizmo 外觀可以完全根據(jù)你的目的進(jìn)行定制(未來以 Gizmo 為重點(diǎn)的文章的主題!)。
因此,UAxisPositionGizmo 實(shí)際上只是“根據(jù)鼠標(biāo)輸入沿線指定位置”這一抽象概念的實(shí)現(xiàn)。3D 線、線位置到抽象參數(shù)的映射(默認(rèn)情況下為 3D 世界位置)以及狀態(tài)變化信息都通過 UInterfaces 實(shí)現(xiàn),因此可以根據(jù)需要進(jìn)行自定義。視覺表示只是為了通知用戶,并為捕獲鼠標(biāo)的 InputBehavior 提供命中目標(biāo)。這允許以最小的難度集成任意捕捉或參數(shù)約束等功能。
但是,這都是旁白。實(shí)際上,要使用 UTransformGizmo,你只需使用以下調(diào)用之一從 GizmoManager 請求一個(gè):
class UInteractiveGizmoManager
{
// ...
UTransformGizmo* Create3AxisTransformGizmo(void* Owner);
UTransformGizmo* CreateCustomTransformGizmo(ETransformGizmoSubElements Elements, void* Owner);
}
然后創(chuàng)建一個(gè)UTransformProxy實(shí)例并將其設(shè)置為 Gizmo 的目標(biāo)。Gizmo 現(xiàn)在將具有完整功能,你可以在 3D 場景中移動(dòng)它,并通過
UTransformProxy::OnTransformChanged 委托響應(yīng)變換更改??梢允褂酶鞣N其他委托,例如開始/結(jié)束轉(zhuǎn)換交互?;谶@些委托,你可以變換場景中的對象、更新對象的參數(shù)等。
稍微復(fù)雜一點(diǎn)的用法是,如果你希望 UTransformProxy 直接移動(dòng)一個(gè)或多個(gè) UPrimitiveComponent,即實(shí)現(xiàn)幾乎每個(gè) 3D 設(shè)計(jì)應(yīng)用程序都有的普通“選擇對象并使用 gizmo 移動(dòng)它們”類型的界面。在這種情況下,可以將組件添加為代理的目標(biāo)。Gizmo 仍然作用于 UTransformProxy,并且 Proxy 將單個(gè)變換重新映射到對象集上的相對變換。
UTransformGizmo 不必為工具所有。在 ToolsFrameworkDemo 中,
USceneObjectTransformInteraction類監(jiān)視運(yùn)行時(shí)對象場景中的選擇變化,如果存在活動(dòng)選擇,則生成合適的新 UTransformGizmo。代碼只有幾行:
TransformProxy = NewObject<UTransformProxy>(this);
for (URuntimeMeshSceneObject* SceneObject : SelectedObjects)
{
TransformProxy->AddComponent(SO->GetMeshComponent());
}
TransformGizmo = GizmoManager->CreateCustomTransformGizmo(ETransformGizmoSubElements::TranslateRotateUniformScale, this);
TransformGizmo->SetActiveTarget(TransformProxy);
在這種情況下,我將傳遞
ETransformGizmoSubElements::TranslateRotateUniformScale以創(chuàng)建沒有非均勻縮放子元素的 TRS gizmo。要銷毀 Gizmo,代碼只需調(diào)用 DestroyAllGizmosByOwner,傳遞創(chuàng)建期間使用的相同 void* 指針:
GizmoManager->DestroyAllGizmosByOwner(this);
UTransformGizmo 自動(dòng)發(fā)出必要的撤消/重做信息,這將在下面進(jìn)一步討論。因此,只要使用中的 ITF 后端支持撤消/重做,Gizmo 轉(zhuǎn)換也將支持。
UTransformGizmo 支持局部和全局坐標(biāo)系。默認(rèn)情況下,它從 ITF 后端請求當(dāng)前的本地/全局設(shè)置。在 UE 編輯器中,其控制方式與默認(rèn) UE 編輯器 Gizmo 相同,方法是在主視口頂部使用相同的世界/本地切換。你也可以覆蓋此行為,請參閱 UTransformGizmoBuilder 標(biāo)頭中的注釋。
一個(gè)警告,不過。UE4 僅支持組件的局部坐標(biāo)系中的非均勻縮放變換。這是因?yàn)樵诖蠖鄶?shù)情況下,不能將具有非均勻縮放的兩個(gè)單獨(dú)的 FTransform 組合成一個(gè) FTransform。因此,在全局模式下,TRS Gizmo 將不會(huì)顯示非均勻縮放手柄(軸括號(hào)和外角 V 形)。默認(rèn)的 UE 編輯器 Gizmo 具有相同的限制,但通過僅允許在縮放 Gizmo 中使用本地坐標(biāo)系(不與平移和旋轉(zhuǎn) Gizmo 組合)來處理它。
在這一點(diǎn)上,我們有 Tools 和 ToolManager,還有 Gizmos 和 GizmoManager,但誰來管理 Manager?為什么,當(dāng)然是上下文。UInteractiveToolsContext是交互工具框架的最頂層。它本質(zhì)上是工具和 Gizmo 所在的“宇宙”,并且還擁有 InputRouter。默認(rèn)情況下,你可以簡單地使用此類,這就是我在 ToolsFrameworkDemo 中所做的。在 ITF 的 UE 編輯器使用中,有一些子類可以調(diào)解 ITF 和更高級別的編輯器構(gòu)造(如 FEdMode)之間的通信(例如,參見
UEdModeInteractiveToolsContext)。
ToolsContext 還為 Managers 和 InputRouter 提供了各種 API 的實(shí)現(xiàn),這些 API 提供了“類似編輯器”的功能。這些 API 的目的本質(zhì)上是提供“編輯器”的抽象,這使我們能夠防止 ITF 具有顯式的虛幻編輯器依賴項(xiàng)。在上面的文字中,我多次提到“ITF 后端”——這就是我所指的。
如果仍然不清楚我所說的“編輯器的抽象”是什么意思,也許可以舉個(gè)例子。我還沒有提到任何關(guān)于對象選擇的內(nèi)容。這是因?yàn)檫x定對象的概念在很大程度上超出了 ITF 的范圍。當(dāng) ToolManager 去構(gòu)造一個(gè)新工具時(shí),它會(huì)傳遞一個(gè)選定的 Actor 和組件的列表。但是它通過詢問工具上下文來獲得這個(gè)列表。而且工具上下文也不知道。工具上下文需要通過IToolsContextQueriesAPI詢問創(chuàng)建它的應(yīng)用程序。這個(gè)周圍的應(yīng)用程序必須創(chuàng)建 IToolsContextQueriesAPI 的實(shí)現(xiàn)并將其傳遞給構(gòu)造時(shí)的 ToolsContext。
ITF 無法以通用方式解決“對象選擇的工作原理”,因?yàn)檫@高度依賴于您的應(yīng)用程序。在 ToolsFrameworkDemo 中,我實(shí)現(xiàn)了一個(gè)基本的網(wǎng)格對象和選擇列表機(jī)制,其行為類似于大多數(shù) DCC 工具。虛幻編輯器在主視口中有一個(gè)類似的系統(tǒng)。但是,在資產(chǎn)編輯器中,只有一個(gè)對象,根本沒有選擇。所以 Asset Editors 中使用的 IToolsContextQueriesAPI 是不同的。如果你在游戲環(huán)境中使用 ITF,您可能會(huì)對什么是“選擇”,甚至什么是“對象”有一個(gè)非常不同的概念。
因此,我們使用 ToolContext API 的目標(biāo)是需要最少的函數(shù)集,以允許工具在“類似編輯器的容器”中工作。隨著需要查詢編輯器容器的新情況的出現(xiàn),這些 API 隨著時(shí)間的推移而增長。它們在文件 ToolContextInterfaces.h 中定義并總結(jié)如下
該 API 提供了從 Editor 容器中查詢狀態(tài)信息的功能。最關(guān)鍵的是GetCurrentSelectionState(),ToolManager 將使用它來確定要傳遞給 ToolBuilder 的選定參與者和組件。在使用 ITF 時(shí),你可能需要對此進(jìn)行自定義實(shí)現(xiàn)。許多工具和 TRS Gizmo 也需要GetCurrentViewState()才能正常工作,因?yàn)樗峁?3D 相機(jī)/視圖信息。然而,ToolsFrameworkDemo 中的示例實(shí)現(xiàn)可能足以滿足任何運(yùn)行時(shí)使用,即標(biāo)準(zhǔn)全屏單一 3D 視圖。這里的其他函數(shù)可以有簡單的實(shí)現(xiàn),只返回一個(gè)默認(rèn)值。
class IToolsContextQueriesAPI
{
void GetCurrentSelectionState(FToolBuilderState& StateOut);
void GetCurrentViewState(FViewCameraState& StateOut);
EToolContextCoordinateSystem GetCurrentCoordinateSystem();
bool ExecuteSceneSnapQuery(const FSceneSnapQueryRequest& Request, TArray<FSceneSnapQueryResult>& Results );
UMaterialInterface* GetStandardMaterial(EStandardToolContextMaterials MaterialType);
}
IToolsContextTransactionsAPI主要用于將數(shù)據(jù)發(fā)送回編輯器容器。DisplayMessage()由工具調(diào)用,其中包含各種用戶信息消息、錯(cuò)誤和狀態(tài)消息等。如果愿意,可以忽略這些。PostInvalidation()用于指示需要重繪,在引擎以最大/固定幀速率持續(xù)重繪的運(yùn)行時(shí)上下文中,通??梢院雎赃@一點(diǎn)。RequestSelectionChange()是某些工具提供的提示,通常在它們創(chuàng)建新對象時(shí)提供,可以忽略。
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();
}
追加更改()由想要發(fā)出 FCommandChange 記錄 — 實(shí)際上是 FToolCommandChange 子類 — 的工具調(diào)用,這是 ITF 撤消/重做方法的核心組件。為了理解為什么這個(gè)設(shè)計(jì)是這樣的,我必須解釋一下撤銷/重做是如何在 UE 編輯器中工作的。
編輯器不使用命令對象/模式方法來撤消/重做,這通常是大多數(shù) 3D 內(nèi)容創(chuàng)建/編輯工具執(zhí)行此操作的方式。相反,編輯器使用事務(wù)系統(tǒng)。打開事務(wù)后,對任何即將被修改的對象調(diào)用 UObject::Modify(),這會(huì)保存所有 UObject 當(dāng)前 UProperty 值的副本。當(dāng) Transaction 關(guān)閉時(shí),比較修改對象的 UProperties,并序列化任何更改。這個(gè)系統(tǒng)真的是對像 UObjects 這樣的東西的唯一方法,可以通過 UProperties 擁有任意用戶定義的數(shù)據(jù)。
然而,眾所周知,事務(wù)系統(tǒng)在處理大型復(fù)雜數(shù)據(jù)結(jié)構(gòu)(如網(wǎng)格)時(shí)表現(xiàn)不佳。例如,將任意部分更改存儲(chǔ)為一個(gè)巨大的網(wǎng)格作為事務(wù)將涉及預(yù)先制作完整副本,然后搜索和編碼對復(fù)雜網(wǎng)格數(shù)據(jù)結(jié)構(gòu)(本質(zhì)上是非結(jié)構(gòu)化圖)的更改。這是一個(gè)非常困難(閱讀:慢)的計(jì)算問題。類似地,一個(gè)簡單的 3D 平移將修改每個(gè)頂點(diǎn),需要一個(gè) Transaction 中所有位置數(shù)據(jù)的完整副本,但在 Change 中可以僅存儲(chǔ)為平移向量和一些關(guān)于應(yīng)用什么操作的信息,然后搜索并編碼對復(fù)雜網(wǎng)格數(shù)據(jù)結(jié)構(gòu)(本質(zhì)上是非結(jié)構(gòu)化圖)的更改。這是一個(gè)非常困難(閱讀:慢)的計(jì)算問題。
因此,在構(gòu)建 ITF 時(shí),我們添加了對在 UE 編輯器事務(wù)中嵌入 FCommandChange 對象的支持。這有點(diǎn)雜亂無章,但通常有效,并且有用的副作用是這些 FCommandChanges 也可以在不存在 UE 編輯器事務(wù)系統(tǒng)的運(yùn)行時(shí)使用。當(dāng)用戶與工具交互時(shí),我們的大多數(shù)建模模式工具都會(huì)不斷調(diào)用 AppendChange(),而 Gizmo 也這樣做。因此,我們可以構(gòu)建一個(gè)基本的撤消/重做歷史系統(tǒng),只需按照它們進(jìn)入的順序存儲(chǔ)這些更改,然后在撤消/重做列表中后退/前進(jìn),在每個(gè) FToolCommandChange 對象上調(diào)用 Revert()/Apply() .
BeginUndoTransaction()和EndUndoTransaction()是相關(guān)函數(shù),它們標(biāo)記應(yīng)分組的一組變更記錄的開始和結(jié)束 - 通常 AppendChange() 將在其間被調(diào)用一次或多次。為了提供正確的用戶體驗(yàn) - 即單個(gè)撤消/重做熱鍵/命令一次處理所有更改 - ToolsFrameworkDemo 有一個(gè)非?;镜南到y(tǒng),用于存儲(chǔ)一組 FCommandChanges。
IToolsContextRenderAPI
此 API 被傳遞給 UInteractiveTool::Render() 和 UInteractiveGizmo::Render() 以提供常見渲染任務(wù)所需的信息。GetPrimitiveDrawInterface()返回抽象FPrimitiveDrawInterface API 的實(shí)現(xiàn),這是一個(gè)標(biāo)準(zhǔn)的 UE 接口,提供線和點(diǎn)繪制功能(通??s寫為 PDI)。各種工具使用 PDI 來繪制基本的線反饋,例如在繪制多邊形工具中繪制的當(dāng)前多邊形的邊緣。但是請注意,運(yùn)行時(shí)的 PDI 線圖與編輯器中的 PDI 線圖不同 - 它的質(zhì)量較低,無法繪制編輯器可以繪制的隱藏線。
GetCameraState()、GetSceneView() 和 GetViewInteractionState() 返回有關(guān)當(dāng)前視圖的信息。這些在編輯器中很重要,因?yàn)橛脩艨赡苡卸鄠€(gè)可見的 3D 視口(例如,在 4-up 視圖中),并且工具必須在每個(gè)視口中正確繪制。在運(yùn)行時(shí),通常只有一個(gè)相機(jī)/視圖,您應(yīng)該可以使用 ToolsFramworkDemo 中的基本實(shí)現(xiàn)。但是,如果您想實(shí)現(xiàn)多個(gè)視圖,則需要在此 API 中正確提供它們。
class IToolsContextRenderAPI
{
FPrimitiveDrawInterface* GetPrimitiveDrawInterface();
FViewCameraState GetCameraState();
const FSceneView* GetSceneView();
EViewInteractionState GetViewInteractionState();
}
ITooslContextAssetAPI 可用于發(fā)出新對象。這是一個(gè)可選的 API,我只列出了下面的頂級函數(shù),API 還包含其他一些特定于 UE 編輯器的函數(shù)。這是最難抽象的部分,因?yàn)樗枰恍╆P(guān)于“對象”是什么的固有假設(shè)。但是,它也不是您必須在自己的工具中使用的東西。GenerateStaticMeshActor( )編輯器建模工具使用該函數(shù)來生成新的靜態(tài)網(wǎng)格體資源/組件/Actor,例如在繪制多邊形工具中,使用拉伸多邊形(AssetConfig 參數(shù)的一部分)調(diào)用此函數(shù)來創(chuàng)建資源。此創(chuàng)建過程涉及查找位置(可能會(huì)產(chǎn)生對話框/等)、創(chuàng)建新包等。
class IToolsContextAssetAPI
{
AActor* GenerateStaticMeshActor(
UWorld* TargetWorld,
FTransform Transform,
FString ObjectBaseName,
FGeneratedStaticMeshAssetConfig&& AssetConfig);
}
在運(yùn)行時(shí),你不能創(chuàng)建資產(chǎn),所以這個(gè)函數(shù)必須做“其他事情”。在 ToolsFrameworkDemo 中,我實(shí)現(xiàn)了 GenerateStaticMeshActor(),這樣一些建模模式工具(如 Draw Polygon Tool)就可以正常工作了。但是,它會(huì)發(fā)出完全不同的 Actor 類型。
在上面的工具和工具構(gòu)建器部分中,我描述了 FToolBuilderState,以及 ToolManager 如何構(gòu)造一個(gè)選定的 Actor 和組件列表以傳遞給 ToolBuilder。如果你的工具應(yīng)該作用于Actor或組件,可以將該選擇傳遞給新的工具實(shí)例。但是,如果你瀏覽建模模式工具代碼,會(huì)看到大多數(shù)工具都作用于稱為FPrimitiveComponentTarget的東西,它是在 ToolBuilders 中基于選定的 UPrimitiveComponents 創(chuàng)建的。我們有基類USingleSelectionTool和UMultiSelectionTool,大多數(shù)建模模式工具都派生自它們,它們包含這些選擇。
如果你是從頭開始構(gòu)建自己的工具,這不是你需要做的事情。但是,如果你想利用建模模式工具,你需要理解它,所以我會(huì)解釋。FPrimitiveComponentTarget 的目的是為工具提供“可以編輯的網(wǎng)格”的抽象。這很有用,因?yàn)槲覀冊?Unreal 中有許多不同的 Mesh 類型(您可能有自己的)。有 FMeshDescription(UStaticMesh 使用)、USkeletalMesh、FRawMesh、Cloth Meshes、Geometry Collections(即網(wǎng)格)等等。必須操縱低級網(wǎng)格數(shù)據(jù)結(jié)構(gòu)的網(wǎng)格編輯工具本質(zhì)上需要許多并行代碼路徑來支持其中的每一個(gè)。此外,在 Unreal 中更新網(wǎng)格的成本很高。正如我在之前的教程中所解釋的,當(dāng)您在 UStaticMesh 中修改 FMeshDescription 時(shí),“構(gòu)建”步驟是重新生成渲染數(shù)據(jù)所必需的,這在大型網(wǎng)格上可能需要幾秒鐘。這在例如用戶期望即時(shí)反饋的 3D 雕刻工具中是不可接受的。
因此,通常建模模式工具不能直接編輯上面列出的任何 UE 組件網(wǎng)格格式。相反,ToolBuilder 將目標(biāo)組件包裝在 FPrimitiveComponentTarget 實(shí)現(xiàn)中,該實(shí)現(xiàn)必須提供 API 來讀取和寫入其內(nèi)部網(wǎng)格(無論格式如何)作為 FMeshDescription。這允許想要編輯網(wǎng)格以支持單一標(biāo)準(zhǔn)輸入/輸出格式的工具,但需要(潛在)網(wǎng)格轉(zhuǎn)換成本。在大多數(shù)建模模式工具中,我們隨后將該 FMeshDescription 轉(zhuǎn)換為 FDynamicMesh3 以進(jìn)行實(shí)際編輯,并創(chuàng)建一個(gè)新的
USimpleDynamicMeshComponent 用于快速預(yù)覽,并且僅在 Tool Accept 上寫回更新的 FMeshDescription。但這被封裝在 Tool 內(nèi)部,與 FPrimtiveComponentTarget 沒有真正的關(guān)系。
我們需要允許交互式工具框架為它不知道的組件創(chuàng)建一個(gè) FPrimitiveComponentTarget 子類包裝器 —因?yàn)樵S多組件是 ITF 不可見的插件的一部分。例如,UProceduralMeshComponent 或
USimpleDynamicMeshComponent。為此,我們提供了一個(gè) FComponentTargetFactory 實(shí)現(xiàn),它有兩個(gè)功能:
class INTERACTIVETOOLSFRAMEWORK_API FComponentTargetFactory
{
public:
virtual bool CanBuild( UActorComponent* Candidate ) = 0;
virtual TUniquePtr<FPrimitiveComponentTarget> Build( UPrimitiveComponent* PrimitiveComponent ) = 0;
};
這些一般都很簡單,舉個(gè)例子,見
EditorComponentSourceFactory.cpp中的
FStaticMeshComponentTargetFactory,它為UStaticMeshComponents構(gòu)建了
FStaticMeshComponentTarget實(shí)例。在這種情況下,
FStaticMeshComponentTarget 也很簡單。我們將利用這個(gè) API 來解決下面運(yùn)行時(shí)使用的一些問題。
最后,一旦 FComponentTargetFactory 可用,全局函數(shù)AddComponentTargetFactory()用于注冊它。不幸的是,在 UE4.26 中,此函數(shù)將工廠存儲(chǔ)在全局靜態(tài) TArray 中,該 TArray 對
ComponentSourceInterfaces.cpp 是私有的,因此無法以任何方式修改或操作。在啟動(dòng)時(shí),編輯器將注冊默認(rèn)的
FStaticMeshComponentTargetFactory 以及處理 PMC 的
FProceduralMeshComponentTargetFactory。這兩個(gè)工廠都存在無法在運(yùn)行時(shí)用于網(wǎng)格編輯工具的問題,因此,在改進(jìn)此系統(tǒng)之前,我們不能使用 SMC 或 PMC 進(jìn)行運(yùn)行時(shí)網(wǎng)格編輯。我們將為
USimpleDynamicMeshComponent 創(chuàng)建一個(gè)新的 ComponentTarget(有關(guān)此網(wǎng)格組件類型的詳細(xì)信息,請參閱之前的教程)。
如果你查看大多數(shù)工具的 ToolBuilders,會(huì)發(fā)現(xiàn) CanBuildTool() 和 BuildTool() 實(shí)現(xiàn)通常調(diào)用ToolBuilderUtil命名空間中的靜態(tài)函數(shù),以及函數(shù)CanMakeComponentTarget()和MakeComponentTarget()。后兩個(gè)函數(shù)通過已注冊的 ComponentTargetFactory 實(shí)例列表進(jìn)行枚舉,以確定任何工廠是否可以處理特定的 UPrimitiveComponent 類型。ToolBuilderUtil 函數(shù)在很大程度上只是迭代FToolBuilderState中的選定組件(如上所述)并調(diào)用 lambda 謂詞(通常是上述函數(shù)之一)。
我將在這里重申,你不需要在自己的工具中使用 FPrimitiveComponentTarget 系統(tǒng),甚至不需要在 FToolBuilderState 中使用。你可以在 ToolBuilders 中輕松查詢其他(全局)選擇系統(tǒng),檢查目標(biāo)組件類型的強(qiáng)制轉(zhuǎn)換,并將 UPrimitiveComponent* 或子類傳遞給您的工具。然而,正如我所提到的,建模模式工具以這種方式工作,它將成為我現(xiàn)在將描述的運(yùn)行時(shí)網(wǎng)格編輯工具框架設(shè)計(jì)的重要驅(qū)動(dòng)力。
為交互式工具框架創(chuàng)建運(yùn)行時(shí)后端并不是那么復(fù)雜。我們要弄清楚的主要事情是:
就是這樣。一旦這些事情完成(甚至跳過第 3 步),那么基本的工具和Gizmo (甚至UTransformGizmo)就會(huì)起作用。
在這個(gè)示例項(xiàng)目中,完成上述所有相關(guān)代碼都在RuntimeToolsSystem模塊中,分為四個(gè)子目錄:
在高層次上,這里是所有東西的連接方式,用簡單的英語(希望這會(huì)讓下面的描述更容易理解)。自定義游戲模式
AToolsFrameworkDemoGameModeBase在 Play 上初始化,這反過來又初始化了管理工具框架的
URuntimeToolsFrameworkSubsystem和
URuntimeMeshSceneSubsystem。后者管理一組URuntimeMeshSceneObject,它們是圍繞網(wǎng)格 Actor 和組件的包裝,可以通過單擊選擇并使用 UTransformGizmo 進(jìn)行轉(zhuǎn)換。
URuntimeToolsFrameworkSubsystem 初始化并擁有 UInteractiveToolsContext,以及各種幫助類,如
USceneObjectSelectionInteraction(實(shí)現(xiàn)單擊選擇)、
USceneObjectTransformInteraction(管理變換 Gizmo 狀態(tài))和USceneHistoryManager(提供撤消/重做系統(tǒng))。
URuntimeToolsFrameworkSubsystem 還創(chuàng)建了一個(gè)
UToolsContextRenderComponent,用于允許在工具和 Gizmo 中進(jìn)行 PDI 渲染。在內(nèi)部,
URuntimeToolsFrameworkSubsystem 還定義了各種 API 實(shí)現(xiàn),這些都完全包含在 cpp 文件中。最后一塊是 Game Mode 的默認(rèn) Pawn,它是由 GameMode 在 Play 上生成的AToolsContextActor 。這個(gè) Actor 監(jiān)聽各種輸入事件并將它們轉(zhuǎn)發(fā)到
URuntimeToolsFrameworkSubsystem。一種
FSimpleDynamicMeshComponentTargetFactory也在 Play 上注冊,它允許在 URuntimeMeshSceneObject 中使用的 Mesh 組件由現(xiàn)有的建模模式工具進(jìn)行編輯。
哇!由于它相對獨(dú)立于工具框架方面,讓我們從網(wǎng)格場景方面開始。
此演示的目的是展示在運(yùn)行時(shí)通過 ITF 選擇和編輯網(wǎng)格。可以想象,這樣做可以編輯任何 StaticMeshActor/Component,類似于建模模式在 UE 編輯器中的工作方式。但是,正如我在之前的教程中所建議的,如果你正在構(gòu)建某種建模工具應(yīng)用程序或游戲關(guān)卡編輯器,我認(rèn)為你不希望直接使用 Actor 和組件構(gòu)建所有內(nèi)容。至少,你可能需要一種序列化“場景”的方法。而且可能希望在你的環(huán)境中擁有不可編輯的可見網(wǎng)格(即使只是 3D UI 元素)。我認(rèn)為擁有一個(gè)代表可編輯世界的獨(dú)立數(shù)據(jù)模型是很有用的——一個(gè)“對象”的“場景”,不與特定的 Actor 或組件相關(guān)聯(lián)。反而,
所以,這就是我在這個(gè)演示中所做的。URuntimeMeshSceneObject是一個(gè)場景對象,在 UE 級別由ADynamicSDMCActor 表示,我在之前的教程中對此進(jìn)行了描述。此 Actor 是RuntimeGeometryUtils插件的一部分。它生成/管理一個(gè)子網(wǎng)格
USimpleDynamicMeshComponent,可以在需要時(shí)進(jìn)行更新。在這個(gè)項(xiàng)目中,我們不會(huì)使用我之前開發(fā)的任何藍(lán)圖編輯功能,而是使用工具進(jìn)行編輯,并且僅使用 SDMC 作為顯示源網(wǎng)格的一種方式。
URuntimeMeshSceneSubsystem管理現(xiàn)有 URuntimeMeshSceneObjects 的集合,我將在此處(和代碼中)將其縮寫為“SO”。提供了生成新 SO、按 Actor 查找一個(gè)、刪除一個(gè)或多個(gè) SO 以及管理一組選定 SO 的功能。此外,F(xiàn)indNearestHitObject() 可用于將光線投射到場景中,類似于 LineTrace(但只會(huì)命中 SO)。
URuntimeMeshSceneSubsystem 還擁有選中時(shí)分配給 SO 的材質(zhì)以及默認(rèn)材質(zhì)。在這個(gè)演示中只有對材料的基線支持,所有創(chuàng)建的 SO 都分配了 DefaultMaterial(白色),并且在選擇時(shí)交換為 SelectedMaterial(橙色)。但是,SO 確實(shí)會(huì)跟蹤分配的材料,因此你可以相對輕松地?cái)U(kuò)展現(xiàn)有的材料。
對場景的更改 - 場景對象的創(chuàng)建、刪除和編輯、選擇更改、變換更改等 - 由USceneHistoryManager存儲(chǔ)。此類存儲(chǔ)FChangeHistoryTransaction結(jié)構(gòu)的列表,其中存儲(chǔ)FChangeHistoryRecord的序列,它是一個(gè)元組(UObject*、FCommandChange、Text)。該系統(tǒng)大致近似于 UE 編輯器事務(wù)系統(tǒng),但僅支持顯式 FCommandChange 對象,而在編輯器中,對 UObjects 的更改可以自動(dòng)存儲(chǔ)在事務(wù)中。我在上面的
IToolsContextTransactionsAPI 部分中更詳細(xì)地描述了 FCommandChange。本質(zhì)上,這些對象具有 Apply() 和 Revert() 函數(shù),它們必須“重做”或“撤消”它們對任何修改的全局狀態(tài)的影響。
這里的使用模式是調(diào)用BeginTransaction(),然后AppendChange()一次或多次,然后是 EndTransaction()。
IToolsContextTransactionsAPI 實(shí)現(xiàn)將為 ITF 組件執(zhí)行此操作,并且諸如場景選擇更改之類的操作將直接執(zhí)行此操作。Undo()函數(shù)回滾到之前的歷史狀態(tài)/事務(wù),Redo ()函數(shù)向前滾動(dòng)。通常的想法是將所有更改分組到單個(gè)事務(wù)中以用于單個(gè)高級用戶“操作”,因此不必多次撤消/重做即可“通過”復(fù)雜的狀態(tài)更改。為了簡化這一點(diǎn),可以嵌套 BeginTransaction()/EndTransaction() 調(diào)用,這在需要調(diào)用多個(gè)單獨(dú)的函數(shù)并且每個(gè)函數(shù)都需要發(fā)出自己的事務(wù)時(shí)經(jīng)常發(fā)生。與任何支持 Undo/Redo 的應(yīng)用程序一樣,如果用戶執(zhí)行 Undo 一次或多次,然后執(zhí)行推送新事務(wù)/更改的操作,History 序列將被截?cái)唷?/span>
在虛幻引擎游戲中,玩家控制 Pawn Actor,而在第一人稱視角游戲中,場景是從 Pawn 的視點(diǎn)渲染的。在 ToolsFrameworkDemo 中,我們將實(shí)現(xiàn)一個(gè)名為AToolsContextActor的自定義 ADefaultPawn 子類來收集用戶輸入并將其轉(zhuǎn)發(fā)給 ITF。此外,此 Actor 將處理項(xiàng)目設(shè)置中定義的各種熱鍵輸入事件。最后,AToolsContextActor 是我實(shí)現(xiàn)標(biāo)準(zhǔn)鼠標(biāo)右鍵飛行的地方(這是 ADefaultPawn 的標(biāo)準(zhǔn)行為,我只是將調(diào)用轉(zhuǎn)發(fā)給它)和 Maya 風(fēng)格的 alt-mouse 相機(jī)控制的初始步驟(但是圍繞目標(biāo)旋轉(zhuǎn))點(diǎn)尚未實(shí)施)。
所有事件連接設(shè)置都在
AToolsContextActor::SetupPlayerInputComponent()中完成。這是在Project Settings的Input部分中定義的熱鍵事件,以及硬編碼的按鈕 Action 和鼠標(biāo)軸映射。大多數(shù)硬編碼映射——可識(shí)別為對
UPlayerInput::AddEngineDefinedActionMapping()的調(diào)用——可以在項(xiàng)目設(shè)置中替換為可配置的映射。
此 Actor 由游戲模式在啟動(dòng)時(shí)自動(dòng)創(chuàng)建。我將在下面進(jìn)一步描述這一點(diǎn)。
我將在這里只提到另一個(gè)選項(xiàng),而不是讓 Pawn 將輸入轉(zhuǎn)發(fā)到 ITF 的 InputRouter,而是使用自定義 ViewportClient。ViewportClient 是“高于”Actor 和 Pawn 的級別,并且在某種程度上負(fù)責(zé)將原始設(shè)備輸入轉(zhuǎn)換為 Action 和 Axis Mappings。由于就 ITF 而言,我們的主要目標(biāo)只是收集設(shè)備輸入并將其轉(zhuǎn)發(fā)給 ITF,因此自定義 ViewportClient 可能是更自然的地方。然而,這不是我在這個(gè)演示中的做法。
Runtime ITF 后端的核心部分是
URuntimeToolsFrameworkSubsystem。這個(gè) UGameInstanceSubsystem(本質(zhì)上是一個(gè) Singleton)創(chuàng)建并初始化 UInteractiveToolsContext、所有必要的 IToolsContextAPI 實(shí)現(xiàn)、USceneHistoryManager、Selection 和 Transform Interactions,以及將在下面描述的幾個(gè)其他幫助對象。這一切都發(fā)生在::InitializeToolsContext()函數(shù)中。
子系統(tǒng)還具有用于啟動(dòng)工具和管理活動(dòng)工具的各種藍(lán)圖功能。這些是必要的,因?yàn)?ITF 當(dāng)前未向藍(lán)圖公開。最后它做了一點(diǎn)鼠標(biāo)狀態(tài)跟蹤,在::Tick()函數(shù)中,為光標(biāo)位置構(gòu)造一個(gè)世界空間射線(這是一個(gè)相對晦澀的代碼),然后將此信息轉(zhuǎn)發(fā)給 UInputRouter,以及勾選和渲染 ToolManager 和 GizmoManager。
如果這感覺有點(diǎn)像功能性的抓包,那么它就是。
URuntimeToolsFrameworkSubsystem 基本上是 ITF 和我們的“編輯器”之間的“粘合劑”,在這種情況下它非常小。唯一需要注意的其他代碼是各種 API 實(shí)現(xiàn),它們都在 .cpp 文件中定義,因?yàn)樗鼈儾皇枪差悺?/span>
FRuntimeToolsContextQueriesImpl是 IToolsContextQueriesAPI 的實(shí)現(xiàn)。此 API 為 ToolBuilders 提供 SelectionState,并支持對當(dāng)前視圖狀態(tài)和坐標(biāo)系狀態(tài)的查詢(詳情如下)。ExecuteSceneSnapQuery() 函數(shù)未實(shí)現(xiàn),僅返回 false。但是,如果您想支持可選的變換 Gizmo 功能,例如網(wǎng)格捕捉或捕捉到其他幾何體,這將是開始的地方。
FRuntimeToolsContextTransactionImpl是
IToolsContextTransactionsAPI 的實(shí)現(xiàn)。在這里,我們只是將調(diào)用直接轉(zhuǎn)發(fā)到 USceneHistoryManager。目前我還沒有實(shí)現(xiàn) RequestSelectionChange(),一些建模模式工具使用它來將選擇更改為新創(chuàng)建的對象,并且還忽略了 PostInvalidation() 調(diào)用,它們在 UE 編輯器中用于強(qiáng)制在非實(shí)時(shí)模式下刷新視口. 構(gòu)建的游戲始終實(shí)時(shí)運(yùn)行,因此這在標(biāo)準(zhǔn)游戲中不是必需的,但如果您正在構(gòu)建一個(gè)不需要恒定 60fps 重繪的應(yīng)用程序,并且已經(jīng)實(shí)施了避免重繪的方案,則此調(diào)用可以為您提供提示強(qiáng)制重繪以查看實(shí)時(shí)工具更新/等。
FRuntimeToolsFrameworkRenderImpl是 IToolsContextRenderAPI 的實(shí)現(xiàn)。此 API 的主要目的是為工具和 Gizmos 提供 FPrimitiveDrawInterface 實(shí)現(xiàn)。這是在運(yùn)行時(shí)使用建模模式工具時(shí)最有問題的部分之一,我將在下面關(guān)于
UToolsContextRenderComponent 的部分中描述它是如何實(shí)現(xiàn)的。否則,這里的函數(shù)只是轉(zhuǎn)發(fā)
RuntimeToolsFrameworkSubsystem 提供的信息。
最后是
FRuntimeToolsContextAssetImpl實(shí)現(xiàn)了 IToolsContextAssetAPI,在我們的 Runtime 案例中是非常有限的。此 API 中的許多功能旨在用于更復(fù)雜的編輯器使用,因?yàn)?UE 編輯器必須處理其中的 UPackage 和資產(chǎn),可以執(zhí)行諸如彈出內(nèi)部資產(chǎn)創(chuàng)建對話框之類的操作,具有復(fù)雜的游戲資產(chǎn)路徑系統(tǒng),等等。此 API 中的一些函數(shù)可能不應(yīng)該是基本 API 的一部分,因?yàn)楣ぞ卟粫?huì)直接調(diào)用它們,而是調(diào)用使用這些函數(shù)的實(shí)用程序代碼。因此,我們只需要實(shí)現(xiàn) Tools 調(diào)用的 GenerateStaticMeshActor() 函數(shù)來發(fā)射新對象(例如 DrawPolygon Tool,它可以繪制和擠出一個(gè)新的網(wǎng)格)。函數(shù)名稱顯然不合適,因?yàn)槲覀儾幌氚l(fā)出一個(gè)新的 AStaticMeshActor,而是一個(gè)新的 URuntimeMeshSceneObject。幸運(yùn)的是,
就是這樣!當(dāng)我提到“ITF 后端”或“類似編輯器的功能”時(shí),我所指的就是這些。800 多行極其冗長的 C++,其中大部分是不同系統(tǒng)之間相對簡單的“粘合劑”。對于基本的 ITF 實(shí)現(xiàn)來說,甚至很多現(xiàn)有的部分都不是必需的,例如,如果您不想使用建模模式工具,則根本不需要 IToolsContextAssetAPI 實(shí)現(xiàn)。
當(dāng)我介紹 ITF 時(shí),我將工具和 Gizmos 視為 ITF 的頂級“部分”,即實(shí)施用戶輸入的結(jié)構(gòu)化處理(通過 InputBehaviors)、將動(dòng)作應(yīng)用于對象等的認(rèn)可方法。但是,沒有嚴(yán)格的理由使用工具或 Gizmos 來實(shí)現(xiàn)所有用戶交互。為了證明這一點(diǎn),我將“點(diǎn)擊選擇場景對象”交互實(shí)現(xiàn)為獨(dú)立類
USceneObjectSelectionInteraction。
USceneObjectSelectionInteraction是IInputBehaviorSource的子類,所以它可以注冊到 UInputRouter,然后它的 UInputBehaviors 會(huì)被自動(dòng)收集并允許捕獲鼠標(biāo)輸入。USingleClickInputBehavior _實(shí)現(xiàn)了收集鼠標(biāo)左鍵單擊,并支持 Shift+Click 和 Ctrl+Click 修飾鍵,以添加到選擇或切換選擇。IClickBehaviorTarget 實(shí)現(xiàn)函數(shù)只是確定動(dòng)作應(yīng)該指示什么狀態(tài),并通過
URuntimeMeshSceneSubsystem API 函數(shù)將它們應(yīng)用于場景。因此,整個(gè)點(diǎn)擊選擇交互只需要相對少量的代碼。如果你想實(shí)現(xiàn)額外的選擇交互,比如框選框選擇,這可以通過切換到 UClickDragBehavior/Target 并通過鼠標(biāo)移動(dòng)閾值確定用戶是否完成了點(diǎn)擊和拖動(dòng)來相對容易地完成。
URuntimeToolsFrameworkSubsystem 只是在啟動(dòng)時(shí)創(chuàng)建這個(gè)類的一個(gè)實(shí)例,將它注冊到 UInputRouter,這就是系統(tǒng)其余部分所知道的一切。當(dāng)然可以將選擇實(shí)現(xiàn)為工具,盡管通常選擇是“默認(rèn)”模式,并且當(dāng)任何其他工具開始退出時(shí)切換出/進(jìn)入默認(rèn)工具需要一點(diǎn)小心?;蛘撸梢允褂脹]有場景內(nèi)表示的 Gizmo 來完成,并且僅在支持選擇更改時(shí)始終可用。這可能是我的偏好,因?yàn)?Gizmo 獲得 Tick() 和 Render() 調(diào)用,這可能很有用(例如,可以通過 Render() 繪制選取框矩形)。
隨著選擇狀態(tài)的變化,3D 變換 Gizmo 會(huì)不斷更新 - 它在選定對象的原點(diǎn)之間移動(dòng),如果有多個(gè)選定對象則移動(dòng)到共享原點(diǎn),或者如果沒有選定對象則消失。此行為在
USceneObjectTransformInteraction中實(shí)現(xiàn),它同樣由
URuntimeToolsFrameworkSubsystem 創(chuàng)建。
URuntimeMeshSceneSubsystem 的委托 OnSelectionModified 用于在修改場景選擇時(shí)啟動(dòng)更新。生成的 UTransformGizmo 作用于 UTransformProxy,它被賦予當(dāng)前選擇集。請注意,任何選擇更改都會(huì)生成一個(gè)新的 UTransformGizmo,而現(xiàn)有的 UTransformGizmo 會(huì)被銷毀。這有點(diǎn)重,可以對其進(jìn)行優(yōu)化以重用單個(gè) Gizmo(各種建模模式工具就是這樣做的)。
最后一點(diǎn)是活動(dòng)坐標(biāo)系的管理。這主要在后臺(tái)處理,UTransformGizmo 將查詢可用的 IToolsContextQueriesAPI 以確定世界或局部坐標(biāo)系。這可以是硬編碼的,但是為了支持兩者,我們需要在某個(gè)地方放置這個(gè)狀態(tài)。目前我已將它放在
URuntimeToolsFrameworkSubsystem 中,并暴露了一些 BP 函數(shù)以允許 UI 切換選項(xiàng)。
我在上面提到過,IToolsContextRenderAPI 實(shí)現(xiàn)需要返回一個(gè)可用于繪制線和點(diǎn)的 FPrimitiveDrawInterface(或“PDI”),這有點(diǎn)問題。這是因?yàn)樵?UE 編輯器中,承載 ITF 的編輯器模式具有自己的 PDI,可以簡單地傳遞給工具和 Gizmo。但是在運(yùn)行時(shí),這并不存在,我們可以訪問 PDI 實(shí)現(xiàn)的唯一地方是在渲染線程上運(yùn)行的 UPrimitiveComponent 的渲染代碼中(哎呀?。?。
如果這不完全有意義,那么基本上你需要了解的是,我們不能只是從 C++ 代碼中的任何地方“渲染”。我們只能在“內(nèi)部”渲染組件,例如 UStaticMeshComponent 或 UProceduralMeshComponent。但是,我們的工具和 Gizmo 具有運(yùn)行在游戲線程上的 ::Render() 函數(shù),并且與任何組件相距甚遠(yuǎn)。
所以,我所做的是制作一個(gè)自定義組件,稱為
UToolsContextRenderComponent,它可以充當(dāng)橋梁。這個(gè)組件有一個(gè)函數(shù)::GetPDIForView(),它返回一個(gè)自定義的 FPrimitiveDrawInterface 實(shí)現(xiàn)(準(zhǔn)確地說是
FToolsContextRenderComponentPDI,盡管它隱藏在組件內(nèi)部)。
URuntimeToolsFrameworkSubsystem 每幀創(chuàng)建一個(gè)此 PDI 的實(shí)例以傳遞給工具和 Gizmo。PDI DrawLine() 和 DrawPoint() 實(shí)現(xiàn)不是試圖立即渲染,而是將每個(gè)函數(shù)調(diào)用的參數(shù)存儲(chǔ)在一個(gè)列表中。然后,組件 SceneProxy 獲取這些 Line 和 Point 參數(shù)集,并將它們傳遞給
FToolsContextRenderComponentSceneProxy::GetDynamicMeshElements() 實(shí)現(xiàn)內(nèi)的標(biāo)準(zhǔn) UPrimitiveComponent PDI(渲染器調(diào)用它來獲取每幀動(dòng)態(tài)幾何圖形以進(jìn)行繪制)。
該系統(tǒng)是功能性的,并且允許建模模式工具通常像在編輯器中一樣工作。然而,一個(gè)障礙是游戲和渲染線程并行運(yùn)行。因此,如果什么都不做,我們可能會(huì)在工具和 Gizmo 完成繪制之前調(diào)用 GetDynamicMeshElements(),這會(huì)導(dǎo)致閃爍。目前我已經(jīng)通過在
URuntimeToolsFrameworkSubsystem::Tick() 的末尾調(diào)用FlushRenderingCommands()來“修復(fù)”這個(gè)問題,這會(huì)強(qiáng)制渲染線程處理所有未完成的提交幾何圖形。但是,這可能無法完全解決問題。
另一個(gè)復(fù)雜之處在于,在 UE 編輯器中,PDI 線和點(diǎn)繪圖可以繪制“隱藏線”,即在正面幾何圖形后面帶有點(diǎn)畫圖案的線。這涉及將自定義深度/模板渲染與后處理通道結(jié)合使用。這在運(yùn)行時(shí)也不存在。但是,在你自己的應(yīng)用程序中,實(shí)際上有更多的能力來制作這些效果,因?yàn)槟阃耆刂七@些渲染系統(tǒng),而在編輯器中,它們需要添加到任何游戲內(nèi)效果的“頂部”因此必然受到更多限制。本文很好地概述了如何實(shí)現(xiàn)隱藏對象渲染,以及類似于 UE 編輯器的對象輪廓。
正如我在 PrimitiveComponentTargets 部分中所描述的,為了允許在此演示中使用建模模式中的網(wǎng)格編輯工具,我們需要在我們要編輯的 UPrimitiveComponents 周圍提供一種“包裝器”。在這種情況下,這將是
USimpleDynamicMeshComponent。
FSimpleDynamicMeshComponentTarget及其關(guān)聯(lián)的 Factory的代碼相對簡單。如果您深入研究,您可能會(huì)注意到,SDMC 中的 FDynamicMesh3 正在轉(zhuǎn)換為 FMeshDescription 以傳遞給工具,然后工具將其轉(zhuǎn)換回 FDynamicMesh3 進(jìn)行編輯。這是當(dāng)前設(shè)計(jì)的一個(gè)限制,該設(shè)計(jì)專注于靜態(tài)網(wǎng)格體。如果您正在構(gòu)建自己的網(wǎng)格編輯工具,則無需進(jìn)行此轉(zhuǎn)換,但要使用建模模式工具集,則不可避免。
請注意,對網(wǎng)格的更改(存儲(chǔ)在 ::CommitMesh() 中)在更改歷史記錄中保存為FMeshReplacementChange,其中存儲(chǔ)了兩個(gè)完整的網(wǎng)格副本。這對于大型網(wǎng)格來說并不理想,但是建模工具在內(nèi)部創(chuàng)建的用于存儲(chǔ)預(yù)覽網(wǎng)格上的更改(例如在 3D 雕刻中)的網(wǎng)格“增量”當(dāng)前不會(huì)“冒泡”。
最后,我將再次重申,由于 FPrimitiveComponentTarget 部分中描述的工廠注冊問題,無法在 UE4.26 的運(yùn)行時(shí)使用建模模式工具集直接編輯 UStaticMeshComponent 或 UProceduralMeshComponent。雖然,由于在很大程度上只有 ToolBuilders 使用
FPrimitiveComponentTargetFactory 注冊表,您也許可以讓它們與直接創(chuàng)建替代 FPrimitiveComponentTarget 實(shí)現(xiàn)的自定義 ToolBuilders 一起使用。這不是我探索過的路線。
教程項(xiàng)目的最終 C++ 代碼組件是
AToolsFrameworkDemoGameModeBase。這是 AGameModeBase 的子類,我們將在編輯器中將其配置為默認(rèn)游戲模式。本質(zhì)上,這就是“啟動(dòng)”我們的運(yùn)行時(shí)工具框架的原因。請注意,這不是 RuntimeToolsFramework 模塊的一部分,而是基本游戲模塊,你無需在自己的應(yīng)用程序中以這種方式初始化。例如,如果你想實(shí)現(xiàn)某種游戲內(nèi)關(guān)卡設(shè)計(jì)/編輯工具,可能會(huì)將此代碼折疊到您現(xiàn)有的游戲模式中(或者可能會(huì)根據(jù)需要啟動(dòng)一個(gè)新模式)。你也不需要使用游戲模式來執(zhí)行此操作,盡管在這種情況下復(fù)雜的是默認(rèn) pawn AToolsContextActor,它可能也需要替換。
在這種游戲模式中很少發(fā)生。我們將其配置為 Tick,在 Tick() 函數(shù)中,我們 Tick()
URuntimeToolsFrameworkSubsystem。否則所有動(dòng)作都在
AToolsFrameworkDemoGameModeBase::InitializeToolsSystem()中,我們在其中初始化
URuntimeMeshSceneSubsystem 和
URuntimeToolsFrameworkSubsystem,然后將可用工具集注冊到 ToolManager。所有這些代碼都可以(也許應(yīng)該)從游戲模式本身中移出,并移到一些實(shí)用功能中。
如果你打算根據(jù)本教程設(shè)置自己的項(xiàng)目或進(jìn)行更改,則需要了解涉及的各種資產(chǎn)和項(xiàng)目設(shè)置。下面的內(nèi)容瀏覽器屏幕截圖顯示了主要資產(chǎn)。DefaultMap是我使用的關(guān)卡,它只包含地平面并在關(guān)卡藍(lán)圖中初始化 UMG 用戶界面(見下文)。
BP_ToolsContextActor是 AToolsContextActor 的藍(lán)圖子類,在游戲模式中被配置為默認(rèn) Pawn。在這個(gè) BP Actor 中,我禁用了Add Default Movement Bindings設(shè)置,因?yàn)槲以?Actor 中手動(dòng)設(shè)置了這些綁定。DemoPlayerController是
AToolsFrameworkDemoPlayerController 的 Blueprint 子類,這再次存在只是為了在 BP 中配置一些設(shè)置,特別是我啟用了Show Mouse Cursor以便繪制標(biāo)準(zhǔn) Windows 光標(biāo)(這是人們在 3D 工具中可能期望的)并禁用 Touch事件。最后DemoGameMode是我們
AToolsFrameworkDemoGameModeBase的 BP 子類C++ 類,在這里我們配置游戲模式以生成我們的 DemoPlayerController 和 BP_ToolsContextActor,而不是默認(rèn)值。
最后在Project Settings對話框中,我將Default GameMode配置為我們的DemoGameMode藍(lán)圖,并將DefaultMap配置為 Editor 和 Game 啟動(dòng)圖。我還在Input部分添加了各種操作,我在 AToolsContextActor 的描述中顯示了上面這些設(shè)置的屏幕截圖。最后在Packaging部分,我添加了兩條到 Materials 的路徑到Additional Asset Directories to Cook部分。這對于強(qiáng)制將這些材料包含在構(gòu)建的游戲可執(zhí)行文件中是必要的,因?yàn)殛P(guān)卡中的任何資產(chǎn)都沒有特別引用它們。
在我之前的教程中,我一直在 RuntimeGeometryUtils 插件中積累各種 Runtime 網(wǎng)格生成功能。為了實(shí)現(xiàn)本教程,我做了一個(gè)重要的補(bǔ)充,
URuntimeDynamicMeshComponent。這是
USimpleDynamicMeshComponent (SDMC) 的子類,增加了對碰撞和物理的支持。如果您還記得之前的教程,建模模式工具使用
USimpleDynamicMeshComponent 來支持在編輯期間實(shí)時(shí)預(yù)覽網(wǎng)格。在這種情況下,SDMC 針對原始渲染性能的快速更新進(jìn)行了優(yōu)化,并且由于它僅用于“預(yù)覽”,因此不需要對碰撞或物理的支持。
但是,我們也一直在使用 SDMC 作為渲染運(yùn)行時(shí)生成的幾何圖形的一種方式。在許多方面它與 UProceduralMeshComponent (PMC) 非常相似,但是 PMC 的一個(gè)顯著優(yōu)勢是它支持碰撞幾何,這意味著它可以與 UE 光線投射/線跟蹤系統(tǒng)以及物理/碰撞正常工作系統(tǒng)。事實(shí)證明,支持這一點(diǎn)相對簡單,所以我創(chuàng)建了
URuntimeDynamicMeshComponent 子類。SDMC的這個(gè)變種,我想我們可以稱之為RDMC,支持簡單和復(fù)雜的碰撞,還有一個(gè)函數(shù)
SetSimpleCollisionGeometry()可用,它可以采用任意簡單的碰撞幾何(甚至 PMC 也不支持)。但是請注意,目前不支持異步物理烹飪。這不是要添加的主要內(nèi)容,但我沒有這樣做。
我已將ADynamicSDMCActor中的組件類型切換為這個(gè)新組件,因?yàn)槠渌矫娴墓δ苁窍嗤?,但現(xiàn)在基礎(chǔ) Actor 上的碰撞選項(xiàng)的工作方式與它們在 PMC 變體上的工作方式相同。最終結(jié)果是以前的教程演示,如兔子槍和程序世界,應(yīng)該與 SDMC 以及 PMC 一起使用。這將為將來更有趣(或高性能)的運(yùn)行時(shí)程序網(wǎng)格工具打開大門。
這花費(fèi)了相當(dāng)長的時(shí)間,但我們現(xiàn)在可以在運(yùn)行時(shí)游戲的 MeshModelingToolset 中公開現(xiàn)有的網(wǎng)格編輯工具,并使用它們來編輯選定的 URuntimeMeshSceneObject。從概念上講,這個(gè)“正常工作”并添加工具工作的基本能力只需要在
AToolsFrameworkDemoGameModeBase::RegisterTools()中注冊一個(gè) ToolBuilder ,然后添加一些方式(熱鍵、UMG 按鈕等)以通過
URuntimeToolsFrameworkSubsystem 啟動(dòng)它: :BeginToolByName()。這適用于許多工具,例如 PlaneCutTool 和 EditMeshPolygonsTool 開箱即用。
但是,并非所有工具都能立即發(fā)揮作用。與全局 ToolTargetTargetFactory 系統(tǒng)類似,各種當(dāng)時(shí)可能看起來微不足道的小設(shè)計(jì)決策可能會(huì)阻止工具在構(gòu)建的游戲中工作。通常,通過一些實(shí)驗(yàn),可以在基本工具的子類中使用少量代碼來解決這些問題。我已經(jīng)在幾種情況下這樣做了,我將解釋這些,以便如果你嘗試公開其他工具,你可能會(huì)有一個(gè)嘗試做什么的策略。如果你發(fā)現(xiàn)自己陷入困境,請?jiān)谠u論中發(fā)布有關(guān)該工具不起作用的信息,我會(huì)盡力提供幫助。
請注意,要?jiǎng)?chuàng)建 Tool 子類,你還需要?jiǎng)?chuàng)建一個(gè)新的 ToolBuilder 來啟動(dòng)該子類。通常,這意味著子類化基礎(chǔ) Builder 并覆蓋創(chuàng)建工具的函數(shù),無論是基礎(chǔ) ::BuildTool() 還是基礎(chǔ) Builder 的調(diào)用 NewObject<T> 的函數(shù)(這些通常更容易處理)。
在某些情況下,默認(rèn)工具設(shè)置是有問題的。例如,URemeshTool默認(rèn)啟用僅編輯器的線框渲染。因此,有必要重寫 Setup() 函數(shù),調(diào)用基本的 Setup(),然后禁用此標(biāo)志(不幸的是,目前在 Builder 中沒有辦法這樣做,因?yàn)?Builder 沒有機(jī)會(huì)接觸分配新實(shí)例后的工具)。
創(chuàng)建新對象的工具,例如UDrawPolygonTool,通常在未經(jīng)修改的情況下無法在運(yùn)行時(shí)工作。在許多情況下,發(fā)出新對象的代碼是#ifdef 出來的,而是用 check() 代替。但是,我們可以將這些工具子類化并替換 Shutdown() 函數(shù)或工具的內(nèi)部函數(shù),以實(shí)現(xiàn)新對象的創(chuàng)建(通常來自工具生成的 FDynamicMesh3)。
URuntimeDrawPolygonTool::EmitCurrentPolygon()是為 UDrawPolygonTool 執(zhí)行此操作的示例,而
URuntimeMeshBooleanTool::Shutdown()為 UCSGMeshesTool 執(zhí)行此操作的示例。在后一種情況下,覆蓋執(zhí)行基本工具代碼的子集,因?yàn)槲抑恢С痔鎿Q第一個(gè)選定的輸入對象。
這是我遇到的兩個(gè)主要問題。第三個(gè)復(fù)雜因素是許多現(xiàn)有工具,尤其是舊工具,不使用 WatchProperty() 系統(tǒng)來檢測其
UInteractiveToolPropertySet 設(shè)置對象的值何時(shí)被修改。它們不依賴于輪詢,而是依賴于僅編輯器的回調(diào),這在構(gòu)建的游戲中不會(huì)發(fā)生。因此,如果你以編程方式更改這些 PropertSet 的設(shè)置,工具將不會(huì)更新以反映它們的值而無需輕推。不過,我已經(jīng)將這些“輕推”與一種將工具設(shè)置公開給藍(lán)圖的方式相結(jié)合,我現(xiàn)在將對此進(jìn)行解釋。
4.26 中工具框架的一個(gè)主要限制是,盡管它是由 UObject 構(gòu)建的,但它們都沒有暴露給藍(lán)圖。因此,你不能輕易地做一件微不足道的事情,例如將 UMG UI 連接到活動(dòng)工具,以直接更改工具設(shè)置。但是,如果我們對現(xiàn)有工具進(jìn)行子類化,我們可以將子類標(biāo)記為UCLASS(BlueprintType),然后將活動(dòng)工具(通過
URuntimeToolsFrameworkSubsystem::GetActiveTool()訪問)轉(zhuǎn)換為該類型。類似地,我們可以定義一個(gè)新的
UInteractiveToolPropertySet,也就是 UCLASS(BlueprintType),并公開標(biāo)記為 BlueprintReadWrite 的新 UProperties 以使它們可以從 BP 訪問。
為了包含這個(gè)新的 Property Set,我們將繼承 Tool ::Setup()函數(shù),調(diào)用基類::Setup(),然后創(chuàng)建并注冊我們的新 PropertySet。對于每個(gè)屬性,我們將添加一個(gè) WatchProperty() 調(diào)用,將更改從我們的新 PropertySet 轉(zhuǎn)發(fā)到基本工具設(shè)置,然后在必要時(shí)調(diào)用一個(gè)函數(shù)來啟動(dòng)重新計(jì)算或更新(例如 URuntimeMeshBooleanTool 必須調(diào)用Preview->InvalidateResult () )。
一個(gè)復(fù)雜的問題是枚舉值設(shè)置,它在編輯器中會(huì)自動(dòng)生成下拉列表,但是這對于 UMG 是不可能的。因此,在這些情況下,我使用整數(shù) UProperties 并將整數(shù)映射到自己的枚舉。因此,例如,這里是 UDrawPolygonTool 的 URuntimeDrawPolygonTool 的所有 PropertySet 相關(guān)代碼(我省略了上面提到的 EmitCurrentPolygon() 覆蓋和 new ToolBuilder)。這是一種剪切和粘貼模式,我可以在我的所有工具覆蓋中重復(fù)使用它來為我的 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);
}
我在嘗試讓 MeshModelingToolset 工具在構(gòu)建的游戲中工作時(shí)遇到的一個(gè)主要問題是,事實(shí)證明它們使用 UObjects 做了一些……非法的事情。這真的很麻煩,但我會(huì)簡要解釋一下,以防它與您相關(guān)。我之前提到
UInteractiveToolPropertySet 用于在幾乎所有工具中以結(jié)構(gòu)化的方式公開“工具設(shè)置”。像這樣的系統(tǒng)的一個(gè)理想屬性是能夠保存工具?調(diào)用之間的設(shè)置狀態(tài)。為此,我們可以只保留屬性集本身的一個(gè)實(shí)例,但我們需要將它保留在某個(gè)地方。
各種編輯器系統(tǒng)通過在其他一些 UObject 的 CDO 中保存指向已保存設(shè)置 UObject 的指針來執(zhí)行此操作 - 每個(gè) UObject 都有一個(gè) CDO(類默認(rèn)對象),它就像一個(gè)用于構(gòu)造附加實(shí)例的“模板”。CDO 是全球性的,因此這是放置東西的好地方。然而,在編輯器中,CDO 將阻止這個(gè) UObject 被垃圾收集(GC'd),但在運(yùn)行時(shí),它不會(huì)!事實(shí)上,在運(yùn)行時(shí),垃圾收集器會(huì)進(jìn)行安全檢查以確定這是否尚未完成,如果它檢測到這一點(diǎn),就會(huì)終止游戲(?。?。這需要在 UE 的未來版本中修復(fù),但要讓這個(gè)演示在二進(jìn)制 4.26 版本中運(yùn)行,我們需要一個(gè)解決方法。
首先,我必須通過在
URuntimeToolsFrameworkSubsystem::InitializeToolsContext() 中設(shè)置全局
GShouldVerifyGCAssumptions = false來禁用 GC 安全檢查。這可以防止硬殺,但是當(dāng)工具嘗試訪問它們并假定它們?nèi)匀淮嬖跁r(shí),保存的 PropertySet 仍將被垃圾收集并導(dǎo)致崩潰。因此,在
URuntimeToolsFrameworkSubsystem::OnToolStarted() 事件處理程序中,調(diào)用了
AddAllPropertySetKeepalives() 函數(shù),該函數(shù)遍歷新工具的所有已注冊 PropertySet UObjects 的 CDO,并將這些“保存的設(shè)置”UObjects 添加到將防止他們被GC'd。
這是......一個(gè)嚴(yán)重的黑客攻擊。但功能齊全,似乎沒有任何有問題的副作用。但我打算在未來解決底層架構(gòu)問題。
本教程的重點(diǎn)是演示交互式工具框架和網(wǎng)格建模工具集的運(yùn)行時(shí)使用,而不是實(shí)際構(gòu)建功能運(yùn)行時(shí)建模工具。然而,為了真正能夠啟動(dòng)和使用演示工具,我必須構(gòu)建一個(gè)最小的 UMG 用戶界面。我不是 UMG 的專家(這是我第一次使用它)所以這可能不是最好的方法。但是,它有效。在/ToolUI子文件夾中,您將找到幾個(gè) UI 小部件資產(chǎn)。
ToolTestUI是主用戶界面,位于左上角,右下角有一個(gè)圖像。我在教程開始時(shí)描述了各種工具按鈕。Accept、Cancel和Complete按鈕根據(jù)活動(dòng)工具狀態(tài)動(dòng)態(tài)更新可見性和啟用性,此邏輯位于藍(lán)圖中。撤消和重做按照您的預(yù)期進(jìn)行,并且“世界”按鈕可在任何活動(dòng) Gizmo 的“世界”和“本地”幀之間切換。此 UI 由關(guān)卡藍(lán)圖在 BeginPlay 上生成,位于右下方。
還有幾個(gè)每個(gè)工具的 UI 面板顯示工具設(shè)置。這些每個(gè)工具的 UI 面板在它們啟動(dòng)工具后由 ToolUI 按鈕生成,請參閱 ToolUI 藍(lán)圖,它非常簡單。我只為一些工具添加了這些設(shè)置面板,并且只公開了一些設(shè)置。添加設(shè)置的工作并不多,但有點(diǎn)乏味,而且由于這是一個(gè)教程,我不太關(guān)心公開所有可能的選項(xiàng)。下面的截圖來自DrawPolygonToolUI,顯示游戲內(nèi)面板(左)和 UI 藍(lán)圖(右)。本質(zhì)上,在初始化時(shí),Active Tool 被轉(zhuǎn)換為正確的類型,我們提取 RuntimeProperties 屬性集,然后初始化所有 UI 小部件(在這種情況下只有一個(gè))。然后在小部件事件更新時(shí),我們將新值轉(zhuǎn)發(fā)到屬性集。不涉及火箭科學(xué)。
我曾有很多人詢問 UE Editor Modeling Mode Tools 和 Gizmos 是否可以在運(yùn)行時(shí)使用,我的回答一直是“嗯,這很復(fù)雜,但可能”。我希望這個(gè)示例項(xiàng)目和文章能回答這個(gè)問題!這絕對是可能的,在 GeometryProcessing 庫和 MeshModelingToolset 工具和組件之間,UE4.26中提供了大量可用于構(gòu)建交互式 3D 內(nèi)容創(chuàng)建應(yīng)用程序的功能,從基本的“放置和移動(dòng)對象”工具到從字面上看,一個(gè)功能齊全的 3D 網(wǎng)格雕刻應(yīng)用程序。你真正需要做的就是設(shè)計(jì)和實(shí)現(xiàn) UI。
根據(jù)我過去構(gòu)建的設(shè)計(jì)工具,我可以肯定地說,當(dāng)前的建模模式工具可能并不完全是您自己的應(yīng)用程序所需要的。它們是一個(gè)不錯(cuò)的起點(diǎn),但我認(rèn)為它們提供的實(shí)際上是關(guān)于如何實(shí)現(xiàn)不同交互和行為的參考指南。你想要一個(gè)可以使用 Gizmo 移動(dòng)的 3D 工作平面嗎?查看
UConstructionPlaneMechanic以及它是如何在各種工具中使用的。在該平面上繪制和編輯 2D 多邊形怎么樣?請參閱UDrawAndRevolveTool中的
UCurveControlPointsMechanic用法。用于在網(wǎng)格上繪制最短邊路徑的界面?USeamSculptTool這樣做。想要制作一個(gè)運(yùn)行一些第三方幾何處理代碼的工具,具有設(shè)置和實(shí)時(shí)預(yù)覽以及為您預(yù)先計(jì)算的各種有用的東西?只是子類UBaseMeshProcessingTool。需要在工具期間在后臺(tái)線程中運(yùn)行昂貴的操作,以便您的 UI 不會(huì)鎖定?
UMeshOpPreviewWithBackgroundCompute和
TGenericDataBackgroundCompute實(shí)現(xiàn)模式,URemeshMeshTool 等工具展示了如何使用它。
我可以繼續(xù),很長一段時(shí)間。建模模式中有超過 50 種工具,它們可以做各種各樣的事情,遠(yuǎn)遠(yuǎn)超過我可能有時(shí)間解釋的。但是,如果您可以在 UE 編輯器中找到與您想要的內(nèi)容相近的內(nèi)容,則基本上可以復(fù)制 Tool .cpp 和 .h,重命名類型,然后開始根據(jù)您的目的對其進(jìn)行自定義。
所以,玩得開心!
原文鏈接:
http://www.bimant.com/blog/ue4-runtime-interactive-tool-framework/
熱門資訊
探討游戲引擎的文章,介紹了10款游戲引擎及其代表作品,涵蓋了RAGE Engine、Naughty Dog Game Engine、The Dead Engine、Cry Engine、Avalanche Engine、Anvil Engine、IW Engine、Frostbite Engine、Creation引擎、Unreal Engine等引擎。借此分析引出了游戲設(shè)計(jì)領(lǐng)域和數(shù)字藝術(shù)教育的重要性,歡迎點(diǎn)擊咨詢報(bào)名。
2. 手機(jī)游戲如何開發(fā)(如何制作傳奇手游,都需要準(zhǔn)備些什么?)
?如何制作傳奇手游,都需要準(zhǔn)備些什么?提到傳奇手游相信大家都不陌生,他是許多80、90后的回憶;從起初的端游到現(xiàn)在的手游,說明時(shí)代在進(jìn)步游戲在更新,更趨于方便化移動(dòng)化。而如果我們想要制作一款傳奇手游的
3. B站視頻剪輯軟件「必剪」:免費(fèi)、炫酷特效,小白必備工具
B站視頻剪輯軟件「必剪」,完全免費(fèi)、一鍵制作炫酷特效,適合新手小白??靵碓囋?!
4. Steam值得入手的武俠游戲盤點(diǎn),各具特色的快意江湖
游戲中玩家將面臨武俠人生的掙扎抉擇,戰(zhàn)或降?殺或放?每個(gè)抉定都將觸發(fā)更多愛恨糾葛的精彩奇遇。《天命奇御》具有多線劇情多結(jié)局,不限主線發(fā)展,高自由...
5. Bigtime加密游戲經(jīng)濟(jì)體系揭秘,不同玩家角色的經(jīng)濟(jì)活動(dòng)
Bigtime加密游戲經(jīng)濟(jì)模型分析,探討游戲經(jīng)濟(jì)特點(diǎn),幫助玩家更全面了解這款GameFi產(chǎn)品。
6. 3D動(dòng)畫軟件你知道幾個(gè)?3ds Max、Blender、Maya、Houdini大比拼
當(dāng)提到3D動(dòng)畫軟件或動(dòng)畫工具時(shí),指的是數(shù)字內(nèi)容創(chuàng)建工具。它是用于造型、建模以及繪制3D美術(shù)動(dòng)畫的軟件程序。但是,在3D動(dòng)畫軟件中還包含了其他類型的...
7. 3D動(dòng)漫建模全過程,不是一般人能學(xué)的會(huì)的,會(huì)的多不是人?
步驟01:面部,頸部,身體在一起這次我不準(zhǔn)備設(shè)計(jì)圖片,我從雕刻進(jìn)入。這一次,它將是一種純粹關(guān)注建模而非整體繪畫的形式。像往常一樣,我從Sphere創(chuàng)建它...
8. 如何自己開發(fā)一款游戲(游戲開發(fā)入門必看:五大獨(dú)立游戲開發(fā)技巧)
?游戲開發(fā)入門必看:五大獨(dú)立游戲開發(fā)技巧無論您是剛剛起步開發(fā)自己的第一款游戲,還是已經(jīng)制作了幾款游戲,本篇文章中的5大獨(dú)立游戲開發(fā)技巧都可以幫助您更好地設(shè)計(jì)下一款游戲。無論你對游戲有著什么樣的概念,都
?三昧動(dòng)漫對于著名ARPG游戲《巫師》系列,最近CD Projekt 的高層回應(yīng)并不會(huì)推出《巫師4》。因?yàn)椤段讕煛废盗性诓邉澋臅r(shí)候一直定位在“三部曲”的故事框架,所以在游戲的出品上不可能出現(xiàn)《巫師4》
10. 3D打印技巧揭秘!Cura設(shè)置讓你的模型更堅(jiān)固
想讓你的3D打印模型更堅(jiān)固?不妨嘗試一下Cura參數(shù)設(shè)置和設(shè)計(jì)技巧,讓你輕松掌握!
最新文章
同學(xué)您好!