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 Feature
和Modular Feature
插件:
开启之后项目设置中会有GameFeature
的设置选项:
新建项目的GameFeature需要新建插件,可以看到Game Feature(Content Only)
的插件模板:
会创建一个插件,位于项目的Plugins/GameFeatures
目录下:
1 | C:\Users\lipengzha\Documents\Unreal Projects\ThirdPerson_UE5\Plugins\GameFeatures>tree /a /f |
它的uplugin内容为:
1 | { |
在插件的Content根目录创建一个GameFeatureData
的资源,注意必须要与插件名相同:
在EditPlugin选项中可以设置当前Game Feature的初始状态,Active会在引擎启动时默认加载并激活。在编辑器编辑时建议设置成Registered
,若为Installed
,则引擎启动时不会加载该插件,在Content Browser中无法查看,并且无法打入包中:
如果该Game Feature
的Initial State
为Active
,引擎启动时会自动加载,调用栈:
一个Game Feature具有的状态为:
1 | /** The states a game feature plugin can be in before fully active */ |
在业务端常用的操作为:
- 加载(load)
- 激活(active)
- 取消激活(deactive)
- 卸载(unload)
在调用它们时需要传递PluginURL
,则是uplugin
文件的路径,可以通过以下方式传递插件名获得:
1 | GameFeatureSubsystem->GetPluginURLForBuiltInPluginByName(PluginName,OutPluginURL); |
这些函数都没有暴露给蓝图,可以做一个封装(注意添加GameFeatures
和Projects
的模块依赖):
1 | // .h |
在引擎内编辑时,可以将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:
运行效果:
Game Features设置
在UE5的Project Settings
-Game Features
下,可以设置GameFeatureManagerClass
,用于启动项目中的GameFeature
,可以在这里控制哪些Feature
被加载。
我们也可以继承UGameFeaturesProjectPolicies
实现自己控制的流程:
1 | void UGameFeaturesUtilsProjectPolicies::InitGameFeatureManager() |
通过在LoadBuiltInGameFeaturePlugins
时传递一个谓词实现Feature的加载控制,在加载Feature
时会通过该回调来决定是否加载:
1 | void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugin(const TSharedRef<IPlugin>& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter) |
其他资料
Epic官方也出了两个介绍Game Feature的视频:
Demo
为了展示Game Feature的用法,我利用Game Feature的机制做了一个Demo,实现一个简易玩法的例子:
- 加载Feature时在场景中随机创建30个可拾取物件
- 倒计时20s,由玩家在场景中拾取物件
- 记录积分
- 计时结束时清除所有创建的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,在运行时挂载。
它能够将引擎依赖的插件信息打包,包括:
- 插件的uplugin
- 插件中的所有资源
- 插件中资源的AssetRegistry
- 插件中资源编译的ShaderLibrary
引擎加载插件和其中的资源,依赖这些数据,引擎具体的加载流程和代码分析,后面有时间会具体展开。
以Demo中的GF_Feature
为例,打包之后包含的文件:
1 | 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 |
在运行时将该pak挂载到游戏中,让UFS能够找到该Feature的文件。
但是,运行时还需要解决三个问题:
- 动态下载的Feature插件,让引擎识别
- 让引擎能够索引到插件中的资源
- 加载插件中资源的Shader Library
之前的笔记中,分析过引擎加载插件的流程,默认情况下,是将项目中所有的插件信息生成了一份upluginmanifest
文件,在引擎启动时,通过分析该文件得到启用的插件。相关wiki:upluginmanifest,引擎中相关的代码在PluginManager.cpp
的ReadAllPlugins
函数中。
而PluginManager还提供了一种单独加载某个插件uplugin的方法:
1 | /** |
可以把不存在于upluginmanifest
的插件动态地添加至引擎中,引擎添加插件时调用的也是这个接口:
注意,不能添加具有C++代码的插件,只能是Content Only。
以上文中的GF_Feature
为例:
1 | IPluginManager::Get().AddToPluginsList(TEXT("../../../BlankExample/Plugins/GameFeatures/GF_Feature/GF_Feature.uplugin")); |
将GF_Feature
添加至PluginManager中,作为游戏的已启用插件。
还需要调用下面的函数,把GF插件注册插件列表里:
1 | IPluginManager::Get().MountNewlyCreatedPlugin(TEXT("GF_Feature")); |
最后,但还需要将插件中的资源能够被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数据。
还有插件中的Shader Library,默认情况下,会把插件中资源Cook时编译的Shader存储为同名的Shader Library,我同样封装了加载函数:
1 | bool UFlibPakHelper::LoadShaderbytecode(TEXT("GF_Feature"),TEXT("../../../BlankExample/Plugins/GameFeatures/GF_Feature/")); |
Shader Library的详细内容可以看我之前的一篇文章:UE热更新:Shader更新策略。
当上面三步都执行完毕之后,就可以通过GameFeaturesSubsystem
来LoadGameFeature
了,就像它一直存在于基础包内一样。