복제(Replication) 시스템 이해
bReplicates·DOREPLIFETIME·OnRep를 사용해 액터 생성과 중요 변수의 서버 상태를 복제합니다.
언리얼 네트워킹 기본 개념에서 서버가 상태를 확정하고 복제로 나눠 준다는 큰 그림을 보았습니다. 그런데 실제로 코드를 쓰다 보면 질문이 생깁니다. 서버에서 체력을 세 번 바꿨는데 왜 클라이언트의 RepNotify는 한 번만 불릴까요? 인벤토리처럼 본인만 알아야 하는 정보를 다른 플레이어에게 보내지 않으려면 어떻게 할까요? 이런 질문에 답하려면 복제가 무엇을, 누구에게, 언제 보내는지를 더 자세히 알아야 합니다.
이 글에서는 액터 복제, 변수 복제와 RepNotify, 조건부 복제, RPC, 컴포넌트 복제, 그리고 네트워크 역할(role)을 차례로 살펴봅니다. 서버가 권한을 가지고 상태를 확정하고, DOREPLIFETIME 규칙으로 복제할 대상을 고르고, 클라이언트에서는 OnRep_와 RPC로 반응을 나누는 흐름이 이 글의 중심입니다.
액터 복제
언리얼에서 네트워크로 게임 월드의 객체를 동기화하는 가장 기본 단위는 액터입니다. 액터 복제는 서버에 있는 액터의 인스턴스를 네트워크로 클라이언트에도 만들고, 그 액터의 중요한 상태를 계속 맞춰 가는 과정입니다.
액터가 복제되려면 생성자에서 bReplicates를 true로 두거나, 실행 중에 SetReplicates(true)를 불러야 합니다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyReplicatedActor.generated.h"
UCLASS()
class MYPROJECT_API AMyReplicatedActor : public AActor
{
GENERATED_BODY()
public:
AMyReplicatedActor();
};#include "MyReplicatedActor.h"
AMyReplicatedActor::AMyReplicatedActor()
{
// 중요: 이 액터가 네트워크를 통해 복제될 것임을 명시
bReplicates = true;
// 루트 컴포넌트 이동 복제 활성화. 이 부분 예제는 루트 생성을 생략합니다.
SetReplicateMovement(true);
}bReplicates = true는 서버의 액터를 복제 대상으로 정합니다. 복제 대상이 되었다고 모든 연결에 늘 보내지는 것은 아니며, 연결마다의 관련성, 휴면, 업데이트 시점 같은 조건을 따릅니다. SetReplicateMovement(true)는 루트 컴포넌트의 위치, 회전, 속도 같은 이동 데이터를 복제합니다. 스케일 변경까지 모두 포함하는 설정은 아니고, UMovementComponent가 있어야만 쓸 수 있는 것도 아닙니다. 다만 움직일 루트 컴포넌트와 프로젝트의 이동 로직은 따로 있어야 하며, 이 부분 예제는 루트를 만드는 코드를 생략했습니다.
액터의 생성과 파괴도 복제됩니다. 서버에서 GetWorld()->SpawnActor<AMyActor>(...)로 액터를 만들면, bReplicates가 true일 때 복제 조건을 만족하는 클라이언트에도 그 액터가 만들어집니다. 서버에서 Destroy()로 액터를 파괴하면 그 액터의 사본을 가진 클라이언트에서도 사본이 파괴됩니다.
변수 복제
액터가 있다는 사실만큼이나 액터 안의 값도 서버와 클라이언트가 맞아야 합니다. 변수 복제는 UPROPERTY()와 GetLifetimeReplicatedProps()로 관리합니다.
DOREPLIFETIME으로 등록하기
변수를 복제하려면 UPROPERTY()에 Replicated 지정자를 붙이고, 액터의 GetLifetimeReplicatedProps()에서 DOREPLIFETIME() 매크로로 등록합니다. 아래 예제는 앞 절의 액터에 int32 변수 하나를 더해 이 두 단계를 보여 줍니다. 헤더에서는 지정자를 붙이고 GetLifetimeReplicatedProps의 재정의를 선언하며, cpp에서는 부모 구현을 먼저 부른 뒤 변수를 등록합니다. Super 호출을 빼면 부모 클래스가 등록해 둔 복제 속성이 목록에서 빠지므로 지우지 않습니다.
#pragma once
// ...
#include "GameFramework/Actor.h"
#include "MyReplicatedActor.generated.h"
UCLASS()
class MYPROJECT_API AMyReplicatedActor : public AActor
{
GENERATED_BODY()
public:
AMyReplicatedActor();
protected:
// 서버에서만 값을 변경하고 클라이언트로 복제할 변수
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Replication")
int32 ReplicatedInteger;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};#include "MyReplicatedActor.h"
#include "Net/UnrealNetwork.h" // DOREPLIFETIME 매크로를 위해 반드시 포함
AMyReplicatedActor::AMyReplicatedActor()
{
bReplicates = true;
SetReplicateMovement(true);
ReplicatedInteger = 0; // 초기값 설정
}
void AMyReplicatedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyReplicatedActor, ReplicatedInteger);
}UPROPERTY(Replicated)는 이 변수를 네트워크로 복제할 수 있다고 엔진에 알리고, GetLifetimeReplicatedProps()를 재정의해 DOREPLIFETIME으로 등록하면 엔진의 네트워크 코드가 이 변수를 복제 목록에 넣습니다. 둘 중 하나라도 빠지면 복제되지 않습니다. DOREPLIFETIME 매크로를 쓰려면 Net/UnrealNetwork.h를 꼭 include해야 합니다.
OnRep_로 변경에 반응하기
변수가 클라이언트에 복제되어 값이 바뀌었을 때 어떤 로직을 실행하고 싶다면 ReplicatedUsing 지정자를 씁니다. 주로 효과 재생, 사운드, UI 갱신처럼 클라이언트에서만 필요한 처리에 씁니다.
#pragma once
// ...
UCLASS()
class MYPROJECT_API AMyReplicatedActor : public AActor
{
GENERATED_BODY()
public:
// ...
protected:
// 클라이언트가 복제된 값 변경을 수신하면 알림을 실행할 변수
UPROPERTY(ReplicatedUsing = OnRep_ReplicatedFloat, BlueprintReadOnly, Category = "Replication")
float ReplicatedFloat;
// OnRep_ReplicatedFloat 함수 선언 (UFUNCTION으로 선언되어야 함)
UFUNCTION()
void OnRep_ReplicatedFloat();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
public:
// 서버에서 호출하여 복제된 변수들을 업데이트하는 함수
UFUNCTION(BlueprintCallable, Category = "Replication")
void Server_SetReplicatedFloat(float NewFloat);
};#include "MyReplicatedActor.h"
#include "Net/UnrealNetwork.h"
// ...
void AMyReplicatedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyReplicatedActor, ReplicatedInteger); // 기존
DOREPLIFETIME(AMyReplicatedActor, ReplicatedFloat); // OnRep_ 함수와 연결된 변수 복제
}
void AMyReplicatedActor::OnRep_ReplicatedFloat()
{
// 이 함수는 ReplicatedFloat 변수의 값이 클라이언트로 복제될 때 클라이언트에서 자동으로 호출됩니다.
// 서버에서는 직접 값을 변경하므로 호출되지 않습니다.
UE_LOG(LogTemp, Warning, TEXT("Client: ReplicatedFloat changed to: %f (OnRep_ called)"), ReplicatedFloat);
// 여기에 이펙트 재생, UI 업데이트, 애니메이션 전환 등 클라이언트에서만 필요한 로직을 구현합니다.
}
void AMyReplicatedActor::Server_SetReplicatedFloat(float NewFloat)
{
if (HasAuthority()) // 서버에서만 실행
{
ReplicatedFloat = NewFloat; // 중간 변경 값은 합쳐질 수 있으며 수신 조건에 따라 OnRep가 호출됩니다.
UE_LOG(LogTemp, Warning, TEXT("Server: ReplicatedFloat set to: %f"), ReplicatedFloat);
// 서버에서도 동일한 로직을 실행하고 싶다면 OnRep_ReplicatedFloat()를 직접 호출할 수 있습니다.
// OnRep_ReplicatedFloat(); // 서버에서도 강제로 OnRep_ 함수 호출
}
}UPROPERTY(ReplicatedUsing = FunctionName)은 이 변수의 값이 복제되어 클라이언트에서 바뀔 때 지정한 함수를 부르게 합니다. 이 함수는 UFUNCTION()으로 선언해야 하고, 이름에 OnRep_ 접두사를 붙이는 것이 관례입니다.
Server_SetReplicatedFloat는 일반적인 BlueprintCallable 함수이며, 이름의 Server_ 접두사가 원격 호출을 만들지 않는다는 점은 앞 글과 같습니다. 이 함수는 권한을 가진 쪽에서만 값을 바꾸는데, C++에서는 서버가 값을 대입해도 서버의 OnRep가 저절로 불리지 않습니다. 서버에서도 같은 처리가 필요하다면 주석처럼 OnRep_ReplicatedFloat()를 직접 부릅니다.
복제는 상태를 맞추는 것이지 모든 변경을 하나하나 전달하는 것이 아닙니다. 예를 들어 클라이언트가 마지막으로 받은 ReplicatedFloat가 20이고, 다음 전송 전에 서버가 값을 30, 40, 50으로 차례로 바꿨다고 합시다. 다음에 받는 값은 50일 수 있으며, 클라이언트가 30과 40을 각각 받았다거나 OnRep가 세 번 불렸다고 기대할 수 없습니다. 각 변화를 반드시 별도의 게임 이벤트로 처리해야 한다면, 그 이벤트를 구별하고 전달하고 놓쳤을 때 되살리는 설계를 상태 복제와 따로 준비합니다.
더 깊이: OnRep_가 받는 이전 값과 호출 조건
OnRep_ 함수에는 그 속성과 같은 타입의 매개변수를 하나 둘 수 있습니다. 그러면 복제 시스템이 바뀌기 전의 값을 그 매개변수로 넘겨주므로, void OnRep_ReplicatedFloat(float OldValue)처럼 선언해 이전 값과 새 값의 차이로 연출을 고를 수 있습니다. 복사본 대신 const 참조로 받아도 되고, 지금처럼 매개변수를 두지 않아도 됩니다. 앞의 예처럼 여러 번의 변경이 합쳐져 도착했다면, 이 이전 값은 서버의 중간값이 아니라 그 클라이언트가 직전에 가지고 있던 값입니다.
RepNotify가 언제 불릴지도 고를 수 있습니다. GetLifetimeReplicatedProps에서 DOREPLIFETIME 대신 DOREPLIFETIME_CONDITION_NOTIFY로 등록하면서 REPNOTIFY_OnChanged를 넘기면 값이 바뀌었을 때만, REPNOTIFY_Always를 넘기면 그 속성이 복제될 때마다 클라이언트에서 RepNotify가 실행됩니다. 더 많은 옵션은 FDoRepLifetimeParams 구조체와 DOREPLIFETIME_WITH_PARAMS로 정합니다.
블루프린트는 이 점에서 C++과 다르게 동작합니다. Replicate Actor Properties 문서에 따르면 블루프린트에서 복제 속성에 Set 노드를 쓰면 그 속성의 RepNotify 함수가 자동으로 불리지만, 속성을 참조로 받아 값을 바꾸는 블루프린트 매크로나 함수는 RepNotify를 부르지 않습니다. 그래서 같은 변수를 C++과 블루프린트에서 함께 바꾸는 프로젝트라면, 값을 바꾼 쪽에서 연출이 실행되는지가 경로마다 달라질 수 있습니다.
조건부 복제
특정 변수를 모든 클라이언트에 보낼 필요가 없거나, 특정 조건에서만 보내고 싶을 때는 DOREPLIFETIME_COND 매크로를 씁니다. 조건은 매크로의 세 번째 인자로 넘기며, 아래 예제는 소유자만 알면 되는 인벤토리를 COND_OwnerOnly로 등록합니다. 헤더의 UPROPERTY(Replicated)는 조건이 없는 변수와 똑같고, 조건은 cpp의 등록에서만 정한다는 점을 보면 됩니다.
#pragma once
// ...
UCLASS()
class MYPROJECT_API AMyReplicatedActor : public AActor
{
GENERATED_BODY()
public:
// ...
protected:
// 소유자에게만 복제될 변수 (예: 인벤토리)
UPROPERTY(Replicated, Category = "Replication")
TArray<FString> PlayerInventory;
// 비소유자에게만 복제될 변수 (예: 공개된 상태 정보)
// UPROPERTY(Replicated, Category = "Replication")
// int32 PublicStateValue;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};#include "MyReplicatedActor.h"
#include "Net/UnrealNetwork.h"
// ...
void AMyReplicatedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// COND_OwnerOnly: 이 변수는 액터의 소유자에게만 복제됩니다.
DOREPLIFETIME_COND(AMyReplicatedActor, PlayerInventory, COND_OwnerOnly);
// COND_SkipOwner: 이 변수는 소유자를 제외한 모든 클라이언트에게 복제됩니다.
// DOREPLIFETIME_COND(AMyReplicatedActor, PublicStateValue, COND_SkipOwner);
}이 예제는 PlayerInventory를 COND_OwnerOnly로 등록해 액터의 소유 연결에만 보내고, 주석으로 남긴 PublicStateValue는 COND_SkipOwner로 소유 연결을 뺀 나머지 연결에 보내는 예를 보여 줍니다.
UNREAL C++ · CH8-2
속성의 조건은 액터 복제 조건 위에 추가됩니다
COND_None도 관련성·휴면·업데이트 시점을 무시하지 않습니다. 초기 전달, 소유 연결, 수신 역할을 구분하여 실제 PlayerInventory의 OwnerOnly 등록을 읽습니다.
그림은 전달 대상 조건이며 즉시 전송·수신을 측정한 결과가 아닙니다. 초기 조건과 역할 조건은 원 비교표에서 별도로 읽습니다.
| 조건 | 전달 대상·시점 | 혼동할 점 |
|---|---|---|
| 추가 조건 없음 | COND_None은 속성에 별도 조건을 더하지 않습니다. | 액터 관련성·휴면·업데이트 시점까지 무시하는 항상 전송은 아닙니다. |
| 초기 전달 | COND_InitialOnly는 해당 연결의 초기 복제 묶음에 전달합니다. | 서버에서 한 번 생성될 때 전역으로 단 한 번만 전송한다는 뜻이 아닙니다. |
| 소유 연결 | COND_OwnerOnly는 소유 연결, COND_SkipOwner는 그 외 연결을 대상으로 합니다. | PlayerInventory의 OwnerOnly 예제도 액터의 소유 연결이 올바르게 설정되어야 합니다. |
| 프록시 역할 | COND_SimulatedOnly와 COND_AutonomousOnly는 수신 측 역할로 선택합니다. | 역할은 Owner/Non-Owner의 단순 별칭이 아닙니다. |
- 추가 조건 없음
- 전달 대상·시점: COND_None은 속성에 별도 조건을 더하지 않습니다.혼동할 점: 액터 관련성·휴면·업데이트 시점까지 무시하는 항상 전송은 아닙니다.
- 초기 전달
- 전달 대상·시점: COND_InitialOnly는 해당 연결의 초기 복제 묶음에 전달합니다.혼동할 점: 서버에서 한 번 생성될 때 전역으로 단 한 번만 전송한다는 뜻이 아닙니다.
- 소유 연결
- 전달 대상·시점: COND_OwnerOnly는 소유 연결, COND_SkipOwner는 그 외 연결을 대상으로 합니다.혼동할 점: PlayerInventory의 OwnerOnly 예제도 액터의 소유 연결이 올바르게 설정되어야 합니다.
- 프록시 역할
- 전달 대상·시점: COND_SimulatedOnly와 COND_AutonomousOnly는 수신 측 역할로 선택합니다.혼동할 점: 역할은 Owner/Non-Owner의 단순 별칭이 아닙니다.
상태 복제는 모든 중간 대입을 이벤트처럼 전달하지 않습니다. C++ 서버 대입은 OnRep를 자동 호출하지 않습니다. Server_SetReplicatedFloat는 이름과 달리 Server 지정자가 없는 일반 함수입니다.
위 그림은 속성의 복제 조건이 액터의 복제 조건 위에 더해진다는 점을 보여 줍니다. A가 소유한 복제 액터의 Inventory를 COND_OwnerOnly로 등록했다면, 서버에서 값이 바뀌어 등록된 속성이 먼저 액터의 수신 조건인 관련성, 휴면, 시점을 거칩니다. 그 조건을 만족한 연결 가운데 소유 연결인 A만 Inventory의 대상이 되고, 소유하지 않은 B와 C에는 이 속성이 빠집니다. 그림은 전달 대상의 조건을 나타낼 뿐, 즉시 보내고 받았다는 측정 결과가 아닙니다.
그림의 표는 조건마다 전달 대상과 혼동하기 쉬운 점을 정리합니다. COND_None은 속성에 따로 조건을 더하지 않는다는 뜻이지, 액터의 관련성, 휴면, 업데이트 시점까지 무시하고 늘 보낸다는 뜻이 아닙니다. COND_InitialOnly는 각 연결이 받는 초기 복제 묶음에 보내는 조건으로, 서버에서 처음 만들어질 때 전체에서 한 번만 보낸다는 뜻이 아닙니다. COND_OwnerOnly는 소유 연결에, COND_SkipOwner는 그 밖의 연결에 보내며, 이 예제가 동작하려면 액터의 소유 연결이 올바르게 정해져 있어야 합니다. COND_SimulatedOnly와 COND_AutonomousOnly는 받는 쪽의 역할로 고르는 조건인데, 역할은 소유자와 비소유자를 바꿔 부르는 말이 아닙니다. 그림 아래의 설명처럼 상태 복제는 모든 중간 대입을 이벤트처럼 전달하지 않고, C++의 서버 대입은 OnRep를 저절로 부르지 않으며, Server_SetReplicatedFloat는 Server 지정자가 없는 일반 함수입니다.
RPC
RPC는 함수 호출을 네트워크로 보내 서버나 클라이언트에서 특정 로직을 실행하게 하는 방식입니다. 변수 복제는 상태를 맞추는 데는 알맞지만, 지금 일어난 액션이나 한 번뿐인 이벤트를 전달하기는 어렵기 때문에 RPC를 씁니다.
RPC는 UFUNCTION()의 지정자로 선언합니다. Server는 클라이언트가 서버에 함수 실행을 요청할 때 쓰며 서버에서 실행됩니다. Client는 서버에서 부르면 원격의 소유 클라이언트로 전달되고, 액터를 서버가 소유하거나 소유 연결이 없으면 서버에서 실행됩니다. NetMulticast는 서버에서 부르면 서버와, 액터가 지금 관련 있는 클라이언트에 전달됩니다.
전송 방식은 Reliable과 Unreliable로 정합니다. Reliable은 연결 안에서 확인 응답과 재전송을 쓰고, Unreliable은 손실과 순서 바뀜을 허용합니다. 어느 쪽도 게임 규칙의 검증을 대신하지는 않습니다. WithValidation은 Server RPC에 붙이는 선택적인 검증 함수로, false를 돌려주면 호출한 클라이언트의 연결이 끊기므로 게임 규칙에 따른 정상적인 거절과 구별해야 합니다.
#pragma once
// ...
UCLASS()
class MYPROJECT_API AMyReplicatedActor : public AActor
{
GENERATED_BODY()
public:
// ...
protected:
// 1. 클라이언트가 서버로 요청하는 RPC
UFUNCTION(Server, Reliable, WithValidation)
void Server_FireWeapon(FVector Location, FRotator Direction);
// _Validate 함수 (선언만)
bool Server_FireWeapon_Validate(FVector Location, FRotator Direction);
// _Implementation 함수 (선언만)
void Server_FireWeapon_Implementation(FVector Location, FRotator Direction);
// 2. 서버가 특정 클라이언트에게 보내는 RPC
UFUNCTION(Client, Reliable)
void Client_ReceiveChatMessage(const FString& SenderName, const FString& Message);
// 3. 서버가 모든 클라이언트에게 보내는 RPC
UFUNCTION(NetMulticast, Unreliable) // 애니메이션 재생 등 비중요, 빈번한 업데이트
void Multicast_PlayParticleEffect(FVector EffectLocation);
};#include "MyReplicatedActor.h"
// ...
// 1. Server RPC 구현: 클라이언트가 서버로 무기 발사 요청
bool AMyReplicatedActor::Server_FireWeapon_Validate(FVector Location, FRotator Direction)
{
// 여기서 클라이언트가 보낸 데이터가 유효한지 검사합니다.
// 예: Location이 너무 멀리 떨어져 있거나, 부정확한 값이 아닌지 등
// 이 부분 예제는 항상 true를 반환하므로 아직 실제 입력 검증을 하지 않습니다.
return true;
}
void AMyReplicatedActor::Server_FireWeapon_Implementation(FVector Location, FRotator Direction)
{
// 이 코드는 서버에서만 실행됩니다.
UE_LOG(LogTemp, Warning, TEXT("Server: Processing weapon fire from client at %s, direction %s"), *Location.ToString(), *Direction.ToString());
// 실제 발사 로직 (충돌 감지, 데미지 적용, 탄약 감소 등)을 여기서 처리합니다.
// 모든 클라이언트에게 발사 효과를 복제 (NetMulticast 호출)
Multicast_PlayParticleEffect(Location);
}
void AMyReplicatedActor::Client_ReceiveChatMessage_Implementation(const FString& SenderName, const FString& Message)
{
// 호출 위치와 소유 연결에 따른 대상에서 실행됩니다.
UE_LOG(LogTemp, Warning, TEXT("Chat Message from %s: %s"), *SenderName, *Message);
// UI 갱신은 로컬 플레이어·위젯의 존재를 별도로 확인해야 합니다.
}
void AMyReplicatedActor::Multicast_PlayParticleEffect_Implementation(FVector EffectLocation)
{
// 서버 호출 시 서버와 현재 관련 있는 클라이언트에서 실행됩니다.
UE_LOG(LogTemp, Warning, TEXT("All: Playing particle effect at %s"), *EffectLocation.ToString());
// 여기에 파티클 시스템 생성, 사운드 재생 등 시각적/청각적 효과를 구현합니다.
}Server_FireWeapon은 클라이언트가 무기 발사를 요청하는 RPC입니다. 이 부분 예제의 _Validate는 늘 true를 돌려주므로 아직 실제 입력 검증을 하지 않으며, 실제로는 보낸 위치가 지나치게 멀지 않은지 같은 검사를 넣어야 합니다. _Implementation은 서버에서만 실행되어 발사 처리를 하고, Multicast_PlayParticleEffect로 관련 있는 클라이언트에 효과를 보냅니다. 효과는 놓쳐도 게임 결과에 영향이 없고 자주 일어나므로 Unreliable로 선언했습니다. Client_ReceiveChatMessage는 서버가 특정 클라이언트에 채팅을 보내는 RPC입니다.
RPC를 부를 때는 몇 가지 규칙을 지킵니다. Server RPC는 서버에서 부르면 서버에서 실행되고, 클라이언트에서 원격으로 부르려면 그 액터의 소유 연결이 호출한 클라이언트와 같아야 합니다. Client와 NetMulticast RPC를 클라이언트에서 직접 부르면 그 클라이언트에서만 실행되고 다른 클라이언트로 퍼지지 않습니다. WithValidation을 붙인 Server RPC는 반드시 _Validate를 구현해야 하며, 이 함수가 false를 돌려주면 _Implementation은 실행되지 않고 연결이 끊깁니다. 실제 로직은 _Implementation 함수에 씁니다.
권한과 소유권을 확인하는 순서
복제 관련 버그를 줄이려면 로직을 시작할 때 확인하는 순서를 정해 두는 편이 좋습니다. 먼저 상태를 바꾸는 로직은 HasAuthority() 블록 안에서만 실행합니다. 그다음 입력에서 시작된 요청이라면 IsLocallyControlled()나 GetOwner()로 누가 보낸 요청인지도 함께 확인합니다. 전파 방식은 상태 값이면 Replicated 변수로, 한 번뿐인 이벤트면 RPC로 나눕니다. 마지막으로 시각 효과나 UI 같은 클라이언트의 후처리는 OnRep_나 Client RPC에서 합니다.
컴포넌트 복제
액터뿐 아니라 컴포넌트도 복제할 수 있습니다. UActorComponent를 상속한 컴포넌트를 복제하려면, 소유 액터의 복제를 켜고, 컴포넌트 생성자에서 SetIsReplicatedByDefault(true)를 부르고, 컴포넌트 자신의 GetLifetimeReplicatedProps()에서 해당 속성을 DOREPLIFETIME으로 등록합니다.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "MyReplicatedComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class MYPROJECT_API UMyReplicatedComponent : public UActorComponent
{
GENERATED_BODY()
public:
UMyReplicatedComponent();
protected:
virtual void BeginPlay() override;
UPROPERTY(Replicated)
int32 ComponentSpecificValue;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};#include "MyReplicatedComponent.h"
#include "Net/UnrealNetwork.h"
UMyReplicatedComponent::UMyReplicatedComponent()
{
SetIsReplicatedByDefault(true); // 컴포넌트 생성자에서 복제 기본값 설정
ComponentSpecificValue = 0;
}
void UMyReplicatedComponent::BeginPlay()
{
Super::BeginPlay();
}
void UMyReplicatedComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UMyReplicatedComponent, ComponentSpecificValue);
}생성자에서 만드는 기본 서브오브젝트는 서버와 클라이언트의 각 인스턴스가 생성자에서 따로 만들고, 서버에서 실행 중에 만든 동적 복제 컴포넌트는 생성과 삭제도 클라이언트로 전파될 수 있습니다. 어느 경우든 소유 액터의 복제, 컴포넌트의 복제 활성화, 컴포넌트 속성의 등록은 각각 따로 필요합니다.
네트워크 역할: GetLocalRole과 HasAuthority
네트워크 환경에서 액터의 현재 상태를 알려면 역할을 확인합니다. GetLocalRole()은 이 액터 인스턴스의 역할을 돌려줍니다. Authority는 이 액터에 대한 권한을 뜻하며, 클라이언트에서만 만든 로컬 액터도 그 클라이언트에서는 이 역할을 가질 수 있습니다. AutonomousProxy는 그 클라이언트가 직접 조종하는 액터의 역할이고, SimulatedProxy는 복제된 상태를 받아 흉내 내기만 하는 역할입니다. 이 역할들이 소유자, 비소유자와 늘 하나씩 짝을 이루는 것은 아닙니다.
#include "MyReplicatedActor.h"
void AMyReplicatedActor::BeginPlay()
{
Super::BeginPlay();
if (HasAuthority()) // Role == ROLE_Authority
{
UE_LOG(LogTemp, Warning, TEXT("AMyReplicatedActor: Authority role"));
}
else if (GetLocalRole() == ENetRole::ROLE_AutonomousProxy)
{
UE_LOG(LogTemp, Warning, TEXT("AMyReplicatedActor: Autonomous Proxy role"));
}
else if (GetLocalRole() == ENetRole::ROLE_SimulatedProxy)
{
UE_LOG(LogTemp, Warning, TEXT("AMyReplicatedActor: Simulated Proxy role"));
}
}HasAuthority()는 GetLocalRole() == ENetRole::ROLE_Authority인지 검사하는 함수입니다. 그래서 서버에서 만든 공유 게임플레이 액터라는 전제 아래에서 상태 변경을 제한하는 데 쓰고, 입력 요청에 대해서는 소유 연결과 서버 검증을 함께 확인합니다.
정리
- 액터는
bReplicates나SetReplicates(true)로 복제 대상이 되고, 서버에서의 생성과 파괴가 사본을 가진 클라이언트로 전파됩니다.SetReplicateMovement는 루트 컴포넌트의 이동을 복제합니다. - 변수는
Replicated나ReplicatedUsing과DOREPLIFETIME등록이 모두 있어야 복제되며,Net/UnrealNetwork.h가 필요합니다. - RepNotify는 클라이언트가 새 값을 받을 때 불리며, 서버의 중간 변경은 합쳐질 수 있으므로 모든 변화를 이벤트로 받는다고 기대하지 않습니다.
DOREPLIFETIME_COND의 조건은 액터의 관련성, 휴면, 시점 조건 위에 더해지며,COND_InitialOnly는 연결마다의 초기 묶음에 적용됩니다.- RPC는
Server,Client,NetMulticast와Reliable,Unreliable로 선언하고, 실행 위치는 호출 위치와 소유 연결이 정합니다. - 컴포넌트는 소유 액터의 복제,
SetIsReplicatedByDefault(true), 속성 등록이 모두 있어야 복제됩니다. GetLocalRole()의 역할은 소유 여부와 같은 말이 아니며,HasAuthority()는 그 역할이 권한인지 검사합니다.
연습 문제
1. 연결마다 달라지는 속성
서버 액터의 소유 연결은 A이고, A와 B 모두 지금 액터의 수신 조건을 만족한다고 합시다. PlayerInventory는 COND_OwnerOnly, 다른 속성 Q는 COND_SkipOwner, 속성 I는 COND_InitialOnly로 등록했습니다. A와 B는 각각 어느 속성의 대상일까요? 나중에 C가 처음으로 이 액터의 초기 복제 묶음을 받는다면, I는 첫 전송 때 이미 끝난 값이라고 빼도 될까요?
해설 보기
PlayerInventory는 소유 연결인 A가, Q는 소유 연결이 아닌 B가 대상입니다. C의 초기 묶음에는 I도 보내질 수 있으므로, 서버에서 만들 때 한 번으로 모두 끝났다고 보면 안 됩니다. COND_InitialOnly는 연결마다의 초기 묶음에 적용되는 조건이기 때문입니다.
실제로 전달되는지는 값의 변경과 등록, 그리고 그 연결에서의 액터 관련성, 휴면, 업데이트 조건까지 확인해야 알 수 있습니다. 소유 연결이 B로 바뀌거나 액터의 수신 조건이 달라지면 같은 표를 새 전제로 다시 읽습니다.
2. 세 번 바꿨는데 한 번만
서버에서 한 프레임 안에 ReplicatedFloat를 30, 40, 50으로 차례로 바꿨습니다. 클라이언트에서 OnRep_ReplicatedFloat가 세 번 불리고 로그에 30, 40, 50이 모두 찍힐 것이라고 기대해도 될까요? 서버에서는 몇 번 불릴까요?
해설 보기
기대할 수 없습니다. 복제는 전송 시점의 상태를 보내므로, 클라이언트는 마지막 값인 50 하나만 받고 OnRep도 한 번만 불릴 수 있습니다. 30과 40을 받았다는 기록은 남지 않을 수 있습니다.
서버에서는 C++ 대입만으로 OnRep가 불리지 않으므로 0번입니다. 서버에서도 같은 처리가 필요하면 직접 불러야 합니다. 각 변화를 모두 처리해야 한다면 상태 복제가 아니라 이벤트를 따로 전달하는 설계가 필요합니다.
3. 모두에게 알리고 싶은 클라이언트
클라이언트 A가 자신의 캐릭터에서 Multicast_PlayParticleEffect를 직접 불렀습니다. 다른 플레이어들의 화면에도 효과가 보일까요? 모두에게 보이게 하려면 어떻게 해야 할까요?
해설 보기
보이지 않습니다. NetMulticast RPC를 클라이언트에서 직접 부르면 그 클라이언트에서만 실행되므로, A의 화면에만 효과가 나타납니다.
모두에게 보이게 하려면 A가 Server RPC로 서버에 요청하고, 서버가 요청을 검증한 뒤 Multicast_PlayParticleEffect를 부르게 합니다. 이 글의 Server_FireWeapon_Implementation이 바로 그렇게 서버에서 멀티캐스트를 부르는 구조입니다.
속성 복제와 조건은 Epic의 Replicate Actor Properties 문서에서, 액터 역할은 Actor Role and Remote Role 문서에서 확인할 수 있습니다.
다음 글 RPC와 서버-클라이언트 통신에서는 RPC를 실제 게임플레이 요청에 쓰는 방법과, 서버가 클라이언트의 요청을 검증하는 흐름을 더 자세히 살펴봅니다.