Unreal TIL 14일차 - Fast Array Serializer

yys·2026년 3월 16일

TIL

목록 보기
12/76

이번에도 개인적으로 인벤토리 시스템을 공부하는 데 있어, FastArraySerializer라는 개념이 나와 정리해보려고 한다.

Fast Array Serializer


언리얼의 복제 시스템은 변수 단위로 이루어진다.
TArray로 공간을 100개를 할당했다고 가정했을 때, 1칸의 데이터가 달라졌어도 100개의 데이터를 복제해버린다.
이러한 공간의 규모가 커지거나 배열의 접근이 자주 이루어진다면 많은 대역폭 손실이 일어날 수 있다.

Fast Array Serializer는 이전 변경 값/현재 변경 값을 비교하여 차이가 나는 부분만 복제하는 특징을 지닌다.

Fast Array Serializer의 선언 순서는 다음과 같이 이루어진다.

1. FFastArraySerializerItem 을 상속받는 개별 아이템 구조체를 생성

2. FFastArraySerializer를 상속받는 배열 관리 구조체를 생성

3. 1에서 기반한 아이템들을 담을 TArray를 UPROPERTY()로 선언

4. 2의 구조체 안에 NetDeltaSerialize함수를 선언하고 그 안에서 FastArrayDeltaSerialize를 호출

5. 구조체 바깥에 TStructOpsTypeTraits를 선언해서 WithNetDeltaSerializer = true로 설정

6. 아이템의 생성과 삭제는 각각 MarkItemDirty()/MarkArrayDirty()를 사용하여 신호를 줄 것

7. Fast Array Serializer로 선언된 Array는 GetLifetimeReplicatedProps()에서 Replicated 처리를 해준다.

Fast Array Serializer를 테스트 해보기 위해서 다음과 같은 ActorComponent를 구현해보았다.

// SimpleFastArrayComponent.h
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Net/Serialization/FastArraySerializer.h"
#include "SimpleFastArrayComponent.generated.h"

// 1번
USTRUCT(BlueprintType)
struct FSimpleDataEntry : public FFastArraySerializerItem
{
	GENERATED_BODY()

	UPROPERTY(BlueprintReadWrite, Category = "Test")
	FString Message;

	// 클라이언트 동기화 콜백
	// void PreReplicatedRemove(const struct FSimpleDataArray& InArraySerializer);
	// void PostReplicatedAdd(const struct FSimpleDataArray& InArraySerializer);
};

// 2번
USTRUCT(BlueprintType)
struct FSimpleDataArray : public FFastArraySerializer
{
	GENERATED_BODY()
	// 3번
	UPROPERTY()
	TArray<FSimpleDataEntry> Entries;
	// 4번
	bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParams)
	{
		return FFastArraySerializer::FastArrayDeltaSerialize<FSimpleDataEntry, FSimpleDataArray>(Entries, DeltaParams, *this);
	}
};
// 5번
template<>
struct TStructOpsTypeTraits<FSimpleDataArray> : public TStructOpsTypeTraitsBase2<FSimpleDataArray>
{
	enum { WithNetDeltaSerializer = true };
};


UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class USimpleFastArrayComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	USimpleFastArrayComponent();

	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
	// 7번
	UPROPERTY(Replicated)
	FSimpleDataArray DataArray;
    
	// 클라에서도 함수 호출 요청할 수 있도록 Server RPC 선언
	UFUNCTION(Server, Reliable, BlueprintCallable, Category = "FastArray Test")
	void Server_AddData(const FString& NewMessage);

	UFUNCTION(Server, Reliable, BlueprintCallable, Category = "FastArray Test")
	void Server_RemoveData(const FString& MessageToRemove);
};






// SimpleFastArrayComponent.cpp

USimpleFastArrayComponent::USimpleFastArrayComponent()
{
	PrimaryComponentTick.bCanEverTick = false;
	SetIsReplicatedByDefault(true);
}

void USimpleFastArrayComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(USimpleFastArrayComponent, DataArray);
}

void USimpleFastArrayComponent::Server_AddData_Implementation(const FString& NewMessage)
{
	// DataArray에 공간 1개를 확보하고 이를 참조한 값을 반환
	FSimpleDataEntry& NewEntry = DataArray.Entries.AddDefaulted_GetRef();
	NewEntry.Message = NewMessage;

	// 개별 아이템 추가 신고
	DataArray.MarkItemDirty(NewEntry);

	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("데이터 추가: %s"), *NewMessage));
	}

	FString Message;
	for (const FSimpleDataEntry& Entry : DataArray.Entries)
	{
		Message += Entry.Message + TEXT(" | ");
	}
	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, FString::Printf(TEXT("현재 배열 상태: %s"), *Message));
	}
}

void USimpleFastArrayComponent::Server_RemoveData_Implementation(const FString& MessageToRemove)
{
	for (auto It = DataArray.Entries.CreateIterator(); It; ++It)
	{
		if (It->Message == MessageToRemove)
		{
			It.RemoveCurrent();
			// 배열의 전체 구조 변화 신고
			DataArray.MarkArrayDirty();

			if (GEngine)
			{
				GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Blue, FString::Printf(TEXT("데이터 삭제: %s"), *MessageToRemove));
			}

			FString Message;
			for (const FSimpleDataEntry& Entry : DataArray.Entries)
			{
				Message += Entry.Message + TEXT(" | ");
			}
			if (GEngine)
			{
				GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, FString::Printf(TEXT("현재 배열 상태: %s"), *Message));
			}
			break;
		}
	}
}

내용이 뭔가 많아보이긴 하지만, 구현부(.cpp)보다 헤더 파일(.h)이 좀 더 중요한 내용이 많다.

Rep_Notify처럼 클라에서 호출되는 콜백 함수도 FSimpleDataEntry에서 선언할 수 있다.

  • PreReplicatedRemove(): 아이템 삭제 시 호출
  • PostReplicatedAdd(): 아이템 삽입 후 호출
  • 그 외 여러가지...

이제 3인칭 캐릭터 템플릿에 해당 액터 컴포넌트를 부착한 후 변수를 추가해준 다음

Server RPC를 연결한 후

플레이를 눌러서 테스트를 해보면 정상적으로 배열이 동작하는 것을 볼 수 있다.
(Listen Server에서 테스트)

profile
게임 개발 지망생

0개의 댓글