377 lines
27 KiB
Markdown
377 lines
27 KiB
Markdown
|
# 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里,才可以进行后续的判断。至于要添加什么键,就看各位自己的项目需要了。
|
|||
|
|
|||
|

|
|||
|

|
|||
|
|
|||
|
```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资产。
|
|||
|
|
|||
|

|
|||
|
|
|||
|
## 状态机
|
|||
|
对每一个GF而言,我们在使用的过程中,能够使用到的GF状态就4个:Installed、Registered、Loaded、Active。这4个状态之间可以双向转换以进行加载卸载。
|
|||
|
|
|||
|
也要注意GF状态的切换流程是一条双向的流水线,可以往加载激活的方向前进,在下图上是用黑色的箭头来表示;也可以往失效卸载的方向走,图上是红色的线表示。而箭头上的文字其实就是一个个GFS类里已经提供的GF加载卸载API。 双向箭头中间的圆角矩形表示的是状态,绿色的状态是我们能看见的,但其实内部还有挺多过渡状态的。过渡状态的概念后面也会解释。值得注意的是,UE5预览版增加了一个Terminal状态,可以把整个插件的内存状态释放掉。
|
|||
|

|
|||
|
|
|||
|
在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插件都是已经在本地的,可以比较快通过检测。
|
|||
|

|
|||
|
|
|||
|
#### 加载CF C++模块
|
|||
|
下一个阶段就是开始尝试加载GF的C++模块。这个阶段的初始状态是Installed,这个我用绿色表示,表明它是个目标状态,区分于过渡状态。目标状态意思是在这个状态可以停留住,直到你手动调用API触发迁移到下一个状态,比如你想要注册或激活这个插件,就会把Installed状态向下一个状态转换。卸载的时候往反方向红色的线前进。接着往下:
|
|||
|
|
|||
|
- Mounting阶段内部会触发插件管理器显式的加载这个模块,因此会加载dll,触发StartupModule。在以前Unmounting阶段并不会卸载C++,因此不会调用ShutdownModule。意思就是C++模块一经加载就常驻在内存中了。但在UE5预览版中,加上了这一步,因此现在Unmounting已经可以卸载掉插件的dll了。
|
|||
|
- WaitingForDependencies,会加载之前uplugin里依赖的其他插件模块,递归加载等待所有其他的依赖项完成之后才会进入下一个阶段。这点其实跟普通的插件加载策略是一致的,因此GF插件本质上其实就是以插件的机制在运作,只不过有些地方有些特殊罢了。
|
|||
|
|
|||
|

|
|||
|
|
|||
|
#### 加载GameFeatureData
|
|||
|
在C++模块加载完成之后,下一步就要开始把GF自身注册到GFS里面去。其中最重要的一步是在Registering的时候加载GFD资产,会触发UGameFeaturesSubsystem::LoadGameFeatureData(BackupGameFeatureDataPath);的调用从而完成GFD加载。
|
|||
|

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

|
|||
|
|
|||
|
#### 激活生效
|
|||
|
在加载完成之后,我们就可以激活这个GF了。Activating会为每个GFD里定义的Action触发OnGameFeatureActivating,而Deactivating触发OnGameFeatureDeactivating。激活和反激活是Action真正做事的时机。
|
|||
|

|
|||
|
|
|||
|
### 详细状态切换图
|
|||
|
位于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。
|
|||
|
- 
|
|||
|
|
|||
|
## 扩展
|
|||
|
### 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类型来实现。
|
|||
|
|
|||
|

|
|||
|
|
|||
|
### 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也是可以配合热更新来使用的。
|