LyraLog5 角色初始化状态机

在虚幻引擎中,Actor 的生命周期(BeginPlay)并不保证所有依赖的外部对象(如 PlayerState, Controller, InputComponent, AbilitySystemComponent)都已经准备好,尤其是在网络环境中。

项目定义了四个状态:

InitState.Spawned: Actor 已生成,可以通过基础检查。

InitState.DataAvailable: 必要的基础数据已存在(例如 PlayerState 已复制,Controller 已拥有 Pawn)。

InitState.DataInitialized: 核心数据不仅存在,而且已完成初始化配置(例如 ASC 已设置,输入映射已添加)。

InitState.GameplayReady: 我们可以开始游玩了(UI 准备好,不仅数据好了,表现层也好了)。

状态机使用概述

UGameFrameworkComponentManager 使用这个类的状态机来推进流程

使用需要这几个接口

// YourComponent.h
class UYourComponent : public UPawnComponent, public IGameFrameworkInitStateInterface
{
    // ...
public:
    // 定义特性的唯一名称,例如 "Hero", "PawnExtension"
    static const FName NAME_ActorFeatureName;

    //~ IGameFrameworkInitStateInterface 接口
    virtual FName GetFeatureName() const override { return NAME_ActorFeatureName; }
    virtual bool CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const override;
    virtual void HandleChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) override;
    virtual void OnActorInitStateChanged(const FActorInitStateChangedParams& Params) override;
    virtual void CheckDefaultInitialization() override; // 驱动状态机的入口
    //~ End IGameFrameworkInitStateInterface

    // ...
};

注意注册和反注册

// YourComponent.cpp
const FName UYourComponent::NAME_ActorFeatureName("YourFeatureName");

void UYourComponent::OnRegister()
{
    Super::OnRegister();
    // 尽早注册,告诉管理器"我来了"
    RegisterInitStateFeature();
}

void UYourComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    // 销毁时注销,避免野指针回调
    UnregisterInitStateFeature();
    Super::EndPlay(EndPlayReason);
}

启动状态机

void UYourComponent::BeginPlay()
{
    Super::BeginPlay();

    // 监听所有特性的状态变化(Name=None表示监听所有)
    BindOnActorInitStateChanged(NAME_None, FGameplayTag(), false);

    // 尝试立刻进入 Spawned 状态
    ensure(TryToChangeInitState(EqZeroGameplayTags::InitState_Spawned));

    // 检查是否能继续往下走
    CheckDefaultInitialization();
}

可以理解为状态机是不是的会check一下状态能不能过度,会给你 A->B通过CanChangeInitState 问你,

你在这里写逻辑,能不能过度

bool UYourComponent::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const
{
    check(Manager);
    APawn* Pawn = GetPawn<APawn>();

    // 1. None -> Spawned
    if (!CurrentState.IsValid() && DesiredState == EqZeroGameplayTags::InitState_Spawned)
    {
        return Pawn != nullptr;
    }
    // 2. Spawned -> DataAvailable (等待基础数据,如PlayerState)
    else if (CurrentState == EqZeroGameplayTags::InitState_Spawned && DesiredState == EqZeroGameplayTags::InitState_DataAvailable)
    {
        // 示例:必须要等到 PlayerState 指针有效
        return GetPlayerState<AEqZeroPlayerState>() != nullptr;
    }
    // 3. DataAvailable -> DataInitialized (等待其他组件初始化完)
    else if (CurrentState == EqZeroGameplayTags::InitState_DataAvailable && DesiredState == EqZeroGameplayTags::InitState_DataInitialized)
    {
        // 示例:我依赖 "PawnExtension" 先初始化完
        return Manager->HasFeatureReachedInitState(Pawn, UEqZeroPawnExtensionComponent::NAME_ActorFeatureName, EqZeroGameplayTags::InitState_DataInitialized);
    }

    return false;
}

如果可以,会调用到这一步,状态从A->B了。

你要在这里处理逻辑或者检查下一个状态

void UYourComponent::HandleChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState)
{
    // 如果进入了 DataInitialized 阶段,执行实际的 Setup
    if (CurrentState == EqZeroGameplayTags::InitState_DataAvailable && DesiredState == EqZeroGameplayTags::InitState_DataInitialized)
    {
        // 比如:配置输入、绑定委托、初始化UI引用等
        // InitializePlayerInput(Pawn->InputComponent);
    }
}

这个状态机不是基于tick的,需要逻辑在各种初始化完成的地方调用一下check函数

void UYourComponent::CheckDefaultInitialization()
{
    // 这是一个 helper 方法,会自动查找当前状态的下一个状态,并调用 CanChangeInitState 检查
    // 如果通过,就调用 HandleChangeInitState 并更新状态。
    static const TArray<FGameplayTag> StateChain = { 
        EqZeroGameplayTags::InitState_Spawned, 
        EqZeroGameplayTags::InitState_DataAvailable, 
        EqZeroGameplayTags::InitState_DataInitialized, 
        EqZeroGameplayTags::InitState_GameplayReady 
    };

    CheckDefaultInitializationForImplementer(); // 或者手动循环 TryToChangeInitState
}

初始状态变化的回调。

void UYourComponent::OnActorInitStateChanged(const FActorInitStateChangedParams& Params)
{
    // 如果是我们关心的组件发生了状态变化
    if (Params.FeatureName == UEqZeroPawnExtensionComponent::NAME_ActorFeatureName)
    {
        if (Params.FeatureState == EqZeroGameplayTags::InitState_DataInitialized)
        {
            // 既然依赖已经好了,我们再试一次初始化
            CheckDefaultInitialization();
        }
    }
}

其实核心原理就是一个基于TAG的状态机


FGameplayTag IGameFrameworkInitStateInterface::ContinueInitStateChain(const TArray<FGameplayTag>& InitStateChain)
{

    // 当前状态
    int32 ChainIndex = 0;
    FGameplayTag CurrentState = Manager->GetInitStateForFeature(MyActor, MyFeatureName);

    while (ChainIndex < InitStateChain.Num() - 1)
    {
        if (CurrentState == InitStateChain[ChainIndex])
        {
            FGameplayTag DesiredState = InitStateChain[ChainIndex + 1];

            // 能不能去下一个状态
            if (CanChangeInitState(Manager, CurrentState, DesiredState))
            {
                // 成功了
                HandleChangeInitState(Manager, CurrentState, DesiredState);

                // 因为切状态了,所以要更新当前状态,然后继续循环
                ensure(Manager->ChangeFeatureInitState(MyActor, MyFeatureName, ThisObject, DesiredState));
                CurrentState = Manager->GetInitStateForFeature(MyActor, MyFeatureName);
            }
        }

        ChainIndex++;
    }

    return CurrentState;
}

项目实际使用

OnRegister->RegisterInitStateFeature

void UEqZeroHeroComponent::OnRegister()
{
    Super::OnRegister();
    RegisterInitStateFeature();
}
void UEqZeroPawnExtensionComponent::OnRegister()
{
    Super::OnRegister();
    RegisterInitStateFeature();
}

两个函数除此之外都是些合法性检查

void IGameFrameworkInitStateInterface::RegisterInitStateFeature()
{
    UObject* ThisObject = Cast<UObject>(this);
    AActor* MyActor = GetOwningActor();
    UGameFrameworkComponentManager* Manager = UGameFrameworkComponentManager::GetForActor(MyActor);
    const FName MyFeatureName = GetFeatureName();

    if (MyActor && Manager)
    {
        // Manager will be null if this isn't in a game world
        // Actor, FName, ActorComponent
        Manager->RegisterFeatureImplementer(MyActor, MyFeatureName, ThisObject);
    }
}
bool UGameFrameworkComponentManager::RegisterFeatureImplementer(AActor* Actor, FName FeatureName, UObject* Implementer)
{
    // 基于Actor为key,维护了一个 FeatureData的 Map
    FActorFeatureData& ActorStruct = FindOrAddActorData(Actor);

    FActorFeatureState* FoundState = nullptr;
    for (FActorFeatureState& State : ActorStruct.RegisteredStates)
    {
        if (State.FeatureName == FeatureName)
        {
            // TODO what if it's already in the desired state?
            // 如果逻辑上保证了只注册一次,不应该找到。
            FoundState = &State;
        }
    }

    // 把 FeatureName,ActorComponent 加到这个结构里面
    if (!FoundState)
    {
        FoundState = &ActorStruct.RegisteredStates.Emplace_GetRef(FeatureName);
    }

    FoundState->Implementer = Implementer;
    return true;
}

简单来说

UGameFrameworkComponentManager

维护了一个 Map

TMap<FObjectKey, FActorFeatureData> ActorFeatureMap;

key是Actor,Value是一个结构,每个结构代表一个 Feature。这里的Feature指的就是例如这个ActorComponent的

应该只有一个才对,但是这里为什么是TArray呢

反正第一步是注册,Actor->ActorComponent 到框架里面

不要忘了EndPlay的时候,取消注册

void UEqZeroPawnExtensionComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    UnregisterInitStateFeature();
    Super::EndPlay(EndPlayReason);
}
void UEqZeroHeroComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    UnregisterInitStateFeature();
    Super::EndPlay(EndPlayReason);
}

BeginPlay->启动

void UEqZeroHeroComponent::BeginPlay()
{
    Super::BeginPlay();

    BindOnActorInitStateChanged(UEqZeroPawnExtensionComponent::NAME_ActorFeatureName, FGameplayTag(), false);
    ensure(TryToChangeInitState(EqZeroGameplayTags::InitState_Spawned));
    CheckDefaultInitialization();
}
void UEqZeroPawnExtensionComponent::BeginPlay()
{
    Super::BeginPlay();

    BindOnActorInitStateChanged(NAME_None, FGameplayTag(), false);
    ensure(TryToChangeInitState(EqZeroGameplayTags::InitState_Spawned));
    CheckDefaultInitialization();
}

BindOnActorInitStateChanged

其中

BindOnActorInitStateChanged是为了注册Actor状态变化的时候会回调自己的OnActorInitStateChanged函数

void IGameFrameworkInitStateInterface::BindOnActorInitStateChanged(FName FeatureName, FGameplayTag RequiredState, bool bCallIfReached)
{
    UObject* ThisObject = Cast<UObject>(this);
    AActor* MyActor = GetOwningActor();
    UGameFrameworkComponentManager* Manager = UGameFrameworkComponentManager::GetForActor(MyActor);

    if (ensure(MyActor && Manager))
    {
        // Bind as a weak lambda because this is not a UObject but is guaranteed to be valid as long as ThisObject is
        FActorInitStateChangedDelegate Delegate = FActorInitStateChangedDelegate::CreateWeakLambda(ThisObject,
            [this](const FActorInitStateChangedParams& Params)
            {
                this->OnActorInitStateChanged(Params);
            });

        ActorInitStateChangedHandle = Manager->RegisterAndCallForActorInitState(MyActor, FeatureName, RequiredState, MoveTemp(Delegate), bCallIfReached);
    }
}

TryToChangeInitState

TryToChangeInitState

这里会执行第一次流程,简单来说激素

if (CanChangeInitState(...)) // 子类复写检查条件
{
    HandleChangeInitState(...); // 子类复写
    Manager->ChangeFeatureInitState(...); // 这里会修改当前状态的TAG
}

具体来说

bool IGameFrameworkInitStateInterface::TryToChangeInitState(FGameplayTag DesiredState)
{
    UObject* ThisObject = Cast<UObject>(this);
    AActor* MyActor = GetOwningActor();
    UGameFrameworkComponentManager* Manager = UGameFrameworkComponentManager::GetForActor(MyActor);
    const FName MyFeatureName = GetFeatureName();

    if (!Manager || !ThisObject || !MyActor)
    {
        return false;   
    }

    FGameplayTag CurrentState = Manager->GetInitStateForFeature(MyActor, MyFeatureName);

    if (CurrentState == DesiredState)
    {
        return false;
    }

    if (!CanChangeInitState(Manager, CurrentState, DesiredState))
    {
        return false;
    }

    HandleChangeInitState(Manager, CurrentState, DesiredState);
    return ensure(Manager->ChangeFeatureInitState(MyActor, MyFeatureName, ThisObject, DesiredState));
}

CheckDefaultInitialization

这个函数会在各种地方经常调,但是内容是自己 重写的

void UEqZeroHeroComponent::CheckDefaultInitialization()
{
    static const TArray<FGameplayTag> StateChain = {
        EqZeroGameplayTags::InitState_Spawned,
        EqZeroGameplayTags::InitState_DataAvailable,
        EqZeroGameplayTags::InitState_DataInitialized,
        EqZeroGameplayTags::InitState_GameplayReady };

    ContinueInitStateChain(StateChain);
}
void UEqZeroPawnExtensionComponent::CheckDefaultInitialization()
{
    CheckDefaultInitializationForImplementers();

    static const TArray<FGameplayTag> StateChain = {
        EqZeroGameplayTags::InitState_Spawned,
        EqZeroGameplayTags::InitState_DataAvailable,
        EqZeroGameplayTags::InitState_DataInitialized,
        EqZeroGameplayTags::InitState_GameplayReady };

    ContinueInitStateChain(StateChain);
}

CheckDefaultInitializationForImplementers

遍历所有Feature,触发CheckDefaultInitialization

void IGameFrameworkInitStateInterface::CheckDefaultInitializationForImplementers()
{
    UObject* ThisObject = Cast<UObject>(this);
    AActor* MyActor = GetOwningActor();
    UGameFrameworkComponentManager* Manager = UGameFrameworkComponentManager::GetForActor(MyActor);
    const FName MyFeatureName = GetFeatureName();

    if (Manager)
    {
        TArray<UObject*> Implementers;
        Manager->GetAllFeatureImplementers(Implementers, MyActor, FGameplayTag(), MyFeatureName);

        for (UObject* Implementer : Implementers)
        {
            if (IGameFrameworkInitStateInterface* ImplementerInterface = Cast<IGameFrameworkInitStateInterface>(Implementer))
            {
                ImplementerInterface->CheckDefaultInitialization();
            }
        }
    }
}

FGameplayTag IGameFrameworkInitStateInterface::ContinueInitStateChain(const TArray<FGameplayTag>& InitStateChain)
{

    int32 ChainIndex = 0;
    FGameplayTag CurrentState = Manager->GetInitStateForFeature(MyActor, MyFeatureName);

    /*
        输入参数 InitStateChain 就是定义的那四个状态的Array
    */
    while (ChainIndex < InitStateChain.Num() - 1)
    {
        if (CurrentState == InitStateChain[ChainIndex])
        {
            // 能否转状态
            FGameplayTag DesiredState = InitStateChain[ChainIndex + 1];
            if (CanChangeInitState(Manager, CurrentState, DesiredState))
            {
                // 能转
                HandleChangeInitState(Manager, CurrentState, DesiredState);
                ensure(Manager->ChangeFeatureInitState(MyActor, MyFeatureName, ThisObject, DesiredState));
                CurrentState = Manager->GetInitStateForFeature(MyActor, MyFeatureName);
            }
        }
        ChainIndex++;
    }

    return CurrentState;
}

所以外部想推状态就 CheckDefaultInitialization 就可以了。

状态机的设计

    static const TArray<FGameplayTag> StateChain = {
        EqZeroGameplayTags::InitState_Spawned,
        EqZeroGameplayTags::InitState_DataAvailable,
        EqZeroGameplayTags::InitState_DataInitialized,
        EqZeroGameplayTags::InitState_GameplayReady };

对象创建,数据合法,数据初始化完成,GameplayReady

UEqZeroHeroComponent PawnExtensionComponent
InitState_Spawned 挂在Pawn就过 挂在Pawn就过
InitState_DataAvailable 1. PlayerState加载完成
2. Controller和PlayerState加载配对
3. 主玩家的InputComponent,LocalPlayer加载完成
1. 体验加载完成PawnData有了
2. Authority和LocalControlled端检查"possessed by a controller"
InitState_DataInitialized 等PawnExtension的DataInitialized状态(等 -->) 所有组件都达到了DataAvailable(楼上)
InitState_GameplayReady 直接过 直接过

所有check的地方

  1. 启动引擎 (Start the Engine)

UEqZeroPawnExtensionComponent::BeginPlay

UEqZeroHeroComponent::BeginPlay

  1. 数据与依赖变化 (Data & Dependency Changes)

UEqZeroPawnExtensionComponent::SetPawnData:体验加载完成PlayerStates设置过来

UEqZeroPawnExtensionComponent::OnRep_PawnData:属性同步到客户端

UEqZeroPawnExtensionComponent::OnActorInitStateChanged:监听其他组件状态

UEqZeroHeroComponent::OnActorInitStateChanged:监听其他组件状态

  1. 控制权变化 (Controller Ownership Changes)

UEqZeroPawnExtensionComponent::HandleControllerChanged

  • 当 Pawn 被 Possess 或 Unpossess 时调用

UEqZeroPawnExtensionComponent::HandlePlayerStateReplicated

  • PlayerState 复制下来了。这意味着 Controller 可能已经和 PlayerState 关联好了。

AEqZeroPlayerState::ClientInitialize

  • 客户端拥有权确认。
  • 这是 APlayerState 的一个虚函数。当 Controller 在客户端被分配给 PlayerState 时调用(Controller 和 PS 握手成功)。
  1. 辅助与兼容 (Helpers & Misc)

UEqZeroPawnExtensionComponent::SetupPlayerInputComponent

  • 这是 APawn 的标准接口。虽然我们的输入逻辑主要在 Hero 组件里,但这里调用一次是为了兜底,确保在输入组件就绪的时机再检查一次状态机。
上一篇
下一篇