体验=>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的动画能直接在游戏中播放了。