【UE】Unreal Game Feature浅析

人的痛苦来自于想要表现出自己不具备的能力,因此当你在工作中感到焦躁烦闷的时候,正视自己的不足,接受自身的微渺,实事求是,脚踏实地,或许能让你快速找回属于自己的节奏。

近日在工作中了解到UE5推出了一个叫做Game Feature(以下简称GF)的新特性,粗看之下这个特性似乎跟Plugin有点相似,但深入了解之后发现两者存在较大的不同。

GF跟Plugin都可以用于实现功能的解耦,对于程序同学来说,这是一种能够在架构上产生深远影响的特性,值得投入时间去理解学习,因此这里单开一篇来对GF一窥究竟。

在深入之前,我们先设置一些问题,带着问题我们来对其进行深入探究:

  1. GF跟Plugin有什么不同,其定位是什么?

  2. GF在日常工作中要如何使用,结合案例来进行梳理与分析。

  3. GF的实现原理是什么,这样的设计有怎样的深意,其中是否存在一些不足,该如何改进与优化?

  4. 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?",可以通过两种方式来解决:

    1. 点击"Add entry to PrimaryAssetTypesToScan?",这个会自动对DefaultEngine.ini文件进行修改

    2. 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

你可能感兴趣的:(【UE】Unreal Game Feature浅析)