Delta Serialization
NetSerialize, NetDeltaSerialize, Fast TArray Replication 구현
커스텀 NetSerialize
구조체의 네트워크 직렬화 최적화
USTRUCT()
struct FMyNetSerializableStruct
{
GENERATED_BODY()
UPROPERTY()
float Value1;
UPROPERTY()
int32 Value2;
// 커스텀 직렬화 구현
bool NetSerialize(FArchive& Ar,
UPackageMap* Map,
bool& bOutSuccess)
{
// 압축된 형태로 직렬화 (float를 uint8로)
uint8 ByteValue1 = FMath::Clamp(
FMath::RoundToInt(Value1 * 255.f),
0, 255);
Ar << ByteValue1;
Ar << Value2;
if (Ar.IsLoading())
{
Value1 = ByteValue1 / 255.f;
}
bOutSuccess = true;
return true;
}
};
// TStructOpsTypeTraits 설정
template<>
struct TStructOpsTypeTraits<FMyNetSerializableStruct>
: public TStructOpsTypeTraitsBase2<FMyNetSerializableStruct>
{
enum { WithNetSerializer = true };
};
float(4바이트)를 uint8(1바이트)로 압축하면 75% 대역폭 절약. 정밀도가 덜 중요한 값(0~1 범위)에 유용합니다.
Fast TArray Replication (FTR)
배열의 효율적인 델타 복제
Step 1: 아이템 엔트리 정의
USTRUCT()
struct FInventoryItem : public FFastArraySerializerItem
{
GENERATED_BODY()
UPROPERTY()
int32 ItemID;
UPROPERTY()
int32 Quantity;
// 클라이언트 콜백
void PreReplicatedRemove(
const struct FInventoryArray& ArraySerializer);
void PostReplicatedAdd(
const struct FInventoryArray& ArraySerializer);
void PostReplicatedChange(
const struct FInventoryArray& ArraySerializer);
};
Step 2: 배열 래퍼 정의
USTRUCT()
struct FInventoryArray : public FFastArraySerializer
{
GENERATED_BODY()
UPROPERTY()
TArray<FInventoryItem> Items;
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
{
return FFastArraySerializer::FastArrayDeltaSerialize<
FInventoryItem,
FInventoryArray>(Items, DeltaParms, *this);
}
};
template<>
struct TStructOpsTypeTraits<FInventoryArray>
: public TStructOpsTypeTraitsBase2<FInventoryArray>
{
enum { WithNetDeltaSerializer = true };
};
Step 3: Actor에서 사용
UCLASS()
class AMyCharacter : public ACharacter
{
GENERATED_BODY()
UPROPERTY(Replicated)
FInventoryArray Inventory;
void AddItem(int32 ItemID, int32 Quantity)
{
FInventoryItem NewItem;
NewItem.ItemID = ItemID;
NewItem.Quantity = Quantity;
Inventory.MarkItemDirty(
Inventory.Items.Add_GetRef(NewItem));
}
void RemoveItem(int32 Index)
{
if (Inventory.Items.IsValidIndex(Index))
{
Inventory.Items.RemoveAt(Index);
Inventory.MarkArrayDirty();
}
}
};
- PreReplicatedRemove - 아이템 제거 직전 호출
- PostReplicatedAdd - 새 아이템 추가 후 호출
- PostReplicatedChange - 기존 아이템 변경 후 호출
일반 TArray vs Fast TArray
언제 어떤 것을 사용할지
| 기능 | 일반 TArray | Fast TArray |
|---|---|---|
| 전송 방식 | 전체 배열 재전송 | 변경분만 전송 |
| 콜백 | 없음 | Add/Remove/Change |
| 구현 복잡도 | 단순 | 복잡 |
| 적합한 케이스 | 작은 배열, 드문 변경 | 인벤토리, 버프 목록 |
Struct의 기본 Delta Serialization은 원자성(Atomicity)이 보장되지 않습니다. 패킷 손실 시 불완전한 상태가 될 수 있으므로, 중요한 데이터는 NetSerialize로 원자적 전송을 구현하세요.
NetDeltaSerialize 고급 패턴
조건부 직렬화와 버전 관리
조건부 프로퍼티 직렬화
bool FMyStruct::NetSerialize(FArchive& Ar,
UPackageMap* Map, bool& bOutSuccess)
{
// 비트마스크로 어떤 필드가 변경되었는지 표시
uint8 Flags = 0;
if (Ar.IsSaving())
{
if (bHealthChanged) Flags |= 0x01;
if (bPositionChanged) Flags |= 0x02;
if (bRotationChanged) Flags |= 0x04;
}
Ar << Flags;
if (Flags & 0x01) Ar << Health;
if (Flags & 0x02) Ar << Position;
if (Flags & 0x04) Ar << Rotation;
bOutSuccess = true;
return true;
}
커스텀 NetSerialize를 변경할 때는 클라이언트와 서버가 동일한 직렬화 형식을 사용해야 합니다. 핫픽스로 형식이 변경되면 버전 불일치로 크래시가 발생할 수 있습니다. 버전 번호를 추가하여 하위 호환성을 유지하세요.
FTR과 Push Model 결합
void AMyActor::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
FDoRepLifetimeParams Params;
Params.bIsPushBased = true;
DOREPLIFETIME_WITH_PARAMS_FAST(
AMyActor, Inventory, Params);
}
void AMyActor::AddItem(int32 ItemID, int32 Qty)
{
FInventoryItem Item;
Item.ItemID = ItemID;
Item.Quantity = Qty;
Inventory.Items.Add(Item);
Inventory.MarkItemDirty(Inventory.Items.Last());
// Push Model과 FTR 동시 사용 시 둘 다 마킹!
MARK_PROPERTY_DIRTY_FROM_NAME(
AMyActor, Inventory, this);
}
핵심 요약
- NetSerialize - 구조체의 커스텀 직렬화, 대역폭 최적화에 유용
- TStructOpsTypeTraits - WithNetSerializer, WithNetDeltaSerializer 플래그 설정
- Fast TArray Replication - 변경된 요소만 복제, 콜백 지원
- MarkItemDirty/MarkArrayDirty - FTR에서 변경 알림 필수
- 콜백 - PreReplicatedRemove, PostReplicatedAdd, PostReplicatedChange
도전 과제
배운 내용을 직접 실습해보세요
FVector를 16비트 정밀도로 압축하는 FCompressedPosition 구조체를 만들고, NetSerialize를 구현하여 일반 FVector 복제 대비 대역폭 절감량을 측정해보세요.
FFastArraySerializerItem을 상속한 FInventorySlot 구조체와 FFastArraySerializer를 상속한 FInventoryArray를 만들고, PostReplicatedAdd에서 UI 위젯을 업데이트하는 시스템을 구현해보세요.
Push Model과 Fast TArray Replication을 결합한 버프/디버프 시스템을 구현하세요. MarkItemDirty와 MARK_PROPERTY_DIRTY_FROM_NAME을 동시에 사용하고, 네트워크 프로파일러(stat net)로 대역폭 사용량을 비교해보세요.