Multiplayer Desyncs: 상태를 깨뜨리는 Unreal Engine RPC Replication Issue 해결 방법
모든 멀티플레이어 인디 개발자는 넷코드가 자신을 배신하는 정확한 순간을 알고 있습니다. 무기를 장착하기 위해 Run on Server RPC를 실행합니다. 서버 로그는 무기가 장착되었음을 확인합니다. 서버의 콜리전 실린더는 당신이 조준 자세를 취하고 있음을 보여줍니다. 하지만 클라이언트 화면에서는? 캐릭터는 기본 idle 포즈로 서 있을 뿐, 상태 변화에 전혀 반응하지 않습니다.
무기 장착 로직, 조준 상태, 인벤토리 상호작용 및 제작 시스템이 클라이언트에서 갑자기 업데이트되지 않으면 패닉이 찾아옵니다. RPC를 Multicast로 전환하면 시각적 버그가 마법처럼 해결된다는 것을 발견할 수도 있습니다.
하지만 Multicast 상태로 두지 마십시오.
지속적인 상태(persistent state) 버그를 수정하기 위해 Multicast를 사용하는 것은 결국 게임의 네트워크 성능을 파괴하고 나중에 접속하는 플레이어(late-joining)의 경험을 망치는 임시방편일 뿐입니다. 이번 심층 분석에서는 공포의 unreal engine rpc replication issue의 근본 원인을 파헤치고, 왜 서버 상태가 클라이언트를 무시하는지 설명하며, C++를 사용하여 견고한 server-authoritative 상태 동기화를 설계하는 방법을 알아보겠습니다.
Multicast의 함정: 왜 "작동하는지" (그리고 왜 게임을 망치는지)
개발자가 이 버그를 만났을 때, 보통 다음과 같은 사고 과정을 거칩니다:
- 클라이언트가
Server_EquipWeapon()을 호출한다. - 서버가 무기를 장착한다.
- 클라이언트 비주얼이 업데이트되지 않는다.
Server_EquipWeapon()이Multicast_EquipWeapon()을 호출하도록 변경한다.- 클라이언트 비주얼이 업데이트된다! 버그 수정 완료, 맞죠?
틀렸습니다. 그 이유를 이해하려면 **RPCs (Remote Procedure Calls)**와 Property Replication의 근본적인 차이를 알아야 합니다.
RPC는 일시적인 네트워크 이벤트입니다. 그것은 허공에 외치는 것과 같습니다. Multicast가 실행될 때 플레이어가 network cull distance 내에 있다면, 그 외침을 듣고 장착 애니메이션을 재생합니다.
하지만 10초 후에 서버에 접속한 플레이어는 어떻게 될까요? 5,000 Unreal Units 떨어져 있던 플레이어가 relevancy range 안으로 걸어 들어와 당신의 캐릭터를 본다면 어떻게 될까요? Multicast는 이미 과거에 실행되었기 때문에 새로운 클라이언트는 이벤트를 절대 받지 못합니다. 그들은 당신의 캐릭터가 투명한 무기를 들고, idle 포즈로 미끄러지듯 움직이며 가슴에서 총알을 발사하는 것을 보게 될 것입니다.
Multicast는 폭발 비주얼, 사운드 이펙트 또는 코스메틱 파티클과 같이 게임플레이에 중요하지 않은 일시적인 이벤트를 위한 것입니다.
들고 있는 무기, 조준 여부, 인벤토리 내용물 등 시간이 지나도 지속되는 모든 것에 대해서는 반드시 Property Replication을 사용해야 합니다.
근본 원인: 왜 갑자기 깨졌을까?
이전에 작동하던 Run on Server RPC가 여러 시스템(무기, 조준, 제작)에서 갑자기 깨졌다면, 프로젝트의 다음 세 가지 아키텍처 변화 중 하나의 희생양일 가능성이 높습니다:
1. Listen Server vs. Dedicated Server의 착각
이전에 Listen Server를 사용하여 Play-In-Editor (PIE)에서 테스트했다면, 호스트 플레이어는 클라이언트이자 서버입니다. 호스트가 실행한 "Run on Server" RPC는 호스트가 곧 서버이기 때문에 즉시 로컬 비주얼 상태를 업데이트합니다. 마침내 Dedicated Server 테스트로 전환하거나 클라이언트 2로 테스트할 때, 이 착각은 깨집니다. 서버는 격리된 메모리를 업데이트하고 클라이언트는 뒤처지게 됩니다.
2. 깨진 ActorComponent Ownership
최근 인벤토리나 무기 로직을 UActorComponent 클래스로 리팩토링했다면 복제 체인이 깨졌을 수 있습니다. RPC는 클라이언트가 Actor를 *소유(owns)*하고 있는 경우에만 클라이언트에서 호출할 수 있습니다. 컴포넌트가 동적으로 스폰되고 SetOwner(PlayerController)를 통해 명시적으로 소유자가 할당되지 않은 경우, 서버는 RPC를 무시하거나 상태를 다시 복제하는 데 실패합니다. 이 아키텍처적 악몽에 대해서는 Multiplayer Inventory Nightmares Fixing Swapped Actorcomponent Owners In Unreal Engine 가이드에서 자세히 다룹니다.
3. 로컬 상태 우회
이전에는 클라이언트 측 입력 이벤트가 Server RPC를 호출하기 전에 로컬 bIsAiming 불리언 값을 설정했을 수 있습니다. 코드를 순수하게 "Server Authoritative"(서버가 상태를 결정하기를 기다림)로 리팩토링했지만 해당 상태를 클라이언트로 다시 복제하는 것을 잊었다면, 클라이언트는 영원히 오지 않을 업데이트를 기다리게 됩니다.
단계별 튜토리얼: 견고한 상태 복제 설계하기
이 unreal engine rpc replication issue를 해결하려면 RPC 기반 아키텍처에서 RepNotifies를 사용한 상태 기반 아키텍처로 전환해야 합니다.
클라이언트를 원활하게 업데이트하는 server-authoritative 무기 장착 및 조준 시스템을 올바르게 구현하는 방법은 다음과 같습니다.
1단계: RepNotifies를 사용하여 복제 속성 정의
애니메이션을 트리거하기 위해 RPC를 신뢰하는 대신 지속적인 변수를 선언합니다. 서버가 이 변수를 변경하면 Unreal의 Net Driver가 자동으로 클라이언트와 동기화합니다. ReplicatedUsing 함수(RepNotify)를 연결하면 클라이언트가 상태 변경을 알게 되는 정확한 순간에 애니메이션을 트리거할 수 있습니다.
캐릭터 헤더(.h) 파일:
UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AMyCharacter();
// 지속적인 상태. 모든 클라이언트에 복제됨.
UPROPERTY(ReplicatedUsing = OnRep_EquippedWeapon)
AWeapon* EquippedWeapon;
UPROPERTY(ReplicatedUsing = OnRep_IsAiming)
bool bIsAiming;
// RepNotify 함수. 서버가 변수를 업데이트할 때 클라이언트에서 실행됨.
UFUNCTION()
void OnRep_EquippedWeapon();
UFUNCTION()
void OnRep_IsAiming();
// 상태 변경을 요청하는 Server RPC
UFUNCTION(Server, Reliable, WithValidation)
void Server_EquipWeapon(AWeapon* NewWeapon);
UFUNCTION(Server, Reliable, WithValidation)
void Server_SetAiming(bool bWantsToAim);
// 핵심 복제 설정
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
2단계: Server RPC 및 복제 규칙 구현
.cpp 파일에서 GetLifetimeReplicatedProps에 이 변수들을 등록해야 합니다. 그런 다음 권한이 있는(authoritative) 상태만 업데이트하도록 Server RPC를 정의합니다.
#include "MyCharacter.h"
#include "Net/UnrealNetwork.h"
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 이 변수들을 연결된 모든 클라이언트에 복제
DOREPLIFETIME(AMyCharacter, EquippedWeapon);
DOREPLIFETIME(AMyCharacter, bIsAiming);
}
// --- AIMING LOGIC ---
bool AMyCharacter::Server_SetAiming_Validate(bool bWantsToAim)
{
// 안티 치트: 플레이어가 조준할 수 있는 상태인지 확인 (예: 사망하지 않음)
return !bIsDead;
}
void AMyCharacter::Server_SetAiming_Implementation(bool bWantsToAim)
{
bIsAiming = bWantsToAim;
// 중요: C++에서 RepNotifies는 서버에서 자동으로 실행되지 않습니다.
// 서버가 Listen Server인 경우 수동으로 호출해야 합니다.
if (GetNetMode() != NM_DedicatedServer)
{
OnRep_IsAiming();
}
}
3단계: 시각적 업데이트를 위한 RepNotifies 구현
여기에서 애니메이션 로직, UI 업데이트 및 메시 부착을 정의합니다. 이것은 복제된 상태에 의존하기 때문에 나중에 접속한 플레이어도 캐릭터가 relevancy 범위에 들어오는 순간 이 로직이 자동으로 트리거됩니다.
void AMyCharacter::OnRep_IsAiming()
{
if (UAnimInstance* AnimInst = GetMesh()->GetAnimInstance())
{
if (UMyAnimInstance* MyAnim = Cast<UMyAnimInstance>(AnimInst))
{
MyAnim->bIsAiming = bIsAiming;
}
}
GetCharacterMovement()->MaxWalkSpeed = bIsAiming ? 300.f : 600.f;
}
void AMyCharacter::OnRep_EquippedWeapon()
{
if (EquippedWeapon)
{
EquippedWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, FName("WeaponSocket"));
PlayAnimMontage(EquipMontage);
}
}
전문가의 터치: Client-Side Prediction
위의 내용만 구현하면 새로운 문제인 Input Latency가 발생합니다. 핑이 100ms인 플레이어가 조준 버튼을 누르면 서버에 도달하고 복제가 돌아오는 데 200ms의 지연을 느끼게 됩니다. 현대적인 슈팅 게임에서는 최악의 경험입니다.
이를 해결하기 위해 Client-Side Prediction을 구현합니다. 클라이언트는 즉시 시각적으로 상태 변화를 시뮬레이션하는 동시에 서버에 허가를 요청합니다.
void AMyCharacter::StartAiming()
{
// 1. 로컬에서 즉시 예측 (플레이어에게 지연 시간 제로)
bIsAiming = true;
OnRep_IsAiming();
// 2. 서버에 공식적으로 요청
if (!HasAuthority())
{
Server_SetAiming(true);
}
}
서버가 거부하면(예: 50ms 전에 스턴 상태였음), 복제된 bIsAiming은 false로 유지되고 클라이언트는 부드럽게 조준 해제 상태로 되돌아갑니다. 이것이 견고한 멀티플레이어 아키텍처의 기초이며, The Unreal Engine Multiplayer Sync Bug Ruining Your World States And How To Fix It에서 논의한 개념과 일맥상통합니다.
매치 그 이상: 플레이어 상태 유지
게임 내 복제를 수정하면 매치 중에 서버와 클라이언트가 일치하게 됩니다. 하지만 매치가 끝나거나 서버가 내려가면 동기화된 인벤토리와 무기 데이터는 어떻게 될까요?
플레이어가 제작한 무기나 장비를 유지하게 하려면 해당 상태가 Unreal Engine 인스턴스를 벗어나 안전한 데이터베이스에 저장되어야 합니다. 이를 직접 구축하려면 로드 밸런서, 데이터베이스 샤딩, REST API, SSL 인증서 관리 등 수주가 걸리는 백엔드 인프라 작업이 필요합니다.
horizOn을 사용하면 이러한 백엔드 서비스가 미리 구성되어 제공됩니다. 네이티브 SDK를 사용하여 서버에서 직접 클라우드로 플레이어 데이터를 즉시 저장할 수 있습니다. 인프라 대신 게임 출시에 집중하십시오.
Unreal Engine 복제를 위한 5가지 베스트 프랙티스
- 지속적인 상태에 Multicast를 사용하지 마십시오: 인벤토리, 무기, 체력, 조준 상태 등은 반드시 Replicated Property여야 합니다. Multicast는 폭발 효과와 같은 일회성 연출에만 사용하십시오.
- 서버에서 RepNotifies를 수동으로 호출하십시오: C++에서
OnRep_함수는 서버에서 자동으로 트리거되지 않습니다. Listen Server인 경우 수동으로 호출해야 합니다. - Server RPC를 검증하십시오: 클라이언트를 믿지 마십시오.
_Validate함수를 사용하여 상태 변경이 논리적으로 가능한지 확인하십시오. - NetUpdateFrequency를 확인하십시오: 시각적 상태가 무작위로 지연되는 것 같다면 Actor의 업데이트 빈도가 병목 현상을 일으키고 있는지 확인하십시오.
- 컴포넌트 소유권을 확인하십시오:
UActorComponent에서 Server RPC를 호출할 때 컴포넌트가 복제 설정되어 있고 소유 Actor가APlayerController에 의해 소유되고 있는지 확인하십시오.
Net Driver와의 싸움을 멈추십시오
Unreal Engine의 복제 시스템은 강력하지만 규칙을 어기려 하면 가차 없습니다. 클라이언트 상태가 업데이트되지 않을 때 Multicast를 남발하고 싶은 유혹을 뿌리치십시오. 권한의 경로를 따르십시오: 클라이언트가 요청하고, 서버가 결정하며, 속성이 복제됩니다.
멀티플레이어 게임을 다음 단계로 끌어올릴 준비가 되셨나요? 인프라 관리에 대한 걱정은 horizOn에 맡기고 플레이어에게 지속적인 진행과 원활한 매치메이킹을 제공하십시오. 지금 무료로 시작해 보세요.