최근 언리얼 엔진 멀티플레이를 공부하면서 Replication이라는 개념을 자주 보게 되었다.
Standalone 환경에서는 하나의 실행 환경 안에서 대부분의 상태를 바로 확인할 수 있지만,
멀티플레이 환경에서는 서버와 클라이언트가 나뉘기 때문에
서버의 상태를 클라이언트에 어떻게 전달할지 생각해야 한다.
예를 들어 플레이어의 체력, 위치, 점수, 문이 열렸는지 여부 같은 값은
서버에서만 바뀌고 끝나는 것이 아니라 클라이언트 화면에도 반영되어야 한다.
이때 사용하는 언리얼의 네트워크 동기화 시스템이 Replication이다.
이번 글에서는 Replication의 기본 개념과
Actor 복제, Property Replication, RepNotify, RPC, Ownership까지 간단히 정리해보려고 한다.
Replication이란?
Replication은 서버에 있는 Actor나 Property의 상태를
클라이언트에 동기화하는 언리얼 엔진의 네트워크 시스템이다.
간단히 말하면 서버의 상태를 클라이언트에 복제해서 반영하는 기능이다.
멀티플레이에서 게임 상태에 대한 최종 권한은 보통 서버가 가지고 있다.
클라이언트는 서버의 상태를 전달받아 화면에 표시하거나 로컬 상태를 갱신한다.
예를 들어 서버에서 캐릭터의 체력이 변경되면 그 체력 값이 클라이언트에도 전달되어 UI에 반영될 수 있다.
이런 상태 동기화를 처리하는 것이 Replication이다.
Replication의 기본 단위는 Actor
언리얼에서 Replication의 기본 단위는 Actor이다.
복제하려는 Actor는 보통 생성자에서 다음과 같이 설정한다.
AMyActor::AMyActor()
{
bReplicates = true;
}
bReplicates를 true로 설정하면 해당 Actor가 네트워크 복제 대상이 될 수 있다.
여기서 주의할 점은 UObject는 Actor처럼 독립적으로 바로 복제되는 대상이 아니라는 것이다.
UObject를 복제하려면 보통 이를 소유한 Actor가 복제 가능해야 하고,
해당 UObject를 Subobject Replication 방식으로 등록해야 한다.
ActorComponent도 마찬가지로 소유한 Actor가 복제 대상이어야 하고,
컴포넌트 자체도 SetIsReplicatedByDefault(true) 또는 SetIsReplicated(true) 같은 복제 설정이 필요하다.
Replicates를 중간에 바꿔도 될까?
Actor를 생성자에서 bReplicates = true로 설정하는 것이 일반적이다.
물론 코드 중간에 SetReplicates(true)를 호출할 수도 있다.
하지만 이미 게임이 진행 중인 상태에서 복제 여부를 바꾸면
클라이언트가 해당 Actor를 이미 알고 있는지,
새롭게 알아야 하는 Actor인지에 따라 처리 타이밍이 복잡해질 수 있다.
또한 초기 상태가 언제 복제되는지, Relevancy 조건에 걸리는지,
Net Update 타이밍이 언제인지에 따라 예상보다 늦게 클라이언트에 반영될 수도 있다.
그래서 특별한 이유가 없다면 복제 여부는 생성자나 스폰 직후처럼 초기 단계에서 정하는 것이 안전하다.
Property Replication
Actor 자체가 복제된다고 해서 그 안의 모든 변수가 자동으로 복제되는 것은 아니다.
복제할 변수는 명시적으로 표시해야 한다.
UPROPERTY(Replicated)
int32 Health;
UPROPERTY(Replicated)는 이 변수가 복제 대상이라는 것을 표시한다.
하지만 이것만으로 끝나는 것은 아니고 GetLifetimeReplicatedProps 함수에 해당 변수를 등록해야 한다.
#include "Net/UnrealNetwork.h"
void AMyCharacter::GetLifetimeReplicatedProps(
TArray<FLifetimeProperty>& OutLifetimeProps
) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyCharacter, Health);
}
DOREPLIFETIME은 해당 Property를 Replication 목록에 추가하는 매크로이다.
정리하면 Property를 복제하려면 보통 다음 과정이 필요하다.
UPROPERTY(Replicated) 선언
-> GetLifetimeReplicatedProps 오버라이드
-> DOREPLIFETIME으로 등록
중요한 점은 Property Replication은 기본적으로 서버에서 클라이언트 방향으로 동작한다는 것이다.
즉, 클라이언트에서 복제 변수를 직접 변경한다고 해서 그 값이 서버로 자동 전송되는 것은 아니다.
클라이언트가 서버에 상태 변경을 요청해야 한다면 보통 Server RPC를 사용해야 한다.
ReplicatedUsing과 RepNotify
값이 복제된 뒤 추가 처리가 필요할 때는 ReplicatedUsing을 사용할 수 있다.
예를 들어 체력 값이 복제된 뒤 UI를 갱신해야 한다면 다음과 같이 작성할 수 있다.
UPROPERTY(ReplicatedUsing = OnRep_Health)
int32 Health;
UFUNCTION()
void OnRep_Health();
void AMyCharacter::OnRep_Health()
{
// 클라이언트에서 체력 UI 갱신
}
OnRep 함수는 클라이언트가 복제된 값을 수신했을 때 호출된다.
따라서 UI 갱신, 이펙트 재생, 사운드 처리처럼 값이 바뀐 뒤 필요한 후처리를 넣기 좋다.
다만 서버에서 값을 변경했다고 해서 서버에서도 OnRep 함수가 자동으로 호출되는 것은 아니다.
서버에서도 같은 처리가 필요하다면 값을 변경한 뒤 별도로 처리 함수를 호출하는 방식이 필요할 수 있다.
Replication 조건과 Notify 조건
모든 Property를 모든 클라이언트에 항상 복제할 필요는 없다.
특정 클라이언트에게만 보내거나, Owner를 제외하고 보내야 하는 경우도 있다.
이때 조건부 복제를 사용할 수 있다.
예를 들어 DOREPLIFETIME_CONDITION_NOTIFY 같은 매크로를 사용하면
복제 조건과 RepNotify 호출 조건을 함께 지정할 수 있다.
대표적인 조건에는 다음과 같은 것들이 있다.
COND_OwnerOnly: 오너에게만 복제COND_SkipOwner: 오너를 제외하고 복제COND_SimulatedOnly: Simulated Proxy에만 복제COND_AutonomousOnly: Autonomous Proxy에만 복제
RepNotify 호출 방식도 지정할 수 있다.
REPNOTIFY_OnChanged: 수신한 값이 로컬 값과 다를 때만 호출REPNOTIFY_Always: 값을 수신하면 항상 호출
즉, Replication은 단순히 변수를 복제하는 것뿐만 아니라
누구에게 보낼지, 언제 후처리를 실행할지도 제어할 수 있다.
오브젝트 레퍼런스 Replication
Replication은 단순한 숫자나 문자열만 복제하는 것이 아니다.
Actor나 UObject에 대한 참조도 복제할 수 있다.
다만 객체 포인터 자체가 그대로 네트워크를 통해 전달되는 것은 아니다.
언리얼은 네트워크에서 객체를 식별하기 위해 FNetworkGUID 같은 식별자를 사용한다.
서버에서 객체 참조가 복제되면 클라이언트는 이 식별자를 이용해 해당 객체를 찾는다.
하지만 이 방식이 동작하려면 참조하는 객체도 클라이언트에서 알 수 있는 복제 가능한 대상이어야 한다.
즉, 복제되지 않는 객체를 참조로 보내려고 하면 클라이언트에서 올바르게 해석할 수 없다.
Relevancy와 NetUpdateFrequency
Replication은 서버의 모든 Actor를 모든 클라이언트에 무조건 보내는 방식이 아니다.
서버는 Actor가 특정 클라이언트와 관련 있는지 판단한다.
이것을 Relevancy라고 한다.
예를 들어 멀리 떨어져 있어 클라이언트가 알 필요 없는 Actor라면
그 클라이언트에 복제하지 않을 수 있다.
또한 Actor가 얼마나 자주 네트워크 업데이트를 시도할지도 중요하다.
이때 사용하는 값이 NetUpdateFrequency이다.
NetUpdateFrequency가 높으면 더 자주 업데이트를 시도하고,
낮으면 상대적으로 덜 자주 업데이트를 시도한다.
다만 무조건 높인다고 좋은 것은 아니다.
네트워크 대역폭과 성능을 고려해야 하므로 중요한 Actor와 덜 중요한 Actor를 구분해서 설정해야 한다.
RPC란?
RPC는 Remote Procedure Call의 약자이다.
Replication이 변수 상태를 동기화하는 기능이라면,
RPC는 네트워크를 통해 다른 쪽에서 함수를 실행시키는 기능이다.
간단히 구분하면 다음과 같다.
Replication: 변수 상태 동기화
RPC: 함수 호출 전달
언리얼에는 대표적으로 세 가지 RPC가 있다.
Server RPCClient RPCNetMulticast RPC
Server RPC
Server RPC는 클라이언트가 서버에서 함수를 실행하도록 요청할 때 사용한다.
예를 들어 클라이언트가 공격 버튼을 눌렀다면 서버에 공격 요청을 보내야 할 수 있다.
UFUNCTION(Server, Reliable)
void Server_Attack();
클라이언트가 Server_Attack()을 호출하면 서버에서 해당 함수가 실행된다.
다만 아무 Actor에서나 Server RPC를 호출할 수 있는 것은 아니다.
Server RPC를 호출하려면 해당 Actor가 클라이언트의 Owning Connection과 연결되어 있어야 한다.
Client RPC
Client RPC는 서버가 특정 클라이언트에서 함수를 실행하도록 요청할 때 사용한다.
예를 들어 서버가 특정 플레이어에게만 UI를 띄우고 싶다면 Client RPC를 사용할 수 있다.
UFUNCTION(Client, Reliable)
void Client_ShowMessage();
Client RPC는 보통 해당 Actor를 소유한 클라이언트에서 실행된다.
그래서 특정 플레이어에게만 알려야 하는 UI 처리나 개별 클라이언트 전용 효과 처리에 사용할 수 있다.
NetMulticast RPC
NetMulticast RPC는 서버가 관련 있는 클라이언트들에게 함수를 실행하도록 요청할 때 사용한다.
예를 들어 폭발 이펙트나 사운드처럼 여러 클라이언트에게 동시에 보여줘야 하는 이벤트에 사용할 수 있다.
UFUNCTION(NetMulticast, Unreliable)
void Multicast_PlayExplosionEffect();
일반적으로 NetMulticast RPC는 서버에서 호출해야 관련 클라이언트들에게 전달된다.
Ownership과 RPC
RPC를 이해하려면 Ownership도 함께 알아야 한다.
특히 Server RPC는 호출하는 Actor가 해당 클라이언트의 Owning Connection과 연결되어 있어야 한다.
예를 들어 월드에 배치된 문 Actor가 있다고 하자.
Door->Server_Open();
클라이언트가 이런 식으로 Door Actor의 Server RPC를 직접 호출하려고 하면 동작하지 않을 수 있다.
Door Actor는 보통 특정 클라이언트가 소유한 Actor가 아니기 때문이다.
이럴 때는 클라이언트가 소유한 PlayerController나 Character를 통해 서버에 요청을 보내고,
서버에서 Door를 열도록 처리하는 구조가 더 적절하다.
Client
-> PlayerController 또는 Character의 Server RPC 호출
-> Server에서 Door 열기 처리
-> Door 상태 Replication
이 구조를 사용하면 Ownership 조건을 만족하면서 서버 권한으로 게임 상태를 변경할 수 있다.
정리
Replication은 서버의 Actor나 Property 상태를 클라이언트에 동기화하는 언리얼 네트워크 시스템이다.
복제의 기본 단위는 Actor이며 Actor를 복제하려면 bReplicates = true를 설정해야 한다.
변수를 복제하려면 UPROPERTY(Replicated) 또는 ReplicatedUsing을 사용하고,GetLifetimeReplicatedProps에서 DOREPLIFETIME으로 등록해야 한다.
RepNotify는 복제된 값이 클라이언트에 도착했을 때 UI 갱신이나 이펙트 처리 같은 후처리에 사용할 수 있다.RPC는 변수 동기화가 아니라 네트워크를 통해 다른 쪽에서 함수를 실행시키는 기능이다.
그리고 RPC를 제대로 사용하려면 Actor가 누구에게 소유되어 있는지,
Owning Connection과 연결되어 있는지도 함께 고려해야 한다.
마무리
이번에 Replication을 정리하면서 언리얼 멀티플레이에서는 단순히 기능을 구현하는 것보다
그 기능이 서버와 클라이언트 중 어디에서 실행되어야 하는지를 먼저 생각하는 것이 중요하다고 느꼈다.
또한 어떤 값은 Property Replication으로 동기화해야 하고,
어떤 동작은 RPC로 요청하거나 실행해야 하는지 구분하는 것도 중요하다고 생각한다.
즉, 멀티플레이에서는
“이 함수는 어디에서 실행되어야 하는가?”,
“이 상태는 어떤 방식으로 동기화해야 하는가?”를 계속 고민해야 한다.
앞으로 멀티플레이 기능을 구현할 때는
Replication, RPC, Ownership, Relevancy 같은 개념을 따로 보지 않고,
서버와 클라이언트의 역할을 기준으로 함께 연결해서 이해해야겠다.
'언리얼 엔진' 카테고리의 다른 글
| [언리얼 엔진] Delegate 정리 (1) | 2026.06.16 |
|---|---|
| [언리얼 엔진] 스마트 포인터 정리 (0) | 2026.06.15 |
| [언리얼 엔진] std::map과 TMap 차이 정리 (0) | 2026.06.11 |
| [언리얼 엔진] FString, FName, FText 차이 정리 (0) | 2026.06.10 |
| [언리얼 엔진] GameMode, GameState, PlayerState, PlayerController, Pawn 정리 (0) | 2026.06.09 |