GameFeature浅析

一套支持动态增删游戏玩法的框架,往 CoreGameAdd/Remove 其它游戏资源/逻辑;

Lyra(UE5.3) 为例,GameFeature 提供了很好的解耦资源/逻辑的方案,可以看出这里的 ShooterCore 被实现为了一个相对比较独立的 Plugin

image-20240220173041938

初始化

首先在游戏内,GameFeaturesSubsystem 管理着所有的 GameFeature,看到对应的初始化 UGameFeaturesSubsystem::Initialize,可以分为几步:

  1. 加载Policy:根据 GameFeaturesSubsystemSettings->GameFeaturesManagerClassName 加载对应的 GameSpecificPolicies

  2. 注册资源回调:UAssetManager::CallOrRegister_OnAssetManagerCreated

  3. 注册 ConsoleCommand 方便 Debug

在资源加载完的回调后,会走到 UGameFeaturesSubsystem::OnAssetManagerCreated,进行 GameSpecificPolicies->InitGameFeatureManager,根据 Policies 执行初始化逻辑;

这里的 GameFeaturesProjectPolicies 决定了一些 GameFeaturePlugin 的加载规则,比如需要在什么地方(Server/Client) 加载什么 GameFeature,同时可以在这里的 Init 扩展需要的额外功能,比如注册一些 ObserversSubsystems

1
2
3
4
5
6
virtual void InitGameFeatureManager() override;
virtual void ShutdownGameFeatureManager() override;
virtual TArray<FPrimaryAssetId> GetPreloadAssetListForGameFeature(const UGameFeatureData* GameFeatureToLoad, bool bIncludeLoadedAssets = false) const override;
virtual bool IsPluginAllowed(const FString& PluginURL) const override;
virtual const TArray<FName> GetPreloadBundleStateForGameFeature() const override;
virtual void GetGameFeatureLoadingMode(bool& bLoadClientData, bool& bLoadServerData) const override;

UDefaultGameFeaturesProjectPolicies::InitGameFeatureManager 中会执行 UGameFeaturesSubsystem::Get().LoadBuiltInGameFeaturePlugins(AdditionalFilter),根据 Filter 来加载 GameFeaturePlugin,可以发现,会进行状态机 GameFeaturePluginStateMachine 的初始状态设置并初始化。

状态机的底层的数据都存在 GameFeatureData 上,可以从这里面开始分析:

image-20240220173210508

(需要注意的是,GameFeatureData 本身只记录了 ActionsPrimaryAssetTypesToScan,这里的 FeatureState 对应的是 GameFeaturePlugin 的对应状态机的信息,由 FGameFeaturesEditorModule::StartModule -> FGameFeatureDataDetailsCustomization::CustomizeDetails 注册进 Details 面板)

状态机

GameFeature 由一个 UGameFeaturesSubsystem : UEngineSubsystem 管理其生命周期:

classDiagram
    class UGameFeaturesSubsystem {
        GameFeaturePluginStateMachines : TMap[GameFeature, StateMachine] 
        ListGameFeaturePlugins()
        LoadGameFeaturePlugin()
        DeactivateGameFeaturePlugin()
        UnloadGameFeaturePlugin()
        ReleaseGameFeaturePlugin()
        UninstallGameFeaturePlugin()
        TerminateGameFeaturePlugin()
    }
    
    UGameFeaturesSubsystem..>UGameFeaturePluginStateMachine
class UGameFeaturePluginStateMachine {
        AllStates : TUniquePtr[FGameFeaturePluginState] 
        CurrentStateInfo : FGameFeaturePluginStateInfo
        SetDestination()
    }
    
    UGameFeaturePluginStateMachine..>FGameFeaturePluginState
class FGameFeaturePluginState {
        TickHandle : FTSTicker-FDelegateHandle 
        BeginState()
        UpdateState()
        TryCancelState()
        EndState()
    }
    
    UGameFeaturePluginStateMachine..>FGameFeaturePluginStateInfo
class FGameFeaturePluginStateInfo {
    	State : EGameFeaturePluginState
    }

最基本的有 LoadDeactivateUnloadReleaseUninstallTerminate 几个功能,可以由外部调用。

核心内容围绕着每个 GameFeaturePlugin 对应的 StateMachine 展开;GameFeatureSubSystem 上记录着 TMap<FString, TObjectPtr<UGameFeaturePluginStateMachine>> GameFeaturePluginStateMachines ,也就是 PluginIdentifier->StateMachine 的多个映射。

最后都会走到 UGameFeaturesSubsystem::ChangeGameFeatureDestination 修改状态机的状态:

1
2
3
4
5
6
7
8
9
10
11
12
void ChangeGameFeatureDestination(UGameFeaturePluginStateMachine* Machine, const FGameFeaturePluginStateRange& StateRange, FGameFeaturePluginChangeStateComplete CompleteDelegate);

// 暴露给外部的主要目标状态
enum class EGameFeatureTargetState : uint8
{
Installed,
Registered,
Loaded,
Active,
Count UMETA(Hidden)
};
void ChangeGameFeatureTargetState(const FString& PluginURL, EGameFeatureTargetState TargetState, const FGameFeaturePluginChangeStateComplete& CompleteDelegate);

GameFeaturePluginStateMachine

主要维护对应 GameFeaturePlugin 的状态。

最重要的设置状态入口是 UGameFeaturePluginStateMachine::SetDestination

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// UE5.0: 传入的目标状态为一个 GameFeaturePluginState
void UGameFeaturePluginStateMachine::SetDestinationState(EGameFeaturePluginState InDestinationState, FGameFeatureStateTransitionComplete OnFeatureStateTransitionComplete)
{
check(IsValidDestinationState(InDestinationState));

// TODO: If we aren't in a destination state and our new destination is in the opposite direction of
// our current destination, cancel the current state transition (if possible)
// The completion delegate may be stomped in these cases. Should probably callback with a cancelled error

StateProperties.DestinationState = InDestinationState;
StateProperties.OnFeatureStateTransitionComplete = OnFeatureStateTransitionComplete;

UpdateStateMachine();
}

// UE5.1-5.3:将传入的状态改为了一个 FGameFeaturePluginStateRange(MinState, MaxState)
bool UGameFeaturePluginStateMachine::SetDestination(FGameFeaturePluginStateRange InDestination, FGameFeatureStateTransitionComplete OnFeatureStateTransitionComplete, FDelegateHandle* OutCallbackHandle /*= nullptr*/)

可以发现 StateMachine 维护了一个 FGameFeaturePluginStateMachineProperties& StateProperties,记录了状态机运行中可以切换到的状态的一些属性,其中的 DestinationState 表示在状态变化中期望到达的状态(UE5.3 改为了状态区间,为了适配 Terminal 以及状态转移中不丢失对应的 CompletionHandle

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
36
37
/** 当前状态是否不在 Destination 内,不在的话说明需要尝试到一个新的 Destination,标记为 IsRunning */
bool IsRunning() const
{
return !StateProperties.Destination.Contains(CurrentStateInfo.State);
}


// 已经在 Destination 内,那么可以尝试到任意的 Destination
if (!IsRunning())
{
if (InDestination.Contains(CurrentStateInfo.State)) return true;
StateProperties.Destination = InDestination;
UpdateStateMachine();
return true;
}

// 不在 Destination 内,说明状态还在过渡状态,这时如果收到了新的 Destination,为了保证原有状态可以顺利过渡,与原有的 Destination 求交,继续尝试切换状态
if (TOptional<FGameFeaturePluginStateRange> NewDestination = StateProperties.Destination.Intersect(InDestination))
{
// The machine is already running so we can only transition to this range if it overlaps with our current range.
// We can satisfy both ranges in this case.

if (CurrentStateInfo.State < StateProperties.Destination)
{
StateProperties.Destination = *NewDestination;

if (InDestination.Contains(CurrentStateInfo.State)) return true;
}
else if(CurrentStateInfo.State > StateProperties.Destination)
{
StateProperties.Destination = *NewDestination;

if (InDestination.Contains(CurrentStateInfo.State)) return true;
}

return true;
}

FeaturePluginState

内部维护了很多状态:

image-20240221105036685

image-20240220225411173

可以被作为DestinationState的状态有:UnknownStatusUninstalledStatusKnownInstalledRegisteredLoadActiveTerminal、其它ErrorStates

状态间的关系如图所示:

flowchart LR

Uninitialized-->*UnknownStatus

*UnknownStatus-->CheckingStatus
*UnknownStatus-->*Terminal

CheckingStatus-->*StatusKnown

*StatusKnown-->*Installed
*StatusKnown-->Downloading
*StatusKnown-->Uninstalling
*StatusKnown-->*Terminal

Downloading-->*Installed

Uninstalling-->*Uninstalled

*Uninstalled-->*Terminal
*Uninstalled-->CheckingStatus

Releasing-->*StatusKnown

*Installed-->Mounting
*Installed-->Releasing

Mounting-->WaitingForDependencies

Unmounting-->*Installed

WaitingForDependencies-->Registering

Registering-->*Registered

*Registered-->Unregistering
*Registered-->Loading

Unregistering-->Unmounting

Loading-->*Loaded

*Loaded-->Activating
*Loaded-->Unloading

Unloading-->*Registered

Activating-->*Active

*Active-->Deactivating

Deactivating-->*Loaded

状态修改完会通过UGameFeaturesSubsystem::Get().OnGameFeature... 进行通知,GameFeatureSubSystem 会调用到 Actions 的回调,同时提供了一个 CallbackObservers 通知已注册的 Observers

1
2
3
4
5
6
7
8
9
10
11
12
enum class EObserverCallback
{
CheckingStatus,
Terminating,
Registering,
Unregistering,
Loading,
Activating,
Deactivating,
PauseChanged,
Count
};

Actions

对于一个 GameFeature ,在 RegisterUnregisterLoadActivateDeactive 等行为时(也可以自己添加)会调用到 GameFeatureData 上配置的 Actions 的对应回调:

image-20240221155727741 image-20240221155957459

可以自定义各种各样的 Actions

image-20240221155442578

GameFeatureAction 中有一个关键 UGameFeatureAction_AddComponents ,其主要是将 Component 注册进对应的 Actor,应用到了另一个模块 ModularGameplay

AddComponent 通过 ModularGameplayUGameFrameworkComponentManager::AddComponentRequest 实现,简单介绍一下 ModularGameplay

ModularGameplay

一套往 Actor 上注册 Component/ExtensionHandler 的解决方案。

这套框架核心由一个 UGameFrameworkComponentManager : USubSystem 在全局管理各个 ActorClassComponent/ExtensionHandler,维护 Component 的生命周期,并且提供接口供外部调用(Add/Remove/SendEvent 等)

ActorReceiver

注册 Receiver

首先将需要使用这套框架维护 ComponentActorClass 作为 Receiver ,通过 UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver 注册进 Manager

同时,在注册的时候,会判断 ActorClass 是否已经提前注册的 ComponentClass 或者 EventDelegate ,如果有 ComponentClass 则会在此时创建对应实例 CreateComponentOnInstance,有 EventDelagte 则会通知一下 NAME_ReceiverAdded

(一般在 AActor::PreInitializeComponents 调用)

移除 Receiver

通过 UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver 移除;

调用对应 Component 的销毁,通知一下 NAME_ReceiverRemoved

(一般在 AActor::EndPlay 调用)

Component

classDiagram
    class UGameFrameworkComponentManager 
    UGameFrameworkComponentManager  : ReceiverClassToComponentClassMap
    UGameFrameworkComponentManager : AddComponentRequest()
    UGameFrameworkComponentManager : RemoveComponentRequest()

维护了一个 ActorClass->ComponentClass ,也就是 TMap<FComponentRequestReceiverClassPath, TSet<TObjectPtr<UClass>>> ReceiverClassToComponentClassMap,(其中 FComponentRequestReceiverClassPath 是一个结构体,用字符串数组记录 Class->Root 这条链路,作为 Class 的标识,不直接用 UClass 是因为以 Component 的视角进行操作,不期望在这里直接依赖 Receiver 对应模块的指针)

注册 Component

通过 UGameFrameworkComponentManager::AddComponentRequest 实现;

这里的 CompoentRequest 也就是一个 FComponentRequestHandle(OwningManager, ReceiverClass, ComponentClass)

内部维护了一个 RequestTrackingMap 用于 CompoentRequest 的计数,如果 ==1 则说明需要尝试创建实例;

(有可能不同的模块都依赖对应的 ComponentClass ,所以需要计数一下)

注册的时候判断对应的 Actor 是否已经 Initialize ,如果是,则 CreateComponentOnInstance

CreateComponentOnInstance:创建 ComponentInstance ,维护 TMap<UClass*, TSet<FObjectKey>> ComponentClassToComponentInstanceMap 用于记录这个 ComponentClass 有哪些对应的 Instance

移除 Component

通过 UGameFrameworkComponentManager::RemoveComponentRequest 实现;

修改计数,==0 则执行销毁。

销毁的时候通过 ComponentClassToComponentInstanceMap 找到所有的 Component,判断对应的 Actor 是否是期望的,如果是,则销毁;

ExtensionHandler

Component 类似,也就是注册 Event 的部分;

classDiagram
    class UGameFrameworkComponentManager 
	UGameFrameworkComponentManager  : ReceiverClassToEventMap
    UGameFrameworkComponentManager : AddExtensionHandler()
    UGameFrameworkComponentManager : RemoveExtensionHandler()
    UGameFrameworkComponentManager : SendExtensionEvent()

资源加载

image-20240221163647066

FGameFeaturePluginState_Registering 状态时,会根据配置的 GameFeatureToAdd->GetPrimaryAssetTypesToScan(),执行对应的 UGameFeaturesSubsystem::AddGameFeatureToAssetManager,进行资源是否存在等判定之后,尝试加载资源到 AssetManager

类似的,在 FGameFeaturePluginState_Unregistering 状态时,通过 UGameFeaturesSubsystem::RemoveGameFeatureFromAssetManager 进行资源卸载。

参考

Lyra (UE5.0-5.3) GameFeature 部分源码

UE5:Game Feature 预研

《InsideUE5》GameFeatures架构