Post

UE - 라이라 Cosmetic 분석-2

UnrealEngine, Lyra

UE - 라이라 Cosmetic 분석-2

개요

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Replicated list of applied character parts  
USTRUCT(BlueprintType)  
struct FLyraCharacterPartList : public FFastArraySerializer  
{  
    GENERATED_BODY()  
  
    FLyraCharacterPartList()  
       : OwnerComponent(nullptr)  
    {  
    }  
  
public:  
    //~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  
  
    bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)  
    {  
       return FFastArraySerializer::FastArrayDeltaSerialize<FLyraAppliedCharacterPartEntry, FLyraCharacterPartList>(Entries, DeltaParms, *this);  
    }  
  
    FLyraCharacterPartHandle AddEntry(FLyraCharacterPart NewPart);  
    void RemoveEntry(FLyraCharacterPartHandle Handle);  
    void ClearAllEntries(bool bBroadcastChangeDelegate);  
  
    FGameplayTagContainer CollectCombinedTags() const;  
  
    void SetOwnerComponent(ULyraPawnComponent_CharacterParts* InOwnerComponent)  
    {  
       OwnerComponent = InOwnerComponent;  
    }  
      
private:  
    friend ULyraPawnComponent_CharacterParts;  
  
    bool SpawnActorForEntry(FLyraAppliedCharacterPartEntry& Entry);  
    bool DestroyActorForEntry(FLyraAppliedCharacterPartEntry& Entry);  
  
private:  
    // Replicated list of equipment entries  
    UPROPERTY()  
    TArray<FLyraAppliedCharacterPartEntry> Entries;  
  
    // The component that contains this list  
    UPROPERTY(NotReplicated)  
    TObjectPtr<ULyraPawnComponent_CharacterParts> OwnerComponent;  
  
    // Upcounter for handles  
    int32 PartHandleCounter = 0;  
};

FFastArraySerializer: 네트워크 동기화가 필요한 최적화된 방식으로 복제할 때 사용.

서버-클라이언트 간 플레이어 인벤토리, 스코어보드, 액티브 버프 목록 등 자주 변하는 데이터 구조에서 사용한다.

배열 전체를 복제하는 대신 변경된 부분만 동기화하는 방식을 제공하는 클래스라고 한다?

데이터 동기화 함수들

PreReplicatedRemove PostReplicatedAdd PostReplicatedChange

위 세 함수들은 데이터 동기화를 위해 필요한 함수들이다.

PreReplicatedRemove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void FLyraCharacterPartList::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)  
{  
    bool bDestroyedAnyActors = false;  
    for (int32 Index : RemovedIndices)  
    {  
       FLyraAppliedCharacterPartEntry& Entry = Entries[Index];  
       bDestroyedAnyActors |= DestroyActorForEntry(Entry);  
    }  
  
    if (bDestroyedAnyActors && ensure(OwnerComponent))  
    {  
       OwnerComponent->BroadcastChanged();  
    }  
}

복제되기 전, 삭제할 요소들을 제거하는 함수다. DestrotyActorForEntry 함수로

1
2
3
4
5
6
7
8
9
10
11
12
13
bool FLyraCharacterPartList::DestroyActorForEntry(FLyraAppliedCharacterPartEntry& Entry)  
{  
    bool bDestroyedAnyActors = false;  
  
    if (Entry.SpawnedComponent != nullptr)  
    {  
       Entry.SpawnedComponent->DestroyComponent();  
       Entry.SpawnedComponent = nullptr;  
       bDestroyedAnyActors = true;  
    }  
  
    return bDestroyedAnyActors;  
}

EntrySpawnedComponent가 존재하면 삭제하고 true를 반환한다.

이제 하나라도 삭제된 것이 있고, 컴포넌트의 주인이 남아있다면 주인에게 알림을 보내준다.

PostReplicatedAdd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void FLyraCharacterPartList::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)  
{  
    bool bCreatedAnyActors = false;  
    for (int32 Index : AddedIndices)  
    {  
       FLyraAppliedCharacterPartEntry& Entry = Entries[Index];  
       bCreatedAnyActors |= SpawnActorForEntry(Entry);  
    }  
  
    if (bCreatedAnyActors && ensure(OwnerComponent))  
    {  
       OwnerComponent->BroadcastChanged();  
    }  
}

이건 새로 추가할 때 사용하는 함수

PostReplicatedChange

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void FLyraCharacterPartList::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)  
{  
    bool bChangedAnyActors = false;  
  
    // We don't support dealing with propagating changes, just destroy and recreate  
    for (int32 Index : ChangedIndices)  
    {  
       FLyraAppliedCharacterPartEntry& Entry = Entries[Index];  
  
       bChangedAnyActors |= DestroyActorForEntry(Entry);  
       bChangedAnyActors |= SpawnActorForEntry(Entry);  
    }  
  
    if (bChangedAnyActors && ensure(OwnerComponent))  
    {  
       OwnerComponent->BroadcastChanged();  
    }  
}

이 함수는 변경된 파츠들을 삭제하고 재생성하는 함수,

위 세 함수는 서버에서 배열이 변경될 때 클라이언트에서 자동으로 실행되는 함수다.

배열에 요소를 추가하는 함수

AddEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;  
}

캐릭터 파츠를 매개변수로 받아 핸들에 카운터 증가.

서버면 새로운 엔트리를 생성하고, 엔트리에 파츠랑 핸들 인덱스를 설정 후 SpawnActorForEntry, MakeItemDirty 호출

마지막으로 핸들을 반환해준다.

1
2
3
4
5
6
7
ElementType& AddDefaulted_GetRef() UE_LIFETIMEBOUND
{  
    const SizeType Index = AddUninitialized();  
    ElementType* Ptr = GetData() + Index;  
    DefaultConstructItems<ElementType>((void*)Ptr, 1);  
    return *Ptr;  
}

Entry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
USTRUCT()  
struct FLyraAppliedCharacterPartEntry : public FFastArraySerializerItem  
{  
    GENERATED_BODY()  
  
    FLyraAppliedCharacterPartEntry()  
    {}  
  
    FString GetDebugString() const;  
  
private:  
    friend FLyraCharacterPartList;  
    friend ULyraPawnComponent_CharacterParts;  
  
private:  
    // The character part being represented  
    UPROPERTY()  
    FLyraCharacterPart Part;  
  
    // Handle index we returned to the user (server only)  
    UPROPERTY(NotReplicated)  
    int32 PartHandle = INDEX_NONE;  
  
    // The spawned actor instance (client only)  
    UPROPERTY(NotReplicated)  
    TObjectPtr<UChildActorComponent> SpawnedComponent = nullptr;  
};

보유한 변수로 보아 단일 캐릭터 파츠를 관리하는 구조체로 예상

이것도 FFastArraySerializerItem을 상속받았기에 자동으로 복제되는 구조체다.

마지막 변수는 리플랙션으로 동기화를 막아줬는데, 이건 클라이언트에서 생성된 액터의 인스턴스를 저장하는 용도로 보인다.

SpawnActorForEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
bool FLyraCharacterPartList::SpawnActorForEntry(FLyraAppliedCharacterPartEntry& Entry)  
{  
    bool bCreatedAnyActors = false;  
  
    if (ensure(OwnerComponent) && !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())  
             {  
                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  
                if (USceneComponent* SpawnedRootComponent = SpawnedActor->GetRootComponent())  
                {  
                   SpawnedRootComponent->AddTickPrerequisiteComponent(ComponentToAttachTo);  
                }  
             }  
  
             Entry.SpawnedComponent = PartComponent;  
             bCreatedAnyActors = true;  
          }  
       }  
    }  
  
    return bCreatedAnyActors;  
}

서버 전용으로 보이며, 주인의 Scene을 가져온다.
UChildActorComponent* 변수를 생성한다.

이걸 소켓에 붙이고, SetChildActorClass, RegisterComponent 두 함수 실행

제대로 생성되었는지 확인하고, 생성했으면 충돌 모드에 따라 파츠의 충돌 설정

파츠의 루트에 AddTickPrerequisiteComponent를 해준다?

EntrySpawnedComponent를 여기서 생성한 PartComponent로 설정해준다.

MakeItemDrity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** This must be called if you add or change an item in the array */  
void MarkItemDirty(FFastArraySerializerItem & Item)  
{  
    if (Item.ReplicationID == INDEX_NONE)  
    {  
       Item.ReplicationID = ++IDCounter;  
       if (IDCounter == INDEX_NONE)  
       {  
          IDCounter++;  
       }  
    }  
  
    Item.ReplicationKey++;  
    MarkArrayDirty();  
}

만약 아이템이 추가되거나 변경될 때 반드시 호출되는 함수
매개변수로 받은 ItemReplicationID가 -1 이면 0으로 세팅하고, IDCounter가 -1이면 한 번 더 해준다.

그냥 -1 이상으로 세팅하는, 인덱스 부여 함수.

여기서 호출하는 MarkArrayDirty 까지 포함해서 클라이언트 동기화를 하는 부분으로 보인다.

Owner 결정 함수

1
2
3
4
void SetOwnerComponent(ULyraPawnComponent_CharacterParts* InOwnerComponent)  
{  
    OwnerComponent = InOwnerComponent;  
}

OwnerULyraPawnComponent_CharacterParts* 클래스, 그 자식만 사용 가능.

결론

  • FLyraCharacterPartList는 캐릭터 파츠를 관리하는 구조체
  • Entry라는 구조체로 단일 캐릭터 파츠를 관리하고, 이 Entry를 여러 개 가지게 된다.
1
2
UPROPERTY()  
TArray<FLyraAppliedCharacterPartEntry> Entries;

AddEntry를 통해 Entry를 추가 가능하고,

1
FLyraAppliedCharacterPartEntry& NewEntry = Entries.AddDefaulted_GetRef();

이 코드로 새로운 요소를 추가 후 데이터 세팅

완료된 후 MakeItemDirty로 동기화를 해준다.

This post is licensed under CC BY 4.0 by the author.