UE5:Game Feature预研

UE5的预览宣传视频里,介绍了一种模块化开发GamePlay的机制,类似于Mod式地开发和管理游戏功能和资源,被称作“Game Feature”,在UE4.27和UE5中已经可以启用了,我觉得这个新的Gameplay Modualr形式很棒,所以做一个技术预研。

本篇文章介绍Game Feature的启用流程和运行机制介绍,文末分享了一个基于Game Feature的展示Demo。并为HotPatcher实现了能够将Feature独立打包的机制,Game Feature不必预先打包进基础包内,可以在运行中按需下载与加载,实现真正意义上的独立地模块化加载。

目前UE5 EA版本中还有不完善的地方,相关的内容也会随着引擎的更新进行补充。

Game Feature要解决的问题是Game Play的模块化,把某种功能和相关联的资源聚合,作为一个单独的Feature,在需要时加载,不需要时卸载。动态地管理游戏运行时的资源,而不用在启动时就依赖完整的游戏资源。

这一点类似于热更新机制,因为Game Feature本质上也是一组资源包,只是UE在资源包的基础上增加了动态执行逻辑的功能,可以比较方便地给已有的Actor添加功能,把一些小型玩法单独做成一个Game Feature,甚至有些ECS的味道。但与热更不同的是,Game Feature的主要思路是在运行时动态地添加和卸载功能,而不是替换已有的资源和功能。

UE的Game Feature的实现基于Plugin系统,每个Game Feature都可以看作是一个Content Only的插件,管理着一堆资源和可插拔的游戏逻辑,通过GameFeatureData来管理Feature的资源和行为。

不过,目前UE的实现中似乎只是将其当作模块化管理的机制,无法单独打包。经过研究,也能够实现Game Feature的动态下载和启用,我基于HotPatcher实现了Game Feature的打包,可以将某个Game Feature单独打包,按需下载并在游戏中启用,文末独立打包GameFeature中有具体介绍。

常规介绍

启用和创建

首先开启Game FeatureModular Feature插件:

开启之后项目设置中会有GameFeature的设置选项:

新建项目的GameFeature需要新建插件,可以看到Game Feature(Content Only)的插件模板:

会创建一个插件,位于项目的Plugins/GameFeatures目录下:

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
卷 Windows 的文件夹 PATH 列表
卷序列号为 0C49-9EA3
C:.
\---GF_Examles
| GF_Examles.uplugin
|
+---Content
| GF_Examles.uasset
|
\---Resources
Icon128.png

它的uplugin内容为:

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"
}

在插件的Content根目录创建一个GameFeatureData的资源,注意必须要与插件名相同:

在EditPlugin选项中可以设置当前Game Feature的初始状态,Active会在引擎启动时默认加载并激活。在编辑器编辑时建议设置成Registered,若为Installed,则引擎启动时不会加载该插件,在Content Browser中无法查看,并且无法打入包中:

如果该Game FeatureInitial StateActive,引擎启动时会自动加载,调用栈:

一个Game Feature具有的状态为:

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/contnet 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
};

在业务端常用的操作为:

  1. 加载(load)
  2. 激活(active)
  3. 取消激活(deactive)
  4. 卸载(unload)

在调用它们时需要传递PluginURL,则是uplugin文件的路径,可以通过以下方式传递插件名获得:

1
GameFeatureSubsystem->GetPluginURLForBuiltInPluginByName(PluginName,OutPluginURL);

这些函数都没有暴露给蓝图,可以做一个封装(注意添加GameFeaturesProjects的模块依赖):

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

在引擎内编辑时,可以将Feature设置为Active,这样在引擎启动时会自动加载并在Content Browser中显示,否则没被加载不会显示也无法编辑。

Feature Actions

可以在Game Feature的配置中添加Action及当前的GameFeature管理的资源,实现需要的模块化功能。

Add Component为例,可以在指定某个类上自动创建一个组件:

当该Feature被加载并设置为Active时,会自动在所指定的Actor Class的所有实例上添加该组件。

注意,需要预先在目标Actor中将自身添加到Receiver

当Feature被Deactive时,就会把该组件从Actor上移除,所以在组件的BeginPlay/EndPlay就可以执行自定义的功能。

基于上面的函数库,我创建了一个测试加载Feature的UMG:

Feature加载测试:
创建了一个BP_Component的Compont,将其添加到BP_Pawn上,在Componment的BeginPlay/EndPlay创建和移除一个UMG:

BP_Component

运行效果:

Game Features设置

在UE5的Project Settings-Game Features下,可以设置GameFeatureManagerClass,用于启动项目中的GameFeature,可以在这里控制哪些Feature被加载。

我们也可以继承UGameFeaturesProjectPolicies实现自己控制的流程:

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

通过在LoadBuiltInGameFeaturePlugins时传递一个谓词实现Feature的加载控制,在加载Feature时会通过该回调来决定是否加载:

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);
// ...
}
// ...
}

其他资料

Epic官方也出了两个介绍Game Feature的视频:

Demo

为了展示Game Feature的用法,我利用Game Feature的机制做了一个Demo,实现一个简易玩法的例子:

  1. 加载Feature时在场景中随机创建30个可拾取物件
  2. 倒计时20s,由玩家在场景中拾取物件
  3. 记录积分
  4. 计时结束时清除所有创建的Actor

所有的功能和资源都在GF_Feature这个Game Feature的插件中,即一个独立的模块,从而实现游戏中可插拔。

展示视频:

该Demo下载:GameFeatureDemo_BlankUE5.7z

打包

打入基础包

当创建了Game Feature的插件之后,重启项目会提示Add entry to PrimaryAssetTypesToScan,点击会自动添加至项目设置的Asset Manager中,会有GameFeatureData的选项,可以管理打包的GameFeature:

在打包时需要将Game Feature的Init State设置为Registed,保证在打包时Feature的插件会被加载,不然无法将Feature打包。

独立打包GameFeature

在上一小节,把GameFeatureData的配置添加到AssetManager之后,在打基础包时会把设置为Registered的Feature一起打包。

不过,既然是Modular Gameplay,不把Game Feature打包到基础包内,在运行时按需下载并加载是否可行呢?答案是肯定的,因为Game Feature本质上是Content Only的Plugin,所以可以单独将插件内的资源打包,在运行时将插件添加至引擎。

基于HotPatcher,我实现了单独打包Game Feature的模块,可以将某个Content Only的插件打包成pak,在运行时挂载。

它能够将引擎依赖的插件信息打包,包括:

  1. 插件的uplugin
  2. 插件中的所有资源
  3. 插件中资源的AssetRegistry
  4. 插件中资源编译的ShaderLibrary

引擎加载插件和其中的资源,依赖这些数据,引擎具体的加载流程和代码分析,后面有时间会具体展开。

以Demo中的GF_Feature为例,打包之后包含的文件:

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

在运行时将该pak挂载到游戏中,让UFS能够找到该Feature的文件。

但是,运行时还需要解决三个问题:

  1. 动态下载的Feature插件,让引擎识别
  2. 让引擎能够索引到插件中的资源
  3. 加载插件中资源的Shader Library

之前的笔记中,分析过引擎加载插件的流程,默认情况下,是将项目中所有的插件信息生成了一份upluginmanifest文件,在引擎启动时,通过分析该文件得到启用的插件。相关wiki:upluginmanifest,引擎中相关的代码在PluginManager.cppReadAllPlugins函数中。

而PluginManager还提供了一种单独加载某个插件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;

可以把不存在于upluginmanifest的插件动态地添加至引擎中,引擎添加插件时调用的也是这个接口:

注意,不能添加具有C++代码的插件,只能是Content Only。

以上文中的GF_Feature为例:

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

GF_Feature添加至PluginManager中,作为游戏的已启用插件。

但还需要将插件中的资源能够被UE查找,需要将插件中的AssetRegistry.bin追加到引擎AssetRegistry中,我在插件中封装了一个方法:

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

同样以GF_Feature为例:

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

会将插件中的所有资源信息反序列化添加到AssetRegistry模块中,供Game Feature加载。

注意:AssetRegistry在普通的游戏中不是必要的,但是在开启了Game Feature之后,读取插件资源时会从AssetRegistry查找资源是否存在,所以当启用了人Game Feature时,必须要加载AssetRegistry数据。

还有插件中的Shder Library,默认情况下,会把插件中资源Cook时编译的Shader存储为同名的Shader Library,我同样封装了加载函数:

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

Shader Library的详细内容可以看我之前的一篇文章:UE热更新:Shader更新策略

当上面三步都执行完毕之后,就可以通过GameFeaturesSubsystemLoadGameFeature了,就像它一直存在于基础包内一样。

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:UE5:Game Feature预研
文章作者:查利鹏
发布时间:2021年11月17日 12时31分
本文字数:本文一共有4.1k字
原始链接:https://imzlp.com/posts/17658/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!