引言
上篇基础用法的文章其实是有点水的,当然这绝对不是因为我懒,而是因为GameFeatures的用法就是如此的简单,嗯,一定是这样! 我个人的习惯是常常会花巨量的篇幅在尝试努力解释清楚一个事物背后的概念和设计动机,继而再努力尝试去推演出应该如何正确的使用它,最佳姿势是什么。如果文章只是在对着API描绘这个是干什么用的,而不去解释背后的Why,那就只是浮于表层的了解。而我相信只有在深层的机制原理理解之上,才有足够的信心能够用好一个模块。希望一起学习一起共勉。
核心概念
在解释各种机制之前,,还是得向简明扼要的梳理一遍会遇见的各种关键类和名词术语缩写,一是避免歧义,二当然是为了省事,缩写比较方便。
- GameFeature,缩写为GF,就是代表一个GameFeature插件。
- CoreGame,特意加上Core,是指的把游戏的本体Module和GameFeature相区分开,即还没有GF发挥作用的游戏本体。
- UGameFeatureData,缩写为GFD,游戏功能的纯数据配置资产,用来描述了GF要做的动作。
- UGameFeatureAction,单个动作,缩写GFA。引擎已经内建了几个Action,我们也可以自己扩展。
- UGameFeatureSubsystem,缩写为GFS,GF框架的管理类,全局的API都可以在这里找到。
- UGameFeaturePluginStateMachine,缩写为GFSM,每个GF插件都关联着一个状态机来管理自身的加载卸载逻辑。
- UGameFrameworkComponentManager,缩写为GFCM,支撑AddComponent Action作用的管理类,记录了为哪些Actor身上添加了哪些Component,以便在GF卸载的时候移除掉。
现在如果对这些概念是云里雾里也没有关系,后文会细细讲述。此时只要记住各个缩写代表的是什么东西就行了。
GameFeatures 简易理解
我知道到这里,可能还是会有朋友对这个框架运行机制有些模糊。这里我画了一张简易的示意图。用简单的话来说,就是创建不同的GF,每个GF里都有一个同名的GFD,每个GFD描述了这个GF要执行的动作列表。我们可以扩充这个Action以符合我们的项目需要。 我们的操作是在各GF的GFD里配置上各种Action。GF激活的时候,会执行Action::OnGameFeatureActivating();GF取消激活的时候,执行Action:: OnGameFeatureDeactivating()。因此一个GFA典型的动作发生在OnGameFeatureActivating和OnGameFeatureDeactivating这两个回调上,正如BeginPlay和EndPlay相对于Actor的作用一样。前文也说过,我们可以继承Action下来进行自定义扩展,GFA中最重要的一个就是AddComponents,其重要性大概会占整个GFA体系的一半,因此后面也会花更多篇章来解释。
GameFeatures 初始化
每次在讲解一个新模块的时候,总会感觉初始的时候有点千头万绪难以掐头入手。但久病成医,也渐渐摸索出一些门路。在以往的文章里也曾经说过,可以以时空的视角,从时间和空间的两个维度来理解一个系统。在时间上关心最开始是什么样的、中间经过哪些步骤、最后是什么结果;在空间上关心定义了哪些数据、数据是怎么被配置和修改的、数据流向哪里。如果能理清这二者的作用反应过程,也就差不多理解了其架构和逻辑流转方式。因此对于GameFeatures也是如此,想要理解它的机制,首先要理解它是怎么从无到有一步步初始化起来的。
一,最初初始化
GameFeature框架的使用过程其实涉及到了有好几个类,首先框架的核心管理类是UGameFeaturesSubsystem,它是继承自EngineSubsystem,这意味着它是跟随着引擎启动的。在Runtime的时候无所谓,但是在Editor情况下,我们要注意,UGameFeaturesSubsystem内部管理的GF状态是跟着引擎编辑器一起的。因此即使你停止PIE播放,GF插件也依然是保持加载状态的。因此在实践中,我会推崇大家自己手动在游戏中通过API来激活或反激活GF,而不是利用编辑器上的UI按钮。
最先开始运行的当然是重载的Subsystem的Initialize方法调用。
void UGameFeaturesSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
const FSoftClassPath& PolicyClassPath = GetDefault<UGameFeaturesSubsystemSettings>()->GameFeaturesManagerClassName;
UClass* SingletonClass = nullptr;
if (!PolicyClassPath.IsNull())
{
SingletonClass = LoadClass<UGameFeaturesProjectPolicies>(nullptr, *PolicyClassPath.ToString());
}
if (SingletonClass == nullptr)
{
SingletonClass = UDefaultGameFeaturesProjectPolicies::StaticClass();
}
GameSpecificPolicies = NewObject<UGameFeaturesProjectPolicies>(this, SingletonClass);//创建策略对象
//注册UAssetManager回调,以便之后配置GFD的PrimaryAssetType
UAssetManager::CallOrRegister_OnAssetManagerCreated(FSimpleMulticastDelegate::FDelegate::CreateUObject(this, &ThisClass::OnAssetManagerCreated));
//注册控制台命令,有:istGameFeaturePlugins,LoadGameFeaturePlugin,DeactivateGameFeaturePlugin,UnloadGameFeaturePlugin
IConsoleManager::Get().RegisterConsoleCommand(...);
}
注意:GF状态在编辑器模式下依然存续,因此可能会跨游戏依然存在,要记得自己取消激活状态。
二,创建GF加载策略
如上文代码所述,Subsystem内部会创建一个Policy策略对象来负责决定哪些GF应该加载,哪些是被禁用的,也可用来决定在服务端和客户端是否应该允许加载某些插件。 引擎已经内建实现了一个默认的策略对象UDefaultGameFeaturesProjectPolicies,默认是会加载所有的GF插件。如果我们需要自定义自己的策略,比如某些GF插件只是用来测试的,后期要关闭掉,就可以继承重载一个自己的策略对象,如UMyGameFeaturesProjectPolicies。在里面可以实现自己的过滤器和判断逻辑。
三,配置GF加载策略
一般的应用场景是,根据自己游戏的需要灵活的定义额外的配置项,比如游戏版本、补丁修复、游戏活动、AB测试等。举个例子,假设2.0版本的游戏要禁用掉以前1.0版本搞活动时的一个GF插件,可以继承定义一个我们自己的Policy对象,然后在Init里的过滤器里实现自己的筛选逻辑,比如截图里就示例了根据uplugin里的MyGameVersion键来指定版本号,然后对比。这里要注意的是,要先在项目设置里配置上Additional Plugin Metadata Keys,才能把uplugin文件里的自定义键识别解析到PluginDetails.AdditionalMetadata里,才可以进行后续的判断。至于要添加什么键,就看各位自己的项目需要了。
在GF.uplugin文件里可以加上自定义的项:
在代码里获取项的值进行判断:
UCLASS()
class LEARNGF_API UMyGameFeaturesProjectPolicies : public UGameFeaturesProjectPolicies
{
GENERATED_BODY()
public:
//~UGameFeaturesProjectPolicies interface
virtual void InitGameFeatureManager() override;
virtual void GetGameFeatureLoadingMode(bool& bLoadClientData, bool& bLoadServerData) const override;
virtual TArray<FPrimaryAssetId> GetPreloadAssetListForGameFeature(const UGameFeatureData* GameFeatureToLoad) const { return TArray<FPrimaryAssetId>(); }
virtual bool IsPluginAllowed(const FString& PluginURL) const { return true; }
//~End of UGameFeaturesProjectPolicies interface
};
//自定义的写法,和UDefaultGameFeaturesProjectPolicies的默认写法差不多
void UMyGameFeaturesProjectPolicies::InitGameFeatureManager()
{
auto AdditionalFilter = [&](const FString& PluginFilename, const FGameFeaturePluginDetails& PluginDetails, FBuiltInGameFeaturePluginBehaviorOptions& OutOptions) -> bool
{ //可以自己写判断逻辑
if (const FString* myGameVersion = PluginDetails.AdditionalMetadata.Find(TEXT("MyGameVersion")))
{
float verison = FCString::Atof(**myGameVersion);
if (verison > 2.0)
{
return true;
}
}
return false;
};
UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugins(AdditionalFilter); //加载内建的所有可能为GF的插件
}
再来说一下Policy对象里面比较重要的几个方法:
- GetPreloadAssetListForGameFeature,返回一个GF进入Loading要预先加载的资产列表,方便预载一些资产,比如数据配置表之类的。
- IsPluginAllowed,可重载这个函数来进一步判断某个插件是否允许加载,可以做更细的判断。
四,GFS设置 - UGameFeaturesSubsystemSettings
关于项目设置里GF的设置,是由UGameFeaturesSubsystemSettings对象来定义的。里面目前有两个键比较重要:
- GameFeaturesManagerClassName,可以自定义Policy子类,上文已经讲述过了。
- AdditionalPluginMetadataKeys,只有在这里面定义的配置项才会从uplugin里被解析。
当然里面也有别的键可以自己配置,甚至可以自己扩展新的配置项。
五,GFD Asset Manager 配置
在创建完Policy对象之后,Subsystem会开始在内部配置AssetManager来识别GFD对象。GameFeature强烈依赖AM来识别加载资产,因此AssetManager的配置里一定要添加GFD的主资产类型,此项一般来说在创建完GF后默认就已配置好。但如果你是手动迁移的GF插件到一个新项目,就要好好检查一下了。
只有配置了上述选项,才能在后续正确的加载该GFD资产:
如果深究代码,体现该逻辑的地方就在于,但是也请注意LoadGameFeatureData是在后续GF的状态机Registered的时候加载,这个时候提前亮相只是说明跟之后的关联。而且只有通过Policy的Filters的GF插件,之后才有机会被尝试加载。
TSharedPtr<FStreamableHandle> UGameFeaturesSubsystem::LoadGameFeatureData(const FString& GameFeatureToLoad)
{
UAssetManager& LocalAssetManager = UAssetManager::Get();
IAssetRegistry& LocalAssetRegistry = LocalAssetManager.GetAssetRegistry();
FAssetData GameFeatureAssetData = LocalAssetRegistry.GetAssetByObjectPath(FName(*GameFeatureToLoad));
if (GameFeatureAssetData.IsValid())
{
FPrimaryAssetId AssetId = GameFeatureAssetData.GetPrimaryAssetId();
// Add the GameFeatureData itself to the primary asset list
LocalAssetManager.RegisterSpecificPrimaryAsset(AssetId, GameFeatureAssetData); //如果之前没配置,就会失败
// LoadPrimaryAsset will return a null handle if the AssetID is already loaded. Check if there is an existing handle first.
TSharedPtr<FStreamableHandle> ReturnHandle = LocalAssetManager.GetPrimaryAssetHandle(AssetId);
if (ReturnHandle.IsValid())
{
return ReturnHandle;
}
else
{
return LocalAssetManager.LoadPrimaryAsset(AssetId);
}
}
return nullptr;
}
六,加载解析GF.uplugin
在上文的UDefaultGameFeaturesProjectPolicies::InitGameFeatureManager()
最后一步是UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugins(AdditionalFilter);
,会遍历当前项目Plugins目录下所有插件,然后尝试加载其中的GF插件:
void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugins(FBuiltInPluginAdditionalFilters AdditionalFilter)
{
TArray<TSharedRef<IPlugin>> EnabledPlugins = IPluginManager::Get().GetEnabledPlugins();
for (const TSharedRef<IPlugin>& Plugin : EnabledPlugins)//遍历所有插件
{
LoadBuiltInGameFeaturePlugin(Plugin, AdditionalFilter);
}
}
而LoadBuiltInGameFeaturePlugin正是加载GF插件最关键的函数:
void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugin(const TSharedRef<IPlugin>& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter)
{
const FString& PluginDescriptorFilename = Plugin->GetDescriptorFileName();
if (!PluginDescriptorFilename.IsEmpty() && FPaths::ConvertRelativePathToFull(PluginDescriptorFilename).StartsWith(GetDefault<UGameFeaturesSubsystemSettings>()->BuiltInGameFeaturePluginsFolder) && FPaths::FileExists(PluginDescriptorFilename))//写死了GF必须待在“GameFeatures”目录下
{
const FString PluginURL = TEXT("file:") + PluginDescriptorFilename;
if (GameSpecificPolicies->IsPluginAllowed(PluginURL)) //Policy是否允许该插件
{
FGameFeaturePluginDetails PluginDetails;
if (GetGameFeaturePluginDetails(PluginDescriptorFilename, PluginDetails))//读取json的uplugin文件信息
{
FBuiltInGameFeaturePluginBehaviorOptions BehaviorOptions;
bool bShouldProcess = AdditionalFilter(PluginDescriptorFilename, PluginDetails, BehaviorOptions);//进行过滤器判定
if (bShouldProcess)
{
UGameFeaturePluginStateMachine* StateMachine = GetGameFeaturePluginStateMachine(PluginURL, true);//创建状态机
const EBuiltInAutoState InitialAutoState = (BehaviorOptions.AutoStateOverride != EBuiltInAutoState::Invalid) ? BehaviorOptions.AutoStateOverride : PluginDetails.BuiltInAutoState;
const EGameFeaturePluginState DestinationState = ConvertInitialFeatureStateToTargetState(InitialAutoState);
if (StateMachine->GetCurrentState() >= DestinationState)
{
// If we're already at the destination or beyond, don't transition back
LoadGameFeaturePluginComplete(StateMachine, MakeValue());
}
else
{
StateMachine->SetDestinationState(DestinationState, FGameFeatureStateTransitionComplete::CreateUObject(this, &ThisClass::LoadGameFeaturePluginComplete));//更新到目标的初始状态
}
if (!GameFeaturePluginNameToPathMap.Contains(Plugin->GetName()))
{
GameFeaturePluginNameToPathMap.Add(Plugin->GetName(), PluginURL);
}
}
}
}
}
}
在通过Policy的初步只根据文件名判定之后,就可以进行下一步的插件信息加载解析。只有解析了GF.uplugin之后,才有足够的键值信息来判断这个GF插件是否应该加载或者是禁用掉。GF插件的识别是从解析.uplugin文件开始的。我们可以手动编辑这个json文件来详细的描述GF插件。这里有几个键值得一说:
- BuiltInAutoState,这个GF被识别后默认初始的状态,共有4种。如果你设为Active,就代表这个GF就是默认激活的。关于GF的状态我们稍后再详细说说。
- AdditionalMetadata,这个刚才已经讲过了被Policy识别用的。
- PluginDependencies,我们依然可以在GF插件里设置引用别的插件,别的插件可以是普通的插件,也可以是另外的GF插件。这些被依赖的插件会被递归的进行加载。这个递归加载的机制跟普通的插件机制也是一致的。
- ExplicitlyLoaded=true,必须为true,因为GF插件是被显式加载的,不像其他插件可能会默认开启。
- CanContainContent=true,必须为true,因为GF插件毕竟至少得有GFD资产。
这个json文件可以手动编辑:
初始化流程梳理
整个GF框架初始化的代码的逻辑梳理成图就是:
思考:为何Policy对象要有IsPluginAllowed和AdditionalFilter的两遍过滤?
这个问题比较简单,因为初始的IsPluginAllowed判定只是根据PluginURL即插件的uplugin文件路径来进行的判定,依据的信息只是简单的文件名。如果在此步就可以过滤掉想禁用的GF插件,就可以避免后续的GF.uplugin文件加载解析了,提高一点点性能。当然对于更加细致化的GF插件管理策略(如游戏版本、活动信息等),这些信息就只能存在uplugin的自定义键值对里了。就只能等到读取插件信息之后,才能进行AdditionalFilter的判断。
总结
本篇主要是讲述了GF框架在启动的时候一些初始化流程步骤,和判断一个GF插件是否应该加载的策略。我们也注意到在LoadBuiltInGameFeaturePlugin的最后一步是为每个GF插件创建关联一个状态机。GF状态机是管理GF加载流程最核心的一个流程和概念,我们在下篇讲述。