虚幻引擎编辑器开发基础(一)
虛幻引擎編輯器開發基礎(一)
文章目錄
- 虛幻引擎編輯器開發基礎(一)
- 一、前言
- 二、插件與模塊
- 2.1 插件(Plguin)
- 2.1.1 插件的作用
- 2.1.2 插件的類型
- 2.1.3 插件結構
- 2.2 模塊(Module)
- 2.2.1 build.cs文件
- 2.2.2 創建模塊
- 2.2.3 模塊的加載與卸載
- 三、Slate
- 3.1 獨立窗口插件淺析
- 3.2 什么是Slate
- 3.3 Slate淺析
- 3.3.1 聲明式語法
- 3.3.2 創建自定義控件
- 3.3.3 布局
- 四、資源
- 4.1 路徑
- 4.2 資源
- 4.3 資源的創建與獲取
- 4.3.1 資源的創建
- 4.3.2 資源的獲取
- 4.4 AssetRegistry分析
- 4.4.1 如何搜尋asset文件
- 4.4.2 如何從FAssetData獲取Uobject
- 4.5 對象的創建與銷毀
- 五、縮略圖
- 參考文章
一、前言
虛幻引擎提供了非常強大的編輯器(如藍圖編輯器、材質編輯器、動畫編輯器)。
然而根據項目的不同以及相應的需求也需要擴展或者自定義一些編輯器的相關功能,對引擎做各種工具上的擴展,來滿足高效、快速的開發需要。
虛幻的編輯器開發包含了零零散散的各種內容,包括比如插件和模塊、編輯器窗口的擴展、資源文件的自定義等。
這篇文章,將會整理一下筆者了解的編輯器開發相關的基礎內容(一)。
由于筆者只自己實踐過其中的一部分。一些內容可能會留空,標注出可能的參考文章,后續工作涉及到或做到將補充和修改。
二、插件與模塊
在正式進入相關的開發介紹之前,讓我們來了解一下虛幻的插件與模塊功能。
插件與模塊既是虛幻組織代碼的一種方式,可以我們對編輯器開發的重要幫手。
虛幻中的Module、Plugin是兩個不同的概念。
一個Plugin可以由多個Module組成,Module只能由代碼組成,而Plugin可以由代碼和資源組成;
Plugin可以編譯之后打包跨工程使用,保持代碼的獨立性。
而Module在工程里的耦合則較高,是代碼級別的直接引用。
2.1 插件(Plguin)
2.1.1 插件的作用
虛幻的插件作用非常廣:
- 可添加運行時gameplay功能;
- 修改內置引擎功能(或添加新功能)、新建文件類型、以及使用新菜單/工具欄命令和子模式擴展編輯器的功能;
- 使用插件擴展許多現有UE4子系統;
2.1.2 插件的類型
虛幻的插件插件的類型分為兩類:
- 引擎插件;
- 項目插件;
注:二者除了放置目錄存在區別,基本沒有其他差別。
插件存放的文件目錄:
- 插件可擁有自己的Content文件夾,其中包含特定于該插件的資源文件;
- CanContainContent設置為true
- 引擎將掃描基礎Plugins文件夾下的所有子文件夾,查找要加載的插件;
- 引擎插件: /[UE引擎根目錄]/Engine/Plugins/[插件命名]/
- 游戲插件: /[項目根目錄]/Engine/Plugins/[插件命名]/
有時,在啟動UE項目時,會遇到插件編譯不過的問題。
臨時解決方法:可以編輯 XXX.uproject 去除某些插件。
查看現有的插件方法:
- Edit -> Plugins;可選擇開啟或禁用相應插件
2.1.3 插件結構
虛幻提供了好幾種插件的模板,如Blank(空白)、Content(只包含資源)、藍圖庫等。
通過上述的操作可以快速的創建一個新的插件。
下面讓我們一起看一下插件的目錄(文件)結構。舉例如下:
可以看到一個帶源碼的插件有
-
插件描述文件(.uplugin)
-
模塊配置文件(.Build.cs)
-
源碼目錄(Source)
-
…
插件里還可以包含著色器代碼文件、資源文件等。
插件描述文件(.uplugin)
- 虛幻啟動時,會在Plugin目錄里面搜索所有的.uplugin文件,來查找所有的插件;
- 每個.uplugin文件表示一個插件,其格式為.json;
- 該文件的作用:提供描述插件相關基本信息;
Modules字段
每個模塊,需要配置 名字Name、類型Type、加載階段LoadingPhase;
-
Name 是插件模塊的唯一命名;
-
Type 設置模塊的類型, 如:Runtime、Developer、Editor等;
- Runtime,在任何情況下都會加載;
- Editor,只在編輯器啟動時加載;
-
LoadingPhase 指明在什么階段加載模塊,默認為Default;
- PreDefault,讓模塊在一般模塊前加載
- Default,默認
- PostConfigInit,此模塊在虛幻關鍵模塊加載后加載
源碼目錄(Source)
- 存儲插件的源碼;
- 在Source下,每個目錄代表一個模塊;
- 每個模塊包含Public和Private目錄,以及模塊配置文件(.Build.cs);
模塊配置文件(.Build.cs)
后續再介紹。
2.2 模塊(Module)
為什么虛幻引入模塊機制?
- 編譯模式太多,配置復雜;
由前面介紹可知,一個模塊文件夾應該包含這些內容:
- Public文件夾;
- Private文件夾;
- *.builc.cs文件
UE4的代碼是由模塊來組織的,.build.cs代表一個模塊。
2.2.1 build.cs文件
模塊配置文件是用來告知**UBT(Unreal Build Tool)**如何配置編譯和構造環境。
using UnrealBuildTool; public class pluginDev : ModuleRules {public pluginDev(TargetInfo Target){PublicDependencyModuleNames.AddRange(new string[]{"Core","CoreUObject","Engine","InputCore"});PrivateDependencyModuleNames.AddRange(new string[]{//...});} }其中,
// 添加#inlcude的頭文件路徑 PublicIncludePaths (List<String>) // 公開給其他模塊的文件路徑 / 但是不需要"導入"或鏈接 PrivateIncludePaths (List<String>) // 通向此模塊內部包含文件的所有路徑的列表,不向其他模塊公開// 控制依賴 PublicIncludePathModuleNames(List<String>) // 我們模塊的公共標頭需要對這些標頭文件進行訪問,但是不需要"導入"或鏈接 PublicDependencyModuleNames (List<String>) // 公共源文件所需要的模塊.(需要導入或鏈接?)PrivateIncludePathModuleNames (List<String>) // 我們模塊的私有代碼文件需要對這些標頭文件進行訪問,但是不需要"導入"或鏈接。 PrivateDependencyModuleNames(List<String>) // 私有代碼依賴這些模塊.(需要導入或鏈接?)DynamicallyLoadedModuleNames (List<String>) // 此模塊在運行時可能需要的附加模塊2.2.2 創建模塊
創建一個新的模塊分為如下幾步:
- 創建模塊文件夾結構;
- 創建模塊構建文件 .build.cs;
- 創建模塊的頭文件和實現文件;
- 創建模塊的C++聲明和定義;
模塊源代碼文件示例:
并實現StartUpModule和ShutdownModule函數,功能為: 自定義模塊的加載和卸載時行為。
.h文件
#pragma once #include "ModuleManager.h" class FPluginDevModule : public IModuleInterface {/** IModuleInterface implementation */virtual void StartupModule() override;virtual void ShutdownModule() override; }.CPP文件
#include "XXX.h"void FPluginDevModule::StartupModule() {// ... 模塊加載執行內容 } void FPluginDevModule::ShutdownModule() {// ... 模塊卸載執行內容 } //!!! // 表明FPluginDevModule是實現pluginDev模塊的類 IMPLEMENT_MODULE(FPluginDevModule,pluginDev)由上述代碼可知,虛幻的模塊類繼承自IModuleInterface。
其包含7個虛函數:
- StartupModule
- PreUnloadCallback
- PostLoadCallback
- ShutdownModule
- SupportsDynamicReloading
- SupportAutomaticShutdown
- IsGameMode
StartupModule是模塊的入口函數。
2.2.3 模塊的加載與卸載
在源碼層面,一個包含 *.build.cs 的文件就是一個模塊。
每個模塊編譯鏈接后后,會生成比如一個靜態庫lib或動態庫dll。
虛幻引擎初始化模塊加載順序,由2個部分決定:
總體的順序:
模塊的加載注冊
- 模塊需要提供給外部一個操作的接口,就是一個IModuleInterface指針;
- 這里并不是說調用模塊內的任何函數(或類)都需要通過這個指針
- 實際上,只需要#include了相應頭文件就可以調用對應的功能,如New一個類,調一個全局函數;
- 這個IModuleInterface指針的意義: 操作作為整體的模塊本身,如模塊的加載/初始化/卸載。訪問模塊內的一些全局變量。
- IModuleInterface 在ModuleInterface.h
- 獲取這個指針的方法,只有一個:就是通過 FModuleManager 上的 LoadModule/GetModule。
FModuleManager去哪里加載這些模塊呢?
即調用FModuleManager::LoadModule,其中又對動態和靜態區別處理:
- 動態鏈接庫,根據名字直接加載對應的DLL即可。
- 作為合法UE模塊的dll,必定要導出一些約定的函數來返回自身IModuleInterface指針;
- 靜態鏈接庫,去一個叫StaticallyLinkedModuleInitializers的Map里找。這就要求所有模塊已把自己注冊到這個Map里。
為滿足以上約定,每個模塊在實現的過程中,需要插入一些宏代碼,例如上述示例中的IMPLEMENT_MODULE。
IMPLEMENT_MODULE代碼如下:
靜態鏈接時:
- FStaticallyLinkedModuleRegistrant,是一個注冊輔助類,利用全局變量構造函數自動調用的特性,實現自動注冊。
FStaticallyLinkedModuleRegistrant:
template< class ModuleClass > class FStaticallyLinkedModuleRegistrant { public:FStaticallyLinkedModuleRegistrant( const ANSICHAR* InModuleName ){// Create a delegate to our InitializeModule methodFModuleManager::FInitializeStaticallyLinkedModule InitializerDelegate = FModuleManager::FInitializeStaticallyLinkedModule::CreateRaw(this, &FStaticallyLinkedModuleRegistrant<ModuleClass>::InitializeModule );// Register this moduleFModuleManager::Get().RegisterStaticallyLinkedModule(FName( InModuleName ), // Module nameInitializerDelegate ); // Initializer delegate}IModuleInterface* InitializeModule( ){return new ModuleClass();} };動態鏈接時:
- 聲明了一個dllexport函數,功能就是創建并返回相應模塊;
- ModuleImplClass:具體實現的類;
- ModuleName:模塊的名稱(字符串);
插件重編譯問題:有時候修改了代碼卻發現沒有進行編譯。
如何解決:把插件目錄下的兩個文件夾刪除掉:Binaries(包含DLL)、Intermediate(Obj文件等)。
三、Slate
在了解了虛幻的插件和模塊的概念之后,讓我們來通過一個虛幻的例程,簡單地了解一下編輯器開發的基礎,Slate。即如何用C++編程創建UI。
在游戲開發過程中的UI大多直接UMG進行開發實現。
而在編輯器開發過程中,則可能需要使用Slate來實現一些UI功能。
亦可以用Slate組合一些控件,再封裝成為UMG。
3.1 獨立窗口插件淺析
首先,創建Standalone Window插件。
- 在創建新的插件時,選擇Editor Standalone Window進行創建;
然后,編譯項目之后打開,可以在工具欄上看到按鈕:
點擊之后有:
- 點擊工具欄按鈕彈出一個窗口(里面有一個Tab);
- 提示的內容為:可以添加代碼到 FSimpleWindowModule::OnSpawnPluginTab in SimpleWindow.cpp來重寫窗口的內容;
源碼閱讀與分析
- StandaloneWindow 插件的主入口;
- StandaloneWindowStyle 定義了UI的風格;
- StandaloneWindowCommands 聲明要注冊的命令;
StandaloneWindowCommands類
- 聲明命令和注冊;
StartupModule主要做了幾件事情:
- 初始化風格;
- 注冊UI命令和其回調;
- 添加創建的擴展;
- 注冊標簽頁的創建方法;
其中Extender的回調如下:
- 工具欄和菜單欄調用不同的函數,但注冊的同一個命令。
這個命令對應的回調即:
其中PluginButtonClicked回調的具體實現:
- 根據名字去激活一個Tab標簽UI,即StartupModule函數中最后注冊的。
這個標簽頁的具體生成方法,即OnSpawnPuginTab函數實現的。最后返回一個Slate聲明創建的UI。
對于以上的源碼以及其實現效果,有三點值得研究,分為是:
其中:
- 第一點(菜單欄和工具欄的擴展)為后續文章的內容之一,到時再進行介紹。
- 第二點(Slate創建的簡單UI)為本節后續的內容;
- 第三點,大量地在虛幻使用,在此我們進行介紹。
UI_COMMAND系列
這里說的UI_COMMAND系列,筆者指的是:
// UICommandInfo.h/cpp FUICommandInfo 類// UICommandList.h/cpp FUICommandList 類// Commands.h/cpp UI_COMMAND 宏三者的關系如下:
- UI_COMMAND宏,負責正式注冊FUICommandInfo;
- FUICommandList,用于包含FUICommandInfo,并且映射FUICommandInfo到委托;
UI_COMMAND宏
// -> MakeUICommand_InternalUseOnly // -> -> FUICommandInfo::MakeCommandInfo 在該函數中New了一個FUICommandInfo OutCommand = MakeShareable(new FUICommandInfo(InContext->GetContextName())); FInputBindingManager::Get().CreateInputCommand(InContext,OutCommand.ToSharedRef());將FUICommandInfo注冊到了FInputBindingManager的單例中。
FUICommandList映射委托調用
一系列函數FUICommandList::MapAction(),會將FUICommandList和FUIAction注冊到Map中。
FUICommandInfo會被UI使用,當UI條件觸發,則會調用綁定的委托。
3.2 什么是Slate
現在我們來了解一下3.1中我們提出的第二點研究內容,即Slate。那么什么是Slate呢?
Slate概述 中提到:
- Slate 是完全自定義、與平臺無關的用戶界面框架,旨在讓工具和應用程序(比如虛幻編輯器)的用戶界面或游戲中用戶界面的構建過程變得有趣、高效。
- 它將聲明性語法與輕松設計、布局和風格組件的功能相結合,允許在UI上輕松實現創建和迭代。
筆者是這樣理解的:
- 用聲明式的語法,用C++來定義UI,包含了UI的布局、風格、以及對UI響應的處理。
就3.1中的例子,通過C++代碼直接定義一個包含了一個文本框的標簽頁,并指定了文本內容。
- 可以看出這樣的方式,使得程序員可以訪問構建UI,而無需添加間接層。
3.3 Slate淺析
Slate控件分為三種:
- 無插槽(Slot)的控件,SLeafWidget,例如:SImage、STextBlock。
- 有一個Slot的控件,SCompoundWidget,例如:SButton。
- 有多個Slot的控件,SPanel(布局),例如:SVerticalBox。
其中:
-
在Slate中,容器不存儲控件,容器中的Slot(插槽)存儲控件。
-
插槽可以存儲三種的控件任意一種。
3.3.1 聲明式語法
為了實現聲明式語法,UE提供了一組完整的宏來簡化聲明和創建新控件的過程。
接下來,將對Slate中的宏定義進行一些簡單的解析,以SButton(SButton.h)為例。
看一下SButton類是如何聲明的。
class SLATE_API SButton: public SBorder { // ... 省略部分源碼 public:// SLATE_BEGIN_ARGS( SButton ): _Content(), _ButtonStyle( &FCoreStyle::Get().GetWidgetStyle< FButtonStyle >( "Button" ) ), _TextStyle( &FCoreStyle::Get().GetWidgetStyle< FTextBlockStyle >("NormalText") ), _HAlign( HAlign_Fill ), _VAlign( VAlign_Fill ), _ContentPadding(FMargin(4.0, 2.0)), _Text(), _ClickMethod( EButtonClickMethod::DownAndUp ), _TouchMethod( EButtonTouchMethod::DownAndUp ), _PressMethod( EButtonPressMethod::DownAndUp ), _DesiredSizeScale( FVector2D(1,1) ), _ContentScale( FVector2D(1,1) ), _ButtonColorAndOpacity(FLinearColor::White), _ForegroundColor( FCoreStyle::Get().GetSlateColor( "InvertedForeground" ) ), _IsFocusable( true ){}/** Slot for this button's content (optional) */SLATE_DEFAULT_SLOT( FArguments, Content )/** The visual style of the button */SLATE_STYLE_ARGUMENT( FButtonStyle, ButtonStyle )/** The text style of the button */SLATE_STYLE_ARGUMENT( FTextBlockStyle, TextStyle )// ... 省略部分源碼/** Called when the button is clicked */SLATE_EVENT( FOnClicked, OnClicked )/** Called when the button is pressed */SLATE_EVENT( FSimpleDelegate, OnPressed )/** Called when the button is released */SLATE_EVENT( FSimpleDelegate, OnReleased )SLATE_EVENT( FSimpleDelegate, OnHovered )SLATE_EVENT( FSimpleDelegate, OnUnhovered )SLATE_END_ARGS()// ... 省略部分源碼 };SLATE_BEGIN_ARGS 和 SLATE_END_ARS
#define SLATE_BEGIN_ARGS( WidgetType ) \public: \struct FArguments : public TSlateBaseNamedArgs<WidgetType> \{ \typedef FArguments WidgetArgsType; \FORCENOINLINE FArguments()#define SLATE_END_ARGS() \};在二者的包圍中,創建了一個FArguments參數類。
FArguments的成員變量或函數通過以下幾個宏定義來輔助實現。
- SLATE_ARGUMENT
- SLATE_ATTRIBUTE
- SLATE_EVENT
- SLATE_DEFAULT_SLOT
SLATE_ARGUMENT 宏
- 參數只能是值。
SLATE_ATTRIBUTE 宏
- 屬性可以是值也可以是函數。
SLATE_EVENT 宏
- 事件,其實就是回調。
SLATE_DEFAULT_SLOT 宏
-
根據名稱創建了Widget,可以用來存儲Widget。
-
重載了[]操作符,可以有一個Widget作為輸入。
SButton是SCompoundWidget,只有一個Slot插槽。
讓我們再來看一下多個插槽的情況,看一下布局,以SVerticalBox舉例。
可以看到類中的宏定義如下:
SLATE_BEGIN_ARGS( SVerticalBox ) {_Visibility = EVisibility::SelfHitTestInvisible; }SLATE_SUPPORTS_SLOT(SVerticalBox::FSlot)SLATE_END_ARGS()SLATE_SUPPORTS_SLOT 宏
- 定義了一個Slot數組;
- 在該類中為SVerticalBox::FSlot,并重載了+操作符,添加Slot到數組中。
其中,SVerticalBox::FSlot在類里面中實現如下。
/** A Vertical Box Panel. See SBoxPanel for more info. */ class SLATECORE_API SVerticalBox : public SBoxPanel { public:// SVerticalBox::FSlot的定義class FSlot : public SBoxPanel::FSlot{public:FSlot(): SBoxPanel::FSlot(){}// 省略了部分源碼}// 省略了部分源碼 };3.3.2 創建自定義控件
根據上述的宏,我們很容易可以對自定義Slate的控件。
例如我們可以自定義 CompoundWidget。
- 必須要有的 SLATE_BEGIN_ARGS 和 SLATE_END_ARGS 以及構造器Construct函數。
- 由于是SCompoundWidget的子類,則可以通過ChildSlot構造控件內容。
下面給出一個自定義控件的例子(僅僅是將兩個按鈕組裝在一起)。
.h文件如下:
class SWidgetDemoA : public SCompoundWidget {SLATE_BEGIN_ARGS(SWidgetDemoA) {}SLATE_ATTRIBUTE(FString, InText)SLATE_END_ARGS()public:SWidgetDemoA();/*** Construct this widget* @param InArgs The declaration data for this widget*/void Construct(const FArguments& InArgs);private:void OnClicked();};.cpp文件如下:
SWidgetDemoA::SWidgetDemoA() { }void SWidgetDemoA::Construct(const FArguments& InArgs) {// InArgs._InText 類型為 TAttribute<FString>// 需要用Get函數FString Text = InArgs._InText.Get();this->ChildSlot[SNew(SVerticalBox)+ SVerticalBox::Slot()[SNew(SButton).Text(FText::FromString(Text))]+SVerticalBox::Slot()[SNew(SHorizontalBox)+SHorizontalBox::Slot()[// 測試事件SNew(SButton).OnClicked(this,&SWidgetDemoA::OnClicked)]]]; }void SWidgetDemoA::OnClicked() {UE_LOG(DmoeA, Warning, TEXT("SWidgetDemoA::OnClicked")); }更多可以參考UE源碼文件,或參考Slate控件示例 。
3.3.3 布局
這部分主要摘錄自 虛幻4渲染編程(工具篇)【第九卷:SlateUI布局】
Slate中的SWidget按照插槽Slot數量劃分以下三種。
在Slate中,容器不存儲控件,容器中的Slot(插槽)存儲控件。
插槽(Slot)可以存儲三種的控件任意一種。
每個插槽(Slot)到底占據UI的多少,取決于很多因素。
首先,來看Slot中的SizeParm參數。這是決定UI控件大小的的第一步。
Engine\Source\Runtime\SlateCore\Public\Widgets\SBoxPanel.h
/** * How much space this slot should occupy along panel's direction. * When SizeRule is SizeRule_Auto, the widget's DesiredSize will be used as the space required. * When SizeRule is SizeRule_Stretch, the available space will be distributed proportionately between * peer Widgets depending on the Value property. Available space is space remaining after all the * peers' SizeRule_Auto requirements have been satisfied. */ FSizeParam SizeParam;翻譯翻譯:
- 如果SizeRule是Auto,那么Slot大小會使用DesiredSize。
- 如果SizeRule是Stretch,那么Slot就會盡可能充滿UI空間。
Auto情況:
- 可以看出按鈕的大小是有一定的寬度是一定的。
Stretch情況:
- 當SizeParam不指定的情況下就是默認為Stretch。
下面將第一個Slot設置為Auto,第二個為Stretch。可以看出符合前面所說的情況。
Slot還有HAlignment和VAlignment兩個參數,用來描述Slot的位置。
/** Horizontal positioning of child within the allocated slot */ TEnumAsByte<EHorizontalAlignment> HAlignment;/** Vertical positioning of child within the allocated slot */ TEnumAsByte<EVerticalAlignment> VAlignment;目前只考慮水平方向。
當我們使用**(Auto,Fill)和(Auto,HAlign_Left)**組合時:
假設目前有三個Slot并且向這三個slot中各添加了一個按鈕UI控件,其中一個是(Stretch,HAlign_Fill) 兩個是(Auto,HAlign_Left)
這個結果的計算如下:
如果將第一個插槽的填充方式改為Left,那么發現填充不滿。
- 因為Slot的大小大于控件本身的大小,留下很多可以被填充的Slot空間。
- 這里1的slot大小為窗口-2和3的slot大小,但是控件只有和2或3那么大。
如果將第一個插槽的填充方式改為Center:
如果我將三個控件全部選擇為Auto:
三個Slot的大小被明確的計算出來。
如果三個Slot里的控件的DesireSize之和小于窗口的大小,那么就會有Slot空間剩余。
這時再把控件填充進來。因為是選用的Auto,所以不管控件是什么填充模式,Slot空間的大小始終剛好等于內部控件的大小。
當Slot的大小總和大于窗口的時候會是什么情況呢?
通過SBox自定義DesireSize來定義SHorizontalBox的Slot大小!
如下示例所示,因為是Auto,所以Slot的大小是不會改變的,那么多余的部分直接會被Cull掉。
前面一直如果是Auto的情況下,Slot大小會使用DesiredSize。
那么DesiredSize是如何計算的呢?
看一下這個過程:
/*** A Panel's desired size in the space required to arrange of its children on the screen while respecting all of* the children's desired sizes and any layout-related options specified by the user. See StackPanel for an example.** @return The desired size.*/ FVector2D SBoxPanel::ComputeDesiredSize( float ) const {return (Orientation == Orient_Horizontal)? ComputeDesiredSizeForBox<Orient_Horizontal>(this->Children): ComputeDesiredSizeForBox<Orient_Vertical>(this->Children); }SBoxPanel的ComputeDesiredSize:
- 通過注釋可用看出:這個Desired size是所有子控件的desired size之和加上指定的Margin。
其中調用的GetDesiredSize為:
FVector2D SWidget::GetDesiredSize() const {return DesiredSize.Get(FVector2D::ZeroVector); }這個值是在CacheDesiredSize時計算的存儲起來的:
void SWidget::CacheDesiredSize(float InLayoutScaleMultiplier) { #if SLATE_VERBOSE_NAMED_EVENTSSCOPED_NAMED_EVENT(SWidget_CacheDesiredSize, FColor::Red); #endif// Cache this widget's desired size.SetDesiredSize(ComputeDesiredSize(InLayoutScaleMultiplier)); }看一下SBox的ComputeDesiredSize函數:
- 可以看出這個函數通過Override的寬度和高度自定義大小,反正不管怎么定義計算它就是代表內部控件的大小。
布局小結:
四、資源
了解了Slate,這僅是對編輯器擴展的表面皮毛,即UI僅是用于展示、接收和轉發用戶輸入的一個中間媒介。(當然能把UI做得好看好用更好!)
編輯器功能的核心還是在于對資源、用戶輸入的數據的處理算法(例如,對資源進行創建、計算、修改等)。
因而,對資源、資產數據的了解是處理數據的第一步。
4.1 路徑
UE將目錄分為:引擎(Engine)目錄 和 項目(Game)目錄。
并采用沙盒路徑,即虛擬路徑來標識資源的路徑。
其中有:
- /Game 是一個虛擬的路徑,實際表示的是項目的 FPaths::ProjectContentDir() 。
- /Engine 也是一個虛擬路徑,實際路徑是引擎的 FPaths::EngineContentDir() 。
更多虛擬路徑可以查看源碼PackageName.cpp 中的類 FLongPackagePathsSingleton 的定義。
struct FLongPackagePathsSingleton {FString ConfigRootPath;FString EngineRootPath;FString GameRootPath;FString ScriptRootPath;FString ExtraRootPath;FString MemoryRootPath;FString TempRootPath;TArray<FString> MountPointRootPaths;FString EngineContentPath;FString ContentPathShort;FString EngineShadersPath;FString EngineShadersPathShort;FString GameContentPath;FString GameConfigPath;FString GameScriptPath;FString GameExtraPath;FString GameSavedPath;FString GameContentPathRebased;FString GameConfigPathRebased;FString GameScriptPathRebased;FString GameExtraPathRebased;FString GameSavedPathRebased;//@TODO: Can probably consolidate these into a single array, if it weren't for EngineContentPathShortTArray<FPathPair> ContentRootToPath;TArray<FPathPair> ContentPathToRoot;// ... 省略部分源碼 };路徑相關的API
常用的路徑接口都放在FPaths這個類。
/*** Path helpers for retrieving game dir, engine dir, etc.*/ class CORE_API FPaths {// ... 省略部分源碼 };4.2 資源
資源(Asset) 是用于 虛幻引擎 項目的內容項,可將其看作序列化到文件中的 UObject。
資源對應的數據結構類為:FAssetData。
其包含了以下幾種重要屬性:
- ObjectPath
- PackageName
- PackagePath
- AssetName
- AssetClass
下面,通過一個實際的例子來看下這些路徑都是什么樣的。
在項目的Content目錄創建了一個藍圖Acotr。
則有:
- ObjectPath : /Game/NewBlueprint.NewBlueprint
- PackageName : /Game/NewBlueprint
- PackagePath : /Game
- AssetName : NewBlueprint
可以看出其路徑采用了前面虛擬路徑。
那么,有了路徑如何獲得對應的UObject數據呢?
- 可以通過GetAsset獲得內存中的UObject對象!
4.3 資源的創建與獲取
4.3.1 資源的創建
在編寫工具的時候需要創建資源、初始化資源、管理資源等需求。比如:
-
合并DrawCall時,需要創建貼圖;
-
管理材質時,需要創建并指定材質球、設置材質等。
首先,弄清楚虛幻的資源結構以及和編輯器的關系。
虛幻中有大量的類型,諸如UMaterial、UMaterialInstance、UTexture、UStaticMesh等。
這些資源從外部導入虛幻的時候做了一次數據抽取,如貼圖資源模型資源等。
這些數據被放在一個UObject里,然后這個UObject放在一個Package里。
拿貼圖的數據導入舉例,有代碼UTextureFactory::ImportTexture:
- 引擎會抽取數據,然后創建對應UTexture,使用創建的UTexture填充其Source。
所以Unreal的導入資產,創建有一個固定格式:
- New一個Package;
- New一個資源對應的UObject;
- 在此時指認Package和UObject;
- 向這個UObject填充數據;
- MarkDirty,注冊,通知資源瀏覽器這里創建了一個新的資源,然后保存。
下面給出幾個通過C++創建資源資產的示例。
示例1: 創建一個模型資源,Create Mesh Asset With C++
大概的過程描述如下:
注意:
- 這里創建的是個空的MeshAsset;
- 如果想給MeshAsset添加模型頂點信息,可以通過設置NewStaticMesh->SourceModels來給MeshAsset增加資源信息;
示例2: 創建一個材質實例,Create Material Instance With C++
// Create Empty Material Instance Asset FString MaterialInsBaseName(TEXT("MI_")); MaterialInsBaseName += TEXT("Test"); FString AssetPath = TEXT("/Game/") + FString(TEXT("Test")) + TEXT("/") + MaterialInsBaseName;UPackage* NewMatInsPack = CreatePackage(nullptr, *AssetPath);// New a Factory UMaterialInstanceConstantFactoryNew* Factory = NewObject<UMaterialInstanceConstantFactoryNew>(); // AssetTools Use Factory to New Asset FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked< FAssetToolsModule >("AssetTools"); UMaterialInstanceConstant* ReplaceMI = Cast<UMaterialInstanceConstant>(AssetToolsModule.Get().CreateAsset(MaterialInsBaseName, FPackageName::GetLongPackagePath(AssetPath), UMaterialInstanceConstant::StaticClass(), Factory));ReplaceMI->MarkPackageDirty(); ReplaceMI->PreEditChange(nullptr); ReplaceMI->PostEditChange();//Save the assets TArray<UPackage*> PackagesToSave; PackagesToSave.Add(NewMatInsPack); FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, false, /*bPromptToSave=*/ false);備注:
- 除了采用NewUObject之外,還可以用AssetTool提供的AssetFactory來創建資源,原理是一樣的;
- 但AssetFactory比我們NewObject要多做一些保險的事情;
示例3: 創建一個紋理資源,Create Texture With C++
// Create Texture2D Asset FString AssetName = TEXT("TestTexture"); FString AssetPath = TEXT("/Game/") + FString(TEXT("Test")) + TEXT("/") + AssetName;UPackage* NewAssetPack = CreatePackage(nullptr, *AssetPath);UTexture2D* NewTexture = NewObject<UTexture2D>(NewAssetPack, FName(*AssetName), RF_Public | RF_Standalone);// 可以在代碼中設置貼圖的屬性,如格式尺寸等. {int32 width = 512;int32 height = 512;NewTexture->PlatformData = new FTexturePlatformData;NewTexture->PlatformData->SizeX = width;NewTexture->PlatformData->SizeY = height;NewTexture->PlatformData->PixelFormat = PF_R8G8B8A8;FTexture2DMipMap* Mip = new(NewTexture->PlatformData->Mips) FTexture2DMipMap();Mip->SizeX = width;Mip->SizeY = height;Mip->BulkData.Lock(LOCK_READ_WRITE);uint8* TextureData = (uint8 *)Mip->BulkData.Realloc(512 * 512 * sizeof(uint8) * 4);uint8 PixleSize = sizeof(uint8) * 4;const uint32 TextureDataSize = width * height * PixleSize;TArray<FColor>ColorData;ColorData.AddDefaulted(width * height * 4);for (int32 i = 0; i < height; i++){for (int32 j = 0; j < width; j++){ColorData[i * width + j].R = (float)i / (float)height * 255;ColorData[i * width + j].G = (float)j / (float)width * 255;ColorData[i * width + j].B = (float)j / (float)width * 255;ColorData[i * width + j].A = 255;}}FMemory::Memcpy(TextureData, ColorData.GetData(), TextureDataSize);Mip->BulkData.Unlock();NewTexture->UpdateResource(); }FAssetRegistryModule::AssetCreated(NewTexture); NewAssetPack->MarkPackageDirty();//Save the assets TArray<UPackage*> PackagesToSave; PackagesToSave.Add(NewAssetPack); FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, false, /*bPromptToSave=*/ false);4.3.2 資源的獲取
在實踐中,有時候需要根據路徑從而獲取資源,再進行一系列處理。
Unreal的資源分為美術資源(即導入的)和藍圖資源(即在UE中創建的)。
資源獲取分別采用不同函數獲取到C++中使用。
藍圖資源加載:
- 使用LoadClass函數加載藍圖類。
美術資源加載:
- 使用LoadObject函數加載美術資源。例如加載一個StaticMesh。
除之之外,在開發過程中,還經常需要處理的一個問題就是:
- 需要獲得某種特定格式資產的所有路徑,如Mesh、Material等。
UE篩選某類資源(FAssetData)的資源有兩種方法:
- UObjectLibrary;
- FAssetRegistryModule + FARFilter;
UObjectLibrary方法:
- 只需利用UObjectLibrary和FAssetData兩個關鍵結構即可;
FAssetRegistryModule和FARFilter方法
- 設置路徑和類名;
兩種方法的區別:
- UObjectLibrary加載了相應的所有資源進入內存!
- 而FAssetRegistryModule和FARFilter僅僅是獲取資源的路徑。相應的資源并沒有被加載為UObject!
4.4 AssetRegistry分析
這部分摘錄自 UE4 AssetRegistry分析
Asset Registry是Editor的子系統,負責在Editor加載時收集未加載的Asset信息。
這些信息儲存在內存中,因此Editor可以創建資源列表,而不需要真正加載這些資源。
Content Browser是這個系統的主要消費者,但是Editor的其他部分,以及Editor外的模塊也能訪問它們。
官方文檔-Assets/Registry
簡而言之,AssetRegisty可以搜尋uasset文件,并用FAssetData抽象表示,并在有需要時根據FAssetData中的路徑線索加載UObject。
Asset Registry類圖:
4.4.1 如何搜尋asset文件
使用AssetRegistry,我們可以方便的從磁盤上讀取Asset文件并構建FAssetData描述。
同步方式
通過上層接口進行調用,通常只能通過同步的方式搜尋,通常使用以下接口:
ScanFilesSynchronous(const TArray<FString>& InFilePaths, bool bForceRescan = false)搜尋InFilePaths路徑下的所有asset,不需要返回值,因為搜尋后的結果會保存在FAssetRegistryState中,之后可以使用過濾器進行查詢。
具體方式為新建一個FAssetDataGatherer,把模式設置為同步,然后FAssetDataGatherer就會直接阻塞的調用run()方法,為我們搜尋asset了。
類似的,也可以使用ScanFilesSynchronous方法進行同步搜索。
SearchAllAssets(bool bSynchronousSearch)一種更簡單的方式:同步搜尋所有Assets。
SearchAllAssets方法可以以同步和異步方式運行,該方法的同步模式通常用于CommandLet中,因為此時對時間消耗并不是很敏感。
此方法同步模式最終執行方式其實是和ScanFilesSynchronous相同的,只是為我們自動得到了所有要搜尋的路徑。
異步方法
異步方式通常由編輯器內部調用,尚不清楚如何通過上層進行調用。
SearchAllAssets(bool bSynchronousSearch)之前說了,SearchAllAssets可以以異步模式執行,其實現方法也比較直觀,為新建了一個FAssetDataGatherer對象,然后在UAssetRegistryImpl::Tick中獲取。
直接調用異步SearchAllAssets的地方目前只有一處,就是UAssetRegistryImpl的構造函數中,當在編輯器中,且不通過CommandLet時,就會調用,其作用是初始化ContentBrowser。
我們剛打開編輯器時的初始化加載過程就是在做異步資源發現操作。
讓我們看一下Tick是如何執行的,調用過程為:
- EngineTick()
- UEditorEngine::Tick 編輯器EngineTick
- FAssetRegistryModule::TickAssetRegistry
- UAssetRegistryImpl::Tick
在UAssetRegistryImpl::Tick中,會不斷通過FAssetDataGatherer類型的成員變量BackgroundAssetSearch獲取新搜尋到的FAssetData,并重置BackgroundAssetSearch的搜尋結果。
之后AssetRegistry就會更新自己和State的數據,并發送一些廣播,通知搜尋到 了新的assetdata。
通過調用過程可以看到,在引擎的循環中,如果是編輯器,那FAssetRegistry的Tick始終在執行,以此達到保持編輯器中assetdata始終于磁盤同步的目的。
AddPathToSearch(const FString& Path)這個方法是私有方法,并不能被上層調用,作用為向異步掃描過程中加入待掃描路徑,編輯器自身的一些功能模塊會調用到這。
4.4.2 如何從FAssetData獲取Uobject
如前面提到的:采用FAssetData::GetAsset()即可獲得UObject。
可以再通過Cast<>轉換成對應的資源類型。
4.5 對象的創建與銷毀
既然介紹了一些資源的創建獲取。
下面再整理一些常用的C++層進行對象的創建與銷毀方法:
純C++類的創建與銷毀
創建:
- 通過new來創建對象,推薦用智能指針管理。
銷毀:
- 智能指針計數器會自動消亡。
UObject及其子類創建與銷毀
創建:
UObjectClass* MyClass = NewObject<UObjectClass>();銷毀:
- 虛幻的垃圾回收機制自動回收。
AActor及其子類的創建與銷毀
創建:
- 使用SpawnActor(有七個重載函數)。
銷毀:
- 虛幻的垃圾回收機制自動回收;
Component的創建
構造器中創建組件:
- 可以使用 CreateDefaultSubobject來創建組件,且此函數只能在構建函數中使用。
其它函數中創建組件:
- 組件繼承自UObject類,使用NewObject來創建,但創建完成后需要進行手動注冊才能生效。
五、縮略圖
了解了路徑和資源之后,我們再來看看縮略圖的渲染。(雖然筆者認為這個可能基本不會去改它)
ThumbnailRenderer.h
定義了一個渲染指定Object縮略圖的抽象基類。
/**** This is an abstract base class that is used to define the interface that* UnrealEd will use when rendering a given object's thumbnail. The editor* only calls the virtual rendering function.*/#pragma once #include "CoreMinimal.h" #include "UObject/ObjectMacros.h" #include "UObject/Object.h" #include "ThumbnailRenderer.generated.h"class FCanvas; class FRenderTarget;UCLASS(abstract, MinimalAPI) class UThumbnailRenderer : public UObject {GENERATED_UCLASS_BODY()public:/*** Returns true if the renderer is capable of producing a thumbnail for the specified asset.** @param Object the asset to attempt to render*/virtual bool CanVisualizeAsset(UObject* Object) { return true; }/*** Calculates the size the thumbnail would be at the specified zoom level** @param Object the object the thumbnail is of* @param Zoom the current multiplier of size* @param OutWidth the var that gets the width of the thumbnail* @param OutHeight the var that gets the height*/virtual void GetThumbnailSize(UObject* Object, float Zoom, uint32& OutWidth, uint32& OutHeight) const PURE_VIRTUAL(UThumbnailRenderer::GetThumbnailSize,);/*** Draws a thumbnail for the object that was specified.** @param Object the object to draw the thumbnail for* @param X the X coordinate to start drawing at* @param Y the Y coordinate to start drawing at* @param Width the width of the thumbnail to draw* @param Height the height of the thumbnail to draw* @param Viewport the viewport being drawn in* @param Canvas the render interface to draw with*/UE_DEPRECATED(4.25, "Please override the other prototype of the Draw function.")virtual void Draw(UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget* Viewport, FCanvas* Canvas) { Draw(Object, X, Y, Width, Height, Viewport, Canvas, false); }/*** Draws a thumbnail for the object that was specified.** @param Object the object to draw the thumbnail for* @param X the X coordinate to start drawing at* @param Y the Y coordinate to start drawing at* @param Width the width of the thumbnail to draw* @param Height the height of the thumbnail to draw* @param Viewport the viewport being drawn in* @param Canvas the render interface to draw with* @param bAdditionalViewFamily whether this draw should write over the render target (true) or clear it before (false)*/virtual void Draw(UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height, FRenderTarget* Viewport, FCanvas* Canvas, bool bAdditionalViewFamily) PURE_VIRTUAL(UThumbnailRenderer::Draw, );/*** Checks to see if the specified asset supports realtime thumbnails, which will cause them to always be rerendered to reflect any changes* made to the asset. If this is false, thumbnails should render once and then not update again.* For most renderers, this should remain as true.** @param Object The asset to draw the thumbnail for** @return True if the thumbnail needs to always be redrawn, false if it can be just drawn once and then reused.*/virtual bool AllowsRealtimeThumbnails(UObject* Object) const { return true; }protected:/** Renders the thumbnail's view family. */UNREALED_API static void RenderViewFamily(FCanvas* Canvas, class FSceneViewFamily* ViewFamily); };它有大量的子類:
- UCurveLinearColorThumbnailRenderer
- USoundWaveThumbnailRenderer
- UTextureThumbnailRenderer
- URuntimeVirtualTextureThumbnailRenderer
- UPaperSpriteThumbnailRenderer
- UPaperTileSetThumbnailRenderer
- UGeometryCacheThumbnailRenderer
- UGeometryCollectionThumbnailRenderer
- UDestructibleMeshThumbnailRenderer
- UFoliageType_ISMThumbnailRenderer
- UAnimBlueprintThumbnailRenderer
- UAnimSequenceThumbnailRenderer
- UBlendSpaceThumbnailRenderer
- UBlueprintThumbnailRenderer
- UClassThumbnailRenderer
- ULevelThumbnailRenderer
- UMaterialFunctionThumbnailRenderer
- UMaterialInstanceThumbnailRenderer
- UPhysicsAssetThumbnailRenderer
- USkeletalMeshThumbnailRenderer
- USlateBrushThumbnailRenderer
- UStaticMeshThumbnailRenderer
- UVolumeTextureThumbnailRenderer
- UWorldThumbnailRenderer
詳情可以參考 AssetThumbnail資源縮略圖。
注:
- 筆者在加某個編輯器(例如Mesh編輯器)加功能的過程中,曾遇到過崩潰,是縮略圖或者Preview窗口的渲染引起的。
- 這里是一個需要考慮的問題,當在做對編輯器源碼進行修改的時候。
參考文章
- UE Plugins官方介紹
- UE4 PluginDescriptor字段描述
- UE4模塊官方文檔
- UE4模塊文章
- Slate概述
- UE文檔 Slate UI框架
- 【UE4 Renderer】<02> Slate系統
- Slate渲染流程
- UE資源路徑相關整理分析
- UE4 4.20 UE4獲取所有特定資源(FAssetData)的資源路徑
- 虛幻4渲染編程(工具篇)【第八卷:Asset creation】
- 虛幻4渲染編程(工具篇)【第九卷:SlateUI布局】
- UTexture2D的讀取與寫入數據并保存成為Asset
- UE4 AssetRegistry分析
- 對象創建與資源獲取
- 資源管理之UAssetManager用法
- AssetThumbnail資源縮略圖
總結
以上是生活随笔為你收集整理的虚幻引擎编辑器开发基础(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 什么是项目管理?范围、时间、成本、质量
- 下一篇: 关于 @EnableConfigurat