# GameFeatures学习笔记
主要参考了大钊的系列文章:
- 《InsideUE5》GameFeatures架构(二)基础用法 https://zhuanlan.zhihu.com/p/470184973
- 《InsideUE5》GameFeatures架构(三)初始化 https://zhuanlan.zhihu.com/p/473535854
- 《InsideUE5》GameFeatures架构(四)状态机 https://zhuanlan.zhihu.com/p/484763722


- GameFeatures插件实现了Actions执行和GameFeature的装载
- ModularGameplay插件实现AddComponent等操作
  
通过插件创建向导新创建的GameFeature会放在**GameFeatures**文件夹中,也必须放在**GameFeature**文件夹中。并且会创建**GameFeatureData**并且想AssetManager注册。GameFeatures的运行逻辑为:调用ToggleGameFeaturePlugin=>触发GameFeatureAction=>做出对应的操作。
做出对应Action操作的Actor对象需要调用UGameFrameworkComponentManager的AddReceiver()与RemoveReceiver()进行注册与反注册。

- GameFeature,缩写为GF,就是代表一个GameFeature插件。
- CoreGame,特意加上Core,是指的把游戏的本体Module和GameFeature相区分开,即还没有GF发挥作用的游戏本体。
- UGameFeatureData,缩写为GFD,游戏功能的纯数据配置资产,用来描述了GF要做的动作。
- UGameFeatureAction,单个动作,缩写GFA。引擎已经内建了几个Action,我们也可以自己扩展。
- UGameFeaturesSubsystem,缩写为GFS,GF框架的管理类,全局的API都可以在这里找到。父类为UEngineSubsystem。
- UGameFeaturePluginStateMachine,缩写为GFSM,每个GF插件都关联着一个状态机来管理自身的加载卸载逻辑。
- UGameFrameworkComponentManager,缩写为GFCM,支撑AddComponent Action作用的管理类,记录了为哪些Actor身上添加了哪些Component,以便在GF卸载的时候移除掉。

## 初始化
### UGameFeaturesProjectPolicies
GameFeature的加载规则类。UE实现一个了默认类为UDefaultGameFeaturesProjectPolicies。默认是会加载所有的GF插件。如果我们需要自定义自己的策略,比如某些GF插件只是用来测试的,后期要关闭掉,就可以继承重载一个自己的策略对象,在里面可以实现自己的过滤器和判断逻辑。

- GetPreloadAssetListForGameFeature,返回一个GF进入Loading要预先加载的资产列表,方便预载一些资产,比如数据配置表之类的。
- IsPluginAllowed,可重载这个函数来进一步判断某个插件是否允许加载,可以做更细的判断。

可以在项目设置中修改所使用的类。大钊介绍了Additional Plugin Metadata Keys的用法:

>举个例子,假设2.0版本的游戏要禁用掉以前1.0版本搞活动时的一个GF插件,可以继承定义一个我们自己的Policy对象,然后在Init里的过滤器里实现自己的筛选逻辑,比如截图里就示例了根据uplugin里的MyGameVersion键来指定版本号,然后对比。这里要注意的是,**要先在项目设置里配置上Additional Plugin Metadata Keys**,才能把uplugin文件里的自定义键识别解析到PluginDetails.AdditionalMetadata里,才可以进行后续的判断。至于要添加什么键,就看各位自己的项目需要了。

![](https://pic3.zhimg.com/v2-eb32702550e4d8517b1bcdbc08620c8a_r.jpg)
![](https://pic1.zhimg.com/v2-30bb4136f249d6a50ea0121bd53b4c04_r.jpg)

```c++
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的插件
}
```

GF插件的识别是从解析.uplugin文件开始的。我们可以手动编辑这个json文件来详细的描述GF插件。这里有几个键值得一说:
- BuiltInAutoState,这个GF被识别后默认初始的状态,共有4种。如果你设为Active,就代表这个GF就是默认激活的。关于GF的状态我们稍后再详细说说。
- AdditionalMetadata,这个刚才已经讲过了被Policy识别用的。
- PluginDependencies,我们依然可以在GF插件里设置引用别的插件,别的插件可以是普通的插件,也可以是另外的GF插件。这些被依赖的插件会被递归的进行加载。这个递归加载的机制跟普通的插件机制也是一致的。
- ExplicitlyLoaded=true,必须为true,因为GF插件是被显式加载的,不像其他插件可能会默认开启。
- CanContainContent=true,必须为true,因为GF插件毕竟至少得有GFD资产。

![](https://pic1.zhimg.com/v2-96d6ed62d48871fe9f6d1cda3b297af4_r.jpg)

## 状态机
对每一个GF而言,我们在使用的过程中,能够使用到的GF状态就4个:Installed、Registered、Loaded、Active。这4个状态之间可以双向转换以进行加载卸载。

也要注意GF状态的切换流程是一条双向的流水线,可以往加载激活的方向前进,在下图上是用黑色的箭头来表示;也可以往失效卸载的方向走,图上是红色的线表示。而箭头上的文字其实就是一个个GFS类里已经提供的GF加载卸载API。 双向箭头中间的圆角矩形表示的是状态,绿色的状态是我们能看见的,但其实内部还有挺多过渡状态的。过渡状态的概念后面也会解释。值得注意的是,UE5预览版增加了一个Terminal状态,可以把整个插件的内存状态释放掉。
![](https://pic1.zhimg.com/v2-d67731c19467330cff71f51e2698705c_r.jpg)

在LoadBuiltInGameFeaturePlugin的最后一步GFS会为每一个GF创建一个UGameFeaturePluginStateMachine对象,用来管理内部的GF状态切换。
```c++
void UGameFeaturesSubsystem::LoadBuiltInGameFeaturePlugin(const TSharedRef<IPlugin>& Plugin, FBuiltInPluginAdditionalFilters AdditionalFilter)
{
    //...省略其他代码
    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));//更新到目标的初始状态
    }

    //...省略其他代码
}
```
FGameFeaturePluginState为所有状态机的基础结构体,所有派生结构体都会实现所需的虚函数。
```c++
struct FGameFeaturePluginState
{
	FGameFeaturePluginState(FGameFeaturePluginStateMachineProperties& InStateProperties) : StateProperties(InStateProperties) {}
	virtual ~FGameFeaturePluginState();

	/** Called when this state becomes the active state */
	virtual void BeginState() {}

	/** Process the state's logic to decide if there should be a state transition. */
	virtual void UpdateState(FGameFeaturePluginStateStatus& StateStatus) {}

	/** Called when this state is no longer the active state */
	virtual void EndState() {}

	/** Returns the type of state this is */
	virtual EGameFeaturePluginStateType GetStateType() const { return EGameFeaturePluginStateType::Transition; }

	/** The common properties that can be accessed by the states of the state machine */
	FGameFeaturePluginStateMachineProperties& StateProperties;

	void UpdateStateMachineDeferred(float Delay = 0.0f) const;
	void UpdateStateMachineImmediate() const;

	void UpdateProgress(float Progress) const;

private:
	mutable FTSTicker::FDelegateHandle TickHandle;
};
```

初始化状态机时候会创建所有状态(UGameFeaturePluginStateMachine::InitStateMachine())。

### 状态机更新流程
SetDestinationState()=>UpdateStateMachine()

UpdateStateMachine()除了判断与Log之外,主要执行了:
- 调用当前状态的UpdateState()
- 调用当前状态的EndState()
- 调用新状态的BeginState()

#### 检查存在性 
首先GF插件的最初始状态是Uninitialized,很快进入UnknownStatus,标明还不知道该插件的状态。然后进入CheckingStatus阶段,这个阶段主要目的就是检查uplugin文件是否存在,一般的GF插件都是已经在本地的,可以比较快通过检测。
![](https://pic2.zhimg.com/v2-429db98dfa46f7f387de6841aae718d9_r.jpg)

#### 加载CF C++模块
下一个阶段就是开始尝试加载GF的C++模块。这个阶段的初始状态是Installed,这个我用绿色表示,表明它是个目标状态,区分于过渡状态。目标状态意思是在这个状态可以停留住,直到你手动调用API触发迁移到下一个状态,比如你想要注册或激活这个插件,就会把Installed状态向下一个状态转换。卸载的时候往反方向红色的线前进。接着往下:

- Mounting阶段内部会触发插件管理器显式的加载这个模块,因此会加载dll,触发StartupModule。在以前Unmounting阶段并不会卸载C++,因此不会调用ShutdownModule。意思就是C++模块一经加载就常驻在内存中了。但在UE5预览版中,加上了这一步,因此现在Unmounting已经可以卸载掉插件的dll了。
- WaitingForDependencies,会加载之前uplugin里依赖的其他插件模块,递归加载等待所有其他的依赖项完成之后才会进入下一个阶段。这点其实跟普通的插件加载策略是一致的,因此GF插件本质上其实就是以插件的机制在运作,只不过有些地方有些特殊罢了。

![](https://pic1.zhimg.com/v2-823dae0fa911b90c334f860d436b53f8_r.jpg) 

#### 加载GameFeatureData
在C++模块加载完成之后,下一步就要开始把GF自身注册到GFS里面去。其中最重要的一步是在Registering的时候加载GFD资产,会触发UGameFeaturesSubsystem::LoadGameFeatureData(BackupGameFeatureDataPath);的调用从而完成GFD加载。
![](https://pic3.zhimg.com/v2-9abd72961b0ae6b08f24f22276f370ca_r.jpg)

#### 预加载资产和配置
下一个阶段就是进行加载了。Loading阶段会开始加载两种东西,一是插件的运行时的ini(如…/LearnGF/Saved/Config/WindowsEditor/MyFeature.ini)与C++ dll,另外一项是可以预先加载一些资产,资产列表可以由Policy对象根据每个GF插件的GFD文件来获得,因此我们也可以重载GFD来添加我们想要预先加载的资产列表。其他的资产是在激活阶段根据Action的执行按需加载的。
![](https://pic2.zhimg.com/v2-95b5552a06842280cc361cf929b82351_r.jpg)

#### 激活生效
在加载完成之后,我们就可以激活这个GF了。Activating会为每个GFD里定义的Action触发OnGameFeatureActivating,而Deactivating触发OnGameFeatureDeactivating。激活和反激活是Action真正做事的时机。
![](https://pic4.zhimg.com/v2-645173c77bfeb968145f7eb53576e7bf_r.jpg)

### 详细状态切换图
位于GameFeaturePluginStateMachine.h
```c++
/*
                         +--------------+
                         |              |
                         |Uninitialized |
                         |              |
                         +------+-------+
     +------------+             |
     |     *      |             |
     |  Terminal  <-------------~------------------------------
     |            |             |                             |
     +--^------^--+             |                             |
        |      |                |                             |
        |      |         +------v-------+                     |
        |      |         |      *       |                     |
        |      ----------+UnknownStatus |                     |
        |                |              |                     |
        |                +------+-------+                     |
        |                  |                                  |
        |      +-----------v---+     +--------------------+   |
        |      |               |     |         !          |   |
        |      |CheckingStatus <-----> ErrorCheckingStatus+-->|
        |      |               |     |                    |   |
        |      +------+------^-+     +--------------------+   |
        |             |      |                                |
        |             |      |       +--------------------+   |
        ----------    |      |       |         !          |   |
                 |    |      --------> ErrorUnavailable   +----
                 |    |              |                    |
                 |    |              +--------------------+
                 |    |
               +-+----v-------+                            
               |      *       |
     ----------+ StatusKnown  |
     |         |              |
     |         +-^-----+---+--+
     |           |         |
     |     ------~---------~-------------------------
     |     |     |         |                        |
     |  +--v-----+---+   +-v----------+       +-----v--------------+
     |  |            |   |            |       |         !          |
     |  |Uninstalling|   |Downloading <-------> ErrorInstalling    |
     |  |            |   |            |       |                    |
     |  +--------^---+   +-+----------+       +--------------------+
     |           |         |
     |         +-+---------v-+
     |         |      *      |
     ---------->  Installed  |
               |             |
               +-^---------+-+
                 |         |
		   ------~---------~--------------------------------
           |     |         |                               |
        +--v-----+--+    +-v---------+               +-----v--------------+
        |           |    |           |				 |         !          |
        |Unmounting |    | Mounting  <---------------> ErrorMounting      |
        |           |    |           |				 |                    |
        +--^-----^--+    +--+--------+				 +--------------------+
           |     |          |
           ------~----------~-------------------------------
                 |          |                              |
                 |       +--v------------------- +   +-----v-----------------------+
                 |       |                       |	 |         !                   |
                 |       |WaitingForDependencies <---> ErrorWaitingForDependencies |
                 |       |                       |	 |                             |
                 |       +--+------------------- +	 +-----------------------------+
                 |          |
           ------~----------~-------------------------------
           |     |          |                              |
        +--v-----+----+  +--v-------- +              +-----v--------------+
        |             |  |            |				 |         !          |
        |Unregistering|  |Registering <--------------> ErrorRegistering   |
        |             |  |            |				 |                    |
        +--------^----+  ++---------- +				 +--------------------+
                 |        |
               +-+--------v-+
               |      *     |
               | Registered |
               |            |
               +-^--------+-+
                 |        |
        +--------+--+  +--v--------+
        |           |  |           |
        | Unloading |  |  Loading  |
        |           |  |           |
        +--------^--+  +--+--------+
                 |        |
               +-+--------v-+
               |      *     |
               |   Loaded   |
               |            |
               +-^--------+-+
                 |        |
        +--------+---+  +-v---------+
        |            |  |           |
        |Deactivating|  |Activating |
        |            |  |           |
        +--------^---+  +-+---------+
                 |        |
               +-+--------v-+
               |      *     |
               |   Active   |
               |            |
               +------------+
*/
```

## AddComponents
主要逻辑位于ModularGameplay模块中的UGameFrameworkComponentManager。GFCM内部的实现还是蛮复杂和精巧的,可以做到在一个GF激活后,会把激活前已经存在场景中Actor,还有激活后新生成的Actor,都会被正确的添加上Component。这个顺序无关的逻辑是怎么做到的呢?关键的逻辑分为两大部分:

### AddReceiver的注册
核心逻辑位于AddReceiverInternal()。我觉得大钊这里搞错了。

1. 判断Actor是否存在与当前关卡中。
2. 递归这个Actor类的父类,直到AActor。
3. 从TMap<FComponentRequestReceiverClassPath, TSet<UClass*>> ReceiverClassToComponentClassMap中寻找这个类对应的Component UClass* 集。
4. 如果Component类有效则调用CreateComponentOnInstance(),创建并且挂载到Actor上,并将信息放入TMap<UClass*, TSet<FObjectKey>> ComponentClassToComponentInstanceMap中。
5. 查询TMap<FComponentRequestReceiverClassPath, FExtensionHandlerEvent> ReceiverClassToEventMap,并执行委托。

### AddComponents
每个GFA在Activating的时候都会调用OnGameFeatureActivating。而对于UGameFeatureAction_AddComponents这个来说,其最终会调用到AddToWorld。在判断一番是否是游戏世界,是否服务器客户端之类的配置之后。最后真正发生发生的作用是Handles.ComponentRequestHandles.Add(GFCM->AddComponentRequest(Entry.ActorClass, ComponentClass));

AddComponentRequest会增加ReceiverClassToComponentClassMap中的项。
1. 创建FComponentRequest,并添加RequestTrackingMap中的项
2. 获取LocalGameInstance以及World,并且在World中遍历指定的Actor(ReceiverClass)
3. 如果Actor有效则调用CreateComponentOnInstance(),创建并且挂载到Actor上,并将信息放入TMap<UClass*, TSet<FObjectKey>> ComponentClassToComponentInstanceMap中。

值得注意的是:
1. RequestTrackingMap是TMap<FComponentRequest, int32>的类型,可以看到Key是ReceiverClassPath和ComponentClassPtr的组合,那为什么要用int32作为Value来计数呢?这是因为在不同的GF插件里有可能会出现重复的ActorClass-ComponentClass的组合,比如GF1和GF2里都注册了A1-C2的配置。那在卸载GF的时候,我们知道也只有把两个GF1和GF2统统都卸载之后,这个A1-C2的配置才失效,这个时候这个int32计数才=0。因此才需要有个计数来记录生效和失效的次数。
2. ReceiverClassToComponentClassMap会记录ActorClass-多个ComponentClass的组合,其会在上文的AddReceiver的时候被用来查询。
3. 同样会发现根据代码逻辑,ensureMsgf这个报错也只在WITH_EDITOR的时候才生效。在Runtime下,依然会不管不顾的根据GFA里的配置为相应ActorClass类型的所有Actor实例添加Component。因此这个时候我们明白,AddReceiver的调用准确的说不过是为了GF生效后为新Spawn的Actor添加一个依然能添加相应组件的机会。
4. 返回值为何是TSharedPtr<FComponentRequestHandle>?又为何要Add进Handles.ComponentRequestHandles?其实这个时候就涉及到一个逻辑,当GF失效卸载的时候,之前添加的那些Component应该怎么卸载掉?所以这个时候就采取了一个办法,UGameFeatureAction_AddComponents这个Action实例里(不止一个,不同的GF会生成不同的UGameFeatureAction_AddComponents实例)记录着由它创建出来的组件请求,当这个GF被卸载的时候,会触发UGameFeatureAction_AddComponents的析构,继而释放掉TArray<TSharedPtr<FComponentRequestHandle>> ComponentRequestHandles;,而这是个智能指针,只会在最后一个被释放的时候(其实就是最后一个相关的GF被卸载时候),才会触发FComponentRequestHandle的析构,继而在GFCM里真正的移除掉这个ActorClass-ComponentClass的组合,然后在相应的Actor上删除Component实例。

### 一套UGameFrameworkComponent
UE5增加了UPawnComponent,父类为UGameFrameworkComponent(ModularGameplay模块中的文件)。主要是增加了一些Get胶水函数。

#### Component
Component里面一般会写的逻辑有一种是会把Owner的事件给注册进来,比如为Pawn添加输入绑定的组件,会在UActorComponent的OnRegister的时候,把OwnerPawn的Restarted和ControllerChanged事件注册进来监听,以便在合适时机重新应用输入绑定或移除。这里我是想向大家说明,这也是编写Component的一种常用的范式,提供给大家参考。

```c++
void UPlayerControlsComponent::OnRegister()
{
    Super::OnRegister();

    UWorld* World = GetWorld();
    APawn* MyOwner = GetPawn<APawn>();

    if (ensure(MyOwner) && World->IsGameWorld())
    {
        MyOwner->ReceiveRestartedDelegate.AddDynamic(this, &UPlayerControlsComponent::OnPawnRestarted);
        MyOwner->ReceiveControllerChangedDelegate.AddDynamic(this, &UPlayerControlsComponent::OnControllerChanged);

        // If our pawn has an input component we were added after restart
        if (MyOwner->InputComponent)
        {
            OnPawnRestarted(MyOwner);
        }
    }
}
```
#### Actor
- 蓝图中在BeginPlay与EndPlay事件中调用GameFrameworkComponentManager的AddReceiver与RemoveReceived。
- c++ 可以参考《古代山谷》的ModularGameplayActors。里面定义的是一些更Modular的Gameplay基础Actor,重载的逻辑主要有两部分,一是把自己注册给GFCM,二是把自己的一些有用的事件转发给GameFeature的Component,比如截图上的ReceivedPlayer和PlayerTick。
  - ![](https://pic3.zhimg.com/v2-d380d507c096b8b2ce14ab63c330f442_r.jpg)

## 扩展
### Action扩展(
古代山谷额外实现的GFA有:AddAbility、AddInputContextMapping、AddLevelInstance、AddSpawnedActor、AddWorldSystem、WorldActionBase

在实现Action的时候有一个值得注意的地方是,因为GFS是继承于EngineSubsystem的,因此Action的作用机制也是Engine的,因此在编辑器里播放游戏和停止游戏,这些Action其实都是在激活状态的。因此特意有定义了一个WordActionBase的基类,专门注册了OnStartGameInstance的事件,用来在游戏启动的时候执行逻辑。当然在真正用的时候,还需要通过IsGameWorld来判断哪个世界是要发挥作用的世界。

### GameFeatrues依赖机制
普通的插件一般是被CoreGame所引用,用来实现CoreGame的支撑功能。而GameFeature是给已经运行的游戏注入新的功能的,因此CoreGame理论上来说应该对GameFeature的存在和实现一无所知,所以CoreGame就肯定不能引用GF,而是反过来被GF所引用。资产的引用也是同理,GF的资产可以引用CoreGame的,但不能被反过来引用。 

### 可继承扩展的类
- UGameFeaturesProjectPolicies,决定是否加载GF的策略,CoreGame
- UGameFeatureStateChangeObserver,注册到GFS里监听GF状态变化,CoreGame
- GameFeatureData,为GF添加更多描述数据 UGameFeaturesSubsystemSettings::DefaultGameFeatureDataClass ,CoreGame
- GameFeatureAction,更多Action,CoreGame&GF
- GameFrameworkComponent,更多Component, CoreGame&GF
- ModularActor,更多Actor, CoreGame&GF
- IAnimLayerInterface,改变角色动画, CoreGame&GF

### GameFeature模块协作
一些朋友可能会问一个问题,一个游戏玩法用到的模块还是挺多的,GameFeature都能和他们协作进行逻辑的更改吗?下面这个图,我列了一些模块和GameFeature的协作交互方式,大家可以简单看一下。如果有别的模块需要交互,还可以通过增加新的Action类型来实现。

![](https://pic1.zhimg.com/80/v2-31547d8ae95b2ea17018200c90b01e2c_720w.jpg)

### CoreGame预留好逻辑注入点
GameFeature的出现,虽然说大大解耦了游戏玩法和游戏本体之间的联系,CoreGame理论上来说应该对GF一无所知,极致理想情况下,CoreGame也不需要做任何改变。但GF明显还做不到这点,还是需要你去修改CoreGame的一些代码,事先预留好逻辑的注入点。要修改的点主要有这些:

原先的Gameplay Actor修改为从之前的Modular里继承,或者自己手动添加AddReceiver的调用。
在动画方面,可以利用Anim Layer Interface,在GF里可以定义不同的动画蓝图来实现这个接口。之后在组件里Link Anim Class Layers绑定上就可以修改动画了。
关于数据配置,既可以用AddDataRegistrySource添加数据源到CoreGame里现有的DataRegistry里,也可以在GF里用AddDataRegistry新增新的数据注册表。
至于其他的模块部分,例如UI、输入这些,之前已经讲过可以用Action和Component的配合来做。

移植步骤:
创建Plugins/GameFeatures目录
创建.uplugin文件
资产
移动资产到新的GF目录
在原目录Fixup redirectors
修复所有资产验证问题
代码
创建.Build.cs
迁移项目代码到GF下的Public/Private
修复include路径错误
修复代码引用错误
在DefaultEngine.ini里加CoreRedirects +ClassRedirects=(OldName="/Script/MyGame.MyClass", NewName="/Script/MyFeature.MyClass"

### Rethink in GF
- 首先面对一个项目,你需要去思考哪些游戏功能应该可以拆分成GF?哪些是游戏的基本机制?哪些是可以动态开关的功能?就是确定哪些东西应该抽出来形成一个GF。
- Actor逻辑拆分到Component,重新以(组件==功能)的方式来思考,而不是所有逻辑都堆在Actor中。然后这些逻辑,在之前往往是直接写在各种Actor身上,现在你需要重新以(组件==功能)的方式来思考,在以前我们提倡Component是用来实现相对机械的独立于游戏逻辑的基础功能的,现在我们为了适应GameFeature,就需要把一部分Component用来单纯的当做给Actor动态插拔的游戏逻辑实现了。因此你需要把这些逻辑从Actor拆分到Component中去,并做好事件回调注册和转发的相应胶水衔接工作。
- 在Gameplay的各个方面考虑注入逻辑的可能,数据、UI、Input、玩法等。如果你是在做新的项目或者玩法,你在一开始设计的时候就要预想到未来支持GameFeature的可能,留好各种逻辑注入点。
- 进行资产区域划分,哪些是CoreGame,哪些是GameFeature。当然在资产部分,要进行区域划分,哪些是CoreGame用到的资产,哪些是只在Game Feature里使用的资产,各自划分到不同的文件夹里去。
- 考虑GF和CoreGame的通用通信机制,事件总线、分发器、消息队列等。在某些时候,你可能光是利用Action和Component还不够,为了解耦CoreGame和GF,你可能还会需要用到一些通用的通信机制,比如事件总线分发器消息队列之类的。
- PAK和DLC是对游戏主体的补充,GF是动态装载卸载功能,虽然也可通过PAK动态加载。自然的,有些人可能会想到GF跟Pak和dlc有一点点像,是不是可以结合一下呢。这里也注意要辨析一下,pak和dlc是在资产级别的补丁,一般来说是在资产内容上对游戏本体的补充。而GF是在逻辑层面,强调游戏功能的动态开关。但是GF也确实可以打包成一个pak来进行独立的分发下载加载,因此GF也是可以配合热更新来使用的。