深入UE5——GameFeatures架构(四)状态机

引言

前文讲过GF框架在启动时候初始化的流程,也提过GF状态机是管理GF加载流程最核心的一个流程和概念。本篇就将仔细的阐述这一个模块,关于一个GF插件是如何被加载和激活的,理解GF的关键之处就在于理解GF插件的状态机。希望大家乖乖坐好认真听讲。

大局概念

说到状态,对每一个GF而言,我们在使用的过程中,能够使用到的GF状态就4个:Installed、Registered、Loaded、Active。这4个状态之间可以双向转换以进行加载卸载。我知道到现在可能大家依然对这4个状态的区别很模糊,搞不清Installed和Loaded到底是有什么区别。不用急,后文都会解释。

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

状态机创建

回顾一下前文代码,在LoadBuiltInGameFeaturePlugin的最后一步GFS会为每一个GF创建一个UGameFeaturePluginStateMachine对象,用来管理内部的GF状态切换。

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));//更新到目标的初始状态
    }

    //...省略其他代码
}

当然状态机创建的过程也肯定要经过更细致的步骤,最后一步InitStateMachine其实就是为AllStates这个数组里每个元素创建状态对象,有兴趣的朋友可以去看一下代码,比较简单就不赘述了。

UGameFeaturePluginStateMachine* UGameFeaturesSubsystem::GetGameFeaturePluginStateMachine(const FString& PluginURL, bool bCreateIfItDoesntExist)
{
    UGameFeaturePluginStateMachine** ExistingStateMachine = GameFeaturePluginStateMachines.Find(PluginURL);
    if (ExistingStateMachine)
    {
        return *ExistingStateMachine;
    }

    if (!bCreateIfItDoesntExist)
    {
        return nullptr;
    }

    UGameFeaturePluginStateMachine* NewStateMachine = NewObject<UGameFeaturePluginStateMachine>(this);
    GameFeaturePluginStateMachines.Add(PluginURL, NewStateMachine); //GFS里保存了所有GF插件的每个状态机信息,这样你就能随时切换任一个了。
    NewStateMachine->InitStateMachine(PluginURL, FGameFeaturePluginRequestStateMachineDependencies::CreateUObject(this, &ThisClass::HandleRequestPluginDependencyStateMachines)); //初级化状态机

    return NewStateMachine;
}

实际的GF加载步骤是由一个个状态对象在BeginState、UpdateState、EndState这3步中做的。GF插件的内部状态有十几个,但对外暴露的只有图上的4个。

每个插件状态都从以下基类继承。在刚切换到该状态的时候调用BeginState来初始化状态内部的信息,在该状态活跃的时候调用UpdateState来进行业务操作以便可能切换到下一个状态,在从该状态切换走的时候会调用EndState来结束清理自己的内部信息。同时也用CanBeDestinationState这个虚函数来指定一个状态是否可以为目标状态。所谓目标状态即是该状态的UpdateState不会自动切到下一个状态,即自己不是一个中间过渡的状态。目标状态只有在GFS的状态切换API主动调用之后,才会被激发迁移到下一个目标状态。

struct 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 true if this state is allowed to be a destination (i.e. is not a transition state like 'Downloading') */
    virtual bool CanBeDestinationState() const { return false; }
};

状态机更新流程

每次GFS的状态API调用后,其最后都会调用到UGameFeaturePluginStateMachine::SetDestinationState这个函数来指定目标状态,然后调用UpdateStateMachine来更新整个状态机来运行流转到指定状态。

void UGameFeaturePluginStateMachine::SetDestinationState(EGameFeaturePluginState InDestinationState, FGameFeatureStateTransitionComplete OnFeatureStateTransitionComplete)
{
    check(IsValidDestinationState(InDestinationState));
    StateProperties.DestinationState = InDestinationState;
    StateProperties.OnFeatureStateTransitionComplete = OnFeatureStateTransitionComplete;
    UpdateStateMachine();
}

void UGameFeaturePluginStateMachine::UpdateStateMachine()
{
    UE::GameFeatures::FResult TransitionResult(MakeValue());
    bool bKeepProcessing = false;
    EGameFeaturePluginState CurrentState = GetCurrentState();   //获取当前状态
    do
    {
        bKeepProcessing = false;

        FGameFeaturePluginStateStatus StateStatus;
        AllStates[(int32)CurrentState]->UpdateState(StateStatus);   //当前状态的更新

        TransitionResult = StateStatus.TransitionResult;
        if (StateStatus.TransitionToState != EGameFeaturePluginState::Uninitialized) //需要切换到下一个状态
        {
            AllStates[(int32)CurrentState]->EndState(); //先退出当前状态
            CurrentStateInfo = FGameFeaturePluginStateInfo(StateStatus.TransitionToState);
            CurrentState = StateStatus.TransitionToState; //指定下个状态为当前状态

            AllStates[(int32)CurrentState]->BeginState(); //开始当前状态
            OnStateChangedEvent.Broadcast(this);
            bKeepProcessing = true;
        }

        if (!TransitionResult.HasValue())
        {
            StateProperties.DestinationState = CurrentState;
            break;
        }
    } while (bKeepProcessing);

    if (CurrentState == StateProperties.DestinationState)
    {
        StateProperties.OnFeatureStateTransitionComplete.ExecuteIfBound(this, TransitionResult);//触发状态切换的回调
        StateProperties.OnFeatureStateTransitionComplete.Unbind();
    }
}

代码里我省略了很多打印log检查出错的代码,只留下了骨干。

一,检查存在性

这么多状态,想要梳理它们,就得追根溯源从其最开始的源头开始。下面我开始一步步给大家过一下GF状态机的加载流程。首先GF插件的最初始状态是Uninitialized,很快进入UnknownStatus,标明还不知道该插件的状态。然后进入CheckingStatus阶段,开始根据PluginURL看是什么协议。如果是file(比如:file:../../../../../Workspace/LearnGF/Plugins/GameFeatures/MyFeature/MyFeature.uplugin),就在本地磁盘路径检查看文件是否存在。如果是web,就要先尝试去把它下载下来。Web协议的支持可能还得到UE5正式版才能够更加完善。这个阶段主要目的就是检查uplugin文件是否存在,一般的GF插件都是已经在本地的,可以比较快通过检测。其中的各个状态逻辑可在"GameFeaturePluginStateMachine.cpp"中的UpdateState方法查看。

距离我上次在UOD2021上演讲,在UE5预览版中又新增了两种状态: 一是Terminal,代表这个插件已经被终结,由UGameFeaturesSubsystem中新增的方法TerminateGameFeaturePlugin来调用触发。这也是个目标状态,其最终会导致该插件关联的状态机被释放掉。因此如果你真的想再也不需要激活该插件了就可以终结它,当然终结后,也还是可以继续重新Load来加载触发的。

void UGameFeaturesSubsystem::TerminateGameFeaturePlugin(const FString& PluginURL, const FGameFeaturePluginUninstallComplete& CompleteDelegate)
{
    if (UGameFeaturePluginStateMachine* StateMachine = FindGameFeaturePluginStateMachine(PluginURL))
    {
        if (StateMachine->GetCurrentState() > EGameFeaturePluginState::Terminal)
        {   
            //设定目标终结状态
            StateMachine->SetDestinationState(EGameFeaturePluginState::Terminal, FGameFeatureStateTransitionComplete::CreateUObject(this, &ThisClass::TerminateGameFeaturePluginComplete, CompleteDelegate));
        }
        //....
    }
}
void UGameFeaturesSubsystem::TerminateGameFeaturePluginComplete(UGameFeaturePluginStateMachine* Machine, const UE::GameFeatures::FResult& Result, FGameFeaturePluginUninstallComplete CompleteDelegate)
{
    if (Result.HasValue())
    {
        GameFeaturePluginStateMachines.Remove(Machine->GetPluginURL()); //删除状态机
        Machine->MarkAsGarbage();
    }

    CompleteDelegate.ExecuteIfBound(Result);
}

二是各种Error状态,往往在上一个状态检测或加载遇见了什么错误后,就转到该错误状态,指定更加具体的错误信息。当然从这些Error状态有两条路可以走,一是可以重新再次尝试,退回到上个状态再来一次,也许这回就成功了呢;二是干脆就算了,放弃吧,世界毁灭吧,这个时候就可以用Terminate终结掉这个插件。

二,加载CF C++模块

找到uplugin阶段后,下一个阶段就是开始尝试加载GF的C++模块。虽然我们在前面NewPlugin的时候,只有ContentOnly的选项,但GF插件依然可以跟普通的插件一样包含C++代码,因此你可以把一些GF的逻辑用C++实现。

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

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

三,加载GameFeatureData

在C++模块加载完成之后,下一步就要开始把GF自身注册到GFS里面去。Registering这一步其实就开始加载一些GF插件的配置ini文件,关键的一步是会加载GFD资产文件,并触发其中定义Action的OnGameFeatureRegistering,这个回调里一般不需要做什么事情,因为这个时候Action还没激活。但如果你想在Action激活之前做一些静态的预先逻辑,则可以在这里进行。另外GFD里其实也可以再单独添加针对每个GF的额外主资产类型,Registering的一步也会来添加上。

这些都完成之后,就进入到Registered注册完成这个目标状态。表明引擎已经知道明了这个GF插件的结构是什么样的,资产类型有哪些。下一步就等着来加载他们了。

其中最重要的一步是在Registering的时候加载GFD资产,会触发UGameFeaturesSubsystem::LoadGameFeatureData(BackupGameFeatureDataPath);的调用从而完成GFD加载。

四,预加载资产和配置

下一个阶段就是进行加载了。Loading阶段会开始加载两种东西,一是插件的运行时的ini(如…/LearnGF/Saved/Config/WindowsEditor/MyFeature.ini),另外一项是可以预先加载一些资产,资产列表可以由Policy对象根据每个GF插件的GFD文件来获得,因此我们也可以重载GFD来添加我们想要预先加载的资产列表。 Loaded状态表明GF的配置和预加载资产都已经加载完毕。下一步就可以激活了。这里注意一下,Loaded状态并不是说一下子把GF插件里的所有资产都一股脑儿加载进内存,并不会那么傻。我们来回忆一下,到Loaded状态目前为止加载的有:C++模块dll,配置ini文件,定义了Action列表的GFD资产,一些自定义的要预加载的资产。其他的资产是在激活阶段根据Action的执行按需加载的。

五,激活生效

在加载完成之后,我们就可以激活这个GF了。这里的逻辑也很简洁明了,Activating会为每个GFD里定义的Action触发OnGameFeatureActivating,而Deactivating触发OnGameFeatureDeactivating。激活和反激活是Action真正做事的时机。激活的时候可以注册一些引擎事件回调,添加组件到Actor身上,或者在场景里生成Actor等。自然反激活的时候就可以进行一些清理工作,移除事件回调,把之前的添加的组件删除掉等。最后的状态是Active,就代表这个GF正在发挥作用了。

整体流程梳理

最后我们再来梳理一下整个流程,一个GF插件大致会经过这些阶段的加载,里面有4个目标状态,我们可以在这4个目标状态之间根据我们的逻辑需要进行切换。GFS也提供了public的API来让你进行切换。因此你可以在运行时根据你的逻辑来动态切换。

状态监听

为了扩展需要,GFS还提供了状态监听的回调对象,方便我们扩展需要。用法也很简单,自定义一个类继承自UGameFeatureStateChangeObserver,然后重载那4个方法,之后把这个对象通过AddObserver注册给GFS就可以了,就是一个很简单的观察者模式。

总结

在冗长的叙述完GF的状态机之后,不知道你是否还有耐心看到这里。没有也没有关系,之后有用到,需要深入了解的时候再来研究就好了,希望那时GF框架又已经得到了众多的升级迭代。在我自己学习GF框架的时候,其实最想搞懂的流程就是一个GF插件是怎么从加载到释放的,又是怎么生效应用到系统的其他方面的。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值