虚幻引擎编辑器开发基础(一)
一、前言
虚幻引擎提供了非常强大的编辑器(如蓝图编辑器、材质编辑器、动画编辑器)。
然而根据项目的不同以及相应的需求也需要扩展或者自定义一些编辑器的相关功能,对引擎做各种工具上的扩展,来满足高效、快速的开发需要。
虚幻的编辑器开发包含了零零散散的各种内容,包括比如插件和模块、编辑器窗口的扩展、资源文件的自定义等。
这篇文章,将会整理一下笔者了解的编辑器开发相关的基础内容(一)。
由于笔者只自己实践过其中的一部分。一些内容可能会留空,标注出可能的参考文章,后续工作涉及到或做到将补充和修改。
二、插件与模块
在正式进入相关的开发介绍之前,让我们来了解一下虚幻的插件与模块功能。
插件与模块既是虚幻组织代码的一种方式,可以我们对编辑器开发的重要帮手。
虚幻中的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个部分决定:
- 硬编码形式硬性规定,即在源码中直接指定加载;
- 松散加载;
总体的顺序:
- 加载Platform File Module,因为虚幻要读取文件;
- 核心模块加载 FEngineLoop::PreInit->LoadCoreModules
- 加载CoreUObject
- 在初始化引擎之前加载模块:FEngineLoop::LoadPreInitModules
- 加载Engine
- 加载Renderer
- 加载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插件。