기본적으로 Unreal Engine의 Replication System은 갱신 주기가 짧은 작은 사이즈의 데이터(e.g. Character State)에 맞춰 최적화되어 있습니다.
용량이 큰 데이터(e.g. array, struct)를 한꺼번에 보내려 하면 문제가 생길 수 있습니다.
1⃣ 네트워크 패킷 크기 제한 초과
거대한 Array을 단순히 Replicated로 표시해두면, 네트워크 Packet Size 제한을 넘길 수도 있습니다.
2⃣ 과도한 대역폭 사용 → 패킷 손실 및 지연 발생
Bandwidth
을 과다 사용하면 Packet Loss
나 지연이 발생할 수 있습니다
3⃣심한 경우 연결이 끊길 위험
최악의 경우 연결 끊김을 유발할 수 있습니다.
이러한 큰 데이터를 다루기 위해 Unreal Engine에서는 아래와 같은 기능을 제공합니다.
//Module.Bulid.cs
...
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
...
"NetCore" // for FFastArraySerializer
});
...
// InventoryItem.h
#pragma once
#include "CoreMinimal.h"
#include "Net/Serialization/FastArraySerializer.h"
#include "InventoryItem.generated.h"
struct FInventoryArray;
/**
*
*/
USTRUCT(BlueprintType)
struct FInventoryItem : public FFastArraySerializerItem
{
GENERATED_USTRUCT_BODY()
// Your data:
UPROPERTY()
int32 ExampleIntProperty;
UPROPERTY()
float ExampleFloatProperty;
/**
* Optional functions you can implement for client side notification of changes to items;
* Parameter type can match the type passed as the 2nd template parameter in associated call to FastArrayDeltaSerialize
*
* NOTE: It is not safe to modify the contents of the array serializer within these functions, nor to rely on the contents of the array
* being entirely up-to-date as these functions are called on items individually as they are updated, and so may be called in the middle of a mass update.
*/
void PreReplicatedRemove(const struct FInventoryArray& InArraySerializer);
void PostReplicatedAdd(const struct FInventoryArray& InArraySerializer);
void PostReplicatedChange(const struct FInventoryArray& InArraySerializer);
// Optional: debug string used with LogNetFastTArray logging
FString GetDebugString();
};
// InventoryItem.cpp
#include "InventoryItem.h"
#include "InventoryArray.h"
void FInventoryItem::PreReplicatedRemove(const FInventoryArray& InArraySerializer)
{
UE_LOG(LogTemp, Warning, TEXT("Item Removed!"));
}
void FInventoryItem::PostReplicatedAdd(const FInventoryArray& InArraySerializer)
{
UE_LOG(LogTemp, Warning, TEXT("New Item Added!"));
}
void FInventoryItem::PostReplicatedChange(const FInventoryArray& InArraySerializer)
{
UE_LOG(LogTemp, Warning, TEXT("Item Updated!"));
}
// InventoryArray.h
#pragma once
#include "CoreMinimal.h"
#include "Net/Serialization/FastArraySerializer.h"
#include "InventoryItem.h"
#include "InventoryArray.Generated.h"
struct FInventoryItem;
USTRUCT(BlueprintType)
struct FInventoryArray : public FFastArraySerializer
{
GENERATED_USTRUCT_BODY()
UPROPERTY()
TArray<FInventoryItem> Items; /** Step 3: You MUST have a TArray named Items of the struct you made in step 1. */
/** Step 4: Copy this, replace example with your names */
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
{
return FFastArraySerializer::FastArrayDeltaSerialize<FInventoryItem, FInventoryArray>(Items, DeltaParms, *this);
}
};
개발자가 Struct를 만들어 FFastArraySerializer
를 상속받고, element에 해당하는 struct는 FFastArraySerializerItem
을 상속받도록 하는 것입니다.
위 코드스니펫은 Engine source code 내에 FastArraySerializer.h
에서 제공된 sample code를 기반으로 작성되었습니다. 실제 사용은 해당 파일의 설명을 참고해서 진행합니다.
이렇게 구성하면 엔진이 그 Array의 추가/제거/수정 사항
을 추적하여 delta 형태로만 전송합니다.
큰 Struct나 Array일수록, 언제 보내는지가 중요합니다.
Unreal Engine은 Property가 특정 조건에서만 Replication 되도록 설정할 수 있습니다.
만약 아주 대용량 변수를 Replication해야 한다면, Unreal Engine의 내부 한계 (네트워크 bunch 당 약 64KB)를 초과할 위험이 있어 RPC를 통해 조각내어 전송하는 방법을 고려해야 합니다.
예를 들어, Reliable RPC 여러 개로 데이터를 쪼개어 보내고, Client 쪽에서 이를 재조립하도록 하는 겁니다.
이 패턴은 잘못하면 Reliable Buffer를 채워 넣어버릴 수 있으므로 매우 신중해야 합니다.
하지만 많은 양의 데이터를 한 번에 보내야 할 때는 쓸 수 있는 방법입니다.
또는, 아예 Unreal Engine의 Replication System 바깥에서 소켓 통신이나 Online Subsystem
의 파일 전송 기능 등을 사용하고, 전송 완료 후 필요한 상태만 간단히 동기화하는 방법도 있습니다.
요지는, 대용량 데이터 전송은 Replication System의 주 목적인 “실시간 플레이 상태 동기화”에 적합하지 않기 때문에, 그러한 특별한 상황에서는 다른 접근이 필요할 수 있다는 것입니다.
때로는 거대한 데이터 자체를 보내는 대신 Reference나 ID만 보내고 Client가 자체적으로 준비하게 할 수 있습니다.
예를 들어, 수백 개의 오브젝트 위치를 가진 레벨 정보를 통째로 보내는 대신, Server가 Map ID 5번을 사용해
같이 식별자만 보내고, Client는 로컬에 저장된 프리셋 맵 데이터를 로드하게 할 수 있습니다.
또는 아주 많은 하위 객체들이 있는 경우, 한 Actor에 모두 넣어서 복제하는 대신 여러 Actor/컴포넌트로 나눠서 각각 복제하게 할 수도 있습니다.
그러면 엔진이 그것들을 개별적으로 관리하고, 필요하면 일부만 보낼 수도 있겠죠.
(e.g. 1000명의 NPC를 하나의 Actor에 Array로 넣어 Replication 하기보다, 1000개의 Actor로 나눠 두고, 엔진이 거리 등에 따라 개별적으로 Replication 여부를 판단하게 하는 식)
데이터가 얼마나 자주 변하고, 누가 그것을 필요로 하는지를 항상 고려해야 합니다.
많은 양의 데이터가 모든 Client에 자주 전송돼야 한다면, 네트워크에는 최악의 시나리오입니다. 가능하다면 그런 디자인을 피하고, 불가피하면 위의 도구들 (Fast Array
, Conditional Replication
등)을 활용하세요.
실제로 Unreal Engine에는 Replicate 되는 Array 크기의 기본 한계 등이 설정되어 있고, 너무 큰 데이터는 Warning(엔진 기본 설정상 2048개의 Elements가 넘으면 Warning 발생)을 넘어 Error를 냅니다.
그러니 정말 큰 데이터는 한 번에 다 Replicate
하려 하기보다, 여러 프레임에 나눠 전달하거나, 아예 다른 방법으로 동기화
하는 것이 현실적입니다.
Reliable로 지정된 RPC는 전송 대상에게 반드시 도달하는 것이 보장됩니다.
엔진이 해당 RPC를 수신 측에서 확인할 때까지 재전송을 시도합니다. 또한 동일 채널의 Reliable RPC들은 보낸 순서 그대로 실행됨을 보장합니다. 이런 특성은 중요한 Event를 전송하려고 하면 필수입니다. 그러나 Reliable을 남용한다면 성능 저하를 야기할 수 있습니다.
어떤 Packet이 유실되면, 그 RPC가 재전송되어 도착할 때까지 이후에 보내진 다른 Reliable RPC들도 대기하게 됩니다. Reliable RPC를 너무 많이 사용하면 네트워크 Packet Queue(Buffer)를 채워 다른 메시지를 막아버릴 수 있습니다. 그러므로 RPC를 Reliable로 지정하는 것은 필요한 경우에 한정해야 합니다. 반드시 도달해야 하는 Event에만 사용하는 것이 좋습니다.
e.g. 게임 시작/종료 알림, 플레이어 사망 Event, 중요한 아이템 획득, 퀘스트 완료.
Unreliable RPC는 호출이 정상적으로 되었음에도 상황에 따라 유실될 수 있습니다.
네트워크가 해당 Packet을 Drop하면 재전송하지 않고, 순서도 보장하지 않습니다. (동일 채널에선 발신 순서를 유지하지만, 도착은 보장되지 않으므로 결과적으로 일부 누락될 수 있음)
그러나 많은 경우(특히 빈번한 업데이트)에서 한두 번 메시지가 떨어져도 큰 문제는 없습니다.
🟡Unreliable RPC의 장점
예를 들어 gameplay에 영향을 주지 않는 cosmetic한 visual/sound effect를 표현하기 위한 Event들은 누락돼도 큰 문제 없으므로 Unreliable로 처리하는 것이 적합합니다.
기본적으론 모든 RPC를 Unreliable로 설정하고, 꼭 필요한 경우에만 Reliable로 설정하세요.
특히, 매 적마다 발생하는 Event를 Reliable로 보내는 것은 매우 위험합니다. 예를 들어, 플레이어의 위치를 이동할 때마다 Reliable RPC로 보내면, Packet 손실 시 Reliable 버퍼가 순식간에 꽉 차버려 결국 256개 한도를 넘길 수 있고 (기본 Reliable 버퍼 한도는 256개 Packet) , 그렇게 되면 Client가 접속 끊어질 수도 있습니다.
Unreal Engine C++에서는 RPC를 별도로 지정하지 않으면 기본적으로 Unreliable로 간주됩니다. UFUNCTION 매크로에 Reliable 키워드를 추가해야 Reliable이 됩니다. 예를 들어,
UFUNCTION(Client, Reliable)
void Client_ShowGameOver(); // Client RPC, Reliable
일반적인 패턴을 정리하면
📌Client → Server RPC
플레이어 입력처럼 자주 발생하는 건 보통 Unreliable로 합니다 (e.g. 움직임, 연속 발사).
단, “NPC 대화 선택”처럼 가끔 발생하지만 꼭 전달되어야 하는 입력은 Reliable이어야겠죠.
📌Server → Client RPC
중요한 알림(e.g. 게임 오버, 큰 보상 획득)은 Reliable로 보내 Client가 꼭 받게 합니다.
자주 발생하거나 중요하지 않은 Event(e.g. 피격 피드백 소리 재생)는 Unreliable로 할 수 있습니다.
📌Multicast RPC (Server → all clients)
가끔 발생하는 전역 Event(e.g. 보스 소환 알림)는 Reliable Multicast로, 사소하고 빈번한 효과(e.g. 총구 섬광, 폭발 이펙트)는 Unreliable Multicast로 처리하는 식입니다.