UE5: Game Feature Tech Preview

UE5:Game Feature预研

The preview video for UE5 introduces a modular development mechanism for GamePlay, similar to mod-style development and management of game features and resources, referred to as “Game Feature”. This has already been enabled in UE4.27 and UE5. I think this new modular gameplay format is great, so I’ve conducted a technical research.

This article introduces the activation process and operational mechanism of Game Features, and at the end shares a demo based on Game Features. Additionally, I implemented a mechanism in HotPatcher that allows features to be packaged independently; Game Features do not need to be pre-packaged into the base package and can be downloaded and loaded on demand during runtime, achieving true modular loading.

Currently, the UE5 EA version still has some imperfections, and related content will be supplemented as the engine updates.

The problem that Game Feature aims to solve is the modularization of Game Play, aggregating certain functions and associated resources as a separate feature that can be loaded when needed and unloaded when not needed. This allows for dynamic management of game runtime resources without relying on a complete set of game resources at startup.

This is similar to a hot update mechanism, because Game Feature is essentially a set of resource packages; however, UE adds dynamic execution logic on top of the resource package, allowing one to conveniently add functionalities to existing actors, and some small gameplay can be made into a standalone Game Feature, which even has a flavor of ECS. But unlike hot updates, the main idea of Game Feature is to dynamically add and unload functionalities at runtime rather than replace existing resources and functionalities.

The implementation of UE’s Game Feature is based on the Plugin system, where each Game Feature can be regarded as a Content Only plugin that manages a number of resources and pluggable game logic through GameFeatureData to manage resources and behaviors of the feature.

However, currently, it seems that UE’s implementation only treats it as a modular management mechanism without the ability to package it separately. Through research, it’s also possible to implement dynamic downloading and enabling of Game Features. I realized packaging of Game Features based on HotPatcher, allowing any Game Feature to be packaged separately, downloaded as needed, and enabled in the game. The specific introduction can be found in 独立打包GameFeature.

General Introduction

Activation and Creation

First, enable the Game Feature and Modular Feature plugins:

After enabling, there will be a GameFeature setting option in the project settings:

Creating a new GameFeature requires creating a plugin, and you can see the Game Feature(Content Only) plugin template:

This will create a plugin located in the project’s Plugins/GameFeatures directory:

1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Plugins\GameFeatures>tree /a /f
Volume in drive C has no label.
Volume Serial Number is 0C49-9EA3
C:.
\---GF_Examles
| GF_Examles.uplugin
|
+---Content
| GF_Examles.uasset
|
\---Resources
Icon128.webp

Its uplugin content is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "GF_Examles",
"Description": "",
"Category": "Game Features",
"CreatedBy": "",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"CanContainContent": true,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"ExplicitlyLoaded": true,
"BuiltInInitialFeatureState": "Installed"
}

Create a resource of GameFeatureData in the root directory of the plugin’s Content; note that it must have the same name as the plugin:

In the EditPlugin options, you can set the initial state of the current Game Feature. If set to Active, it will automatically load and activate by default when the engine starts. It is recommended to set it to Registered during editor editing; if set to Installed, the engine will not load the plugin at startup, making it invisible in the Content Browser and unable to be packaged:

If the Initial State of the Game Feature is Active, it will auto-load upon engine startup, as shown in the call stack:

The states a Game Feature can have 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
/** The states a game feature plugin can be in before fully active */
enum class EGameFeaturePluginState : uint8
{
Uninitialized, // Unset. Not yet been set up.
UnknownStatus, // Initialized, but the only thing known is the URL to query status.
CheckingStatus, // Transition state UnknownStatus -> StatusKnown. The status is in the process of being queried.
StatusKnown, // The plugin's information is known, but no action has taken place yet.
Uninstalling, // Transition state Installed -> StatusKnown. In the process of removing from local storage.
Downloading, // Transition state StatusKnown -> Installed. In the process of adding to local storage.
Installed, // The plugin is in local storage (i.e. it is on the hard drive)
WaitingForDependencies, // Transition state Installed -> Registered. In the process of loading code/content for all dependencies into memory.
Unmounting, // Transition state Registered -> Installed. The content file(s) (i.e. pak file) for the plugin is unmounting.
Mounting, // Transition state Installed -> Registered. The content files(s) (i.e. pak file) for the plugin is getting mounted.
Unregistering, // Transition state Registered -> Installed. Cleaning up data gathered in Registering.
Registering, // Transition state Installed -> Registered. Discovering assets in the plugin, but not loading them, except a few for discovery reasons.
Registered, // The assets in the plugin are known, but have not yet been loaded, except a few for discovery reasons.
Unloading, // Transition state Loaded -> Registered. In the process of removing code/content from memory.
Loading, // Transition state Registered -> Loaded. In the process of loading code/content into memory.
Loaded, // The plugin is loaded into memory, but not registered with game systems and active.
Deactivating, // Transition state Active -> Loaded. Currently unregistering with game systems.
Activating, // Transition state Loaded -> Active. Currently registering plugin code/content with game systems.
Active, // Plugin is fully loaded and active. It is affecting the game.

MAX
};

Common operations on the business side include:

  1. load
  2. activate
  3. deactivate
  4. unload

To call them, you need to pass the PluginURL, which is the path to the uplugin file, and you can obtain it by using:

1
GameFeatureSubsystem->GetPluginURLForBuiltInPluginByName(PluginName,OutPluginURL);

These functions are not exposed to Blueprints, so you can create a wrapper (note to add module dependencies for GameFeatures and Projects):

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
// .h
UCLASS()
class GAMEFEATUREUTILS_API UFlibGameFeature : public UBlueprintFunctionLibrary
{
GENERATED_UCLASS_BODY()
public:
UFUNCTION(BlueprintCallable)
static bool GetPluginURLForBuiltInPluginByName(class UGameFeaturesSubsystem* Subsystem, const FString& PluginName, FString& OutPluginURL);
UFUNCTION(BlueprintCallable)
static void LoadGameFeature(class UGameFeaturesSubsystem* Subsystem, const FString& InFeature);
UFUNCTION(BlueprintCallable)
static void ActiveGameFeature(class UGameFeaturesSubsystem* Subsystem, const FString& InFeature);

UFUNCTION(BlueprintCallable)
static void LoadBuiltInGameFeaturePlugin(UGameFeaturesSubsystem* Subsystem, const FString& PluginName);

UFUNCTION(BlueprintCallable)
static void LoadBuiltInGameFeaturePlugins(UGameFeaturesSubsystem* Subsystem);

UFUNCTION(BlueprintCallable)
static void UnloadGameFeature(UGameFeaturesSubsystem* Subsystem, const FString& InFeature, bool bKeepRegistered = false);

UFUNCTION(BlueprintCallable)
static void DeactivateGameFeature(UGameFeaturesSubsystem* Subsystem, const FString& InFeature);

static void OnStatus(const UE::GameFeatures::FResult& InStatus);
UFUNCTION(BlueprintCallable, BlueprintPure, meta = (WorldContext = "WorldContextObject"))
static class UGameFrameworkComponentManager* GetGameFrameworkComponentManager(UObject* WorldContextObject);

UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", DefaultToSelf = "Owner"))
static void AddReceiver(UObject* WorldContextObject, AActor* Owner);

UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", DefaultToSelf = "Owner"))
static void RemoveReceiver(UObject* WorldContextObject, AActor* Owner);
};

While editing in the engine, you can set the Feature to Active, which will auto-load it at engine startup and display it in the Content Browser; otherwise, it will not display or be editable if it hasn’t been loaded.

Feature Actions

You can add Action and resources managed by the current GameFeature in the configuration of Game Feature to implement the necessary modular functionality.

For example, with Add Component, you can automatically create a component on a specified class:

When the Feature is loaded and set to Active, it will automatically add that component to all instances of the specified Actor Class.

Note, you need to pre-add the target Actor to Receiver:

When the Feature is Deactive, that component will be removed from the Actor, allowing custom functionality to be executed in the component’s BeginPlay/EndPlay.

Based on the function library above, I’ve created a test loading Feature UMG:

Feature loading test:
I created a BP_Component component and added it to BP_Pawn; in the component’s BeginPlay/EndPlay, I create and remove a UMG:

BP_Component

Runtime effect:

Game Features Settings

In UE5’s Project Settings - Game Features, you can set GameFeatureManagerClass, which is used to launch the GameFeature in the project, allowing you to control which Features are loaded.

We can also inherit UGameFeaturesProjectPolicies to implement our own control flow:

1
2
3
4
5
6
7
8
9
10
11
void UGameFeaturesUtilsProjectPolicies::InitGameFeatureManager()
{
UE_LOG(LogGameFeatures, Log, TEXT("Scanning for built-in game feature plugins"));

auto AdditionalFilter = [&](const FString& PluginFilename, const FGameFeaturePluginDetails& PluginDetails, FBuiltInGameFeaturePluginBehaviorOptions& OutOptions) -> bool
{
return true;
};

UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugins(AdditionalFilter);
}

By passing a predicate in LoadBuiltInGameFeaturePlugins, you can control the loading of Features, and during Feature loading, this callback will decide whether to load:

Engine\Plugins\Experimental\GameFeatures\Source\GameFeatures\Private\GameFeaturesSubsystem.cpp
1
2
3
4
5
6
7
8
9
10
11
12
void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugin(const TSharedRef<IPlugin>& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter)
{
// ...
bool bShouldProcess = AdditionalFilter(PluginDescriptorFilename, PluginDetails, BehaviorOptions);

if (bShouldProcess)
{
UGameFeaturePluginStateMachine* StateMachine = GetGameFeaturePluginStateMachine(PluginURL, true);
// ...
}
// ...
}

Other Resources

Epic has also released two videos introducing Game Features:

Demo

To demonstrate the usage of Game Feature, I created a demo using the Game Feature mechanism to implement a simple gameplay example:

  1. Load the Feature while randomly creating 30 collectible items in the scene.
  2. A countdown of 20 seconds for the player to collect items in the scene.
  3. Score recording.
  4. Clear all created Actors when the timer ends.

All functionalities and resources are within the GF_Feature Game Feature plugin, making it an independent module for plug-and-play in the game.

Demo video:

Download the Demo: GameFeatureDemo_BlankUE5.7z

Packaging

Into the Base Package

After creating the Game Feature plugin, restarting the project will prompt an Add entry to PrimaryAssetTypesToScan. Clicking it will automatically add it to the project’s Asset Manager, where there will be a GameFeatureData option to manage packaged Game Features:

When packaging, you need to set the Init State of the Game Feature to Registered to ensure that the Feature’s plugin will be loaded during packaging; otherwise, the Feature cannot be packaged.

Independent Packaging of Game Feature

In the previous section, after adding the configuration of GameFeatureData to AssetManager, when packaging the base package, the Features set to Registered will be packaged along with it.

However, since this is Modular Gameplay, is it feasible not to package Game Features into the base package and to download and load them on demand at runtime? The answer is definitely yes, because Game Feature is essentially a Content Only Plugin, so you can package the resources within the plugin separately and mount the plugin during runtime.

Based on HotPatcher, I implemented a module for independently packaging Game Features that can be packaged into a pak for any Content Only plugin, mounted at runtime.

It can package the needed plugin information, including:

  1. The plugin’s uplugin
  2. All resources within the plugin
  3. The AssetRegistry of resources in the plugin
  4. The ShaderLibrary compiled for resources in the plugin

The engine relies on this data to load plugins and their resources. I will analyze the specific loading process and code details when I have time.

Taking the GF_Feature in the demo as an example, the files included after packaging are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
D:\UnrealProjects\BlankExample\Package\GF_Feature\Windows>"C:\Program Files\Epic Games\UE_5.0ea\Engine\Binaries\Win64\UnrealPak.exe" -list GF_Feature_Windows_001_P.pak
LogPakFile: Display: Using command line for crypto configuration
LogPakFile: Display: Mount point ../../../BlankExample/Plugins/
LogPakFile: Display: "GameFeatures/GF_Feature/AssetRegistry.bin" offset: 0, size: 7545 bytes, sha1: 6C6010E9298EABA621CE2CD95A8A6D96F90E4465, compression: Zlib.
LogPakFile: Display: "GameFeatures/GF_Feature/Content/GF_Component.uasset" offset: 7618, size: 1324 bytes, sha1: 955C7F3B1BB8721AD468A6E3AE6FB8B398C6D5B8, compression: Zlib.
LogPakFile: Display: "GameFeatures/GF_Feature/Content/GF_Component.uexp" offset: 9015, size: 1161 bytes, sha1: 7EAACE73CF68A72BF1673B8F8F97AFDED725286B, compression: None.
LogPakFile: Display: "GameFeatures/GF_Feature/Content/GF_Feature.uasset" offset: 10229, size: 1330 bytes, sha1: FEE0C1E68CCFC845EAA5C824C3FB38C14AB5AE33, compression: None.
LogPakFile: Display: "GameFeatures/GF_Feature/Content/GF_Feature.uexp" offset: 11612, size: 56 bytes, sha1: 47913DCCC03900CC01E9B4666759275267D8ACDD, compression: None.
LogPakFile: Display: "GameFeatures/GF_Feature/Content/UI/GF_UMG.uasset" offset: 16697, size: 1104 bytes, sha1: 5D1C10BE607042A7540384F217921DFE1DD65AE2, compression: Zlib.
LogPakFile: Display: "GameFeatures/GF_Feature/Content/UI/GF_UMG.uexp" offset: 17874, size: 243 bytes, sha1: 2B17E9E182662754205E81987FDEB08BE4B0E3ED, compression: None.
LogPakFile: Display: "GameFeatures/GF_Feature/GF_Feature.uplugin" offset: 18170, size: 626 bytes, sha1: 79EFDB00D8F3ECC08767F1B2F4AD43F1CDBFA304, compression: None.
LogPakFile: Display: "GameFeatureUtils/Content/GamePlay/BP_Pawn.uasset" offset: 18849, size: 1359 bytes, sha1: BABF48B38EEC32F4667F4D0EA2888A92AF608BA5, compression: Zlib.
LogPakFile: Display: "GameFeatureUtils/Content/GamePlay/BP_Pawn.uexp" offset: 20281, size: 851 bytes, sha1: 3307F3E3504C90ED837B9C2F7A6C5B38435FF031, compression: None.
LogPakFile: Display: 12 files (20449 bytes), (0 filtered bytes).
LogPakFile: Display: Unreal pak executed in 0.002602 seconds

You can mount this pak to the game at runtime so that UFS can find the Feature’s files.

However, there are three issues that need to be resolved at runtime:

  1. Dynamically downloaded Feature plugins for the engine to recognize.
  2. Allow the engine to index resources in the plugin.
  3. Load the Shader Library for resources in the plugin.

In previous notes, I analyzed the engine’s plugin loading process. By default, a upluginmanifest file is generated with information about all plugins in the project, which allows the engine to determine which plugins are enabled at startup. Relevant wiki: upluginmanifest, and the related code in the engine can be found in the PluginManager.cpp‘s ReadAllPlugins function.

The PluginManager also provides a method to load a specific plugin uplugin:

1
2
3
4
5
6
/**
* Adds a single plugin to the list of plugins. Faster than refreshing all plugins with RefreshPluginsList() when you only want to add one. Does nothing if already in the list.
*
* @return True if the plugin was added or already in the list. False if it failed to load.
*/
virtual bool AddToPluginsList( const FString& PluginFilename ) = 0;

You can dynamically add plugins that are not in the upluginmanifest to the engine, and this interface is also called when the engine adds plugins:

Note: Plugins with C++ code cannot be added; only Content Only plugins are allowed.

Using the GF_Feature as an example:

1
IPluginManager::Get().AddToPluginsList(TEXT("../../../BlankExample/Plugins/GameFeatures/GF_Feature/GF_Feature.uplugin"));

This adds GF_Feature to the PluginManager as an enabled plugin in the game.

Next, you need to call the following function to register the GF plugin in the plugin list:

1
IPluginManager::Get().MountNewlyCreatedPlugin(TEXT("GF_Feature"));

Finally, to enable UE to find the resources in the plugin, the AssetRegistry.bin from the plugin needs to be added to the engine’s AssetRegistry; I encapsulated a method in the plugin:

1
bool UFlibPakHelper::LoadAssetRegistry(const FString& LibraryName, const FString& LibraryDir);

Taking GF_Feature as an example:

1
UFlibPakHelper::LoadAssetRegistry(TEXT("GF_Feature"),TEXT("../../../BlankExample/Plugins/GameFeatures/GF_Feature/"));

This deserializes and adds all resource information in the plugin to the AssetRegistry module for Game Feature loading.

Note: The AssetRegistry is not necessary in normal games, but when Game Feature is activated, it will check whether resources exist in the AssetRegistry when loading plugin resources. Therefore, it is mandatory to load AssetRegistry data when Game Feature is enabled.

Lastly, for the Shader Library in the plugin, by default, the compiled Shaders for resources in the plugin are stored as a similarly named Shader Library. I also encapsulated a loading function:

1
bool UFlibPakHelper::LoadShaderbytecode(TEXT("GF_Feature"),TEXT("../../../BlankExample/Plugins/GameFeatures/GF_Feature/"));	

For the detailed content of the Shader Library, you can refer to my previous article: UE Hot Update: Shader Update Strategy.

Once the three steps above are completed, you can use GameFeaturesSubsystem to LoadGameFeature, as if it has always existed in the base package.

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

Scan the QR code on WeChat and follow me.

Title:UE5: Game Feature Tech Preview
Author:LIPENGZHA
Publish Date:2021/11/17 12:31
Word Count:8.7k Words
Link:https://en.imzlp.com/posts/17658/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!