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 | 원격 프로시저 호출 (클라이언트 ↔ 서버 함수 호출) |
1. Server/Client/Authority 역할 구분
Section titled “1. Server/Client/Authority 역할 구분”1.1 HasAuthority()
Section titled “1.1 HasAuthority()”HasAuthority()는 현재 실행 컨텍스트가 서버(Authority)인지 확인하는 가장 기본적인 방법입니다.
void AMyActor::SomeFunction(){ if (HasAuthority()) { // 서버에서만 실행되는 코드 // 예: 게임 상태 변경, 데미지 계산, 스폰 ApplyDamageToTarget(); } else { // 클라이언트에서 실행되는 코드 // 예: 예측(Prediction), 시각 효과 PlayLocalEffect(); }}1.2 GetLocalRole()과 GetRemoteRole()
Section titled “1.2 GetLocalRole()과 GetRemoteRole()”#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”2.1 기본 복제 설정
Section titled “2.1 기본 복제 설정”복제할 속성에는 UPROPERTY(Replicated) 지정자를 추가하고, GetLifetimeReplicatedProps()에 등록합니다.
#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; // 서버 전용 상태 — 복제 불필요};#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_SimulatedOnly | Simulated Proxy에게만 복제 |
COND_InitialOnly | 최초 1회만 복제 |
COND_ReplayOnly | 리플레이 시에만 복제 |
3. OnRep 함수 — 복제 수신 콜백
Section titled “3. OnRep 함수 — 복제 수신 콜백”ReplicatedUsing을 사용하면 클라이언트가 복제 데이터를 수신했을 때 호출될 콜백 함수를 지정할 수 있습니다.
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();};#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가 자동 실행되지 않습니다. 서버와 클라이언트 모두 같은 처리가 필요하다면 별도의 공통 함수를 만들어 양쪽에서 호출하세요.
4. RPC — 원격 프로시저 호출
Section titled “4. RPC — 원격 프로시저 호출”RPC(Remote Procedure Call)는 한 머신에서 함수를 호출하면 다른 머신에서 실행되는 메커니즘입니다.
4.1 RPC 3종 비교
Section titled “4.1 RPC 3종 비교”| RPC 유형 | 호출 위치 | 실행 위치 | 선언 키워드 |
|---|---|---|---|
| Server RPC | 클라이언트 | 서버 | Server |
| NetMulticast RPC | 서버 | 서버 + 모든 클라이언트 | NetMulticast |
| Client RPC | 서버 | 특정 클라이언트 (소유자) | Client |
4.2 Server RPC — 클라이언트 → 서버
Section titled “4.2 Server RPC — 클라이언트 → 서버”플레이어 입력처럼 클라이언트의 행동을 서버에 알릴 때 사용합니다.
protected: // Server: 클라이언트가 호출하면 서버에서 실행 // Reliable: 패킷 유실 없이 반드시 전달 보장 (중요 이벤트) // WithValidation: _Validate 함수로 치트 방지 검증 가능 UFUNCTION(Server, Reliable, WithValidation) void ServerRequestAttack();
// Unreliable: 패킷 유실 허용 (위치 업데이트 같은 잦은 호출) UFUNCTION(Server, Unreliable) void ServerUpdateAimDirection(FVector AimDir);// 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 — 서버 → 전체”서버에서 발생한 이벤트를 모든 클라이언트에서 동시에 시각적으로 표현할 때 사용합니다.
protected: UFUNCTION(NetMulticast, Reliable) void MulticastPlayAttackEffect();
UFUNCTION(NetMulticast, Unreliable) void MulticastSpawnBloodParticle(FVector Location);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 — 서버 → 특정 클라이언트”서버가 특정 플레이어(소유자)에게만 메시지를 보낼 때 사용합니다.
protected: // Client: 서버에서 호출하면 소유자(이 컨트롤러를 가진) 클라이언트에서 실행 UFUNCTION(Client, Reliable) void ClientShowNotification(const FText& Message);
UFUNCTION(Client, Unreliable) void ClientUpdatePing(float PingMs);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!"))); }}5. 복제 활성화 설정
Section titled “5. 복제 활성화 설정”Actor의 복제를 활성화하려면 생성자에서 bReplicates를 true로 설정해야 합니다.
AMyCharacter::AMyCharacter(){ // Actor 복제 활성화 — 이것 없이는 UPROPERTY(Replicated)와 RPC 모두 동작하지 않음 bReplicates = true;
// 이동 컴포넌트 복제 (ACharacter는 기본적으로 활성화됨) // GetCharacterMovement()->SetIsReplicated(true);
// 컴포넌트 복제 활성화 // CustomComponent->SetIsReplicated(true);}6. 실전 — 체력 동기화 시스템
Section titled “6. 실전 — 체력 동기화 시스템”6.1 완전한 체력 복제 예시
Section titled “6.1 완전한 체력 복제 예시”#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(); // 서버/클라이언트 공통 처리};#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 | 복제 수신 시 클라이언트에서 콜백 실행 |
DOREPLIFETIME | GetLifetimeReplicatedProps에서 복제 대상 등록 필수 |
| Server RPC | 클라이언트 → 서버 함수 호출 |
| NetMulticast RPC | 서버 → 전체 클라이언트 함수 호출 |
| Client RPC | 서버 → 소유 클라이언트 함수 호출 |
bReplicates = true | Actor 복제 활성화 (생성자에서 설정) |
자주 하는 실수
Section titled “자주 하는 실수”bReplicates = true를 빠뜨려 복제가 아예 동작하지 않는 경우GetLifetimeReplicatedProps에DOREPLIFETIME등록을 누락하는 경우OnRep함수에UFUNCTION()매크로를 빠뜨리는 경우- Server RPC를 서버에서 직접 호출하면
_Implementation이 바로 실행됨 (RPC 우회됨) OnRep함수는 서버에서는 호출되지 않음 — 서버/클라이언트 공통 로직은 별도 함수로 분리