日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

虚幻引擎编辑器开发基础(一)

發(fā)布時(shí)間:2023/12/20 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 虚幻引擎编辑器开发基础(一) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

虛幻引擎編輯器開發(fā)基礎(chǔ)(一)

文章目錄

  • 虛幻引擎編輯器開發(fā)基礎(chǔ)(一)
    • 一、前言
    • 二、插件與模塊
      • 2.1 插件(Plguin)
        • 2.1.1 插件的作用
        • 2.1.2 插件的類型
        • 2.1.3 插件結(jié)構(gòu)
      • 2.2 模塊(Module)
        • 2.2.1 build.cs文件
        • 2.2.2 創(chuàng)建模塊
        • 2.2.3 模塊的加載與卸載
    • 三、Slate
      • 3.1 獨(dú)立窗口插件淺析
      • 3.2 什么是Slate
      • 3.3 Slate淺析
        • 3.3.1 聲明式語法
        • 3.3.2 創(chuàng)建自定義控件
        • 3.3.3 布局
    • 四、資源
      • 4.1 路徑
      • 4.2 資源
      • 4.3 資源的創(chuàng)建與獲取
        • 4.3.1 資源的創(chuàng)建
        • 4.3.2 資源的獲取
      • 4.4 AssetRegistry分析
        • 4.4.1 如何搜尋asset文件
        • 4.4.2 如何從FAssetData獲取Uobject
      • 4.5 對象的創(chuàng)建與銷毀
    • 五、縮略圖
    • 參考文章

一、前言

虛幻引擎提供了非常強(qiáng)大的編輯器(如藍(lán)圖編輯器、材質(zhì)編輯器、動(dòng)畫編輯器)。

然而根據(jù)項(xiàng)目的不同以及相應(yīng)的需求也需要擴(kuò)展或者自定義一些編輯器的相關(guān)功能,對引擎做各種工具上的擴(kuò)展,來滿足高效、快速的開發(fā)需要。

虛幻的編輯器開發(fā)包含了零零散散的各種內(nèi)容,包括比如插件和模塊、編輯器窗口的擴(kuò)展、資源文件的自定義等。

這篇文章,將會(huì)整理一下筆者了解的編輯器開發(fā)相關(guān)的基礎(chǔ)內(nèi)容(一)。

由于筆者只自己實(shí)踐過其中的一部分。一些內(nèi)容可能會(huì)留空,標(biāo)注出可能的參考文章,后續(xù)工作涉及到或做到將補(bǔ)充和修改。

二、插件與模塊

在正式進(jìn)入相關(guān)的開發(fā)介紹之前,讓我們來了解一下虛幻的插件與模塊功能。

插件與模塊既是虛幻組織代碼的一種方式,可以我們對編輯器開發(fā)的重要幫手。

虛幻中的Module、Plugin是兩個(gè)不同的概念。

一個(gè)Plugin可以由多個(gè)Module組成,Module只能由代碼組成,而Plugin可以由代碼和資源組成;
Plugin可以編譯之后打包跨工程使用,保持代碼的獨(dú)立性。

而Module在工程里的耦合則較高,是代碼級別的直接引用。

2.1 插件(Plguin)

2.1.1 插件的作用

虛幻的插件作用非常廣:

  • 可添加運(yùn)行時(shí)gameplay功能;
  • 修改內(nèi)置引擎功能(或添加新功能)、新建文件類型、以及使用新菜單/工具欄命令和子模式擴(kuò)展編輯器的功能;
  • 使用插件擴(kuò)展許多現(xiàn)有UE4子系統(tǒng);

2.1.2 插件的類型

虛幻的插件插件的類型分為兩類:

  • 引擎插件;
  • 項(xiàng)目插件;

注:二者除了放置目錄存在區(qū)別,基本沒有其他差別。

插件存放的文件目錄:

  • 插件可擁有自己的Content文件夾,其中包含特定于該插件的資源文件;
    • CanContainContent設(shè)置為true
  • 引擎將掃描基礎(chǔ)Plugins文件夾下的所有子文件夾,查找要加載的插件;
  • 引擎插件: /[UE引擎根目錄]/Engine/Plugins/[插件命名]/
  • 游戲插件: /[項(xiàng)目根目錄]/Engine/Plugins/[插件命名]/

有時(shí),在啟動(dòng)UE項(xiàng)目時(shí),會(huì)遇到插件編譯不過的問題。

臨時(shí)解決方法:可以編輯 XXX.uproject 去除某些插件。

查看現(xiàn)有的插件方法:

  • Edit -> Plugins;可選擇開啟或禁用相應(yīng)插件

2.1.3 插件結(jié)構(gòu)

虛幻提供了好幾種插件的模板,如Blank(空白)、Content(只包含資源)、藍(lán)圖庫等。

通過上述的操作可以快速的創(chuàng)建一個(gè)新的插件。

下面讓我們一起看一下插件的目錄(文件)結(jié)構(gòu)。舉例如下:

可以看到一個(gè)帶源碼的插件有

  • 插件描述文件(.uplugin)

  • 模塊配置文件(.Build.cs)

  • 源碼目錄(Source)

插件里還可以包含著色器代碼文件、資源文件等。

插件描述文件(.uplugin)

  • 虛幻啟動(dòng)時(shí),會(huì)在Plugin目錄里面搜索所有的.uplugin文件,來查找所有的插件;
  • 每個(gè).uplugin文件表示一個(gè)插件,其格式為.json;
  • 該文件的作用:提供描述插件相關(guān)基本信息;

Modules字段

每個(gè)模塊,需要配置 名字Name、類型Type、加載階段LoadingPhase;

  • Name 是插件模塊的唯一命名;

  • Type 設(shè)置模塊的類型, 如:Runtime、Developer、Editor等;

    • Runtime,在任何情況下都會(huì)加載;
    • Editor,只在編輯器啟動(dòng)時(shí)加載;
  • LoadingPhase 指明在什么階段加載模塊,默認(rèn)為Default;

    • PreDefault,讓模塊在一般模塊前加載
    • Default,默認(rèn)
    • PostConfigInit,此模塊在虛幻關(guān)鍵模塊加載后加載

源碼目錄(Source)

  • 存儲(chǔ)插件的源碼;
  • 在Source下,每個(gè)目錄代表一個(gè)模塊
  • 每個(gè)模塊包含Public和Private目錄,以及模塊配置文件(.Build.cs);

模塊配置文件(.Build.cs)

后續(xù)再介紹。

2.2 模塊(Module)

為什么虛幻引入模塊機(jī)制?

  • 編譯模式太多,配置復(fù)雜;

由前面介紹可知,一個(gè)模塊文件夾應(yīng)該包含這些內(nèi)容:

  • Public文件夾;
  • Private文件夾;
  • *.builc.cs文件

UE4的代碼是由模塊來組織的,.build.cs代表一個(gè)模塊。

2.2.1 build.cs文件

模塊配置文件是用來告知**UBT(Unreal Build Tool)**如何配置編譯和構(gòu)造環(huán)境。

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>) // 公開給其他模塊的文件路徑 / 但是不需要"導(dǎo)入"或鏈接 PrivateIncludePaths (List<String>) // 通向此模塊內(nèi)部包含文件的所有路徑的列表,不向其他模塊公開// 控制依賴 PublicIncludePathModuleNames(List<String>) // 我們模塊的公共標(biāo)頭需要對這些標(biāo)頭文件進(jìn)行訪問,但是不需要"導(dǎo)入"或鏈接 PublicDependencyModuleNames (List<String>) // 公共源文件所需要的模塊.(需要導(dǎo)入或鏈接?)PrivateIncludePathModuleNames (List<String>) // 我們模塊的私有代碼文件需要對這些標(biāo)頭文件進(jìn)行訪問,但是不需要"導(dǎo)入"或鏈接。 PrivateDependencyModuleNames(List<String>) // 私有代碼依賴這些模塊.(需要導(dǎo)入或鏈接?)DynamicallyLoadedModuleNames (List<String>) // 此模塊在運(yùn)行時(shí)可能需要的附加模塊

2.2.2 創(chuàng)建模塊

創(chuàng)建一個(gè)新的模塊分為如下幾步:

  • 創(chuàng)建模塊文件夾結(jié)構(gòu);
  • 創(chuàng)建模塊構(gòu)建文件 .build.cs;
  • 創(chuàng)建模塊的頭文件和實(shí)現(xiàn)文件;
  • 創(chuàng)建模塊的C++聲明和定義;

模塊源代碼文件示例:

并實(shí)現(xiàn)StartUpModule和ShutdownModule函數(shù),功能為: 自定義模塊的加載和卸載時(shí)行為。

.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() {// ... 模塊加載執(zhí)行內(nèi)容 } void FPluginDevModule::ShutdownModule() {// ... 模塊卸載執(zhí)行內(nèi)容 } //!!! // 表明FPluginDevModule是實(shí)現(xiàn)pluginDev模塊的類 IMPLEMENT_MODULE(FPluginDevModule,pluginDev)

由上述代碼可知,虛幻的模塊類繼承自IModuleInterface。

其包含7個(gè)虛函數(shù):

  • StartupModule
  • PreUnloadCallback
  • PostLoadCallback
  • ShutdownModule
  • SupportsDynamicReloading
  • SupportAutomaticShutdown
  • IsGameMode

StartupModule是模塊的入口函數(shù)。

2.2.3 模塊的加載與卸載

在源碼層面,一個(gè)包含 *.build.cs 的文件就是一個(gè)模塊。

每個(gè)模塊編譯鏈接后后,會(huì)生成比如一個(gè)靜態(tài)庫lib或動(dòng)態(tài)庫dll。

虛幻引擎初始化模塊加載順序,由2個(gè)部分決定:

  • 硬編碼形式硬性規(guī)定,即在源碼中直接指定加載;
  • 松散加載;
  • 總體的順序:

  • 加載Platform File Module,因?yàn)樘摶靡x取文件;
  • 核心模塊加載 FEngineLoop::PreInit->LoadCoreModules
  • 加載CoreUObject
  • 在初始化引擎之前加載模塊:FEngineLoop::LoadPreInitModules
  • 加載Engine
  • 加載Renderer
  • 加載AnimationGraphRuntime
  • 模塊的加載注冊

    • 模塊需要提供給外部一個(gè)操作的接口,就是一個(gè)IModuleInterface指針
      • 這里并不是說調(diào)用模塊內(nèi)的任何函數(shù)(或類)都需要通過這個(gè)指針
      • 實(shí)際上,只需要#include了相應(yīng)頭文件就可以調(diào)用對應(yīng)的功能,如New一個(gè)類,調(diào)一個(gè)全局函數(shù);
    • 這個(gè)IModuleInterface指針的意義: 操作作為整體的模塊本身,如模塊的加載/初始化/卸載。訪問模塊內(nèi)的一些全局變量
      • IModuleInterface 在ModuleInterface.h
      • 獲取這個(gè)指針的方法,只有一個(gè):就是通過 FModuleManager 上的 LoadModule/GetModule


    FModuleManager去哪里加載這些模塊呢?

    即調(diào)用FModuleManager::LoadModule,其中又對動(dòng)態(tài)和靜態(tài)區(qū)別處理:

    • 動(dòng)態(tài)鏈接庫,根據(jù)名字直接加載對應(yīng)的DLL即可。
      • 作為合法UE模塊的dll,必定要導(dǎo)出一些約定的函數(shù)來返回自身IModuleInterface指針;
    • 靜態(tài)鏈接庫,去一個(gè)叫StaticallyLinkedModuleInitializers的Map里找。這就要求所有模塊已把自己注冊到這個(gè)Map里。

    為滿足以上約定,每個(gè)模塊在實(shí)現(xiàn)的過程中,需要插入一些宏代碼,例如上述示例中的IMPLEMENT_MODULE。

    IMPLEMENT_MODULE代碼如下:

    靜態(tài)鏈接時(shí):

    • FStaticallyLinkedModuleRegistrant,是一個(gè)注冊輔助類,利用全局變量構(gòu)造函數(shù)自動(dòng)調(diào)用的特性,實(shí)現(xiàn)自動(dòng)注冊。
    // If we're linking monolithically we assume all modules are linked in with the main binary.#define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \/** Global registrant object for this module when linked statically */ \static FStaticallyLinkedModuleRegistrant< ModuleImplClass > ModuleRegistrant##ModuleName( #ModuleName ); \/** Implement an empty function so that if this module is built as a statically linked lib, */ \/** static initialization for this lib can be forced by referencing this symbol */ \void EmptyLinkFunctionForStaticInitialization##ModuleName(){} \PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)

    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();} };

    動(dòng)態(tài)鏈接時(shí):

    • 聲明了一個(gè)dllexport函數(shù),功能就是創(chuàng)建并返回相應(yīng)模塊;
    • ModuleImplClass:具體實(shí)現(xiàn)的類;
    • ModuleName:模塊的名稱(字符串);
    #define IMPLEMENT_MODULE( ModuleImplClass, ModuleName ) \\/**/ \/* InitializeModule function, called by module manager after this module's DLL has been loaded */ \/**/ \/* @return Returns an instance of this module */ \/**/ \extern "C" DLLEXPORT IModuleInterface* InitializeModule() \{ \return new ModuleImplClass(); \} \PER_MODULE_BOILERPLATE \PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)

    插件重編譯問題:有時(shí)候修改了代碼卻發(fā)現(xiàn)沒有進(jìn)行編譯。

    如何解決:把插件目錄下的兩個(gè)文件夾刪除掉:Binaries(包含DLL)、Intermediate(Obj文件等)

    三、Slate

    在了解了虛幻的插件和模塊的概念之后,讓我們來通過一個(gè)虛幻的例程,簡單地了解一下編輯器開發(fā)的基礎(chǔ),Slate。即如何用C++編程創(chuàng)建UI。

    在游戲開發(fā)過程中的UI大多直接UMG進(jìn)行開發(fā)實(shí)現(xiàn)。

    而在編輯器開發(fā)過程中,則可能需要使用Slate來實(shí)現(xiàn)一些UI功能。

    亦可以用Slate組合一些控件,再封裝成為UMG。

    3.1 獨(dú)立窗口插件淺析

    首先,創(chuàng)建Standalone Window插件。

    • 在創(chuàng)建新的插件時(shí),選擇Editor Standalone Window進(jìn)行創(chuàng)建;

    然后,編譯項(xiàng)目之后打開,可以在工具欄上看到按鈕:

    點(diǎn)擊之后有:

    • 點(diǎn)擊工具欄按鈕彈出一個(gè)窗口(里面有一個(gè)Tab);
    • 提示的內(nèi)容為:可以添加代碼到 FSimpleWindowModule::OnSpawnPluginTab in SimpleWindow.cpp來重寫窗口的內(nèi)容;

    源碼閱讀與分析

    • StandaloneWindow 插件的主入口;
    • StandaloneWindowStyle 定義了UI的風(fēng)格;
    • StandaloneWindowCommands 聲明要注冊的命令;

    StandaloneWindowCommands類

    • 聲明命令和注冊;
    // StandaloneWindowCommands.h TSharedPtr< FUICommandInfo > OpenPluginWindow; // 聲明FUICommandInfo的指針// StandaloneWindowCommands.cpp void FStandaloneWindowCommands::RegisterCommands() {UI_COMMAND(OpenPluginWindow,"StandaloneWindow","Bring up StandaloneWindow Window",EUserInterfaceActionType::Button,FInputGesure()); }

    StartupModule主要做了幾件事情:

    • 初始化風(fēng)格;
    • 注冊UI命令和其回調(diào);
    • 添加創(chuàng)建的擴(kuò)展;
    • 注冊標(biāo)簽頁的創(chuàng)建方法;

    其中Extender的回調(diào)如下:

    • 工具欄和菜單欄調(diào)用不同的函數(shù),但注冊的同一個(gè)命令。

    這個(gè)命令對應(yīng)的回調(diào)即:

    其中PluginButtonClicked回調(diào)的具體實(shí)現(xiàn):

    • 根據(jù)名字去激活一個(gè)Tab標(biāo)簽UI,即StartupModule函數(shù)中最后注冊的。

    這個(gè)標(biāo)簽頁的具體生成方法,即OnSpawnPuginTab函數(shù)實(shí)現(xiàn)的。最后返回一個(gè)Slate聲明創(chuàng)建的UI。

    對于以上的源碼以及其實(shí)現(xiàn)效果,有三點(diǎn)值得研究,分為是:

  • 對菜單欄和工具欄的擴(kuò)展;
  • Tab的UI創(chuàng)建方法;
  • UI_COMMAND系列;
  • 其中:

    • 第一點(diǎn)(菜單欄和工具欄的擴(kuò)展)為后續(xù)文章的內(nèi)容之一,到時(shí)再進(jìn)行介紹。
    • 第二點(diǎn)(Slate創(chuàng)建的簡單UI)為本節(jié)后續(xù)的內(nèi)容;
    • 第三點(diǎn),大量地在虛幻使用,在此我們進(jìn)行介紹。

    UI_COMMAND系列

    這里說的UI_COMMAND系列,筆者指的是:

    // UICommandInfo.h/cpp FUICommandInfo 類// UICommandList.h/cpp FUICommandList 類// Commands.h/cpp UI_COMMAND 宏

    三者的關(guān)系如下:

    • UI_COMMAND宏,負(fù)責(zé)正式注冊FUICommandInfo;
    • FUICommandList,用于包含F(xiàn)UICommandInfo,并且映射FUICommandInfo到委托;

    UI_COMMAND宏

    // -> MakeUICommand_InternalUseOnly // -> -> FUICommandInfo::MakeCommandInfo 在該函數(shù)中New了一個(gè)FUICommandInfo OutCommand = MakeShareable(new FUICommandInfo(InContext->GetContextName())); FInputBindingManager::Get().CreateInputCommand(InContext,OutCommand.ToSharedRef());

    將FUICommandInfo注冊到了FInputBindingManager的單例中。

    FUICommandList映射委托調(diào)用

    一系列函數(shù)FUICommandList::MapAction(),會(huì)將FUICommandList和FUIAction注冊到Map中。

    FUICommandInfo會(huì)被UI使用,當(dāng)UI條件觸發(fā),則會(huì)調(diào)用綁定的委托。

    3.2 什么是Slate

    現(xiàn)在我們來了解一下3.1中我們提出的第二點(diǎn)研究內(nèi)容,即Slate。那么什么是Slate呢?

    Slate概述 中提到:

    • Slate 是完全自定義、與平臺(tái)無關(guān)的用戶界面框架,旨在讓工具和應(yīng)用程序(比如虛幻編輯器)的用戶界面或游戲中用戶界面的構(gòu)建過程變得有趣、高效。
    • 它將聲明性語法與輕松設(shè)計(jì)、布局和風(fēng)格組件的功能相結(jié)合,允許在UI上輕松實(shí)現(xiàn)創(chuàng)建和迭代。

    筆者是這樣理解的:

    • 用聲明式的語法,用C++來定義UI,包含了UI的布局、風(fēng)格、以及對UI響應(yīng)的處理。

    就3.1中的例子,通過C++代碼直接定義一個(gè)包含了一個(gè)文本框的標(biāo)簽頁,并指定了文本內(nèi)容。

    • 可以看出這樣的方式,使得程序員可以訪問構(gòu)建UI,而無需添加間接層。

    3.3 Slate淺析

    Slate控件分為三種:

    • 無插槽(Slot)的控件,SLeafWidget,例如:SImage、STextBlock。
    • 有一個(gè)Slot的控件,SCompoundWidget,例如:SButton。
    • 有多個(gè)Slot的控件,SPanel(布局),例如:SVerticalBox。

    其中:

    • 在Slate中,容器不存儲(chǔ)控件,容器中的Slot(插槽)存儲(chǔ)控件。

    • 插槽可以存儲(chǔ)三種的控件任意一種。

    3.3.1 聲明式語法

    為了實(shí)現(xiàn)聲明式語法,UE提供了一組完整的宏來簡化聲明和創(chuàng)建新控件的過程。

    接下來,將對Slate中的宏定義進(jìn)行一些簡單的解析,以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() \};

    在二者的包圍中,創(chuàng)建了一個(gè)FArguments參數(shù)類。

    FArguments的成員變量或函數(shù)通過以下幾個(gè)宏定義來輔助實(shí)現(xiàn)。

    • SLATE_ARGUMENT
    • SLATE_ATTRIBUTE
    • SLATE_EVENT
    • SLATE_DEFAULT_SLOT

    SLATE_ARGUMENT

    • 參數(shù)只能是值。
    /*** Use this macro to declare a slate argument.* Arguments differ from attributes in that they can only be values*/ #define SLATE_ARGUMENT( ArgType, ArgName ) \ArgType _##ArgName; \WidgetArgsType& ArgName( ArgType InArg ) \{ \_##ArgName = InArg; \return this->Me(); \}

    SLATE_ATTRIBUTE

    • 屬性可以是值也可以是函數(shù)。
    /*** Use this macro to add a attribute to the declaration of your widget.* An attribute can be a value or a function.*/ #define SLATE_ATTRIBUTE( AttrType, AttrName ) \TAttribute< AttrType > _##AttrName; \WidgetArgsType& AttrName( const TAttribute< AttrType >& InAttribute ) \{ \_##AttrName = InAttribute; \return this->Me(); \} \\// ... 省略部分源碼

    SLATE_EVENT

    • 事件,其實(shí)就是回調(diào)。
    /*** Use this macro to add event handler support to the declarative syntax of your widget.* It is expected that the widget has a delegate called of type 'EventDelegateType' that is* named 'EventName'.*/ #define SLATE_EVENT( DelegateName, EventName ) \WidgetArgsType& EventName( const DelegateName& InDelegate ) \{ \_##EventName = InDelegate; \return *this; \} \\// 省略了部分源碼

    SLATE_DEFAULT_SLOT

    • 根據(jù)名稱創(chuàng)建了Widget,可以用來存儲(chǔ)Widget。

    • 重載了[]操作符,可以有一個(gè)Widget作為輸入。

    /*** Use this macro to add support for named slot properties such as Content and Header. See NamedSlotProperty for more details.** NOTE: If you're using this within a widget class that is templated, then you might have to specify a full name for the declaration.* For example: SLATE_NAMED_SLOT( typename SSuperWidget<T>::Declaration, Content )*/ #define SLATE_NAMED_SLOT( DeclarationType, SlotName ) \NamedSlotProperty< DeclarationType > SlotName() \{ \return NamedSlotProperty< DeclarationType >( *this, _##SlotName ); \} \TAlwaysValidWidget _##SlotName; \#define SLATE_DEFAULT_SLOT( DeclarationType, SlotName ) \SLATE_NAMED_SLOT(DeclarationType, SlotName) ; \DeclarationType & operator[]( const TSharedRef<SWidget> InChild ) \{ \_##SlotName.Widget = InChild; \return *this; \}

    SButton是SCompoundWidget,只有一個(gè)Slot插槽。

    讓我們再來看一下多個(gè)插槽的情況,看一下布局,以SVerticalBox舉例。

    可以看到類中的宏定義如下:

    SLATE_BEGIN_ARGS( SVerticalBox ) {_Visibility = EVisibility::SelfHitTestInvisible; }SLATE_SUPPORTS_SLOT(SVerticalBox::FSlot)SLATE_END_ARGS()

    SLATE_SUPPORTS_SLOT

    • 定義了一個(gè)Slot數(shù)組;
    • 在該類中為SVerticalBox::FSlot,并重載了+操作符,添加Slot到數(shù)組中。
    /*** Use this macro between SLATE_BEGIN_ARGS and SLATE_END_ARGS* in order to add support for slots.*/ #define SLATE_SUPPORTS_SLOT( SlotType ) \TArray< SlotType* > Slots; \WidgetArgsType& operator + (SlotType& SlotToAdd) \{ \Slots.Add( &SlotToAdd ); \return *this; \}

    其中,SVerticalBox::FSlot在類里面中實(shí)現(xiàn)如下。

    /** 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 創(chuàng)建自定義控件

    根據(jù)上述的宏,我們很容易可以對自定義Slate的控件。

    例如我們可以自定義 CompoundWidget。

    • 必須要有的 SLATE_BEGIN_ARGSSLATE_END_ARGS 以及構(gòu)造器Construct函數(shù)。
    • 由于是SCompoundWidget的子類,則可以通過ChildSlot構(gòu)造控件內(nèi)容。

    下面給出一個(gè)自定義控件的例子(僅僅是將兩個(gè)按鈕組裝在一起)。

    .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函數(shù)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數(shù)量劃分以下三種。

    在Slate中,容器不存儲(chǔ)控件,容器中的Slot(插槽)存儲(chǔ)控件

    插槽(Slot)可以存儲(chǔ)三種的控件任意一種。

    每個(gè)插槽(Slot)到底占據(jù)UI的多少,取決于很多因素。

    首先,來看Slot中的SizeParm參數(shù)。這是決定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大小會(huì)使用DesiredSize
    • 如果SizeRule是Stretch,那么Slot就會(huì)盡可能充滿UI空間。

    Auto情況:

    • 可以看出按鈕的大小是有一定的寬度是一定的。


    Stretch情況:

    • 當(dāng)SizeParam不指定的情況下就是默認(rèn)為Stretch。


    下面將第一個(gè)Slot設(shè)置為Auto,第二個(gè)為Stretch??梢钥闯龇锨懊嫠f的情況。


    Slot還有HAlignmentVAlignment兩個(gè)參數(shù),用來描述Slot的位置

    /** Horizontal positioning of child within the allocated slot */ TEnumAsByte<EHorizontalAlignment> HAlignment;/** Vertical positioning of child within the allocated slot */ TEnumAsByte<EVerticalAlignment> VAlignment;

    目前只考慮水平方向。

    當(dāng)我們使用**(Auto,Fill)(Auto,HAlign_Left)**組合時(shí):


    假設(shè)目前有三個(gè)Slot并且向這三個(gè)slot中各添加了一個(gè)按鈕UI控件,其中一個(gè)是(Stretch,HAlign_Fill) 兩個(gè)是(Auto,HAlign_Left)


    這個(gè)結(jié)果的計(jì)算如下:

  • 首先,有三個(gè)插槽,其中有兩個(gè)是Auto,則插槽的大小等于插槽內(nèi)控件的DisireSize。
  • 第一個(gè)插槽為Stretch,則其大小為總大小減去其余兩個(gè)Slot占據(jù)的固定大小(這里的總大小應(yīng)該指的是窗口的大小)。
  • 三個(gè)Slot的大小準(zhǔn)備好了 以后就往Slot里面填充UI。
  • 第一個(gè)Slot的填充模式為Fill,所以會(huì)填滿整個(gè)Slot插槽;
  • 后面兩個(gè)Slot的填充模式為Left,又因?yàn)椴宀鄞笮『涂丶笮∠嗤?#xff0c;所以剛好填滿了兩個(gè)插槽;
  • 如果將第一個(gè)插槽的填充方式改為Left,那么發(fā)現(xiàn)填充不滿。

    • 因?yàn)?strong>Slot的大小大于控件本身的大小,留下很多可以被填充的Slot空間。
    • 這里1的slot大小為窗口-2和3的slot大小,但是控件只有和2或3那么大。

    如果將第一個(gè)插槽的填充方式改為Center

    如果我將三個(gè)控件全部選擇為Auto:


    三個(gè)Slot的大小被明確的計(jì)算出來。

    如果三個(gè)Slot里的控件的DesireSize之和小于窗口的大小,那么就會(huì)有Slot空間剩余。

    這時(shí)再把控件填充進(jìn)來。因?yàn)槭沁x用的Auto,所以不管控件是什么填充模式,Slot空間的大小始終剛好等于內(nèi)部控件的大小。

    當(dāng)Slot的大小總和大于窗口的時(shí)候會(huì)是什么情況呢?

    通過SBox自定義DesireSize來定義SHorizontalBox的Slot大小!

    如下示例所示,因?yàn)槭茿uto,所以Slot的大小是不會(huì)改變的,那么多余的部分直接會(huì)被Cull掉。


    前面一直如果是Auto的情況下,Slot大小會(huì)使用DesiredSize。

    那么DesiredSize是如何計(jì)算的呢?

    看一下這個(gè)過程:

    /*** 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:

    • 通過注釋可用看出:這個(gè)Desired size是所有子控件的desired size之和加上指定的Margin。

    其中調(diào)用的GetDesiredSize為:

    FVector2D SWidget::GetDesiredSize() const {return DesiredSize.Get(FVector2D::ZeroVector); }

    這個(gè)值是在CacheDesiredSize時(shí)計(jì)算的存儲(chǔ)起來的:

    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函數(shù):

    • 可以看出這個(gè)函數(shù)通過Override的寬度和高度自定義大小,反正不管怎么定義計(jì)算它就是代表內(nèi)部控件的大小。
    FVector2D SBox::ComputeDesiredSize( float ) const {EVisibility ChildVisibility = ChildSlot.GetWidget()->GetVisibility();if ( ChildVisibility != EVisibility::Collapsed ){// 通過Override的寬度和高度自定義大小,那么Size就是固定的。const FOptionalSize CurrentWidthOverride = WidthOverride.Get();const FOptionalSize CurrentHeightOverride = HeightOverride.Get();return FVector2D(( CurrentWidthOverride.IsSet() ) ? CurrentWidthOverride.Get() : ComputeDesiredWidth(),( CurrentHeightOverride.IsSet() ) ? CurrentHeightOverride.Get() : ComputeDesiredHeight());}return FVector2D::ZeroVector; }

    布局小結(jié):

  • 首先會(huì)對UI空間進(jìn)行分割,分割出每個(gè)Slot插槽的大小
  • 再根據(jù)不同的UI控件和不同的填充模式,去填充Slot空間;
  • 四、資源

    了解了Slate,這僅是對編輯器擴(kuò)展的表面皮毛,即UI僅是用于展示、接收和轉(zhuǎn)發(fā)用戶輸入的一個(gè)中間媒介。(當(dāng)然能把UI做得好看好用更好!)

    編輯器功能的核心還是在于對資源、用戶輸入的數(shù)據(jù)的處理算法(例如,對資源進(jìn)行創(chuàng)建、計(jì)算、修改等)。

    因而,對資源、資產(chǎn)數(shù)據(jù)的了解是處理數(shù)據(jù)的第一步。

    4.1 路徑

    UE將目錄分為:引擎(Engine)目錄項(xiàng)目(Game)目錄。

    并采用沙盒路徑,即虛擬路徑來標(biāo)識(shí)資源的路徑。

    其中有:

    • /Game 是一個(gè)虛擬的路徑,實(shí)際表示的是項(xiàng)目的 FPaths::ProjectContentDir() 。
    • /Engine 也是一個(gè)虛擬路徑,實(shí)際路徑是引擎的 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;// ... 省略部分源碼 };

    路徑相關(guān)的API

    常用的路徑接口都放在FPaths這個(gè)類。

    /*** Path helpers for retrieving game dir, engine dir, etc.*/ class CORE_API FPaths {// ... 省略部分源碼 };

    4.2 資源

    資源(Asset) 是用于 虛幻引擎 項(xiàng)目的內(nèi)容項(xiàng),可將其看作序列化到文件中的 UObject。

    資源對應(yīng)的數(shù)據(jù)結(jié)構(gòu)類為:FAssetData

    其包含了以下幾種重要屬性:

    • ObjectPath
    • PackageName
    • PackagePath
    • AssetName
    • AssetClass
    /** * A struct to hold important information about an assets found by the Asset Registry* This struct is transient and should never be serialized*/ struct FAssetData { public:/** The object path for the asset in the form PackageName.AssetName. Only top level objects in a package can have AssetData */FName ObjectPath;/** The name of the package in which the asset is found, this is the full long package name such as /Game/Path/Package */FName PackageName;/** The path to the package in which the asset is found, this is /Game/Path with the Package stripped off */FName PackagePath;/** The name of the asset without the package */FName AssetName;/** The name of the asset's class */FName AssetClass;// ... 省略部分源碼 };

    下面,通過一個(gè)實(shí)際的例子來看下這些路徑都是什么樣的。

    在項(xiàng)目的Content目錄創(chuàng)建了一個(gè)藍(lán)圖Acotr。

    則有:

    • ObjectPath : /Game/NewBlueprint.NewBlueprint
    • PackageName : /Game/NewBlueprint
    • PackagePath : /Game
    • AssetName : NewBlueprint

    可以看出其路徑采用了前面虛擬路徑。

    那么,有了路徑如何獲得對應(yīng)的UObject數(shù)據(jù)呢?

    • 可以通過GetAsset獲得內(nèi)存中的UObject對象!
    /** Returns the asset UObject if it is loaded or loads the asset if it is unloaded then returns the result */ UObject* GetAsset() const {if ( !IsValid()){// Dont even try to find the object if the objectpath isn't setreturn nullptr;}UObject* Asset = FindObject<UObject>(nullptr, *ObjectPath.ToString());if ( Asset == nullptr){Asset = LoadObject<UObject>(nullptr, *ObjectPath.ToString());}return Asset; }

    4.3 資源的創(chuàng)建與獲取

    4.3.1 資源的創(chuàng)建

    在編寫工具的時(shí)候需要?jiǎng)?chuàng)建資源、初始化資源、管理資源等需求。比如:

    • 合并DrawCall時(shí),需要?jiǎng)?chuàng)建貼圖;

    • 管理材質(zhì)時(shí),需要?jiǎng)?chuàng)建并指定材質(zhì)球、設(shè)置材質(zhì)等。

    首先,弄清楚虛幻的資源結(jié)構(gòu)以及和編輯器的關(guān)系

    虛幻中有大量的類型,諸如UMaterial、UMaterialInstance、UTexture、UStaticMesh等。

    這些資源從外部導(dǎo)入虛幻的時(shí)候做了一次數(shù)據(jù)抽取,如貼圖資源模型資源等。

    這些數(shù)據(jù)被放在一個(gè)UObject里,然后這個(gè)UObject放在一個(gè)Package里。

    拿貼圖的數(shù)據(jù)導(dǎo)入舉例,有代碼UTextureFactory::ImportTexture:

    • 引擎會(huì)抽取數(shù)據(jù),然后創(chuàng)建對應(yīng)UTexture,使用創(chuàng)建的UTexture填充其Source。
    UTexture* UTextureFactory::ImportTexture(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, const TCHAR* Type, const uint8*& Buffer, const uint8* BufferEnd, FFeedbackContext* Warn) {bool bAllowNonPowerOfTwo = false;GConfig->GetBool( TEXT("TextureImporter"), TEXT("AllowNonPowerOfTwoTextures"), bAllowNonPowerOfTwo, GEditorIni );// Validate it.const int32 Length = BufferEnd - Buffer;//// Generic 2D Image//FImportImage Image;if (ImportImage(Buffer, Length, Warn, bAllowNonPowerOfTwo, Image)){UTexture2D* Texture = CreateTexture2D(InParent, Name, Flags);if (Texture){Texture->Source.Init(Image.SizeX,Image.SizeY,/*NumSlices=*/ 1,Image.NumMips,Image.Format,Image.RawData.GetData());Texture->CompressionSettings = Image.CompressionSettings;Texture->SRGB = Image.SRGB;}return Texture;}// ... 省略部分源碼 }

    所以Unreal的導(dǎo)入資產(chǎn),創(chuàng)建有一個(gè)固定格式:

    • New一個(gè)Package
    • New一個(gè)資源對應(yīng)的UObject
    • 在此時(shí)指認(rèn)Package和UObject;
    • 向這個(gè)UObject填充數(shù)據(jù)
    • MarkDirty,注冊,通知資源瀏覽器這里創(chuàng)建了一個(gè)新的資源,然后保存。

    下面給出幾個(gè)通過C++創(chuàng)建資源資產(chǎn)的示例。

    示例1: 創(chuàng)建一個(gè)模型資源,Create Mesh Asset With C++

    大概的過程描述如下:

  • 首先創(chuàng)建一個(gè)Package(注意路徑為虛擬路徑);
  • 然后New一個(gè)Mesh,在NewObject方法中指定Package;
  • 通知AssetRegistryModule,我們創(chuàng)建了這個(gè)新的資源;
  • FString CreatedMeshName = TEXT("TestMesh"); // 注意! 這里的路徑要用虛擬路徑.不能用FPath的獲得的真實(shí)路徑. FString AssetPath = TEXT("/Game/") + FString(TEXT("Test")) + TEXT("/") + CreatedMeshName; UPackage* NewMeshPack = CreatePackage(nullptr, *AssetPath);UStaticMesh* NewStaticMesh = NewObject<UStaticMesh>(NewMeshPack, FName(*CreatedMeshName), RF_Public | RF_Standalone);NewMeshPack->MarkPackageDirty(); FAssetRegistryModule::AssetCreated(NewStaticMesh);

    注意:

    • 這里創(chuàng)建的是個(gè)空的MeshAsset;
    • 如果想給MeshAsset添加模型頂點(diǎn)信息,可以通過設(shè)置NewStaticMesh->SourceModels來給MeshAsset增加資源信息;

    示例2: 創(chuàng)建一個(gè)材質(zhì)實(shí)例,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來創(chuàng)建資源,原理是一樣的;
    • 但AssetFactory比我們NewObject要多做一些保險(xiǎn)的事情;

    示例3: 創(chuàng)建一個(gè)紋理資源,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);// 可以在代碼中設(shè)置貼圖的屬性,如格式尺寸等. {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 資源的獲取

    在實(shí)踐中,有時(shí)候需要根據(jù)路徑從而獲取資源,再進(jìn)行一系列處理。

    Unreal的資源分為美術(shù)資源(即導(dǎo)入的)和藍(lán)圖資源(即在UE中創(chuàng)建的)。

    資源獲取分別采用不同函數(shù)獲取到C++中使用。

    藍(lán)圖資源加載:

    • 使用LoadClass函數(shù)加載藍(lán)圖類。
    UClass* MyActorClass = LoadClass<AActor>(NULL, TEXT("Blueprint'/Game/MyActorBP.MyActorBP_C'"));

    美術(shù)資源加載:

    • 使用LoadObject函數(shù)加載美術(shù)資源。例如加載一個(gè)StaticMesh。
    UStaticMesh* StaticMesh = LoadObject<UStaticMesh>(NULL, TEXT("StaticMesh'/Game/StaticMesh.StaticMesh'"));

    除之之外,在開發(fā)過程中,還經(jīng)常需要處理的一個(gè)問題就是:

    • 需要獲得某種特定格式資產(chǎn)的所有路徑,如Mesh、Material等。

    UE篩選某類資源(FAssetData)的資源有兩種方法:

    • UObjectLibrary
    • FAssetRegistryModule + FARFilter

    UObjectLibrary方法

    • 只需利用UObjectLibrary和FAssetData兩個(gè)關(guān)鍵結(jié)構(gòu)即可;

    FAssetRegistryModule和FARFilter方法

    • 設(shè)置路徑和類名;

    兩種方法的區(qū)別:

    • UObjectLibrary加載了相應(yīng)的所有資源進(jìn)入內(nèi)存!
    • 而FAssetRegistryModule和FARFilter僅僅是獲取資源的路徑。相應(yīng)的資源并沒有被加載為UObject

    4.4 AssetRegistry分析

    這部分摘錄自 UE4 AssetRegistry分析

    Asset Registry是Editor的子系統(tǒng),負(fù)責(zé)在Editor加載時(shí)收集未加載的Asset信息。

    這些信息儲(chǔ)存在內(nèi)存中,因此Editor可以創(chuàng)建資源列表,而不需要真正加載這些資源。

    Content Browser是這個(gè)系統(tǒng)的主要消費(fèi)者,但是Editor的其他部分,以及Editor外的模塊也能訪問它們。

    官方文檔-Assets/Registry

    簡而言之,AssetRegisty可以搜尋uasset文件,并用FAssetData抽象表示,并在有需要時(shí)根據(jù)FAssetData中的路徑線索加載UObject。

    Asset Registry類圖:

    4.4.1 如何搜尋asset文件

    使用AssetRegistry,我們可以方便的從磁盤上讀取Asset文件并構(gòu)建FAssetData描述。

    同步方式

    通過上層接口進(jìn)行調(diào)用,通常只能通過同步的方式搜尋,通常使用以下接口:

    ScanFilesSynchronous(const TArray<FString>& InFilePaths, bool bForceRescan = false)

    搜尋InFilePaths路徑下的所有asset,不需要返回值,因?yàn)樗褜ず蟮慕Y(jié)果會(huì)保存在FAssetRegistryState中,之后可以使用過濾器進(jìn)行查詢。

    具體方式為新建一個(gè)FAssetDataGatherer,把模式設(shè)置為同步,然后FAssetDataGatherer就會(huì)直接阻塞的調(diào)用run()方法,為我們搜尋asset了。

    類似的,也可以使用ScanFilesSynchronous方法進(jìn)行同步搜索。

    SearchAllAssets(bool bSynchronousSearch)

    一種更簡單的方式:同步搜尋所有Assets。

    SearchAllAssets方法可以以同步和異步方式運(yùn)行,該方法的同步模式通常用于CommandLet中,因?yàn)榇藭r(shí)對時(shí)間消耗并不是很敏感。

    此方法同步模式最終執(zhí)行方式其實(shí)是和ScanFilesSynchronous相同的,只是為我們自動(dòng)得到了所有要搜尋的路徑。

    異步方法

    異步方式通常由編輯器內(nèi)部調(diào)用,尚不清楚如何通過上層進(jìn)行調(diào)用。

    SearchAllAssets(bool bSynchronousSearch)

    之前說了,SearchAllAssets可以以異步模式執(zhí)行,其實(shí)現(xiàn)方法也比較直觀,為新建了一個(gè)FAssetDataGatherer對象,然后在UAssetRegistryImpl::Tick中獲取。

    直接調(diào)用異步SearchAllAssets的地方目前只有一處,就是UAssetRegistryImpl的構(gòu)造函數(shù)中,當(dāng)在編輯器中,且不通過CommandLet時(shí),就會(huì)調(diào)用,其作用是初始化ContentBrowser。

    我們剛打開編輯器時(shí)的初始化加載過程就是在做異步資源發(fā)現(xiàn)操作。

    讓我們看一下Tick是如何執(zhí)行的,調(diào)用過程為:

    • EngineTick()
    • UEditorEngine::Tick 編輯器EngineTick
    • FAssetRegistryModule::TickAssetRegistry
    • UAssetRegistryImpl::Tick

    在UAssetRegistryImpl::Tick中,會(huì)不斷通過FAssetDataGatherer類型的成員變量BackgroundAssetSearch獲取新搜尋到的FAssetData,并重置BackgroundAssetSearch的搜尋結(jié)果。

    之后AssetRegistry就會(huì)更新自己和State的數(shù)據(jù),并發(fā)送一些廣播,通知搜尋到 了新的assetdata。

    通過調(diào)用過程可以看到,在引擎的循環(huán)中,如果是編輯器,那FAssetRegistry的Tick始終在執(zhí)行,以此達(dá)到保持編輯器中assetdata始終于磁盤同步的目的。

    AddPathToSearch(const FString& Path)

    這個(gè)方法是私有方法,并不能被上層調(diào)用,作用為向異步掃描過程中加入待掃描路徑,編輯器自身的一些功能模塊會(huì)調(diào)用到這。

    4.4.2 如何從FAssetData獲取Uobject

    如前面提到的:采用FAssetData::GetAsset()即可獲得UObject。

    可以再通過Cast<>轉(zhuǎn)換成對應(yīng)的資源類型。

    4.5 對象的創(chuàng)建與銷毀

    既然介紹了一些資源的創(chuàng)建獲取。

    下面再整理一些常用的C++層進(jìn)行對象的創(chuàng)建與銷毀方法:

    純C++類的創(chuàng)建與銷毀

    創(chuàng)建:

    • 通過new來創(chuàng)建對象,推薦用智能指針管理。
    TSharedPtr<FMyClass> MyClassPtr = MakeShareable(new FMyClass());

    銷毀:

    • 智能指針計(jì)數(shù)器會(huì)自動(dòng)消亡。

    UObject及其子類創(chuàng)建與銷毀

    創(chuàng)建:

    UObjectClass* MyClass = NewObject<UObjectClass>();

    銷毀:

    • 虛幻的垃圾回收機(jī)制自動(dòng)回收。

    AActor及其子類的創(chuàng)建與銷毀

    創(chuàng)建:

    • 使用SpawnActor(有七個(gè)重載函數(shù))。
    AActorClass* MyClass = GetWorld()->SpawnActor<AActorClass>(NewLocation, NewRotation);

    銷毀:

    • 虛幻的垃圾回收機(jī)制自動(dòng)回收;

    Component的創(chuàng)建

    構(gòu)造器中創(chuàng)建組件

    • 可以使用 CreateDefaultSubobject來創(chuàng)建組件,且此函數(shù)只能在構(gòu)建函數(shù)中使用。
    UMySceneComponent* MySceneComponent = CreateDefaultSubobject<UMySceneComponent>(TEXT("MySceneComponent"));

    其它函數(shù)中創(chuàng)建組件

    • 組件繼承自UObject類,使用NewObject來創(chuàng)建,但創(chuàng)建完成后需要進(jìn)行手動(dòng)注冊才能生效。
    UStaticMeshComponent* MyMeshComp = NewObject<UStaticMeshComponent>(this, TEXT("MyMeshComp")); MyMeshComp->SetupAttachment(RootComponent); MyMeshComp->SetRelativeLocation(FVector(0.f, 0.f, 0.f)); UStaticMesh* StaticMesh = LoadObject<UstaticMesh>(NULL, TEXT("StaticMesh'/Game/StaticMesh.StaticMesh'")); MyMeshComp->SetStaticMesh(StaticMesh); MyMeshComp->RegisterComponent(); //RegisterAllComponents();

    五、縮略圖

    了解了路徑和資源之后,我們再來看看縮略圖的渲染。(雖然筆者認(rèn)為這個(gè)可能基本不會(huì)去改它)

    ThumbnailRenderer.h

    定義了一個(gè)渲染指定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資源縮略圖。

    注:

    • 筆者在加某個(gè)編輯器(例如Mesh編輯器)加功能的過程中,曾遇到過崩潰,是縮略圖或者Preview窗口的渲染引起的。
    • 這里是一個(gè)需要考慮的問題,當(dāng)在做對編輯器源碼進(jìn)行修改的時(shí)候。

    參考文章

    • UE Plugins官方介紹
    • UE4 PluginDescriptor字段描述
    • UE4模塊官方文檔
    • UE4模塊文章
    • Slate概述
    • UE文檔 Slate UI框架
    • 【UE4 Renderer】<02> Slate系統(tǒng)
    • Slate渲染流程
    • UE資源路徑相關(guān)整理分析
    • UE4 4.20 UE4獲取所有特定資源(FAssetData)的資源路徑
    • 虛幻4渲染編程(工具篇)【第八卷:Asset creation】
    • 虛幻4渲染編程(工具篇)【第九卷:SlateUI布局】
    • UTexture2D的讀取與寫入數(shù)據(jù)并保存成為Asset
    • UE4 AssetRegistry分析
    • 對象創(chuàng)建與資源獲取
    • 資源管理之UAssetManager用法
    • AssetThumbnail資源縮略圖

    總結(jié)

    以上是生活随笔為你收集整理的虚幻引擎编辑器开发基础(一)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。