UE plug-in and tool development: basic concepts

UE插件与工具开发:基础概念

During the process of project development using Unreal Engine, various types of plugins are often developed and integrated to extend the engine, thereby fulfilling different requirements. For developers, understanding the operational mechanism is more important than using the tools. Thus, it is necessary to understand the principles behind the integration and development of plugins.

Previously, I also developed some tools and plugins for UE, and I hope to write a series of related articles summarizing the common technical content related to UE plugin and tool development and sharing some of my thoughts and functional scaffolding during plugin development. The goal is to achieve the desired functionality with minimal code, minimal invasiveness, and the best implementation strategies.

What is a plugin in UE?

A plugin in UE is a collection of modules organized through uplugin. They can be conveniently enabled in the engine, developed based on the existing functionalities within the engine, and may also include modules from other plugins for extension.

Generally speaking, in terms of functionality distinction, the most commonly used module types can be categorized into the following:

  1. Runtime: the engine’s runtime, which runs both in the Editor and after packaging.
  2. Developer: involved in compilation and execution during non-shipping stages, used for some functionalities in the development stage, automatically excluded during shipping to avoid testing functionalities entering the release package.
  3. Editor: loaded when the Target is an Editor type of application, such as launching the engine’s editor, Commandlet, etc. Usually, Editor-type modules are used to extend editor functionalities, such as creating standalone viewports, adding function buttons to certain resources, and configuration panels, etc.

A single plugin can contain multiple different types of modules, such as Runtime/Developer/Editor, which can simply be described in the uplugin.

In the organizational structure of the engine, plugins are located in the Engine/Plugins directory:

The project’s engineering files are located in the PROJECT_DIR/Plugins directory:

There is a certain dependency hierarchy between plugins in the engine and the project, and it is necessary to manage the dependency hierarchy well to avoid confusion.

  1. Modules in the engine should only depend on the built-in modules of the engine and should not include modules from other plugins.
  2. Modules in engine plugins can depend on built-in engine modules and modules from other built-in plugins of the engine, but should not include project-specific modules.
  3. Modules in project plugins can contain engine built-in modules / engine plugin modules / modules from other project plugins, but should not include modules defined within the project.
  4. Modules in the project can include a collection of all the above modules.

The dependency relationships are shown in the following diagram:

Plugins must rely on a specific project to be initiated, where a project does not solely refer to a game project, but instead refers to a code unit defined by target.cs. A project can contain both plugins and its own modules. The Target configuration of the project will affect the loading of modules within plugins.

Taking the default project created by UE as an example:

In the Source directory, two target*.cs files are created to distinguish between the packaged Runtime and Editor. Certainly, if there’s a DS project, there will also be a corresponding target.cs; here, each definition of target.cs is collectively referred to as the project compilation target.

Their differences are related to file content, not naming. Comparing the two:

The TargetType specifies the type of the current Target.

TargetType is an enumeration defined in UBT with optional values:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace UnrealBuildTool
{
[Serializable]
public enum TargetType
{
/// Cooked monolithic game executable (GameName.exe). Also used for a game-agnostic engine executable (UE4Game.exe or RocketGame.exe)
Game,

/// Uncooked modular editor executable and DLLs (UE4Editor.exe, UE4Editor*.dll, GameName*.dll)
Editor,

/// Cooked monolithic game client executable (GameNameClient.exe, but no server code)
Client,

/// Cooked monolithic game server executable (GameNameServer.exe, but no client code)
Server,

/// Program (standalone program, e.g. ShaderCompileWorker.exe, can be modular or monolithic depending on the program)
Program,
}
}

This represents five different project Target types in UE.

When selecting different BuildConfigurations in the IDE, different Targets will be used:

For instance:

  • Development Editor will use the configuration defined in Blank427Editor.Target.cs (TargetType.Editor)
  • Development will use the configuration defined in Blank427.Target.cs (TargetType.Game)

During compilation, the BuildConfiguration of the project will also be passed to plugins to determine which Modules within the plugins participate in the compilation.

In the plugin’s build.cs, the current Target’s information can be checked to handle different behaviors during the compilation of different Targets:

1
2
3
4
5
6
7
public LearnPlugin(ReadOnlyTargetRules Target) : base(Target)
{
if (Target.Type == TargetType.Editor)
{
// ...
}
}

How to describe a plugin?

uplugin

In UE, plugins are described through the uplugin file, which is a configuration that represents the organizational structure and key information of a plugin based on JSON syntax.

  1. Basic information such as the name of the plugin, the author, etc.
  2. The type of the plugin module
  3. The timing of module activation
  4. Dependencies on other plugins
  5. Platform whitelists and blacklists

For example, the uplugin of HotChunker could look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "HotChunker",
"Description": "",
"Category": "Other",
"CreatedBy": "lipengzha",
"CreatedByURL": "https://imzlp.com/",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"EnabledByDefault" : true,
"CanContainContent" : false,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"SupportedPrograms": [ "UnrealPak" ],
"Plugins": [
{
"Name": "HotPatcher",
"Enabled": true
}
],
"Modules": [
{
"Name": "HotChunker",
"Type": "Program",
"LoadingPhase": "PostConfigInit",
"ProgramAllowList": [ "UnrealPak" ]
},
{
"Name": "HotChunkerCore",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "HotChunkerEditor",
"Type": "Editor",
"LoadingPhase": "Default"
}
]
}

From this information, the quantity, types, and activation timing of modules in the plugin can be clearly known.

  • During compilation, UBT will also read the plugin’s Type to decide whether the module participates in the compilation.
  • When starting the engine, the module definitions will dictate the loading based on the information. For instance, the LoadingPhase configuration loads the plugin at different startup stages.

Modules

The Modules element in uplugin is an array used to describe the current plugin’s modules, with the following basic information for each Module:

  1. Module name, which is the name defined in Build.cs
  2. Type, the type of the module such as Runtime/Developer/Editor/Program, dictating when it participates in compilation and loading for which Targets.
  3. Loading phase, the module’s activation timing, as the modules in the engine are loaded in sequence; by adjusting this configuration, the loading timing of the plugin module can be controlled, which can be very important for processes that depend on execution order. For instance, some modules in the engine might detect startup parameters. If these parameters are not wanted to be manually specified but achieved through plugin code, a module that starts before can be created to append the startup parameters to FCommandLine, thus automating the process without manual intervention.

Furthermore, for some special modules, such as the Type of the HotChunker module being Program, this indicates it will only participate in compilation and startup when the Target is Program, and can specify which Programs are valid through ProgramAllowList.

Similarly, the supported values for LoadingPhase in the engine (4.27+) are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace ELoadingPhase
{
enum Type
{
/** As soon as possible. In other words, uplugin files are loadable from a pak file (as well as right after PlatformFile is set up in case pak files aren't used). Used for plugins needed to read files (compression formats, etc.) */
EarliestPossible,
/** Loaded before the engine is fully initialized, immediately after the config system has been initialized. Necessary only for very low-level hooks */
PostConfigInit,
/** The first screen rendered after the system splash screen */
PostSplashScreen,
/** Loaded before coreUObject for setting up manual loading screens, used for our chunk patching system */
PreEarlyLoadingScreen,
/** Loaded before the engine is fully initialized for modules that need to hook into the loading screen before it triggers */
PreLoadingScreen,
/** Right before the default phase */
PreDefault,
/** Loaded at the default loading point during startup (during engine init, after game modules are loaded.) */
Default,
/** Right after the default phase */
PostDefault,
/** After the engine has been initialized */
PostEngineInit,
/** Do not automatically load this module */
None,
// NOTE: If you add a new value, make sure to update the ToString() method below!
Max
};
}

Taking the following module description as an example:

1
2
3
4
5
6
7
8
9
{
"Name": "HotChunker",
"Type": "Program",
"LoadingPhase": "PostConfigInit",
"ProgramAllowList": [ "UnrealPak" ],
"WhitelistPlatforms": [
"Win64"
]
}

This module indicates that: HotChunker is a Program-type module, and its startup timing is after the configuration file is loaded (PostConfigInit), only allowed to be used in the UnrealPak Program, and will only participate in compilation on the Win64 platform.

By analyzing the contents of the uplugin file, one can describe all the modules in the plugin based on development needs, guiding UBT to find the corresponding modules for compilation and load them during the engine startup.

Note: The white and black list description of modules on platforms (WhitelistPlatforms). If a module A is marked with a platform whitelist, and an all-platform module B references A, A will be brought in to participate in the full-platform compilation, regardless of A’s own definition. This is an issue that arises from the referencer.

Directory Structure of Plugin

Typically, a plugin will contain the following directories and files:

  1. Content: optional, the plugin’s uasset resources, similar to the Content directory of game projects. Requires uplugin to have CanContainContent=true.
  2. Resources: other resources that the plugin depends on, such as icons within the plugin, etc.
  3. Config: optional, the plugin’s configuration files, similar to the project’s Config directory, used for storing some ini configuration items.
  4. Source: directory for the plugin’s code.
  5. *.uplugin: the current plugin’s uplugin description.

Particular attention should be given to the Source directory, which is key for implementing the code plugin. Usually, a corresponding directory for each Module is created under Source to store the code of different Modules, isolating the file organization of different Modules.

For example, if the uplugin defines two Modules:

1
2
3
4
5
6
7
8
9
10
11
12
"Modules": [
{
"Name": "ResScanner",
"Type": "Developer",
"LoadingPhase": "Default"
},
{
"Name": "ResScannerEditor",
"Type": "Editor",
"LoadingPhase": "Default"
}
]

Two corresponding directories named after them are created under the Source directory:

And their respective build.cs and actual C++ code files are created.

Compilation Environment

The compilation environment of UE is determined by UBT, which analyzes participating compiler target.cs as the basic compilation environment.
target.cs controls the entire project and affects every module that will participate in compilation.

*.Target.cs and *.Build.cs are the actual controllers of the Unreal build system, and UBT determines the entire compilation environment by scanning these two files. They are also the focus of this article.
Their roles differ:

  • *.Target.cs controls the external compilation environment of the generated executable program, known as the Target. For example, it defines what Type (Game/Client/Server/Editor/Program) the generated target is, whether to enable RTTI (bForceEnableRTTI), how CRT is linked (bUseStaticCRT), etc.
  • *.Build.cs controls the Module compilation process, governing dependencies on other Modules, file inclusions, linking, macro definitions, and so on. *.Build.cs tells UE’s build system that it’s a Module and what needs to be done during compilation.

In short: everything related to the external compilation environment is managed by *.target.cs, while everything related to modules themselves is managed by *.build.cs.

I have more detailed descriptions in my article UE Build System: Target and Module.

C++ Definition of Module

C++ plugins usually contain a class that inherits from IModuleInterface, which registers itself with the engine. When this module is activated, it serves as the execution entry point of the module, and upon the engine’s shutdown, it handles the module’s unloading and resource cleanup.

The definition is as follows:

NewCreateModule.h
1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once  

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

class FNewCreateModule : public IModuleInterface
{
public:

/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

And the implementation:

NewCreateModule.cpp
1
2
3
4
5
6
7
8
9
10
11

#include "NewCreateModule.h"

#define LOCTEXT_NAMESPACE "FNewCreateModule"

void FNewCreateModule::StartupModule() {}

void FNewCreateModule::ShutdownModule() {}

#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FNewCreateModule, NewCreate)

The key element here is the last IMPLEMENT_MODULE macro, which exposes the module to the outside.

There are two linking modes for executable programs compiled in UE, namely Modular and Monolithic.

This can be controlled in target.cs, but by default, the Editor is Modular, while other target types (like Game, Program) are Monolithic:

1
2
3
4
5
6
7
8
9
10
11
public TargetLinkType LinkType
{
get
{
return (LinkTypePrivate != TargetLinkType.Default) ? LinkTypePrivate : ((Type == global::UnrealBuildTool.TargetType.Editor) ? TargetLinkType.Modular : TargetLinkType.Monolithic);
}
set
{
LinkTypePrivate = value;
}
}

Modular Mode

Modular mode refers to a segmented mode where each module is compiled into an independent executable file. The advantage is that it allows for incremental compilation of changed modules without needing to compile the entire project.

For example, in non-Monolithic mode under Editor (modules are compiled as DLLs), the macro definition is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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(); \
} \
/* Forced reference to this function is added by the linker to check that each module uses IMPLEMENT_MODULE */ \
extern "C" void IMPLEMENT_MODULE_##ModuleName() { } \
PER_MODULE_BOILERPLATE \
PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)

After macro expansion:

1
2
3
4
5
6
// for not-monolithic
extern "C" DLLEXPORT IModuleInterface* InitializeModule()
{
return new FNewCreateModule();
}
extern "C" void IMPLEMENT_MODULE_NewCreate() { }

Viewing the export table of the DLL will reveal the InitializeModule function and other exported symbols (which are all name-mangled):

Monolithic Mode

Monolithic mode means that all C++ code in the project is statically linked and compiled into the same executable program (excluding added dynamic libraries; extra static libraries are also compiled into the executable program).

By default, packaging in UE uses the Monolithic mode, where both the engine and the project code are compiled into one file, such as:

  • WindowsNoEditor: WindowsNoEditor/GameName/Binaries/Win64/GameName.exe
  • Android: lib/arm64-v8a/libUE4.so
  • IOS: Payload/GameName.app/GameName

The advantage of this approach is that there are no extra PE file lookup and loading overhead, and all operations are executed in the current process space.

The definition of modules also differs accordingly, in the Not-Monolithic mode, to enable compilation as an independent executable file, a DLLEXPORT marked InitializeModule function will be created, but this is not needed in Monolithic mode. Instead, it uses static symbols:

1
2
3
4
5
6
7
// 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( TEXT(#ModuleName) ); \
/* Forced reference to this function is added by the linker to check that each module uses IMPLEMENT_MODULE */ \
extern "C" void IMPLEMENT_MODULE_##ModuleName() { } \
PER_MODULE_BOILERPLATE_ANYLINK(ModuleImplClass, ModuleName)

After macro expansion:

1
2
static FStaticallyLinkedModuleRegistrant< FNewCreateModule > ModuleRegistrantNewCreateModule( TEXT("NewCreateModule") );
extern "C" void IMPLEMENT_MODULE_NewCreate() { }

In fact, it defines a static object that leverages a C++ feature: the initialization of objects with static storage duration will occur before the first statement of the main function!

[ISO/IEC 14882:2014(E)] It is implementation-defined whether the dynamic initialization of a non-local variable with static storage duration is done before the first statement of main.

Thus, when the engine starts, all modules create such a static object for initialization.

In the constructor of FStaticallyLinkedModuleRegistrant, a singleton of the registered module class is created and registered with the ModuleManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Utility class for registering modules that are statically linked.
*/
template< class ModuleClass >
class FStaticallyLinkedModuleRegistrant
{
public:
FStaticallyLinkedModuleRegistrant( FLazyName InModuleName )
{
// Create a delegate to our InitializeModule method
FModuleManager::FInitializeStaticallyLinkedModule InitializerDelegate = FModuleManager::FInitializeStaticallyLinkedModule::CreateRaw(
this, &FStaticallyLinkedModuleRegistrant<ModuleClass>::InitializeModule );

// Register this module
FModuleManager::Get().RegisterStaticallyLinkedModule(
InModuleName, // Module name
InitializerDelegate ); // Initializer delegate
}

IModuleInterface* InitializeModule( )
{
return new ModuleClass();
}
};

This is used by the engine to subsequently call based on the startup timing information of the modules defined in the uplugin.

Thus, UE unifies the interface layer between Modular and Monolithic modes.

Conclusion

This article introduces the basic information about plugins in UE, as well as how to describe a plugin (uplugin), and provides an introduction to the directory structure of the Plugin and the definition of Modules.

Previously, I wrote articles related to the C++ compilation model and the UE build system on my blog:

These are the foundational concepts for tool development in UE, understanding and maximizing the use of UE’s own implementation mechanisms to achieve the desired functionality with minimal code.

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE plug-in and tool development: basic concepts
Author:LIPENGZHA
Publish Date:2023/01/29 12:12
World Count:12k Words
Link:https://en.imzlp.com/posts/75405/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!