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

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

一、前言

虚幻引擎提供了非常强大的编辑器(如蓝图编辑器、材质编辑器、动画编辑器)。

然而根据项目的不同以及相应的需求也需要扩展或者自定义一些编辑器的相关功能,对引擎做各种工具上的扩展,来满足高效、快速的开发需要。

虚幻的编辑器开发包含了零零散散的各种内容,包括比如插件和模块、编辑器窗口的扩展、资源文件的自定义等。

这篇文章,将会整理一下笔者了解的编辑器开发相关的基础内容(一)。

由于笔者只自己实践过其中的一部分。一些内容可能会留空,标注出可能的参考文章,后续工作涉及到或做到将补充和修改。

二、插件与模块

在正式进入相关的开发介绍之前,让我们来了解一下虚幻的插件与模块功能。

插件与模块既是虚幻组织代码的一种方式,可以我们对编辑器开发的重要帮手。

虚幻中的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个部分决定:

  1. 硬编码形式硬性规定,即在源码中直接指定加载;
  2. 松散加载;

总体的顺序:

  1. 加载Platform File Module,因为虚幻要读取文件;
  2. 核心模块加载 FEngineLoop::PreInit->LoadCoreModules
  3. 加载CoreUObject
  4. 在初始化引擎之前加载模块:FEngineLoop::LoadPreInitModules
  5. 加载Engine
  6. 加载Renderer
  7. 加载AnimationGraphRuntime

模块的加载注册

  • 模块需要提供给外部一个操作的接口,就是一个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,是一个注册辅助类,利用全局变量构造函数自动调用的特性,实现自动注册。
// 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 method
        FModuleManager::FInitializeStaticallyLinkedModule InitializerDelegate = FModuleManager::FInitializeStaticallyLinkedModule::CreateRaw(
                this, &FStaticallyLinkedModuleRegistrant<ModuleClass>::InitializeModule );
        // Register this module
        FModuleManager::Get().RegisterStaticallyLinkedModule(
            FName( InModuleName ),    // Module name
            InitializerDelegate );    // Initializer delegate
    }
     
    IModuleInterface* InitializeModule( )
    {
        return new ModuleClass();
    }
};

动态链接时:

  • 声明了一个dllexport函数,功能就是创建并返回相应模块;
  • ModuleImplClass:具体实现的类;
  • 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)

插件重编译问题:有时候修改了代码却发现没有进行编译。

如何解决:把插件目录下的两个文件夹删除掉: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类

  • 声明命令和注册;
// StandaloneWindowCommands.h
TSharedPtr< FUICommandInfo > OpenPluginWindow; // 声明FUICommandInfo的指针

// StandaloneWindowCommands.cpp
void FStandaloneWindowCommands::RegisterCommands()
{
	UI_COMMAND(OpenPluginWindow,"StandaloneWindow","Bring up StandaloneWindow Window",EUserInterfaceActionType::Button,FInputGesure());
}

StartupModule主要做了几件事情:

  • 初始化风格;
  • 注册UI命令和其回调;
  • 添加创建的扩展;
  • 注册标签页的创建方法;

在这里插入图片描述

其中Extender的回调如下:

  • 工具栏和菜单栏调用不同的函数,但注册的同一个命令

在这里插入图片描述

这个命令对应的回调即:

在这里插入图片描述

其中PluginButtonClicked回调的具体实现:
在这里插入图片描述

  • 根据名字去激活一个Tab标签UI,即StartupModule函数中最后注册的。

在这里插入图片描述

这个标签页的具体生成方法,即OnSpawnPuginTab函数实现的。最后返回一个Slate声明创建的UI。

对于以上的源码以及其实现效果,有三点值得研究,分为是:

  1. 对菜单栏和工具栏的扩展;
  2. Tab的UI创建方法;
  3. UI_COMMAND系列;

其中:

  • 第一点(菜单栏和工具栏的扩展)为后续文章的内容之一,到时再进行介绍。
  • 第二点(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

  • 参数只能是值。
/**
 * 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

  • 属性可以是值也可以是函数。
/**
 * 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

  • 事件,其实就是回调。
/**
 * 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

  • 根据名称创建了Widget,可以用来存储Widget。

  • 重载了[]操作符,可以有一个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,只有一个Slot插槽。

让我们再来看一下多个插槽的情况,看一下布局,以SVerticalBox举例。

可以看到类中的宏定义如下:

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

SLATE_END_ARGS()

SLATE_SUPPORTS_SLOT

  • 定义了一个Slot数组;
  • 在该类中为SVerticalBox::FSlot,并重载了+操作符,添加Slot到数组中。
/**
 * 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在类里面中实现如下。

/** 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_ARGSSLATE_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还有HAlignmentVAlignment两个参数,用来描述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)

在这里插入图片描述
在这里插入图片描述

这个结果的计算如下:

  1. 首先,有三个插槽,其中有两个是Auto,则插槽的大小等于插槽内控件的DisireSize。
  2. 第一个插槽为Stretch,则其大小为总大小减去其余两个Slot占据的固定大小(这里的总大小应该指的是窗口的大小)。
  3. 三个Slot的大小准备好了 以后就往Slot里面填充UI。
    1. 第一个Slot的填充模式为Fill,所以会填满整个Slot插槽;
    2. 后面两个Slot的填充模式为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_EVENTS
	SCOPED_NAMED_EVENT(SWidget_CacheDesiredSize, FColor::Red);
#endif

	// Cache this widget's desired size.
	SetDesiredSize(ComputeDesiredSize(InLayoutScaleMultiplier));
}

看一下SBox的ComputeDesiredSize函数:

  • 可以看出这个函数通过Override的宽度和高度自定义大小,反正不管怎么定义计算它就是代表内部控件的大小
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;
}

布局小结:

  1. 首先会对UI空间进行分割,分割出每个Slot插槽的大小
  2. 再根据不同的UI控件和不同的填充模式,去填充Slot空间;

四、资源

了解了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 EngineContentPathShort
	TArray<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
/** 
 * 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;

    // ... 省略部分源码
};

下面,通过一个实际的例子来看下这些路径都是什么样的。

在项目的Content目录创建了一个蓝图Acotr。

则有:

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

可以看出其路径采用了前面虚拟路径。

那么,有了路径如何获得对应的UObject数据呢?

  • 可以通过GetAsset获得内存中的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 set
        return nullptr;
    }

    UObject* Asset = FindObject<UObject>(nullptr, *ObjectPath.ToString());
    if ( Asset == nullptr)
    {
        Asset = LoadObject<UObject>(nullptr, *ObjectPath.ToString());
    }

    return Asset;
}

4.3 资源的创建与获取

4.3.1 资源的创建

在编写工具的时候需要创建资源、初始化资源、管理资源等需求。比如:

  • 合并DrawCall时,需要创建贴图;

  • 管理材质时,需要创建并指定材质球、设置材质等。

首先,弄清楚虚幻的资源结构以及和编辑器的关系

虚幻中有大量的类型,诸如UMaterial、UMaterialInstance、UTexture、UStaticMesh等。

这些资源从外部导入虚幻的时候做了一次数据抽取,如贴图资源模型资源等。

这些数据被放在一个UObject里,然后这个UObject放在一个Package里

拿贴图的数据导入举例,有代码UTextureFactory::ImportTexture

  • 引擎会抽取数据,然后创建对应UTexture,使用创建的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的导入资产,创建有一个固定格式:

  • New一个Package
  • New一个资源对应的UObject
  • 在此时指认Package和UObject;
  • 向这个UObject填充数据
  • MarkDirty,注册,通知资源浏览器这里创建了一个新的资源,然后保存。

下面给出几个通过C++创建资源资产的示例。

示例1: 创建一个模型资源,Create Mesh Asset With C++

大概的过程描述如下:

  1. 首先创建一个Package(注意路径为虚拟路径);
  2. 然后New一个Mesh,在NewObject方法中指定Package;
  3. 通知AssetRegistryModule,我们创建了这个新的资源;
FString CreatedMeshName = TEXT("TestMesh");
// 注意! 这里的路径要用虚拟路径.不能用FPath的获得的真实路径.
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);

注意:

  • 这里创建的是个空的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函数加载蓝图类。
UClass* MyActorClass = LoadClass<AActor>(NULL, TEXT("Blueprint'/Game/MyActorBP.MyActorBP_C'"));

美术资源加载:

  • 使用LoadObject函数加载美术资源。例如加载一个StaticMesh。
UStaticMesh* StaticMesh = LoadObject<UStaticMesh>(NULL, TEXT("StaticMesh'/Game/StaticMesh.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来创建对象,推荐用智能指针管理。
TSharedPtr<FMyClass> MyClassPtr = MakeShareable(new FMyClass());

销毁:

  • 智能指针计数器会自动消亡。

UObject及其子类创建与销毁

创建:

UObjectClass* MyClass = NewObject<UObjectClass>();

销毁:

  • 虚幻的垃圾回收机制自动回收。

AActor及其子类的创建与销毁

创建:

  • 使用SpawnActor(有七个重载函数)。
AActorClass* MyClass = GetWorld()->SpawnActor<AActorClass>(NewLocation, NewRotation);

销毁:

  • 虚幻的垃圾回收机制自动回收;

Component的创建

构造器中创建组件

  • 可以使用 CreateDefaultSubobject来创建组件,且此函数只能在构建函数中使用
UMySceneComponent* MySceneComponent = CreateDefaultSubobject<UMySceneComponent>(TEXT("MySceneComponent"));

其它函数中创建组件

  • 组件继承自UObject类,使用NewObject来创建,但创建完成后需要进行手动注册才能生效。
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();

五、缩略图

了解了路径和资源之后,我们再来看看缩略图的渲染。(虽然笔者认为这个可能基本不会去改它)

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窗口的渲染引起的。
  • 这里是一个需要考虑的问题,当在做对编辑器源码进行修改的时候。

参考文章

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值