복제(Replication) 시스템 이해
이전 절에서 우리는 언리얼 엔진 네트워킹의 기본 개념과 클라이언트-서버 모델, 그리고 복제(Replication)의 개요에 대해 살펴보았습니다. 이제 언리얼 엔진 멀티플레이어 게임의 핵심인 복제(Replication) 시스템에 대해 더 깊이 파고들어 보겠습니다. 복제는 서버와 클라이언트 간에 게임 상태를 효율적이고 안정적으로 동기화하는 메커니즘으로, 복잡한 멀티플레이어 게임을 구현하는 데 필수적인 요소입니다.
이번 절에서는 액터 복제, 변수 복제, RPC(원격 프로시저 호출) 등 복제 시스템의 다양한 구성 요소를 심층적으로 분석하고, 각 요소가 어떻게 작동하는지 구체적인 예시와 함께 알아보겠습니다.
액터 복제 (Actor Replication)
언리얼 엔진에서 네트워크를 통해 게임 월드의 객체를 동기화하는 가장 기본적인 단위는 액터(Actor) 입니다. 액터 복제는 서버에 존재하는 액터의 인스턴스를 네트워크를 통해 클라이언트에 생성하고, 해당 액터의 중요한 상태를 지속적으로 동기화하는 과정입니다.
bReplicates
와 SetReplicates(true)
액터가 복제되려면, 해당 액터 클래스의 생성자에서 bReplicates
속성을 true
로 설정하거나 런타임에 SetReplicates(true)
를 호출해야 합니다.
// AMyReplicatedActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyReplicatedActor.generated.h"
UCLASS()
class MYPROJECT_API AMyReplicatedActor : public AActor
{
GENERATED_BODY()
public:
AMyReplicatedActor();
};
// AMyReplicatedActor.cpp
#include "MyReplicatedActor.h"
AMyReplicatedActor::AMyReplicatedActor()
{
// 중요: 이 액터가 네트워크를 통해 복제될 것임을 명시
**bReplicates = true;** // 액터의 움직임(위치, 회전, 스케일)도 복제될 것임을 명시
// UMovementComponent가 액터에 있어야 효과가 있습니다.
**SetReplicatesMovement(true);** }
bReplicates = true;
: 이 액터의 인스턴스가 서버에 생성되면, 언리얼 엔진은 자동으로 네트워크를 통해 연결된 모든 클라이언트에 이 액터의 사본을 생성하고 동기화하기 시작합니다.SetReplicatesMovement(true);
: 이 함수를 호출하면, 액터의 위치, 회전, 스케일 정보가 네트워크를 통해 자동으로 복제됩니다.UMovementComponent
를 사용하는 캐릭터나 폰의 경우 이 속성이 매우 중요합니다.
액터의 생성 및 파괴 복제
- 생성: 서버에서
GetWorld()->SpawnActor<AMyActor>(...)
를 통해 액터가 생성되면,bReplicates
가true
인 경우 해당 액터는 모든 클라이언트에도 자동으로 스폰됩니다. - 파괴: 서버에서
Destroy()
가 호출되어 액터가 파괴되면, 모든 클라이언트에서도 해당 액터의 사본이 파괴됩니다.
변수 복제 (Variable Replication)
액터 자체의 존재 여부 외에도, 액터 내부의 특정 변수들의 값도 서버와 클라이언트 간에 동기화되어야 합니다. 이는 UPROPERTY()
매크로와 GetLifetimeReplicatedProps()
함수를 통해 관리됩니다.
DOREPLIFETIME
매크로
변수를 복제하려면 UPROPERTY()
에 Replicated
지정자를 추가하고, 액터 클래스의 GetLifetimeReplicatedProps()
함수 내에서 DOREPLIFETIME()
매크로를 사용해야 합니다.
// AMyReplicatedActor.h
#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;
};
// AMyReplicatedActor.cpp
#include "MyReplicatedActor.h"
#include "**Net/UnrealNetwork.h**" // DOREPLIFETIME 매크로를 위해 반드시 포함
AMyReplicatedActor::AMyReplicatedActor()
{
bReplicates = true;
SetReplicatesMovement(true);
ReplicatedInteger = 0; // 초기값 설정
}
void AMyReplicatedActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
**DOREPLIFETIME(AMyReplicatedActor, ReplicatedInteger);** }
UPROPERTY(Replicated)
: 이 변수가 네트워크를 통해 복제될 것임을 언리얼 엔진에 알립니다.GetLifetimeReplicatedProps()
: 이 함수를 오버라이드하여DOREPLIFETIME
매크로를 사용함으로써, 언리얼 엔진의 네트워크 코드가 어떤 변수를 복제해야 하는지 알게 됩니다.
OnRep_
노티파이 함수 (ReplicatedUsing
)
특정 변수가 클라이언트로 복제되어 값이 변경될 때, 클라이언트에서 특정 로직을 실행하고 싶을 때 ReplicatedUsing
지정자를 사용합니다. 이는 주로 시각적 효과, 사운드 재생, UI 업데이트 등 클라이언트에서만 필요한 처리에 사용됩니다.
// AMyReplicatedActor.h (OnRep_ 함수 추가)
#pragma once
// ...
UCLASS()
class MYPROJECT_API AMyReplicatedActor : public AActor
{
GENERATED_BODY()
public:
// ...
protected:
// 값이 변경될 때마다 클라이언트에서 OnRep_ReplicatedFloat 함수를 호출할 변수
**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);
};
// AMyReplicatedActor.cpp (OnRep_ 함수 구현)
#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_ReplicatedFloat가 호출됩니다.
UE_LOG(LogTemp, Warning, TEXT("Server: ReplicatedFloat set to: %f"), ReplicatedFloat);
// 서버에서도 동일한 로직을 실행하고 싶다면 OnRep_ReplicatedFloat()를 직접 호출할 수 있습니다.
// OnRep_ReplicatedFloat(); // 서버에서도 강제로 OnRep_ 함수 호출
}
}
UPROPERTY(ReplicatedUsing = FunctionName)
: 이 변수의 값이 복제되어 클라이언트에서 변경될 때, 지정된FunctionName
이 호출됩니다. 함수는 반드시UFUNCTION()
으로 선언되어야 하며,OnRep_
접두사를 사용하는 것이 관례입니다.
조건부 복제 (DOREPLIFETIME_COND
)
모든 클라이언트에게 특정 변수를 복제할 필요가 없거나, 특정 조건에서만 복제하고 싶을 때 DOREPLIFETIME_COND
매크로를 사용합니다.
// AMyReplicatedActor.h
#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;
};
// AMyReplicatedActor.cpp
#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);
}
주요 조건 플래그
COND_None
: 항상 복제 (기본값)COND_InitialOnly
: 액터가 처음 스폰될 때 한 번만 복제COND_OwnerOnly
: 액터의 소유자에게만 복제COND_SkipOwner
: 소유자를 제외한 모든 클라이언트에게 복제COND_SimulatedOnly
: 서버 시뮬레이션 클라이언트(Non-Owner)에게만 복제COND_AutonomousOnly
: 자율 프록시(Owner)에게만 복제- 그 외 다양한 조건들이 있습니다.
RPC (Remote Procedure Call)
RPC는 네트워크를 통해 함수 호출을 전송하여 서버 또는 클라이언트에서 특정 로직을 실행하도록 하는 메커니즘입니다. 변수 복제로는 즉각적인 액션이나 이벤트 처리가 어렵기 때문에 RPC가 사용됩니다.
RPC 지정자
UFUNCTION()
매크로에 다음과 같은 지정자를 사용하여 RPC를 선언합니다.
Server
: 클라이언트가 서버로 함수 실행을 요청할 때 사용. 서버에서만 실행됩니다.Reliable
/Unreliable
: 전송 신뢰성.Reliable
은 전송을 보장하지만 오버헤드가 크고,Unreliable
은 빠르지만 패킷 손실 가능성이 있습니다.WithValidation
:Server
RPC에만 사용되며, 클라이언트가 보낸 인자가 서버에서 유효한지 검증하는_Validate
함수를 강제합니다. 치트 방지에 필수적입니다.
Client
: 서버가 특정 클라이언트(해당 액터의 소유자)로 함수 실행을 요청할 때 사용. 해당 클라이언트에서만 실행됩니다.NetMulticast
: 서버가 모든 클라이언트(서버 자신 포함)로 함수 실행을 요청할 때 사용. 모든 클라이언트에서 실행됩니다.
// AMyReplicatedActor.h (RPC 함수 선언)
#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);
};
// AMyReplicatedActor.cpp (RPC 함수 구현)
#include "MyReplicatedActor.h"
// ...
// 1. Server RPC 구현: 클라이언트가 서버로 무기 발사 요청
**bool AMyReplicatedActor::Server_FireWeapon_Validate(FVector Location, FRotator Direction)**
**{**
** // 여기서 클라이언트가 보낸 데이터가 유효한지 검사합니다.**
** // 예: Location이 너무 멀리 떨어져 있거나, 부정확한 값이 아닌지 등**
** // 치트 방지에 매우 중요합니다.**
** if (!GetActorLocation().Equals(Location, 100.0f)) // 대략적인 위치 일치 여부 확인 (예시)
** {
** UE_LOG(LogTemp, Warning, TEXT("Server_FireWeapon_Validate: Invalid Location from client!"));
** return false;
** }
** 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);** **}**
// 2. Client RPC 구현: 서버가 특정 클라이언트에게 채팅 메시지 전송
**void AMyReplicatedActor::Client_ReceiveChatMessage_Implementation(const FString& SenderName, const FString& Message)**
**{**
** // 이 코드는 해당 액터를 소유한 클라이언트에서만 실행됩니다.**
** // HasAuthority()는 false를 반환합니다.**
** if (!HasAuthority())**
** {**
** UE_LOG(LogTemp, Warning, TEXT("Client: Chat Message from %s: %s"), *SenderName, *Message);**
** // 여기에 UI에 메시지를 표시하는 로직을 구현합니다.**
** }**
**}**
// 3. NetMulticast RPC 구현: 서버가 모든 클라이언트에게 파티클 효과 재생 명령
**void AMyReplicatedActor::Multicast_PlayParticleEffect_Implementation(FVector EffectLocation)**
**{**
** // 이 코드는 서버를 포함하여 모든 연결된 클라이언트에서 실행됩니다.**
** UE_LOG(LogTemp, Warning, TEXT("All: Playing particle effect at %s"), *EffectLocation.ToString());**
** // 여기에 파티클 시스템 생성, 사운드 재생 등 시각적/청각적 효과를 구현합니다.**
**}**
RPC 호출 규칙
Server
RPC: 클라이언트에서만 호출할 수 있습니다. 서버에서Server_FireWeapon()
을 호출하면 아무 일도 일어나지 않습니다.Client
/NetMulticast
RPC: 서버에서만 호출할 수 있습니다. 클라이언트에서 이들을 호출하면 아무 일도 일어나지 않습니다._Validate
함수:WithValidation
이 붙은Server
RPC는 반드시_Validate
함수를 구현해야 합니다. 이 함수가false
를 반환하면_Implementation
함수는 실행되지 않고, 해당 클라이언트는 서버에서 연결이 끊어질 수 있습니다._Implementation
함수: RPC가 실행될 실제 로직을 담는 함수입니다.
컴포넌트 복제 (Component Replication)
액터뿐만 아니라 특정 컴포넌트도 복제될 수 있습니다. UActorComponent
를 상속받는 컴포넌트가 복제되려면:
컴포넌트의 생성자에서 bReplicates = true;
로 설정합니다.
액터의 GetLifetimeReplicatedProps()
에서 해당 컴포넌트의 변수를 DOREPLIFETIME
으로 선언합니다.
// MyReplicatedComponent.h
#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;
};
// MyReplicatedComponent.cpp
#include "MyReplicatedComponent.h"
#include "Net/UnrealNetwork.h"
UMyReplicatedComponent::UMyReplicatedComponent()
{
**bReplicates = true;** // 컴포넌트 복제 활성화
ComponentSpecificValue = 0;
}
void UMyReplicatedComponent::BeginPlay()
{
Super::BeginPlay();
}
void UMyReplicatedComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
**DOREPLIFETIME(UMyReplicatedComponent, ComponentSpecificValue);**
}
- 컴포넌트 자체는
DOREPLIFETIME
을 사용하지 않습니다. 컴포넌트를 소유한 액터가 복제되면 컴포넌트도 함께 복제됩니다.bReplicates
는 컴포넌트 내의 변수들이 복제될 수 있도록 설정하는 역할을 합니다.
AActor::Role
과 AActor::RemoteRole
네트워크 환경에서 액터의 현재 상태를 파악하는 데 중요한 두 가지 속성이 있습니다.
Role
: 액터의 현재 인스턴스가 네트워크상에서 어떤 권한(Authority) 을 가지고 있는지 나타냅니다.ROLE_Authority
: 이 액터의 인스턴스가 서버(Server) 에서 실행되고 있음을 의미합니다. 이 액터의 모든 변수와 로직은 "진실된" 상태를 가지고 있습니다.ROLE_AutonomousProxy
: 이 액터의 인스턴스가 클라이언트에서 실행되고 있으며, 해당 클라이언트가 이 액터의 소유자(Owner) 임을 의미합니다 (예: 현재 플레이어가 조작하는 캐릭터).ROLE_SimulatedProxy
: 이 액터의 인스턴스가 클라이언트에서 실행되고 있으며, 해당 클라이언트가 이 액터의 비소유자임을 의미합니다 (예: 다른 플레이어의 캐릭터).
RemoteRole
: 액터의 인스턴스가 네트워크의 다른 쪽 끝(Remote) 에서 어떤 역할을 하는지 나타냅니다. (거의 사용되지 않습니다.)
// AMyReplicatedActor.cpp (BeginPlay에서 Role 확인)
#include "MyReplicatedActor.h"
void AMyReplicatedActor::BeginPlay()
{
Super::BeginPlay();
if (HasAuthority()) // Role == ROLE_Authority
{
UE_LOG(LogTemp, Warning, TEXT("AMyReplicatedActor: This is the Server (Authority)!"));
}
else if (GetLocalRole() == ENetRole::ROLE_AutonomousProxy)
{
UE_LOG(LogTemp, Warning, TEXT("AMyReplicatedActor: This is the Owner Client (Autonomous Proxy)!"));
}
else if (GetLocalRole() == ENetRole::ROLE_SimulatedProxy)
{
UE_LOG(LogTemp, Warning, TEXT("AMyReplicatedActor: This is a Non-Owner Client (Simulated Proxy)!"));
}
}
HasAuthority()
:GetLocalRole() == ENetRole::ROLE_Authority
의 축약형으로, 현재 코드가 서버에서 실행되고 있는지 확인하는 데 가장 일반적으로 사용됩니다. 대부분의 게임 로직은HasAuthority()
검사 내부에 있어야 합니다.
마치며
언리얼 엔진의 복제 시스템은 멀티플레이어 게임의 동기화를 위한 견고한 기반을 제공합니다. bReplicates
를 통한 액터 복제, UPROPERTY(Replicated)
및 DOREPLIFETIME
을 통한 변수 복제, ReplicatedUsing
을 통한 OnRep_ 함수 활용, 그리고 Server
, Client
, NetMulticast
RPC를 통한 함수 호출 복제는 멀티플레이어 게임의 핵심 구현 요소입니다. 또한, Role
및 HasAuthority()
를 이해하여 네트워크 권한을 올바르게 관리하는 것은 버그 없는 효율적인 멀티플레이어 게임을 개발하는 데 매우 중요합니다.