Lyra的Character-1

找L_ShooterGym -> 体验 -> pawn data 里面有 pawn class

找到 /ShooterCore/Game/B_Hero_ShooterMannequin.B_Hero_ShooterMannequin

这是一个看不见的mesh

他会用这个随机一个模型只有骨骼

/Game/Characters/Cosmetics/B_PickRandomCharacter.B_PickRandomCharacter

走 Add Character Part

装扮的逻辑

就是

/Game/Characters/Cosmetics/B_Manny.B_Manny

/Game/Characters/Cosmetics/B_Quinn.B_Quinn

他的动画蓝图是 /Game/Characters/Heroes/Mannequin/Animations/ABP_Mannequin_CopyPose.ABP_Mannequin_CopyPose

这里面copy pose from mesh

旁边还有一个重定向的,应该是retarget这个才对吧?暂时没搞懂

/Game/Characters/Heroes/Mannequin/Animations/ABP_Mannequin_Retarget.ABP_Mannequin_Retarget

反正我们找到了角色类和父类,三个蓝图上去到了 lyra character

ALyraPawn

看一个类先看数据,维护了队伍ID,实际没用到。

逻辑就是 PossessedBy 和 UnPossessed 的时候从controller获取删除一下队伍信息,然后广播一下

/**
 * ALyraPawn
 */
UCLASS(MinimalAPI)
class ALyraPawn : public AModularPawn, public ILyraTeamAgentInterface
{
    GENERATED_BODY()
private:
    UPROPERTY(ReplicatedUsing = OnRep_MyTeamID)
    FGenericTeamId MyTeamID;

    UPROPERTY()
    FOnLyraTeamIndexChangedDelegate OnTeamChangedDelegate;
};

带 AModularPawn 的类

在 ModularGameplayActors 这个包下,是为了向 UGameFrameworkComponentManager 注册。

以便于Game Feature 例如 UGameFeatureAction_AddComponents 能够例如往某个类加某个组件的时候,能够找到这个类

void AModularPawn::PreInitializeComponents()
{
    Super::PreInitializeComponents();

    UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this);
}

void AModularPawn::BeginPlay()
{
    UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady);

    Super::BeginPlay();
}

void AModularPawn::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this);

    Super::EndPlay(EndPlayReason);
}

ALyraCharacter

这个文件

/**
 * ALyraCharacter
 *
 *  The base character pawn class used by this project.
 *  Responsible for sending events to pawn components.
 *  New behavior should be added via pawn components when possible.
 */
UCLASS(MinimalAPI, Config = Game, Meta = (ShortTooltip = "The base character pawn class used by this project."))
class ALyraCharacter : public AModularCharacter, public IAbilitySystemInterface, public IGameplayCueInterface, public IGameplayTagAssetInterface, public ILyraTeamAgentInterface
{
    GENERATED_BODY()

};

IAbilitySystemInterface 实现了获取GAS, ASC的接口

IGameplayCueInterface 希望actor能回调到 gameplay effect 的 game cue event

IGameplayTagAssetInterface 封装让角色能直接查询访问到ASC的tag,提过了全匹配,部分匹配的接口

ILyraTeamAgentInterface 队伍相关

基本内容

  • 关闭tick,子类有需要再开
  • UCapsuleComponent,USkeletalMeshComponent 初始化,并设置碰撞预设
  • ULyraCharacterMovementComponent 移动组件初始化,数值设置
  • ULyraPawnExtensionComponent 是一个 pawn component

逻辑解耦,有一个pawn data,在讲体验的时候。我们知道pawn data 是一个配置包括pawn class, 技能集合和关系映射,输入配置,和摄像机模式。

这里主要是分管了一些 ASC 初始化,摧毁的逻辑。后面展开。

  • ULyraHealthComponent

生命值组件,里面维护了 ULyraHealthSet,就是GAS那个 attr set 的子类。还有一些死亡状态事件相关的逻辑

  • ULyraCameraComponent

父类 UCameraComponent,里面是 ULyraCameraModeStack,摄像机现在先不展开。

  • ULyraSignificanceManager

目前不太重要,特征管理器,例如用于重要和不重要的角色有不同tick频率,但是lyra没实现完

  • ReplicatedAcceleration

通过转极坐标来压缩加速度为3个uint8,作为网络优化

  • FSharedRepMovement
    /** RPCs that is called on frames when default property replication is skipped. This replicates a single movement update to everyone. */
    UFUNCTION(NetMulticast, unreliable)
    UE_API void FastSharedReplication(const FSharedRepMovement& SharedRepMovement);

    // 发送的 FSharedRepMovement 存一下, 避免重复发
    FSharedRepMovement LastSharedReplication;
void ULyraReplicationGraph::InitGlobalActorClassSettings()
{
    // ...

    // ------------------------------------------------------------------------------------------------------
    //  Setup FastShared replication for pawns. This is called up to once per frame per pawn to see if it wants
    //  to send a FastShared update to all relevant connections.
    // ------------------------------------------------------------------------------------------------------
    CharacterClassRepInfo.FastSharedReplicationFunc = [](AActor* Actor)
    {
        bool bSuccess = false;
        if (ALyraCharacter* Character = Cast<ALyraCharacter>(Actor))
        {
            bSuccess = Character->UpdateSharedReplication(); // 从这里过来
        }
        return bSuccess;
    };
}

到 lyra character 的 UpdateSharedReplication 就是填充数据,看看和上一帧是不是一样,然后发出去。

FastSharedReplication_Implementation

  • 另外就是一些从controler赋值过来的队伍接口,和一些其他的转发逻辑这个类就不详细看了。到具体看组件的时候在关联逻辑

ULyraCharacterMovementComponent

内容比较少

刚刚character的,自己维护了一个加速度转极坐标的压缩。关闭了引擎的加速度(哪里关的?)。然后用这个

    UPROPERTY(Transient, ReplicatedUsing = OnRep_ReplicatedAcceleration)
    FLyraReplicatedAcceleration ReplicatedAcceleration;

在这里覆盖引擎的加速度计算流程。

void ULyraCharacterMovementComponent::SimulateMovement(float DeltaTime)
{
    if (bHasReplicatedAcceleration)
    {
        // Preserve our replicated acceleration
        const FVector OriginalAcceleration = Acceleration;
        Super::SimulateMovement(DeltaTime);
        Acceleration = OriginalAcceleration;
    }
    else
    {
        Super::SimulateMovement(DeltaTime);
    }
}
  • ULyraCharacterMovementComponent::GetGroundInfo

通过射线维护一到到地面的距离以及缓存一下。

GetDeltaRotation,GetMaxSpeed 是移动组件的重写函数,通过ASC Tag返回0值。

什么时候有这个tag暂时不知道。

ULyraAnimInstance

好短一动画蓝图。。。

UCLASS(Config = Game)
class ULyraAnimInstance : public UAnimInstance
{
    GENERATED_BODY()
    // ...
protected:

    // Gameplay tags that can be mapped to blueprint variables. The variables will automatically update as the tags are added or removed.
    // These should be used instead of manually querying for the gameplay tags.
    UPROPERTY(EditDefaultsOnly, Category = "GameplayTags")
    FGameplayTagBlueprintPropertyMap GameplayTagPropertyMap;

    UPROPERTY(BlueprintReadOnly, Category = "Character State Data")
    float GroundDistance = -1.0f;
};

GroundDistance 他想要地面信息,就上面移动组件里面的。

GameplayTagPropertyMap

/**
用于管理游戏玩法标签蓝图属性映射的结构体。
它通过能力系统组件上的委托来注册属性。
此结构体不能在容器(如 TArray)中使用,因为它使用原始指针来绑定委托,
其地址可能会发生变化,从而导致绑定无效。
 */
USTRUCT()
struct FGameplayTagBlueprintPropertyMap
{
    GENERATED_BODY()
    // ...
protected:

    TWeakObjectPtr<UObject> CachedOwner;
    TWeakObjectPtr<UAbilitySystemComponent> CachedASC;

    UPROPERTY(EditAnywhere, Category = GameplayTagBlueprintProperty)
    TArray<FGameplayTagBlueprintPropertyMapping> PropertyMappings;
};

通过查询tag来修改bool值。

对于这个anim ins 的函数逻辑就是,初始化的时候绑定一下ASC

FGameplayTagBlueprintPropertyMapping 这里面就是很多属性可以配置了。一会看。

我们先看map是如何注册的

void FGameplayTagBlueprintPropertyMap::Initialize(UObject* Owner, UAbilitySystemComponent* ASC)
{
    // ... some check

    CachedOwner = Owner; // anin instance
    CachedASC = ASC;

    FOnGameplayEffectTagCountChanged::FDelegate Delegate = FOnGameplayEffectTagCountChanged::FDelegate::CreateRaw(this, &FGameplayTagBlueprintPropertyMap::GameplayTagEventCallback, CachedOwner);

    // Process array starting at the end so we can remove invalid entries.
    // 遍历我们动画蓝图的配置
    for (int32 MappingIndex = (PropertyMappings.Num() - 1); MappingIndex >= 0; --MappingIndex)
    {
        // 这个就是具体的tag结构
        FGameplayTagBlueprintPropertyMapping& Mapping = PropertyMappings[MappingIndex];

        if (Mapping.TagToMap.IsValid())
        {
            // 寻找属性
            FProperty* Property = OwnerClass->FindPropertyByName(Mapping.PropertyName);
            if (Property && IsPropertyTypeValid(Property))
            {
                // 设置属性,和属性变化代理
                Mapping.PropertyToEdit = Property;
                Mapping.DelegateHandle = ASC->RegisterAndCallGameplayTagEvent(Mapping.TagToMap, Delegate, GetGameplayTagEventType(Property));
                continue;
            }
        }

        // 正确的属性前面continue了,这里就是LOG和删除
        PropertyMappings.RemoveAtSwap(MappingIndex, EAllowShrinking::No);
    }
}

这里会看到,tag事件会触发数值变化,但是只有这三个类型吗?继续看下去

void FGameplayTagBlueprintPropertyMap::GameplayTagEventCallback(const FGameplayTag Tag, int32 NewCount, TWeakObjectPtr<UObject> RegisteredOwner)
{
    // ... 

    if (Mapping && Mapping->PropertyToEdit.Get())
    {
        if (const FBoolProperty* BoolProperty = CastField<const FBoolProperty>(Mapping->PropertyToEdit.Get()))
        {
            BoolProperty->SetPropertyValue_InContainer(Owner, NewCount > 0);
        }
        else if (const FIntProperty* IntProperty = CastField<const FIntProperty>(Mapping->PropertyToEdit.Get()))
        {
            IntProperty->SetPropertyValue_InContainer(Owner, NewCount);
        }
        else if (const FFloatProperty* FloatProperty = CastField<const FFloatProperty>(Mapping->PropertyToEdit.Get()))
        {
            FloatProperty->SetPropertyValue_InContainer(Owner, (float)NewCount);
        }
    }
}

ULyraPawnExtensionComponent

https://dev.epicgames.com/documentation/zh-cn/unreal-engine/game-framework-component-manager-in-unreal-engine

Game Framework Component Manager

就是这堆东西

void AModularCharacter::PreInitializeComponents()
{
    Super::PreInitializeComponents();

    UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this);
}

void AModularCharacter::BeginPlay()
{
    UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady);

    Super::BeginPlay();
}

void AModularCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this);

    Super::EndPlay(EndPlayReason);
}

前面通过看数据发现,这个类是维护pawn data,然后是一些分管技能的业务。

现在具体看看

/**
 * Component that adds functionality to all Pawn classes so it can be used for characters/vehicles/etc.
 * This coordinates the initialization of other components.
 */
UCLASS(MinimalAPI)
class ULyraPawnExtensionComponent : public UPawnComponent, public IGameFrameworkInitStateInterface
{
    GENERATED_BODY()

    /** 这个整体功能的名称,它依赖于其他已命名的组件功能。 */
    static UE_API const FName NAME_ActorFeatureName;
};

lyra希望一个组件一个功能,所以 NAME_ActorFeatureName (其他的component也能找到同名变量)

IGameFrameworkInitStateInterface 的使用,例如 lyra hero comp 和 lyra pawn ext comp

在重写的 OnRegister 最后调用 RegisterInitStateFeature

RegisterInitStateFeature

void ULyraPawnExtensionComponent::OnRegister()
{
    Super::OnRegister();

    // 当前组件只能挂在pawn上且只有一个的检查

    // 尽早在初始状态系统中注册,这只有在这是游戏世界的情况下才会生效
    RegisterInitStateFeature();
}

这里面核心是actor所在world的game ins 的 UGameFrameworkComponentManager来注册

RegisterFeatureImplementer 里面就是状态的维护了

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
        Manager->RegisterFeatureImplementer(MyActor, MyFeatureName, ThisObject);
    }
}

BeginPlay

在begin play的时候

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

    // Listen for changes to all features
    BindOnActorInitStateChanged(NAME_None, FGameplayTag(), false);

    // Notifies state manager that we have spawned, then try rest of default initialization
    ensure(TryToChangeInitState(LyraGameplayTags::InitState_Spawned));
    CheckDefaultInitialization();
}

BindOnActorInitStateChanged 注册了actor init state 变化的时候的事件

通过 RegisterAndCallForActorInitState,并从接口重写接口回调到 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);
    }
}

这里是监听其他组件ULyraPawnExtensionComponent 的状态是不是到 InitState_DataInitialized

如果是,就推一下自己的状态。

void ULyraHeroComponent::OnActorInitStateChanged(const FActorInitStateChangedParams& Params)
{
    if (Params.FeatureName == ULyraPawnExtensionComponent::NAME_ActorFeatureName)
    {
        if (Params.FeatureState == LyraGameplayTags::InitState_DataInitialized)
        {
            // If the extension component says all all other components are initialized, try to progress to next state
            CheckDefaultInitialization();
        }
    }
}

开始状态是空的,现在开始过度到第一个状态 InitState_Spawned

ensure(TryToChangeInitState(LyraGameplayTags::InitState_Spawned));

TryToChangeInitState 里面

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 we're already in that state, just return
    if (CurrentState == DesiredState)
    {
        return false;
    }

    // 判断能不能切换状态
    if (!CanChangeInitState(Manager, CurrentState, DesiredState))
    {
        UE_LOG(LogModularGameplay, Verbose, TEXT("TryToChangeInitState: Cannot transition %s:%s (role %d) from %s to %s"),
            *MyActor->GetName(), *MyFeatureName.ToString(), MyActor->GetLocalRole(), *CurrentState.ToString(), *DesiredState.ToString());
        return false;
    }

    // Perform the local change,执行转状态
    HandleChangeInitState(Manager, CurrentState, DesiredState);

    // The local change has completed, notify the system to register change and execute callbacks
    // 转换成功了,notify
    return ensure(Manager->ChangeFeatureInitState(MyActor, MyFeatureName, ThisObject, DesiredState));
}

函数主要流程是

判断能不能转状态,转换状态,状态转换成功后切换。

总体上来看就是一个基于tag标记状态的,状态机

CanChangeInitState,HandleChangeInitState是在 ULyraPawnExtensionComponent 里面实现的

然后

CheckDefaultInitialization 在各种地方尝试能不能推荐一下状态机

void ULyraPawnExtensionComponent::CheckDefaultInitialization()
{
    // Before checking our progress, try progressing any other features we might depend on
    // 在检查我们的进展之前,试着推进任何我们可能依赖的其他功能
    CheckDefaultInitializationForImplementers();

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

    // This will try to progress from spawned (which is only set in BeginPlay) through the data initialization stages until it gets to gameplay ready
    // 这将尝试从生成状态(仅在 BeginPlay 中设置)逐步进入数据初始化阶段,直到达到可进行游戏的就绪状态。
    ContinueInitStateChain(StateChain);
}

CheckDefaultInitializationForImplementers 在检查我们的进展之前,试着推进任何我们可能依赖的其他功能

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();
            }
        }
    }
}

会获取所有 IGameFrameworkInitStateInterface 接口尝试调用 CheckDefaultInitialization

然后 ContinueInitStateChain

FGameplayTag IGameFrameworkInitStateInterface::ContinueInitStateChain(const TArray<FGameplayTag>& InitStateChain)
{
    UObject* ThisObject = Cast<UObject>(this);
    AActor* MyActor = GetOwningActor();
    UGameFrameworkComponentManager* Manager = UGameFrameworkComponentManager::GetForActor(MyActor);
    const FName MyFeatureName = GetFeatureName();

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

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

    // For each state in chain before the last, see if we can transition to the next state
    while (ChainIndex < InitStateChain.Num() - 1)
    {
        // 找到当前状态
        if (CurrentState == InitStateChain[ChainIndex])
        {
            FGameplayTag DesiredState = InitStateChain[ChainIndex + 1]; // 下一个状态
            if (CanChangeInitState(Manager, CurrentState, DesiredState)) // 这是那个子类can接口
            {
                // Perform the local change 尝试改状态,也是子类实现的接口
                HandleChangeInitState(Manager, CurrentState, DesiredState);

                // The local change has completed, notify the system to register change and execute callbacks
                // 成功切状态了,广播一下
                ensure(Manager->ChangeFeatureInitState(MyActor, MyFeatureName, ThisObject, DesiredState));

                // Update state and check again 再次获取当前状态,因为可能这次while能连续更新状态
                CurrentState = Manager->GetInitStateForFeature(MyActor, MyFeatureName);
            }
            else
            {
                UE_LOG(LogModularGameplay, Verbose, TEXT("ContinueInitStateChain: Cannot transition %s:%s (role %d) from %s to %s"),
                    *MyActor->GetName(), *MyFeatureName.ToString(), MyActor->GetLocalRole(), *CurrentState.ToString(), *DesiredState.ToString());
            }
        }

        ChainIndex++;
    }

    return CurrentState;
}

前面我们在多个组件中定义了这个状态链,他会不停的在多个组件的各个地方调用 CanChangeInitState

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

判断状态能否过度。

ULyraPawnExtensionComponent 和 ULyraHeroComponent 一起看

bool ULyraPawnExtensionComponent::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const
{
    check(Manager);

    APawn* Pawn = GetPawn<APawn>();
    if (!CurrentState.IsValid() && DesiredState == LyraGameplayTags::InitState_Spawned)
    {
        // 有合法的pawn
    }
    if (CurrentState == LyraGameplayTags::InitState_Spawned && DesiredState == LyraGameplayTags::InitState_DataAvailable)
    {
        // 有合法的pawn data
        // Pawn->HasAuthority() 或者 Pawn->IsLocallyControlled() 必须是服务器或者本地玩家,且不能是controller正在构建
    }
    else if (CurrentState == LyraGameplayTags::InitState_DataAvailable && DesiredState == LyraGameplayTags::InitState_DataInitialized)
    {
        // Transition to initialize if all features have their data available
        // 所有组件都要达到 InitState_DataAvailable 数据可用阶段才可以继续
        return Manager->HaveAllFeaturesReachedInitState(Pawn, LyraGameplayTags::InitState_DataAvailable);
    }
    else if (CurrentState == LyraGameplayTags::InitState_DataInitialized && DesiredState == LyraGameplayTags::InitState_GameplayReady)
    {
        return true;
    }

    return false;
}
bool ULyraHeroComponent::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const
{
    check(Manager);

    APawn* Pawn = GetPawn<APawn>();

    if (!CurrentState.IsValid() && DesiredState == LyraGameplayTags::InitState_Spawned)
    {
        // 有合法的pawn
    }
    else if (CurrentState == LyraGameplayTags::InitState_Spawned && DesiredState == LyraGameplayTags::InitState_DataAvailable)
    {
        // 必须有player state

        // 如果是服务器或者主玩家,需要等 controller 和 player state 关系已经完成初始化并关联

        // Pawn->IsLocallyControlled() 且不能是机器人 Pawn->IsBotControlled() 的情况下
        // 输入,ALyraPlayerController 都准备好了,且类型没问题
    }
    else if (CurrentState == LyraGameplayTags::InitState_DataAvailable && DesiredState == LyraGameplayTags::InitState_DataInitialized)
    {
        // Wait for player state and extension component
        // ULyraPawnExtensionComponent 需要到 InitState_DataInitialized 数据ok状态了
    }
    else if (CurrentState == LyraGameplayTags::InitState_DataInitialized && DesiredState == LyraGameplayTags::InitState_GameplayReady)
    {
        // TODO add ability initialization checks?
        return true;
    }

    return false;
}

文档这句话:

Lyra角色初始化流程很复杂,但许多网络游戏需要的初始化流程差不多同样复杂。初始状态系统旨在帮助用户更轻松地设置复杂系统,避免竞争条件或随机延迟循环。

总结来说我们定义了一个状态。

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

多个组件都要推状态。

状态完成后,在角色蓝图的begin play,先隐藏角色,在最后ready后再显示角色

到这里这个状态机的流程应该是比较清楚了。

其他

这个类还剩一些东西,主要是 AbilitySystemComponent 的初始化和注销

ULyraHeroComponent

英雄组件,由于AI控制的也是玩家角色,所以lyra项目的角色都有这个组件。

IGameFrameworkInitStateInterface 的内容和前面 pawn ext comp 是类似的

OnRegister 里面 RegisterInitStateFeature

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

InitState_DataAvailable=>InitState_DataInitialized

数据准备好后,

看前面的can里面的逻辑,ALyraPlayerState 和 pawn ext那边的条件,所有组件数据都合法了。

然后他要干嘛呢?

主要是 InitializeAbilitySystem 和 InitializePlayerInput 和 ULyraCameraComponent

void ULyraHeroComponent::HandleChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState)
{
    if (CurrentState == LyraGameplayTags::InitState_DataAvailable && DesiredState == LyraGameplayTags::InitState_DataInitialized)
    {
        APawn* Pawn = GetPawn<APawn>();
        ALyraPlayerState* LyraPS = GetPlayerState<ALyraPlayerState>();
        if (!ensure(Pawn && LyraPS))
        {
            return;
        }

        const ULyraPawnData* PawnData = nullptr;

        // 【1】技能
        if (ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
        {
            PawnData = PawnExtComp->GetPawnData<ULyraPawnData>();

            // The player state holds the persistent data for this player (state that persists across deaths and multiple pawns).
            // The ability system component and attribute sets live on the player state.
            PawnExtComp->InitializeAbilitySystem(LyraPS->GetLyraAbilitySystemComponent(), LyraPS);
        }

        // 【2】输入,就是input mapping contex, bind input action 增强输入那些东西
        if (ALyraPlayerController* LyraPC = GetController<ALyraPlayerController>())
        {
            if (Pawn->InputComponent != nullptr)
            {
                InitializePlayerInput(Pawn->InputComponent);
            }
        }

        // Hook up the delegate for all pawns, in case we spectate later
        // 【3】摄像机代理,用于询问当前要用那个摄像机模式
        // 比如如果技能控制了摄像机,就用ASC的,然后是 pawn data 上的默认摄像机模式
        // ShooterCore/Camera/CM_ThirdPersonADS.CM_ThirdPersonADS 找引用
        // 找到 /ShooterCore/Input/Abilities/GA_ADS.GA_ADS
        // 激活技能的时候走 ULyraGameplayAbility::SetCameraMode 来设置这个摄像机模式
        if (PawnData)
        {
            if (ULyraCameraComponent* CameraComponent = ULyraCameraComponent::FindCameraComponent(Pawn))
            {
                CameraComponent->DetermineCameraModeDelegate.BindUObject(this, &ThisClass::DetermineCameraMode);
            }
        }
    }
}

看下剩下的函数

InitializePlayerInput

ULyraHeroComponent::InitializePlayerInput

这个函数太长了

但是本质就是增强输入的 IMC 绑定,和input action 绑定。复杂的内容就是读表。

最后触发事件

    UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(const_cast<APlayerController*>(PC), NAME_BindInputsNow);
    UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(const_cast<APawn*>(Pawn), NAME_BindInputsNow);

这个tag在 game feature action 里面有,

具体的看看输入吧

输入 ULyraInputComponent

这个类要去设置 DefaultInputComponentClass 配置一下

从上面的 ULyraHeroComponent::InitializePlayerInput 这里面还有

UEnhancedInputLocalPlayerSubsystem

UEnhancedInputUserSettings

一个一个看

Subsystem 里面有 Settings

if (Mapping.bRegisterWithSettings)
{
    if (UEnhancedInputUserSettings* Settings = Subsystem->GetUserSettings())
    {
        Settings->RegisterInputMappingContext(IMC);
    }

    FModifyContextOptions Options = {};
    Options.bIgnoreAllPressedKeysUntilRelease = false;
    // Actually add the config to the local player                          
    Subsystem->AddMappingContext(IMC, Mapping.Priority, Options);
}

Subsystem->AddMappingContext(IMC, Mapping.Priority, Options);

我们以前就是这么写的,别多个参数不认识了。

多了一个保存settings的逻辑,引擎类,有个子类 ULyraInputUserSettings 好像定义了没用,按键存档?

ULyraInputComponent* LyraIC = Cast<ULyraInputComponent>(PlayerInputComponent);
if (ensureMsgf(LyraIC, TEXT("Unexpected Input Component class! The Gameplay Abilities will not be bound to their inputs. Change the input component to ULyraInputComponent or a subclass of it.")))
{
    // Add the key mappings that may have been set by the player
    LyraIC->AddInputMappings(InputConfig, Subsystem);

    // This is where we actually bind and input action to a gameplay tag, which means that Gameplay Ability Blueprints will
    // be triggered directly by these input actions Triggered events. 
    TArray<uint32> BindHandles;
    LyraIC->BindAbilityActions(InputConfig, this, &ThisClass::Input_AbilityInputTagPressed, &ThisClass::Input_AbilityInputTagReleased, /*out*/ BindHandles);

    LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Move, ETriggerEvent::Triggered, this, &ThisClass::Input_Move, /*bLogIfNotFound=*/ false);
    LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Look_Mouse, ETriggerEvent::Triggered, this, &ThisClass::Input_LookMouse, /*bLogIfNotFound=*/ false);
    LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Look_Stick, ETriggerEvent::Triggered, this, &ThisClass::Input_LookStick, /*bLogIfNotFound=*/ false);
    LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Crouch, ETriggerEvent::Triggered, this, &ThisClass::Input_Crouch, /*bLogIfNotFound=*/ false);
    LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_AutoRun, ETriggerEvent::Triggered, this, &ThisClass::Input_AutoRun, /*bLogIfNotFound=*/ false);
}

这一段是hero comp 的注册输入,对输入组件的调用

AddInputMappings 是空的,预留逻辑,【在这里,你可以处理任何自定义逻辑,以便在需要时从你的输入配置中添加内容。】

绑定技能输入,结果从 BindHandles 回给上面的hero comp

template<class UserClass, typename PressedFuncType, typename ReleasedFuncType>
void ULyraInputComponent::BindAbilityActions(const ULyraInputConfig* InputConfig, UserClass* Object, PressedFuncType PressedFunc, ReleasedFuncType ReleasedFunc, TArray<uint32>& BindHandles)
{
    check(InputConfig);

    for (const FLyraInputAction& Action : InputConfig->AbilityInputActions)
    {
        if (Action.InputAction && Action.InputTag.IsValid())
        {
            if (PressedFunc)
            {
                BindHandles.Add(BindAction(Action.InputAction, ETriggerEvent::Triggered, Object, PressedFunc, Action.InputTag).GetHandle());
            }

            if (ReleasedFunc)
            {
                BindHandles.Add(BindAction(Action.InputAction, ETriggerEvent::Completed, Object, ReleasedFunc, Action.InputTag).GetHandle());
            }
        }
    }
}

如果是输入就是这样。

template<class UserClass, typename FuncType>
void ULyraInputComponent::BindNativeAction(const ULyraInputConfig* InputConfig, const FGameplayTag& InputTag, ETriggerEvent TriggerEvent, UserClass* Object, FuncType Func, bool bLogIfNotFound)
{
    check(InputConfig);
    if (const UInputAction* IA = InputConfig->FindNativeInputActionForTag(InputTag, bLogIfNotFound))
    {
        BindAction(IA, TriggerEvent, Object, Func);
    }
}

其实没什么特别的。核心就是BindAction包了一层。技能输入和普通的角色控制输入分开了一下。

这个配置在pawn data里面。比如 native input 就有 move, look, look stick(手柄的) crouch, auto run

技能输入,跳跃,换子弹,治疗,冲刺,开火,自动开火

LyraInputModifiers

这里有一堆modifier

https://www.cwlgame.cn/2024/06/25/ue-%e5%a2%9e%e5%bc%ba%e8%be%93%e5%85%a5%e7%b3%bb%e7%bb%9f/

什么是modifier

/*
* Negate
*/

FInputActionValue UInputModifierNegate::ModifyRaw_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue CurrentValue, float DeltaTime)
{
    return CurrentValue.Get<FVector>() * FVector(bX ? -1.f : 1.f, bY ? -1.f : 1.f, bZ ? -1.f : 1.f);
}

常见的就是打勾,这个分量就取反

lyra这里三个是 死区,手柄灵敏度,

ULyraInputUserSettings

这个输入绑定哪里有他的父类,都是这里什么都没有,

最父类是USaveGame,就可以看出是存档设置偏好的,例如存下当前灵敏度初始设置多少

ULyraPlayerMappableKeyProfile

这里是 Input Action 里面最下面可以配置

多套预设配置,例如键盘鼠标,手柄布局A, B

这里啥也没写,具体怎么用不知道

ULyraPlayerInput

这是原本 UPlayerInput 的 Lyra 专用替代品,处理底层的输入信号。

在 Lyra 中,它主要扩展了输入延迟检测(Input Latency Tracking)功能。

它重写了 InputKey 函数,可以在按键按下的瞬间捕获时间戳,用于性能分析(配合 FramePro 或 Unreal Insights)。

它被配置在 LyraPlayerController 或项目的输入默认类中,作为默认的 PlayerInput 类。

每当玩家按下键盘、鼠标或手柄按钮时,信号首先经过这里。

主要用于底层输入处理和性能调试,通常不需要修改它来实现游戏逻辑(游戏逻辑应使用 Gameplay Tags 和 Input Actions)。

上一篇