Skip to content

UE5 C++ Replication 기초

개요 — 언리얼 엔진 멀티플레이어 아키텍처

Section titled “개요 — 언리얼 엔진 멀티플레이어 아키텍처”

언리얼 엔진은 서버 권위적(Server Authoritative) 멀티플레이어 모델을 사용합니다. 게임 로직의 최종 결정권은 항상 서버에 있으며, 클라이언트는 서버의 상태를 복제받아 표시합니다.

[서버 (Server / Listen Server)]
- 모든 Actor의 Authority 보유
- 게임 상태 최종 결정
- 클라이언트로 복제 데이터 전송
[클라이언트 A] [클라이언트 B]
- Simulated Proxy
- 서버 데이터 복제 수신
- 입력 → 서버 RPC 전송
개념설명
Authority객체의 소유권을 가진 쪽 (서버)
Autonomous Proxy로컬 플레이어가 빙의한 Pawn (입력 처리 가능)
Simulated Proxy다른 클라이언트의 Pawn 복제본 (서버 데이터 표시)
Replication서버 → 클라이언트 속성값 동기화
RPC원격 프로시저 호출 (클라이언트 ↔ 서버 함수 호출)

HasAuthority()는 현재 실행 컨텍스트가 서버(Authority)인지 확인하는 가장 기본적인 방법입니다.

void AMyActor::SomeFunction()
{
if (HasAuthority())
{
// 서버에서만 실행되는 코드
// 예: 게임 상태 변경, 데미지 계산, 스폰
ApplyDamageToTarget();
}
else
{
// 클라이언트에서 실행되는 코드
// 예: 예측(Prediction), 시각 효과
PlayLocalEffect();
}
}
#include "Engine/NetDriver.h"
void AMyActor::DebugRoles()
{
ENetRole LocalRole = GetLocalRole(); // 현재 머신에서의 역할
ENetRole RemoteRole = GetRemoteRole(); // 상대방 머신에서의 역할
// ENetRole 값
// ROLE_None = 복제 없음
// ROLE_SimulatedProxy = 서버 데이터를 받아 시뮬레이션하는 클라이언트
// ROLE_AutonomousProxy = 로컬 플레이어가 제어하는 Pawn (클라이언트)
// ROLE_Authority = 이 Actor의 권위 보유 (서버)
UE_LOG(LogTemp, Log, TEXT("LocalRole: %d, RemoteRole: %d"),
static_cast<int32>(LocalRole),
static_cast<int32>(RemoteRole));
}
// 실용적인 역할 체크 패턴
bool AMyPawn::IsLocallyControlled() const
{
// AutonomousProxy = 이 클라이언트 머신이 제어하는 Pawn
return GetLocalRole() == ROLE_AutonomousProxy || HasAuthority();
}

1.3 서버/클라이언트 판별 실전 패턴

Section titled “1.3 서버/클라이언트 판별 실전 패턴”
void AMyCharacter::TakeDamage_Custom(float DamageAmount, AActor* DamageCauser)
{
// 데미지 계산은 서버(Authority)에서만 수행
if (!HasAuthority())
{
return;
}
// 서버에서 체력 감소 → Replicated 속성이므로 클라이언트에 자동 동기화
CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.f, MaxHealth);
if (CurrentHealth <= 0.f)
{
// 사망 처리도 서버에서
HandleDeath();
}
}

2. UPROPERTY(Replicated)와 GetLifetimeReplicatedProps

Section titled “2. UPROPERTY(Replicated)와 GetLifetimeReplicatedProps”

복제할 속성에는 UPROPERTY(Replicated) 지정자를 추가하고, GetLifetimeReplicatedProps()에 등록합니다.

MyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Net/UnrealNetwork.h" // DOREPLIFETIME 매크로
#include "MyCharacter.generated.h"
UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AMyCharacter();
// 복제 대상 속성 등록 (반드시 오버라이드)
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
protected:
// 복제 속성 — 서버 값이 모든 클라이언트에 동기화됨
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Stats")
float CurrentHealth = 100.f;
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Stats")
int32 Score = 0;
// 복제 대상이 아닌 속성
UPROPERTY(EditDefaultsOnly, Category = "Stats")
float MaxHealth = 100.f;
bool bIsDead = false; // 서버 전용 상태 — 복제 불필요
};
MyCharacter.cpp
#include "MyCharacter.h"
#include "Net/UnrealNetwork.h"
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// DOREPLIFETIME: 조건 없이 모든 클라이언트에 복제
DOREPLIFETIME(AMyCharacter, CurrentHealth);
DOREPLIFETIME(AMyCharacter, Score);
}

2.2 복제 조건(Condition)으로 최적화

Section titled “2.2 복제 조건(Condition)으로 최적화”
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 조건 없이 모든 클라이언트에 복제
DOREPLIFETIME(AMyCharacter, CurrentHealth);
// 소유자(로컬 플레이어)에게만 복제 — 개인 정보 (인벤토리, 골드 등)
DOREPLIFETIME_CONDITION(AMyCharacter, Score, COND_OwnerOnly);
// 소유자 제외 모든 클라이언트에 복제 — 다른 플레이어에게 보여야 하는 정보
DOREPLIFETIME_CONDITION(AMyCharacter, TeamID, COND_SkipOwner);
// 초기 전송만 — 이후 변경 복제 안 함 (변경 없는 설정값)
DOREPLIFETIME_CONDITION(AMyCharacter, CharacterClass, COND_InitialOnly);
}

주요 복제 조건:

조건설명
COND_None항상 복제 (기본값, DOREPLIFETIME과 동일)
COND_OwnerOnly소유자(Autonomous Proxy)에게만 복제
COND_SkipOwner소유자를 제외한 모든 클라이언트에 복제
COND_SimulatedOnlySimulated Proxy에게만 복제
COND_InitialOnly최초 1회만 복제
COND_ReplayOnly리플레이 시에만 복제

ReplicatedUsing을 사용하면 클라이언트가 복제 데이터를 수신했을 때 호출될 콜백 함수를 지정할 수 있습니다.

MyCharacter.h
UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
protected:
// ReplicatedUsing: 복제 수신 시 OnRep_CurrentHealth 호출
UPROPERTY(ReplicatedUsing = OnRep_CurrentHealth, BlueprintReadOnly, Category = "Stats")
float CurrentHealth = 100.f;
UPROPERTY(ReplicatedUsing = OnRep_bIsOnFire, BlueprintReadOnly, Category = "Status")
bool bIsOnFire = false;
// OnRep 함수 — UFUNCTION 매크로 필수
UFUNCTION()
void OnRep_CurrentHealth();
UFUNCTION()
void OnRep_bIsOnFire();
};
MyCharacter.cpp
#include "Net/UnrealNetwork.h"
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyCharacter, CurrentHealth);
DOREPLIFETIME(AMyCharacter, bIsOnFire);
}
void AMyCharacter::OnRep_CurrentHealth()
{
// 클라이언트에서 체력이 변경되었을 때 실행
// CurrentHealth는 이미 새 값으로 업데이트된 상태
UE_LOG(LogTemp, Log, TEXT("Health updated on client: %.1f"), CurrentHealth);
// UI 갱신, 이펙트 재생 등
UpdateHealthBar();
if (CurrentHealth <= 0.f)
{
// 클라이언트 측 사망 처리 (시각 효과)
PlayDeathAnimation();
}
}
void AMyCharacter::OnRep_bIsOnFire()
{
if (bIsOnFire)
{
// 화염 이펙트 시작
StartFireParticle();
}
else
{
// 화염 이펙트 중지
StopFireParticle();
}
}

중요: OnRep 함수는 클라이언트에서만 호출됩니다. 서버에서는 속성을 직접 수정하면 되므로 OnRep가 자동 실행되지 않습니다. 서버와 클라이언트 모두 같은 처리가 필요하다면 별도의 공통 함수를 만들어 양쪽에서 호출하세요.


RPC(Remote Procedure Call)는 한 머신에서 함수를 호출하면 다른 머신에서 실행되는 메커니즘입니다.

RPC 유형호출 위치실행 위치선언 키워드
Server RPC클라이언트서버Server
NetMulticast RPC서버서버 + 모든 클라이언트NetMulticast
Client RPC서버특정 클라이언트 (소유자)Client

4.2 Server RPC — 클라이언트 → 서버

Section titled “4.2 Server RPC — 클라이언트 → 서버”

플레이어 입력처럼 클라이언트의 행동을 서버에 알릴 때 사용합니다.

MyCharacter.h
protected:
// Server: 클라이언트가 호출하면 서버에서 실행
// Reliable: 패킷 유실 없이 반드시 전달 보장 (중요 이벤트)
// WithValidation: _Validate 함수로 치트 방지 검증 가능
UFUNCTION(Server, Reliable, WithValidation)
void ServerRequestAttack();
// Unreliable: 패킷 유실 허용 (위치 업데이트 같은 잦은 호출)
UFUNCTION(Server, Unreliable)
void ServerUpdateAimDirection(FVector AimDir);
MyCharacter.cpp
// RPC 구현 — 함수명 뒤에 _Implementation 접미사
void AMyCharacter::ServerRequestAttack_Implementation()
{
// 서버에서 실행되는 공격 로직
if (HasAuthority())
{
PerformAttack();
// 공격 이펙트를 모든 클라이언트에 전파
MulticastPlayAttackEffect();
}
}
// WithValidation 시 _Validate 함수도 구현 필요
bool AMyCharacter::ServerRequestAttack_Validate()
{
// false 반환 시 호출을 거부하고 클라이언트 연결 끊음
// 스팸 방지, 쿨다운 체크 등
return !bIsAttacking && IsAlive();
}
// 클라이언트에서 입력 처리 후 서버 RPC 호출
void AMyCharacter::HandleAttackInput()
{
// 로컬 예측 (즉각 반응감)
PlayLocalAttackAnimation();
// 서버에 공격 요청
ServerRequestAttack();
}

4.3 NetMulticast RPC — 서버 → 전체

Section titled “4.3 NetMulticast RPC — 서버 → 전체”

서버에서 발생한 이벤트를 모든 클라이언트에서 동시에 시각적으로 표현할 때 사용합니다.

MyCharacter.h
protected:
UFUNCTION(NetMulticast, Reliable)
void MulticastPlayAttackEffect();
UFUNCTION(NetMulticast, Unreliable)
void MulticastSpawnBloodParticle(FVector Location);
MyCharacter.cpp
void AMyCharacter::MulticastPlayAttackEffect_Implementation()
{
// 서버와 모든 클라이언트에서 실행
// 주의: 서버에서도 실행됨 (서버가 Listen Server인 경우 시각 효과 포함)
if (AttackMontage)
{
GetMesh()->GetAnimInstance()->Montage_Play(AttackMontage);
}
UGameplayStatics::SpawnSoundAtLocation(this, AttackSound, GetActorLocation());
}
void AMyCharacter::MulticastSpawnBloodParticle_Implementation(FVector Location)
{
// 파티클은 Unreliable — 가끔 누락되어도 게임플레이에 영향 없음
UNiagaraFunctionLibrary::SpawnSystemAtLocation(GetWorld(), BloodParticle, Location);
}

4.4 Client RPC — 서버 → 특정 클라이언트

Section titled “4.4 Client RPC — 서버 → 특정 클라이언트”

서버가 특정 플레이어(소유자)에게만 메시지를 보낼 때 사용합니다.

MyPlayerController.h
protected:
// Client: 서버에서 호출하면 소유자(이 컨트롤러를 가진) 클라이언트에서 실행
UFUNCTION(Client, Reliable)
void ClientShowNotification(const FText& Message);
UFUNCTION(Client, Unreliable)
void ClientUpdatePing(float PingMs);
MyPlayerController.cpp
void AMyPlayerController::ClientShowNotification_Implementation(const FText& Message)
{
// 이 컨트롤러를 소유한 클라이언트에서만 실행
if (NotificationWidget)
{
NotificationWidget->ShowMessage(Message);
}
}
// 서버에서 특정 플레이어에게 알림 전송
void AMyGameMode::NotifyPlayerOfKill(AMyPlayerController* Killer)
{
if (Killer && HasAuthority())
{
Killer->ClientShowNotification(FText::FromString(TEXT("You got a kill!")));
}
}

Actor의 복제를 활성화하려면 생성자에서 bReplicatestrue로 설정해야 합니다.

MyCharacter.cpp
AMyCharacter::AMyCharacter()
{
// Actor 복제 활성화 — 이것 없이는 UPROPERTY(Replicated)와 RPC 모두 동작하지 않음
bReplicates = true;
// 이동 컴포넌트 복제 (ACharacter는 기본적으로 활성화됨)
// GetCharacterMovement()->SetIsReplicated(true);
// 컴포넌트 복제 활성화
// CustomComponent->SetIsReplicated(true);
}

ReplicatedHealthCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Net/UnrealNetwork.h"
#include "ReplicatedHealthCharacter.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnHealthChangedBP, float, NewHealth, float, MaxHealth);
UCLASS()
class MYGAME_API AReplicatedHealthCharacter : public ACharacter
{
GENERATED_BODY()
public:
AReplicatedHealthCharacter();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// 데미지 처리 — 서버 전용
UFUNCTION(BlueprintCallable, Category = "Health")
void ApplyDamage(float DamageAmount);
// Blueprint 이벤트
UPROPERTY(BlueprintAssignable, Category = "Health|Events")
FOnHealthChangedBP OnHealthChangedBP;
protected:
virtual void BeginPlay() override;
UPROPERTY(ReplicatedUsing = OnRep_CurrentHealth, BlueprintReadOnly, Category = "Health")
float CurrentHealth = 100.f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Health")
float MaxHealth = 100.f;
UFUNCTION()
void OnRep_CurrentHealth();
// Server RPC — 클라이언트 요청으로 데미지 적용
UFUNCTION(Server, Reliable, WithValidation)
void ServerApplyDamage(float DamageAmount, AActor* DamageCauser);
// Multicast RPC — 사망 시 모든 클라이언트에 알림
UFUNCTION(NetMulticast, Reliable)
void MulticastOnDeath();
private:
bool bIsDead = false;
void HandleDeath();
void BroadcastHealthChange(); // 서버/클라이언트 공통 처리
};
ReplicatedHealthCharacter.cpp
#include "ReplicatedHealthCharacter.h"
AReplicatedHealthCharacter::AReplicatedHealthCharacter()
{
bReplicates = true;
PrimaryActorTick.bCanEverTick = false;
}
void AReplicatedHealthCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AReplicatedHealthCharacter, CurrentHealth);
}
void AReplicatedHealthCharacter::BeginPlay()
{
Super::BeginPlay();
if (HasAuthority())
{
CurrentHealth = MaxHealth;
}
}
void AReplicatedHealthCharacter::ApplyDamage(float DamageAmount)
{
// 서버에서 직접 호출하는 경우
if (HasAuthority())
{
ServerApplyDamage_Implementation(DamageAmount, nullptr);
}
}
void AReplicatedHealthCharacter::ServerApplyDamage_Implementation(float DamageAmount, AActor* DamageCauser)
{
if (bIsDead || DamageAmount <= 0.f)
{
return;
}
CurrentHealth = FMath::Clamp(CurrentHealth - DamageAmount, 0.f, MaxHealth);
// 서버 측 UI/로직 처리
BroadcastHealthChange();
if (CurrentHealth <= 0.f)
{
HandleDeath();
}
// CurrentHealth가 Replicated이므로 변경 즉시 클라이언트로 복제됨
// OnRep_CurrentHealth가 클라이언트에서 호출됨
}
bool AReplicatedHealthCharacter::ServerApplyDamage_Validate(float DamageAmount, AActor* DamageCauser)
{
// 비정상적으로 큰 데미지 값 거부
return DamageAmount > 0.f && DamageAmount < 10000.f;
}
void AReplicatedHealthCharacter::OnRep_CurrentHealth()
{
// 클라이언트에서 체력 변경 수신 시 실행
BroadcastHealthChange();
if (CurrentHealth <= 0.f && !bIsDead)
{
// 클라이언트 측 사망 처리 (시각 효과만)
PlayDeathAnimation();
}
}
void AReplicatedHealthCharacter::BroadcastHealthChange()
{
// 서버와 클라이언트 모두 실행되는 공통 처리
OnHealthChangedBP.Broadcast(CurrentHealth, MaxHealth);
}
void AReplicatedHealthCharacter::HandleDeath()
{
if (bIsDead)
{
return;
}
bIsDead = true;
// 모든 클라이언트에 사망 알림
MulticastOnDeath();
}
void AReplicatedHealthCharacter::MulticastOnDeath_Implementation()
{
// 서버 + 모든 클라이언트에서 실행
bIsDead = true;
PlayDeathAnimation();
SetActorEnableCollision(false);
UE_LOG(LogTemp, Log, TEXT("%s has died"), *GetName());
}

개념핵심 요약
HasAuthority()서버 여부 판별 — 게임 로직 결정은 항상 서버에서
UPROPERTY(Replicated)서버 → 클라이언트 속성 자동 동기화
ReplicatedUsing = OnRep_Func복제 수신 시 클라이언트에서 콜백 실행
DOREPLIFETIMEGetLifetimeReplicatedProps에서 복제 대상 등록 필수
Server RPC클라이언트 → 서버 함수 호출
NetMulticast RPC서버 → 전체 클라이언트 함수 호출
Client RPC서버 → 소유 클라이언트 함수 호출
bReplicates = trueActor 복제 활성화 (생성자에서 설정)
  • bReplicates = true를 빠뜨려 복제가 아예 동작하지 않는 경우
  • GetLifetimeReplicatedPropsDOREPLIFETIME 등록을 누락하는 경우
  • OnRep 함수에 UFUNCTION() 매크로를 빠뜨리는 경우
  • Server RPC를 서버에서 직접 호출하면 _Implementation이 바로 실행됨 (RPC 우회됨)
  • OnRep 함수는 서버에서는 호출되지 않음 — 서버/클라이언트 공통 로직은 별도 함수로 분리