라이라의 코스메틱 시스템을 알아보기 전에 Modular Gameplay Actor 먼저

B_SimpleHeroPawn은 메시가 직접 부착되어있다.

그런데 B_Hero_ShooterMannequin을 보면 스태틱 메시가 없다.
라이라에서는 직접 메시를 설정하는게 아니라, GameFeature의 AddComponent 기능을 이용해서 메시를 컴포넌트로 부착한다.
이때 AddComponent 하려는 대상은 Receiver로 등록해야 한다
그런 액터들에서 직접 다 AddReceiver를 호출하지는 않고.. ModularGameplayActor를 사용한다
ModularGameplayActors 플러그인에 있는 클래스들
void AModularCharacter::PreInitializeComponents()
{
Super::PreInitializeComponents();
UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this);
}
초기화할 때 UGameFrameworkComponentManager에 리시버로 등록한다
void AModularCharacter::BeginPlay()
{
UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady);
Super::BeginPlay();
}
다른 곳에서 BeginPlay 시점에 특정 동작을 수행할 수 있도록 이벤트를 보낸다.
void AModularCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this);
Super::EndPlay(EndPlayReason);
}
액터가 사라질 때 리시버 해제해준다.
ALyraCharacter, ALyraPlayerController는 각각 AModularCharacter, AModularPlayerController를 상속받는다
즉 UGameFrameworkComponentManager에 ComponentReceiver로 등록된다.
UCLASS(meta = (BlueprintSpawnableComponent))
class ULyraControllerComponent_CharacterParts : public UControllerComponent
{
// ...
protected:
UPROPERTY(EditAnywhere, Category=Cosmetics)
TArray<FLyraControllerCharacterPartEntry> CharacterParts;
};
FLyraControllerCharacterPartEntry 어레이를 들고 있다
// A character part requested on a controller component
USTRUCT()
struct FLyraControllerCharacterPartEntry
{
// ...
public:
// 캐릭터 파츠에 대한 정의 (메타데이터)
UPROPERTY(EditAnywhere, meta=(ShowOnlyInnerProperties))
FLyraCharacterPart Part;
// 컨트롤러가 보고있는 pawn에서 생성한 캐릭터 파츠 인스턴스
// 이 파츠를 생성하지 않았으면 없을 것..
FLyraCharacterPartHandle Handle;
};
FLyraControllerCharacterPartEntry는 각 캐릭터 파츠에 대한 메타데이터와 캐릭터 파츠를 생성했다면 그 인스턴스를 들고 있다
USTRUCT(BlueprintType)
struct FLyraCharacterPart
{
GENERATED_BODY()
// 생성할 액터 클래스
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSubclassOf<AActor> PartClass;
// 액터 부착할 소켓 정보. 없으면 Root에 부착
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName SocketName;
// ...
};
// A handle created by adding a character part entry, can be used to remove it later
USTRUCT(BlueprintType)
struct FLyraCharacterPartHandle
{
// ...
private:
UPROPERTY()
int32 PartHandle = INDEX_NONE;
};
// A component that handles spawning cosmetic actors attached to the owner pawn on all clients
UCLASS(meta=(BlueprintSpawnableComponent))
class ULyraPawnComponent_CharacterParts : public UPawnComponent
{
// ...
private:
// 캐릭터 파츠 인스턴스
UPROPERTY(Replicated)
FLyraCharacterPartList CharacterPartList;
// 애니메이션 적용을 위한 메시와의 연결고리
UPROPERTY(EditAnywhere, Category=Cosmetics)
FLyraAnimBodyStyleSelectionSet BodyMeshes;
};
// Replicated list of applied character parts
USTRUCT(BlueprintType)
struct FLyraCharacterPartList : public FFastArraySerializer
{
// ...
private:
// 현재 적용되어있는 캐릭터 파츠 정보들
UPROPERTY()
TArray<FLyraAppliedCharacterPartEntry> Entries;
// 이 캐릭터 파츠 인스턴스를 가지고 있는 PawnComponent
UPROPERTY(NotReplicated)
TObjectPtr<ULyraPawnComponent_CharacterParts> OwnerComponent;
// Upcounter for handles
int32 PartHandleCounter = 0;
};
생성되어있는 캐릭터 파츠 인스턴스 정보
USTRUCT()
struct FLyraAppliedCharacterPartEntry : public FFastArraySerializerItem
{
// ...
private:
// 생성한 캐릭터 파츠 액터 정보 (메타데이터)
UPROPERTY()
FLyraCharacterPart Part;
// LyraControllerComponent_CharacterParts의 핸들과 맞춤
UPROPERTY(NotReplicated)
int32 PartHandle = INDEX_NONE;
// 부착한 액터의 스폰 정보
// 액터에 액터를 부착할 때는 ChildActorComponent를 사용해야 함
UPROPERTY(NotReplicated)
TObjectPtr<UChildActorComponent> SpawnedComponent = nullptr;
};
참고로.. FLyraCharacterPartList, FLyraAppliedCharacterPartEntry와 같이 이런식으로 struct가 나뉘어진 이유는
네트워크 데이터 전송 시 변경된 데이터 정보만 보내기 위해서다
(그래서 부모가 각각 FFastArraySerializer, FFastArraySerializerItem)
USTRUCT(BlueprintType)
struct FLyraAnimBodyStyleSelectionSet
{
GENERATED_BODY()
// 애니메이션과 메시 간의 규칙. 애니메이션을 어느 메시에 적용할 지에 대한? 태그 기반
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(TitleProperty=Mesh))
TArray<FLyraAnimBodyStyleSelectionEntry> MeshRules;
// MeshRules로 애니메이션을 적용할 메시를 찾지 못했을 때 디폴트로 적용할 메시
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<USkeletalMesh> DefaultMesh = nullptr;
// 애니메이션은 여러 개여도 PhisicsAsset은 하나로 통일
UPROPERTY(EditAnywhere)
TObjectPtr<UPhysicsAsset> ForcedPhysicsAsset = nullptr;
USkeletalMesh* SelectBestBodyStyle(const FGameplayTagContainer& CosmeticTags) const;
};
USTRUCT(BlueprintType)
struct FLyraAnimBodyStyleSelectionEntry
{
GENERATED_BODY()
// AnimLayer 적용할 스켈레탈 메시
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<USkeletalMesh> Mesh = nullptr;
// 어떤 태그일 때 이 애니메이션을 적용할지
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(Categories="Cosmetic"))
FGameplayTagContainer RequiredTags;
};
정리하자면..
여러 가지 파츠 타입(ex. 여자, 남자, ..) 있고 각 메시가 있을 때
FLyraAnimBodyStyleSelectionEntry에서 여자 메시, 여자 태그를 설정한다.
캐릭터 파츠 액터에 미리 설정된 태그에 따라서, MeshRules를 보고 어떤 메시에 애니메이션을 적용할지 결정한다.
(그러려면 캐릭터 파츠 클래스가 뒤에 나오는 ALyraTaggedActor여야 할 듯)
잠시 에셋 살펴보기..


LyraExperienceDefinition에서 설정하는 Action 중 하나인 AddComponents
모든 Controller 클래스에 B_PickRandomCharacter를 부착한다.

여기서 설정한 스켈레탈 메시는 충돌만 담당하고 머터리얼이 세팅되어있지 않다.

LyraControllerComponent_CharacterParts는 AddComponent로 컨트롤러에 부착했지만,
LyraPawnComponent_CharacterParts는 직접 캐릭터에 배치한다.
FLyraAnimBodyStyleSelection) 모든 캐릭터에 막 부착해서는 안된다
ALyraTaggedActor에셋인 B_Manny
루트는 SkeletalMeshComponent
폰에 B_Manny가 캐릭터 파츠로서 부착되면 B_Manny는 태그가 없으니까 DefaultMesh인 SKM_Manny_Invis가 장착될 것이다
(동작은 이후 설명)
void ULyraControllerComponent_CharacterParts::AddCharacterPartInternal(const FLyraCharacterPart& NewPart, ECharacterPartSource Source)
{
FLyraControllerCharacterPartEntry& NewEntry = CharacterParts.AddDefaulted_GetRef();
NewEntry.Part = NewPart;
NewEntry.Source = Source;
if (ULyraPawnComponent_CharacterParts* PawnCustomizer = GetPawnCustomizer())
{
if (NewEntry.Source != ECharacterPartSource::NaturalSuppressedViaCheat)
{
NewEntry.Handle = PawnCustomizer->AddCharacterPart(NewPart);
}
}
}
FLyraCharacterPartHandle ULyraPawnComponent_CharacterParts::AddCharacterPart(const FLyraCharacterPart& NewPart)
{
return CharacterPartList.AddEntry(NewPart);
}
FLyraCharacterPartList에서 관리한다.void ULyraPawnComponent_CharacterParts::BroadcastChanged()
{
const bool bReinitPose = true;
// Check to see if the body type has changed
if (USkeletalMeshComponent* MeshComponent = GetParentMeshComponent())
{
// Determine the mesh to use based on cosmetic part tags
const FGameplayTagContainer MergedTags = GetCombinedTags(FGameplayTag());
USkeletalMesh* DesiredMesh = BodyMeshes.SelectBestBodyStyle(MergedTags);
// Apply the desired mesh (this call is a no-op if the mesh hasn't changed)
MeshComponent->SetSkeletalMesh(DesiredMesh, /*bReinitPose=*/ bReinitPose);
// Apply the desired physics asset if there's a forced override independent of the one from the mesh
if (UPhysicsAsset* PhysicsAsset = BodyMeshes.ForcedPhysicsAsset)
{
MeshComponent->SetPhysicsAsset(PhysicsAsset, /*bForceReInit=*/ bReinitPose);
}
}
// Let observers know, e.g., if they need to apply team coloring or similar
OnCharacterPartsChanged.Broadcast(this);
}
FLyraCharacterPartHandle FLyraCharacterPartList::AddEntry(FLyraCharacterPart NewPart)
{
FLyraCharacterPartHandle Result;
Result.PartHandle = PartHandleCounter++;
if (ensure(OwnerComponent && OwnerComponent->GetOwner() && OwnerComponent->GetOwner()->HasAuthority()))
{
FLyraAppliedCharacterPartEntry& NewEntry = Entries.AddDefaulted_GetRef();
NewEntry.Part = NewPart;
NewEntry.PartHandle = Result.PartHandle;
if (SpawnActorForEntry(NewEntry))
{
OwnerComponent->BroadcastChanged();
}
MarkItemDirty(NewEntry);
}
return Result;
}
bool FLyraCharacterPartList::SpawnActorForEntry(FLyraAppliedCharacterPartEntry& Entry)
{
bool bCreatedAnyActors = false;
if (!OwnerComponent->IsNetMode(NM_DedicatedServer))
{
if (Entry.Part.PartClass != nullptr)
{
UWorld* World = OwnerComponent->GetWorld();
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();
if (AActor* SpawnedActor = PartComponent->GetChildActor())
{
if (USceneComponent* SpawnedRootComponent = SpawnedActor->GetRootComponent())
{
SpawnedRootComponent->AddTickPrerequisiteComponent(ComponentToAttachTo);
}
}
Entry.SpawnedComponent = PartComponent;
bCreatedAnyActors = true;
}
}
}
return bCreatedAnyActors;
}
이제 B_PickRandomCharacter에서 AddCharacterPart로 B_Manny를 장착하면 폰에 B_Manny가 장착된다