LyraLog4 角色动画重建记录

体验=>pawn data=>default pawn先配置出来

父类是 hero default => character default 的蓝图,再父类直接到 LyraCharacter

很多组件感觉是蓝图拖进来的

角色蓝图Mesh是不可见的,通过加一个子Actor角色来跑动画的

这个流程是什么呢?

AddCharacterPart

ULyraControllerComponent_CharacterParts 这是一个Controller Component 通过 GameFeatureAction加上来的

提供了一个核心接口 AddCharacterPart 函数

参数最重要的一个是 AEqZeroTaggedActor 蓝图,这个蓝图是一个带gameplay tag 和 skele mesh 的普通Actor

void UEqZeroControllerComponent_CharacterParts::AddCharacterPart(const FEqZeroCharacterPart& NewPart)
{
    AddCharacterPartInternal(NewPart, ECharacterPartSource::Natural);
}

输入参数

USTRUCT(BlueprintType)
struct FEqZeroCharacterPart
{
    GENERATED_BODY()

    // 要生成的部件,比如直接把角色作为一个子 actor 附加上去
    // 上面提到的 AEqZeroTaggedActor 蓝图
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TSubclassOf<AActor> PartClass;

    // 要附加部件的插槽(如果有)
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FName SocketName;

    // 如何处理部件中原始组件的碰撞
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    ECharacterCustomizationCollisionMode CollisionMode = ECharacterCustomizationCollisionMode::NoCollision;
};
void UEqZeroControllerComponent_CharacterParts::AddCharacterPartInternal(const FEqZeroCharacterPart& NewPart, ECharacterPartSource Source)
{
    FEqZeroControllerCharacterPartEntry& NewEntry = CharacterParts.AddDefaulted_GetRef();
    NewEntry.Part = NewPart;
    NewEntry.Source = Source;

    if (UEqZeroPawnComponent_CharacterParts* PawnCustomizer = GetPawnCustomizer())
    {
        if (NewEntry.Source != ECharacterPartSource::NaturalSuppressedViaCheat)
        {
            NewEntry.Handle = PawnCustomizer->AddCharacterPart(NewPart);
        }
    }
}

FEqZeroControllerCharacterPartEntry 理解为构建一个请求

Part 是那个 Actor,Source是构建原因,可能的原因还有GM创建等

然后拿到 UEqZeroPawnComponent_CharacterParts

这是一个PawnCommon 直接拖到角色蓝图上的

跑到了里面的AddCharacterPart

这里的 Handle 是一个 int 具体干吗用还不知道

FEqZeroCharacterPartHandle UEqZeroPawnComponent_CharacterParts::AddCharacterPart(const FEqZeroCharacterPart& NewPart)
{
    return CharacterPartList.AddEntry(NewPart);
}

这里只是存一下。把输入参数那个结构丢进去了

FEqZeroCharacterPartHandle FEqZeroCharacterPartList::AddEntry(FEqZeroCharacterPart NewPart)
{
    // 一个只有一个数字的句柄,数值是计数器,指向这里第几个添加的外观部件
    FEqZeroCharacterPartHandle Result;
    Result.PartHandle = PartHandleCounter++;

    if (ensure(OwnerComponent && OwnerComponent->GetOwner() && OwnerComponent->GetOwner()->HasAuthority()))
    {
        // 服务器添加这个部件
        FEqZeroAppliedCharacterPartEntry& NewEntry = Entries.AddDefaulted_GetRef();
        NewEntry.Part = NewPart;
        NewEntry.PartHandle = Result.PartHandle;

        // spawn actor 这里是服务器,但是里面写了 DS 不生成 actor
        // listen server 就会 True,权威且非DS
        if (SpawnActorForEntry(NewEntry))
        {
            OwnerComponent->BroadcastChanged();
        }

        // 标记数组脏,触发复制
        MarkItemDirty(NewEntry);
    }

    return Result;
}
    UPROPERTY(Replicated, Transient)
    FEqZeroCharacterPartList CharacterPartList;

这是一个 FFastArraySerializer 的子类

所以

应该会有一个Tarray的Entries,然后是owner comp 和计数器

USTRUCT(BlueprintType)
struct FEqZeroCharacterPartList : public FFastArraySerializer
{
    GENERATED_BODY()

    // ...

private:
    UPROPERTY()
    TArray<FEqZeroAppliedCharacterPartEntry> Entries;

    UPROPERTY(NotReplicated)
    TObjectPtr<UEqZeroPawnComponent_CharacterParts> OwnerComponent;

    int32 PartHandleCounter = 0;
};

前面dirty后就会触发这个同步

    // 这三在客户端执行
    //~FFastArraySerializer contract
    void PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize);
    void PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize);
    void PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize);
    //~End of FFastArraySerializer contract
void FEqZeroCharacterPartList::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)
{
    bool bCreatedAnyActors = false;
    for (int32 Index : AddedIndices)
    {
        FEqZeroAppliedCharacterPartEntry& Entry = Entries[Index];
        bCreatedAnyActors |= SpawnActorForEntry(Entry);
    }

    if (bCreatedAnyActors && ensure(OwnerComponent))
    {
        OwnerComponent->BroadcastChanged();
    }
}

客户端 SpawnActorForEntry

bool FEqZeroCharacterPartList::SpawnActorForEntry(FEqZeroAppliedCharacterPartEntry& Entry)
{
    bool bCreatedAnyActors = false;

    if (ensure(OwnerComponent) && !OwnerComponent->IsNetMode(NM_DedicatedServer))
    {
        if (Entry.Part.PartClass != nullptr)
        {
            UWorld* World = OwnerComponent->GetWorld();

            // 找一个地方附加这个部件,通常是角色的 Mesh 组件
            if (USceneComponent* ComponentToAttachTo = OwnerComponent->GetSceneComponentToAttachTo())
            {
                const FTransform SpawnTransform = ComponentToAttachTo->GetSocketTransform(Entry.Part.SocketName);
                UChildActorComponent* PartComponent = NewObject<UChildActorComponent>(OwnerComponent->GetOwner());
                PartComponent->SetupAttachment(ComponentToAttachTo, Entry.Part.SocketName);
                PartComponent->SetChildActorClass(Entry.Part.PartClass);
                PartComponent->RegisterComponent(); // 这里会触发生成 actor

                if (AActor* SpawnedActor = PartComponent->GetChildActor())
                {
                    switch (Entry.Part.CollisionMode)
                    {
                    case ECharacterCustomizationCollisionMode::UseCollisionFromCharacterPart:
                        // Do nothing
                        break;

                    case ECharacterCustomizationCollisionMode::NoCollision:
                        SpawnedActor->SetActorEnableCollision(false);
                        break;
                    }

                    // Set up a direct tick dependency to work around the child actor component not providing one
                    // 解决动画抖动问题, 如果是饰品,必须保证饰品在身体之后更新(Tick),
                    if (USceneComponent* SpawnedRootComponent = SpawnedActor->GetRootComponent())
                    {
                        SpawnedRootComponent->AddTickPrerequisiteComponent(ComponentToAttachTo);
                    }
                }

                Entry.SpawnedComponent = PartComponent;
                bCreatedAnyActors = true;
            }
        }
    }

    return bCreatedAnyActors;
}

蓝图部分

/Game/Characters/Cosmetics/B_Manny.B_Manny

父类是 Tagged Actor 里面配置对应特征TAG 和 skelemesh,然后配置mesh和动画蓝图

==

/Game/Characters/Cosmetics/B_PickRandomCharacter.B_PickRandomCharacter

创建 UEqZeroControllerComponent_CharacterParts 是一个Controller Comp

在Begin Player 调用 Add Character Part 参数填上面的Actor

这个组件通过体验加载到Controller上面

==

把 UEqZeroPawnComponent_CharacterParts 拖进角色蓝图的组件里面,然后配置 BodyMeshes

到这里,逻辑跑到了上面的spawn actor 角色出来了

在 FFastArraySerializer 的同步后会触发事件

void UEqZeroPawnComponent_CharacterParts::BroadcastChanged()
{
    const bool bReinitPose = true;

    // 装扮组件变化的时候触发
    if (USkeletalMeshComponent* MeshComponent = GetParentMeshComponent())
    {
        // 从加的Tagged Actor 的标签拿到两个。然后和自己的配置匹配拿到了mesh
        const FGameplayTagContainer MergedTags = GetCombinedTags(FGameplayTag());
        USkeletalMesh* DesiredMesh = BodyMeshes.SelectBestBodyStyle(MergedTags);

        // 如果存在与网格体无关的强制覆盖,请应用所需的物理资源
        MeshComponent->SetSkeletalMesh(DesiredMesh, bReinitPose);

        // 如果存在与网格体无关的强制覆盖,请应用所需的物理资源
        if (UPhysicsAsset* PhysicsAsset = BodyMeshes.ForcedPhysicsAsset)
        {
            MeshComponent->SetPhysicsAsset(PhysicsAsset, bReinitPose);
        }
    }

    // 这个在蓝图里面有东西,队伍装扮的逻辑
    OnCharacterPartsChanged.Broadcast(this);
}

这个在蓝图里面有东西,队伍装扮的逻辑。目前先不管

/ShooterCore/Weapons/Pistol/B_WeaponInstance_Pistol.B_WeaponInstance_Pistol

在武器上有这样的配置

比如

/Game/Characters/Heroes/Mannequin/Animations/Locomotion/Pistol/ABP_PistolAnimLayers_Feminine.ABP_PistolAnimLayers_Feminine

父类

/Game/Characters/Heroes/Mannequin/Animations/LinkedLayers/ABP_ItemAnimLayersBase.ABP_ItemAnimLayersBase

这个 ItemAnimLayersBase是写逻辑的。子类是哪里配置具体的

这个动画层是拿来link的,在没有武器系统的时候外面直接link anim layer

这里发现漏了个包 Animation Locomotion Library

跑通动画蓝图

/Game/Characters/Heroes/Mannequin/Animations/ABP_Mannequin_Base.ABP_Mannequin_Base

角色蓝图链接这个里面返回了一个步行动画

运行时是这样的

下面那个子Actor是前面外面提到的

AEqZeroTaggedActor 里面是 skele mesh

里面的动画蓝图是

/Game/Characters/Heroes/Mannequin/Animations/ABP_Mannequin_CopyPose.ABP_Mannequin_CopyPose

拷贝父节点的动画姿势

输入控制

GameFeatureAction 里面 ShooterCore

通过AddInput Mapping 添加了 Content 和 Shooter 里面 Input/Mapping 的 Input Mapping Context

pawn data => input comfig (lyra input config)

/Game/Input/InputData_Hero.InputData_Hero

里面有一些移动,视角,蹲伏,自动移动的输入事件

还有跳跃,开火,装弹,冲刺,治疗的技能输入事件

== 技能集合

这时候发现 game future 里面也有一个 tag 的配置没有挪过来 ShooterCoreTags.ini

/ShooterCore/Experiences/LAS_ShooterGame_SharedInput.LAS_ShooterGame_SharedInput

这是体验中的输入技能集

通过GameFeatureAction Add InputBind 配置了一个输入配置LyraInputConfig

有一些近战,开镜的技能

通过GameFeatureAction Add Input Mapping 添加了 IMC (重复了)

== 总结

GameFeature 配置了 Action来添加了 Input Mapping Context

PawnData->Input Config (Lyra Input Config) 配置了Input Action

其他的走输入技能集合(Action Set)

里面组合添加了 lyra input config (就是Input Action)

还有通过Action添加了 IMC

==

动画第一部分

主动画蓝图

/Game/Characters/Heroes/Mannequin/Animations/ABP_Mannequin_Base.ABP_Mannequin_Base

/Game/Characters/Heroes/Mannequin/Animations/LinkedLayers/ABP_ItemAnimLayersBase.ABP_ItemAnimLayersBase

动画层接口

/Game/Characters/Heroes/Mannequin/Animations/LinkedLayers/ALI_ItemAnimLayers.ALI_ItemAnimLayers

两个动画蓝图

我们简单称之为Base 和 Item,他们都实现了动画层接口

主角色用的动画蓝图是Base。主角色蓝图begin play 中 link anim layer class 中关联 Item动画蓝图。

正式的流程是在武器里面的。

到此,Base的动画能直接在游戏中播放了。

上一篇
下一篇