人的痛苦来自于想要表现出自己不具备的能力,因此当你在工作中感到焦躁烦闷的时候,正视自己的不足,接受自身的微渺,实事求是,脚踏实地,或许能让你快速找回属于自己的节奏。
近日在工作中了解到UE5推出了一个叫做Game Feature(以下简称GF)的新特性,粗看之下这个特性似乎跟Plugin有点相似,但深入了解之后发现两者存在较大的不同。
GF跟Plugin都可以用于实现功能的解耦,对于程序同学来说,这是一种能够在架构上产生深远影响的特性,值得投入时间去理解学习,因此这里单开一篇来对GF一窥究竟。
在深入之前,我们先设置一些问题,带着问题我们来对其进行深入探究:
GF跟Plugin有什么不同,其定位是什么?
GF在日常工作中要如何使用,结合案例来进行梳理与分析。
GF的实现原理是什么,这样的设计有怎样的深意,其中是否存在一些不足,该如何改进与优化?
GF可以用在哪些场景,可以产生怎样的奇效?
对这上面的几个问题,我们分别划拨一个小节进行分割。
1. Game Feature简介
什么是Game Feature?
Game Feature是Modular Game Feature的简称,是UE用来进一步实现Plugin跟Core Game解耦的方案,这个功能在UE中对应于两个插件,即“Game Features”跟“Modular Gameplay”。
我们知道UE的Plugin是UE对一个功能模块的封装,每个Plugin包含了代码、资源等其功能所需的所有要素,因此可以脱离项目单独存在;但是Plugin需要在Game中启用(即通过.uproject进行开启),且启用之后,Core Game逻辑想要使用,还需要添加Module的引用,并在头文件中添加include,在cpp文件中调用对应的plugin中代码的相关实现。换句话说,Plugin对Core Game的扩展在Core Game来看是有感的,没法做到Core Game跟Plugin之间的相互解耦。
Game Feature则是为了解决Core Game对Plugin有感而推出的,在Game Feature的框架下,Core Game只需要添加一些注册代码(用于实现监听)即可完成其与Game Feature的联系,在运行时,当某个条件满足之后,Game Feature就会被启用,此时就会触发Game Feature设定的一系列Action,这些Action就会完成Game Feature中定义的功能向Core Games的接入或者叫注入,而如果不需要这个Game Feature了,Core Game中的逻辑甚至可以完全不用修改,真正做到万花丛中过,片叶不沾身。
Game Feature跟Plugin的另一个不同的地方在于,Game Feature可以在运行时根据需要进行启用或者禁用,而Plugin则是在项目打包的时候就决定了是启用还是禁用了,这个区别使得Game Feature的使用更灵活,同时也更容易维护项目的洁净。
另外,由于Plugin的启用是在项目开发阶段就确定的,也就是说在打包的时候,就需要清楚的知道哪些Plugins需要打到包里,而Game Feature则是可以在运行时进行动态的开关的,这些内容自然就可以做成热更进行加载,而不需要在打包的时候就确定好,[3]中就给出了将Game Feature单独打包并通过热更挂接到Core Game逻辑中的方案示例,有兴趣的同学可以尝试一下。
因为Game Feature的灵活性,我们在进行项目制作的时候也需要进行详细规划,确定哪些功能应该放在Core Game部分,哪些功能应该用Game Feature来封装。
上面说的都是GF的好处,那么GF相对于Plugins是否有限制呢?我们在创建GF的时候发现,这个插件是Content Only的,这里需要注意的是,Content Only说的并不是后面不能添加代码,而是在创建的时候只包含Content,不过这里有一个疑问是,C++代码要如何通过热更的方式来实现热插拔呢?
下面我们来看下,Game Feature这么神奇,究竟是如何使用的。
2. Game Feature使用方法
先来看看,我们要如何创建一个Game Feature。
- Game Feature功能不是默认打开的,首先需要在项目插件中打开“Game Features”跟“Modular Gameplay”插件,此时会弹出重启请求,但是先不要重启,而是先点击“New Plugin”创建一个包含我们希望用来装载Game Feature的插件(插件类型无特别要求,可以根据需要创建,唯一需要注意的是,这个插件需要放置在Plugins/GameFeatures/目录下),之后再进行重启。
-
重启的过程中如果遇到报错"Asset Manager settings do not include an entry for assets of type GameFeatureData, which is required for game feature plugins to function. Add entry to PrimaryAssetTypesToScan?",可以通过两种方式来解决:
点击"Add entry to PrimaryAssetTypesToScan?",这个会自动对DefaultEngine.ini文件进行修改
在Edit->Project Settings->Asset Manager->Game->Primary Asset Types to Scan下添加一个叫做GameFeatureData的Asset,这个Asset的基类是GameFeatureData,同时将/Game/Unused添加到Directories数组中,并将Rules Section中的Cook Rule设置为Always Cook。
这两种方法都能一劳永逸的解决这个报错。
在编辑器中,导航到刚刚创建的Game Feature的Content目录,右键创建Data Asset,选择基类为GameFeatureData,这个Asset的名字需要保持跟刚才创建的Plugin的名字一致(即两者只是后缀不同)。
经过上述步骤后,Game Feature就创建完成了,并且这个Game Feature会跟随Game Engine一起启动,接下来就可以将精力专注在Game Feature本身上面了。
双击打开Game Feature Data Asset进行编辑:
在这个asset中可以通过点击最上方的“Edit Plugin”按钮对Game Feature进行设置,如我们可以设置这个Game Feature的初始状态:
- Active,表示的是这个GF在引擎启动之后就会处于激活状态,其调用堆栈给出如下:
在编辑阶段的时候,需要将GF的状态设置为Active,否则这个GF在引擎加载的时候不会显示,因而就无法编辑
Registered,在编辑器进行编辑的时候,建议将之设置成Registered;在打包的时候也需要将init state设置为registered,否则GF无法加载,自然也就无法打包
Installed,这个表示这个GF只会存在与本地磁盘了,引擎启动的时候不会自动加载GF,同时,在Content Browser中也无法看到GF的相关内容,打包的时候也不会自动打进去
Loaded,表示GF已经加载到内存,但是没有registered,也没有被激活
实际上,GF还可以有很多其他的状态:
/** 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
};
我们在实际工作中常用的状态有:Active、Load、Deactive以及Unload,而在修正状态的时候,需要传入对应的URL:
// Get the Plugin URL based on the Game Features Name
FString PluginURL;
UGameFeaturesSubsystem::Get().GetPluginURLForBuiltInPluginByName(, PluginURL);
// Deactivate the Game Feature
UGameFeaturesSubsystem::Get().DeactivateGameFeaturePlugin(PluginURL);
// Activate the Game Feature
UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin(PluginURL, FGameFeaturePluginLoadComplete());
// Unload the Game Feature
UGameFeaturesSubsystem::Get().UnloadGameFeaturePlugin(PluginURL);
// Load the Game Feature
UGameFeaturesSubsystem::Get().LoadGameFeaturePlugin(PluginURL, FGameFeaturePluginLoadComplete());
再来说回asset,这个asset是Game Feature设置的核心组件,通过这个组件我们可以决定在Game Feature启动之后,会触发什么样的行为(Action),Game Feature目前提供了多种不同的Action(这些是预置的,不知道是否可以自定义?),这些Action在Game Feature生效的时候会被触发:
-
Add Cheats,这个功能是对CheatManager的扩展,通过这个Action可以创建新的Cheat Code,或者对已有Code进行扩充,而Cheat Code的存在不是为了作弊,而是用于开发时的调试(编辑器中通过~按键唤醒),在shipping包构建时会自动移除,无需担心安全问题。
- 如下图所示,添加这个Action会向游戏注册一个Cheat Manager Extension,即通过添加多个Cheat Managers来完成对Cheat逻辑的扩展。
* 在其中添加对应的CheatManagerExtension,即可完成Action的绑定逻辑
- Add Components,这个功能用于在运行时根据需要为指定Actors挂接一些Component,而由于Component能够承载足够多的游戏逻辑,因此这也成为了Game Feature中最为广泛的用法。
* 这里在添加的时候,会需要指定待修改的Actor类型,以及需要挂接到该类Actor的Component类型,这种绑定是以Actor-Component类型实现的,通过这种方式可以在Actor的逻辑无感知的情况下完成游戏行为的扩展:
* 一旦生效,就会为当前已经Spawned的所有该类Actor添加对应的Component,且此后新增的该类Actor也会在创建的时候自动添加对应Component
* 当Game Feature失效,也会对已添加Component的Actor进行处理,解绑之前挂接的Components。
* 由于Actor在DS跟Client上都会存在,且行为并不完全相同,因此这里还提供了选项选择生效时的场景是DS,还是客户端,或者是两者都生效(默认)
* 进行绑定的Component必须来自于当前的Game Feature中,而对应的Actor则没有限制(目前不支持将这个行为绑定到基类Actor上面,不过出于性能考虑,也不应该这样做;如果某个Component需要挂接到多个只有Actor基类作为共同parent的Actor上面,可以考虑手动添加多个绑定来完成)
* 为了让Actor能够接收到这个Action,需要将这类Actor注册到**Game Framework Component Manager**,这个行为一般可以放在Actor的BeginPlay中完成,注册完成后,当Action被触发后,这个Manager就会从所有注册的Actor中筛选出符合条件的完成Component的挂接(我之前猜测通过反射来完成这个功能,现在看来并没有这么复杂)。好奇Game Feature失效时,要如何解绑Component。
代码则是通过如下方式实现:
if (UGameFrameworkComponentManager* ComponentManager = GetGameInstance()->GetSubsystem())
{
ComponentManager->AddReceiver(this);
}
Add Data Registry,这个功能用于向Project添加Data Registries数据(通过一个指向Data Registry Asset的路径完成配置),而Data Registries则是Project中用于实现全局变量存取的功能。
Add Data Registry Source,这个功能用于将Data Tables添加到已有的Data Registries中,这些Data Tables充当上一步中Data Registry的数据来源,这里需要指定每个数据来源的路径,并同时指出这个数据来源会被哪个Data Registry所依赖
Add Abilities,
Add Input Mapping,
Add Level Instance,这个功能允许在Game Feature启动之后,在当前场景里添加另一个场景实例
Add Spawned Actors,
Add World System,
双击刚刚创建的Data Asset,在里面可以完成Action的添加:
如果添加了Action,功能没有正常运转,可能需要重启编辑器,这是因为GameFeatureData是在编辑器启动的时候加载的,而新建的GameFeatureData则没能经历这个过程,从而导致了这个问题。
如果我们想要在Game Feature中访问其他Plugins的内容,就需要添加一些依赖项,确保Game Feature启动之前,对应的Plugins都已经加载到位了:
在使用Game Feature功能的时候,也不是无脑的,需要时不时的思索,这些功能是否适合做成Game Feature,不同逻辑之间该怎么组合,要在架构层面做好设计,才能确保最终的方案不会搞得一团糟。
如果我们希望在运行时按需启动Game Feature,我们可以通过如下方式来做到:
3. Game Feature设计原理分析
Game Feature的加载逻辑是通过UGameFeaturesProjectPolicies控制的,在Project Settings我们可以选择使用哪个UGameFeaturesProjectPolicies来进行控制:
换句话说,我们是可以对UGameFeaturesProjectPolicies进行继承并添加自定义控制逻辑的:
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中传入一个加载过滤函数即可,这个过滤函数的调用逻辑在Engine\Plugins\Experimental\GameFeatures\Source\GameFeatures\Private\GameFeaturesSubsystem.cpp中可以找到:
void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugin(const TSharedRef& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter)
{
// ...
bool bShouldProcess = AdditionalFilter(PluginDescriptorFilename, PluginDetails, BehaviorOptions);
if (bShouldProcess)
{
UGameFeaturePluginStateMachine* StateMachine = GetGameFeaturePluginStateMachine(PluginURL, true);
// ...
}
// ...
}
4. Game Feature使用情景畅想
由于Game Feature可以实现动态的加载与卸载,比如我们可以通过热更将数据塞入到包体中,完成逻辑的添加,同时也可以通过这种方式完成逻辑的移除,而这种行为对于一些只需要短暂存在的功能就有十分重要的作用:
对于一些短暂的活动,如音乐会,我们可以将对应的逻辑与资源通过热更加入包体(服务器也要做同样处理),等这些活动结束之后,再将对应的逻辑移除,从而避免这些临时资源对包体的持续影响(此前的做法就是跟其他资源一并打入包里,需要手动去查询哪些内容已经不再使用了,并移除,费时费力,且容易出错)
对于预研中的游戏而言,如果玩法并不确定,可以将之作为Game Feature来开发,在验证过程中如果发现不合适,可以一键移除,而无需担心对项目的主工程产生影响
多个团队协作的项目,可以通过Game Feature实现互不干扰的开发,当然,这个功能通过Plugins也能做到
-
在运行时对游戏内容进行修改
添加Level Instance,如添加一个具有独立玩法的游戏关卡作为主关卡的子关卡存在
添加Input Mappings,避免在配置文件中进行绑定,而是通过资源进行绑定,从而完成多套输入事件的解耦
添加AI角色,在达成条件时,在游戏中创建对应的AI角色
在添加新功能的时候,可以不再需要经过繁琐的审核流程(不知道苹果会不会禁用这个功能)
提供一些接口,将这个功能暴露出去,供玩家对游戏进行MOD改造
参考
[1]. Game Features and Modular Gameplay
[2]. Modular Game Features in UE5: plug ‘n play, the Unreal Way
[3]. UE5:Game Feature 预研
[4]. Modular Game Features: What you need to know
[5]. Modular Game Features | Inside Unreal