1. RPC(Remote Procedure Call)
RPC는 네트워크 상에서 원격 함수 호출을 의미하며, 클라이언트-서버 간에 함수를 호출하는 메커니즘입니다. Unreal Engine에서는 함수 선언에 UFUNCTION 매크로에 Server, Client, NetMulticast 키워드를 지정하여 RPC를 구현합니다 (Remote Procedure Calls | An Unreal Engine Blog by Cedric Neukirchen). 예를 들어, 클라이언트에서 서버로 데이터를 보내려면 Server RPC를 사용하고, 서버에서 특정 클라이언트로 보내려면 Client RPC를, 서버에서 모든 클라이언트에게 브로드캐스트하려면 NetMulticast RPC를 사용합니다. RPC를 선언할 때는 함수의 실행 주체와 신뢰도를 지정할 수 있습니다 (Reliable은 반드시 전달되어야 하는 호출, Unreliable은 손실돼도 무방한 호출). 또한 C++에서 RPC 함수를 구현하려면 함수 이름에 _Implementation을 붙여 정의합니다 (Remote Procedure Calls | An Unreal Engine Blog by Cedric Neukirchen).
- Server RPC (클라이언트 → 서버): 클라이언트가 서버에 요청을 보내는 함수입니다. 예를 들어 플레이어가 아이템을 획득했다는 이벤트를 서버에 알릴 때 사용합니다. C++에서는 다음과 같이 선언합니다:그리고 .cpp에서 _Implementation을 붙여 구현합니다. 이 함수는 클라이언트가 소유한 Actor(예: PlayerController나 해당 Pawn)에서 호출해야 하며, 호출 시 자동으로 서버에서 실행됩니다. Server RPC는 HasAuthority()(서버 권한) 체크로 서버에서만 로직을 처리하거나, Ensure 등을 통해 보안 검증을 할 수도 있습니다.
- UFUNCTION(Server, Reliable) void ServerPickupItem(int32 ItemId);
- Client RPC (서버 → 특정 클라이언트): 서버가 특정 클라이언트(소유자)에게 함수를 호출해주는 방식입니다. 예를 들어 서버가 점수 갱신을 해당 플레이어에게 알릴 때 사용합니다. 선언은:구현은 _Implementation으로 작성하며, 서버에서 ClientUpdateScore()를 호출하면 그 함수를 해당 PlayerController를 소유한 클라이언트 머신에서 실행합니다. Client RPC는 기본적으로 호출한 Actor의 Owner인 클라이언트에게만 전달됩니다 (Remote Procedure Calls | An Unreal Engine Blog by Cedric Neukirchen).
- UFUNCTION(Client, Unreliable) void ClientUpdateScore(int32 NewScore);
- NetMulticast RPC (서버 → 모든 클라이언트): 서버에서 자신을 포함한 모든 클라이언트에게 동시에 함수 호출을 전달합니다. 예를 들어 보스 몬스터 처치 시 모든 플레이어에게 폭발 이펙트를 재생하도록 할 때 사용합니다. 선언은:서버에서 MulticastPlayExplosionFX()를 호출하면 서버 자신과 모든 연결된 클라이언트들에서 이 함수가 실행되어 이펙트가 재생됩니다 (Remote Procedure Calls | An Unreal Engine Blog by Cedric Neukirchen). 단, Multicast RPC를 클라이언트가 호출하면 자기 자신에게만 실행되고 다른 곳엔 전달되지 않습니다 (Remote Procedure Calls | An Unreal Engine Blog by Cedric Neukirchen). 그러므로 Multicast는 반드시 서버에서 호출하는 패턴으로 사용해야 합니다.
- UFUNCTION(NetMulticast, Reliable) void MulticastPlayExplosionFX();
RPC 구현 시에는 필수로 <YourProject>.Build.cs에 "Networking" 관련 모듈이 포함되고, .cpp 파일 상단에 #include "Net/UnrealNetwork.h"를 추가해야 합니다. 또한 RPC 함수에는 반환값을 가질 수 없으며, 빈 함수 형태로만 선언 가능합니다. **신뢰성(Reliable)**은 꼭 필요한 RPC에만 지정해야 하며, 너무 자주 호출되는 함수에 Reliable을 남용하면 네트워크 버퍼가 가득 차 문제가 발생할 수 있습니다 (Remote Procedure Calls | An Unreal Engine Blog by Cedric Neukirchen). 아래는 RPC 함수들의 선언과 구현 예시입니다:
// 헤더 파일 (.h)
UFUNCTION(Server, Reliable) void Server_DoAction(int32 Param);
UFUNCTION(Client, Unreliable) void Client_ShowEffect(FVector Location);
UFUNCTION(NetMulticast, Reliable) void Multicast_PlaySound();
// 소스 파일 (.cpp)
void AMyCharacter::Server_DoAction_Implementation(int32 Param) {
// 서버에서 실행: 행동 처리
CurrentValue += Param;
// 모든 클라이언트에게 업데이트 알림
Multicast_PlaySound();
}
void AMyCharacter::Client_ShowEffect_Implementation(FVector Location) {
// 클라이언트(자신)에서 실행: 이펙트 표시
SpawnEffectAt(Location);
}
void AMyCharacter::Multicast_PlaySound_Implementation() {
// 서버와 모든 클라이언트에서 실행: 사운드 재생
UGameplayStatics::PlaySound2D(this, ActionSound);
}
위의 Server_DoAction은 클라이언트가 호출하면 서버의 _Implementation이 수행되고, Multicast_PlaySound를 통해 모든 참가자에게 사운드가 재생됩니다. 이런 방식으로 RPC를 활용하여 클라이언트 입력을 서버가 받아 처리하고 결과를 모든 클라이언트와 동기화할 수 있습니다.
2. 리플리케이션(Replication)
네트워크 리플리케이션은 서버의 객체 상태를 자동으로 클라이언트들과 동기화하는 기능입니다. Unreal Engine에서는 Actor나 Component의 멤버 변수를 Replicated로 선언하면, 서버에서 해당 값이 변경될 때 연결된 클라이언트들의 복제 객체에 변화가 전파됩니다 (Replication | An Unreal Engine Blog by Cedric Neukirchen) (Replication | An Unreal Engine Blog by Cedric Neukirchen). 변수 리플리케이션을 사용하려면 몇 가지 규칙이 있습니다:
- Actor 설정: 해당 Actor 객체 자체가 네트워크에 복제되도록 bReplicates = true;로 설정해야 합니다. 일반적으로 AActor의 생성자에서 bReplicates = true;를 설정합니다 (Replication | Unreal Engine Community Wiki). 또한 Pawn이나 Character의 경우 기본적으로 bReplicateMovement = true;로 되어 있어 위치/회전 이동이 자동 복제됩니다 (Replication | Unreal Engine Community Wiki).
- 변수 선언: 헤더에서 UPROPERTY에 Replicated 또는 ReplicatedUsing=OnRep_Function specifier를 붙입니다. 예를 들어 플레이어 체력 변수는 다음처럼 선언합니다:Replicated만 붙이면 값이 바뀔 때 자동 동기화되고, ReplicatedUsing을 쓰면 클라이언트 측에서 해당 값 갱신 시 지정한 함수(OnRep_Ammo)가 호출됩니다 (Replication | An Unreal Engine Blog by Cedric Neukirchen). RepNotify(OnRep 함수)는 서버가 아닌 클라이언트에서만 호출되므로, 필요하다면 서버에서도 같은 로직을 실행하도록 서버 측에서 변수 변경 시 별도의 함수를 호출해야 합니다 (RepNotify는 서버에는 자동 실행 안 됨) (RepNotify from c++ confusion. - Page 2 - Unreal Engine Forums).
- UPROPERTY(Replicated) float Health; UPROPERTY(ReplicatedUsing=OnRep_Ammo) int32 Ammo; UFUNCTION() void OnRep_Ammo();
- 리플리케이션 등록: .cpp 파일에서 GetLifetimeReplicatedProps를 오버라이드하여, 복제할 변수를 등록해야 합니다. 이 함수 내에서 DOREPLIFETIME(클래스명, 변수명) 매크로를 사용합니다 (Replication | An Unreal Engine Blog by Cedric Neukirchen). 예:이렇게 하면 서버에서 Health나 Ammo 값이 바뀔 때 해당 Actor를 소유한 클라이언트뿐만 아니라 관련 있는 모든 클라이언트에 값이 동기화됩니다. 또한 DOREPLIFETIME_CONDITION 매크로를 통해 특정 조건에서만 복제하도록 설정할 수 있습니다. 예를 들어 COND_OwnerOnly를 사용하면 해당 변수는 그 Actor의 Owner인 플레이어에게만 전송됩니다 (Replication | An Unreal Engine Blog by Cedric Neukirchen). 이는 불필요한 대역폭 낭비를 줄이는 데 도움이 됩니다 (예: 자신의 캐릭터 상태는 자신에게만 보내고 다른 플레이어에게는 보내지 않음).
- void AMyPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutProps) const { Super::GetLifetimeReplicatedProps(OutProps); DOREPLIFETIME(AMyPlayerState, Health); DOREPLIFETIME(AMyPlayerState, Ammo); }
리플리케이션은 항상 서버 → 클라이언트 방향으로 동작합니다 (Replication | An Unreal Engine Blog by Cedric Neukirchen). 클라이언트가 값을 변경해도 기본적으로 서버에 반영되지 않으며, 클라이언트 변경을 전파하려면 Server RPC를 사용하여 서버가 변경하도록 해야 합니다. 예를 들어 플레이어가 아이템을 획득하여 Ammo를 증가시키는 경우, 클라이언트에서 서버 RPC로 서버의 Ammo를 변경하고, 그 변경된 Ammo가 다시 Replication을 통해 모든 클라이언트에 전달되는 식입니다.
RepNotify 예시: 탄약 수(Ammo)가 변할 때 UI를 업데이트한다고 하면, 변수와 OnRep 함수 선언은 위와 같고, 구현은 다음과 같습니다:
void AMyPlayerState::OnRep_Ammo() {
// 클라이언트에서 Ammo 값이 갱신될 때 불린다.
UpdateAmmoUI(Ammo);
}
서버에서 Ammo가 변하면 해당 값을 모든 클라이언트로 보내주고, 각 클라이언트의 OnRep_Ammo가 호출되어 UI를 새로고침하는 식입니다.
3. 세션 관리(Session Management)
세션 관리는 멀티플레이어 게임에서 플레이어들이 로비나 게임 방에 참가하고 나가는 흐름을 제어하는 시스템입니다. 언리얼에서는 Online Subsystem을 통해 세션을 생성하고 검색하며, 참가를 처리할 수 있습니다. 일반적으로 세션 관리는 UGameInstance나 커스텀 Manager 클래스를 만들어 거기서 구현하며, C++로 온라인 세션을 다루려면 OnlineSubsystem 모듈을 프로젝트에 포함시켜야 합니다 (How To Use Sessions In C++ | UE4: Guidebook).
- 세션 생성(Host): 서버 역할을 할 플레이어가 세션을 만들고 다른 사람들이 찾을 수 있도록 설정합니다. C++에서 세션을 생성하려면 IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();로 서브시스템을 얻고, IOnlineSessionPtr SessionInterface = OnlineSub->GetSessionInterface();를 통해 세션 인터페이스를 가져옵니다 (Create and join session in c++ - C++ - Epic Developer Community Forums). 그런 다음 FOnlineSessionSettings 객체를 구성하여 공개 슬롯 수(MaxNumPlayers), LAN 여부, 존재(프레젠스) 여부 등을 설정합니다 (Create and join session in c++ - C++ - Epic Developer Community Forums). 예:위 코드에서 CreateSession을 호출하면 비동기적으로 세션 생성이 시작됩니다 (How To Use Sessions In C++ | UE4: Guidebook). 생성 결과는 OnCreateSessionComplete 델리게이트를 통해 콜백으로 받으며, 성공 시 서버는 Listen 상태로 전환해 다른 플레이어 접속을 기다립니다. 로비 시스템을 구현할 때는 별도의 로비 맵(Level)을 두고, 세션 생성 후 ServerTravel("LobbyMap?listen")이나 UGameplayStatics::OpenLevel을 사용하여 서버 자신을 로비 맵으로 이동시켜 대기하도록 합니다.
- IOnlineSessionPtr Session = OnlineSub->GetSessionInterface(); if(Session.IsValid()) { FOnlineSessionSettings SessionSettings; SessionSettings.bIsLANMatch = false; // LAN 게임 여부 SessionSettings.bUsesPresence = true; // 존재감 (온라인 상태 공개) SessionSettings.NumPublicConnections = 4; // 최대 4명 SessionSettings.bAllowJoinInProgress = true; // 진행 중 참가 허용 SessionSettings.bShouldAdvertise = true; // 세션을 공개로 광고 SessionSettings.bAllowJoinViaPresence = true; // 맵 이름 등 추가 설정 가능 SessionSettings.Set(SETTING_MAPNAME, FString("LobbyMap"), EOnlineDataAdvertisementType::ViaOnlineService); Session->CreateSession(*PlayerId, SESSION_NAME, SessionSettings); }
- 세션 검색(Find): 클라이언트가 현재 열려있는 세션(게임방) 목록을 조회합니다. SessionInterface->FindSessions()를 사용하여 세션 검색을 시작하며, 파라미터로는 플레이어 ID와 검색 설정(FOnlineSessionSearch) 등을 전달합니다. 검색이 완료되면 OnFindSessionsComplete 델리게이트에서 결과 목록(TArray)을 얻을 수 있습니다. 각 SearchResult에는 세션 호스트 정보, 현재 인원 등이 담겨있어 UI에 표시하거나 선택에 사용합니다.
- 세션 참가(Join): 플레이어가 특정 세션에 들어가고자 할 때 SessionInterface->JoinSession(PlayerId, SESSION_NAME, SearchResult)를 호출합니다. JoinSession이 성공하면 엔진은 자동으로 해당 세션 호스트(서버)로 접속을 시도합니다. 이 때 클라이언트에서는 로딩 화면을 보여주거나 ClientTravel을 사용해 서버 주소로 이동합니다. OnlineSubsystem Null의 경우 LAN 연결이므로 IP 직접 입력 없이 SessionInterface가 내부적으로 Open LEVEL을 처리해줍니다. 만약 Steam 같은 OnlineSubsystem을 쓰는 경우, Steam API가 세션을 관리하여 자동 접속이 이루어집니다.
- 세션 종료/파괴: 게임이 끝나거나 플레이어가 방을 나갈 때 SessionInterface->DestroySession(SESSION_NAME)을 호출하여 세션을 정리합니다. DestroySession도 완료 델리게이트가 있으며, 서버에서는 모든 접속을 끊고 세션 정보를 지웁니다. 이후 새로운 세션을 만들거나 메인 메뉴로 돌아갈 수 있습니다.
- 로비 시스템 구현: 세션을 만들고 참가한 후 실제 게임 시작 전까지의 대기실 기능을 로비라고 합니다. 일반적인 흐름은: 호스트는 세션 생성 → 로비 맵으로 이동 → 참가자들은 세션 참가 → 로비 맵에 입장하여 대기. 로비에서는 모든 플레이어의 준비 상태(PlayerState 등을 통해)나 캐릭터 선택 등을 관리하고, 호스트가 시작을 누르면 서버에서 게임 맵으로 seamless travel(끊김 없는 맵 이동)을 실행합니다. 이때 참가자들의 PlayerController/PlayerState는 유지되거나 재생성되며, 세션 연결은 유지된 채로 모두 동일한 새로운 맵으로 이동하게 됩니다. Unreal Engine에서 Seamless Travel을 쓰면 로비 -> 게임맵 간 이동 시 연결 유실 없이 PlayerState를 보존할 수 있습니다.
세션 관리를 C++로 직접 구현하는 대신 온라인 세션 노드(블루프린트 노드, 예: Create Session, Find Session 등)나 Advanced Sessions Plugin 같은 것을 사용할 수도 있습니다. 하지만 C++로 구현하면 더 세밀한 제어와 커스텀 로직이 가능하며, 서버 상태를 관리하는 AGameSession을 서브클래싱해 확장할 수도 있습니다. 클라이언트 프로그래머로서는 세션의 라이프사이클(생성→검색→참가→종료)을 이해하고, 각 단계에서 UI와 게임 흐름을 연동하는 것이 중요합니다.
4. Net Role 및 Authority(권한)
네트워크 롤(Roles)과 권한은 객체가 현재 서버인지 클라이언트인지, 그리고 어떤 종류의 프록시인지를 나타냅니다. Unreal Engine의 모든 네트워크 Actor에는 Role과 RemoteRole (UE4 용어, UE5에서는 GetLocalRole()/GetRemoteRole()로 접근) 속성이 있습니다. 일반적으로 Role은 **자신(로컬)**이 해당 Actor를 어떻게 취급하는지 나타내며, RemoteRole은 **원격(상대)**에서 그 Actor를 어떻게 보는지 나타냅니다. 주요 Net Role 값들은 다음과 같습니다 (Role_Authority explanation needed. - C++ - Epic Developer Community Forums):
- ROLE_Authority: 권한자. 해당 Actor의 진짜 소유자로, 서버에서 해당 Actor는 항상 Authority입니다. 서버는 게임의 권위(authority)를 가지므로 모든 Actor의 공식 상태를 관리합니다. 또한 싱글플레이에서는 로컬 인스턴스가 Authority입니다. C++에서 HasAuthority() 함수로 현재 실행 중인 코드가 서버권한인지 확인할 수 있는데, Role == ROLE_Authority 체크와 동일합니다 (Role_Authority explanation needed. - C++ - Epic Developer Community Forums). 서버에서만 실행되어야 하는 로직(예: 게임 규칙 처리나 리플리케이션 변수 수정)은 보통 if(HasAuthority()) 블록으로 감싸서 안전하게 처리합니다.
- ROLE_AutonomousProxy: 자율 프록시. 자신이 조종하는 플레이어의 Actor(주로 Pawn/Character)에 할당되는 역할입니다 (Role_Authority explanation needed. - C++ - Epic Developer Community Forums). 예를 들어 클라이언트 플레이어가 직접 조종하는 자신의 캐릭터는 그 클라이언트 측에서 AutonomousProxy입니다. AutonomousProxy는 로컬 플레이어 입력을 받아 예측된 움직임을 수행하며, 서버 Authority와 상호작용합니다. 클라이언트에서 자신의 캐릭터 Pawn은 Role = AutonomousProxy이고 원격에서 서버는 그 Pawn을 Authority로 봅니다.
- ROLE_SimulatedProxy: 시뮬레이션 프록시. 원격에서 조종되는 Actor를 나타냅니다 (Role_Authority explanation needed. - C++ - Epic Developer Community Forums). 즉, 해당 클라이언트가 아닌 다른 주체(서버 혹은 다른 플레이어)가 권한을 가진 Actor의 복제본입니다. 예를 들어 다른 플레이어의 캐릭터가 내 클라이언트에 나타날 때, 내 클라이언트 쪽에서 그 캐릭터는 SimulatedProxy입니다. SimulatedProxy는 자체적으로 움직임을 결정하지 않고, 서버의 Authority Actor 상태를 시뮬레이션/보간하여 표시합니다.
- ROLE_None: 초기 상태 등 특별한 경우로, 일반 플레이 중에는 잘 사용되지 않습니다.
언리얼 네트워킹에서 서버가 절대적인 Authority이며, 클라이언트는 자기 소유 Actor(AutonomousProxy)에 대해서만 국지적으로 입력을 받아 움직임을 예측할 수 있습니다. 그 외의 모든 상태 변경은 결국 서버 Authority에서 결정됩니다. 권한 처리 예시로, 총알 발사나 아이템 획득 같은 이벤트는 클라이언트에서 입력이 발생해도 바로 실행하지 않고, 보통 Server RPC로 서버에 알려 서버에서 Authority로 처리하게 합니다. 서버가 이벤트를 수신하여 유효성을 검증한 뒤(예: 탄약이 있는지), 해당 이벤트 결과를 다시 Replication이나 Multicast로 클라이언트들에게 전파합니다. 이렇게 함으로써 해킹을 방지하고 모든 참가자의 게임 상태를 일관성 있게 유지합니다.
코드 예시: 캐릭터가 폭탄을 설치하는 함수를 가정해보겠습니다.
void AMyCharacter::PlaceBomb() {
if (HasAuthority()) {
// 서버 권한일 때 실제 폭탄 오브젝트를 생성
SpawnBombActor();
} else {
// 클라이언트라면 서버 RPC 호출
ServerPlaceBomb();
}
}
UFUNCTION(Server, Reliable)
void ServerPlaceBomb();
void AMyCharacter::ServerPlaceBomb_Implementation() {
// 서버에서 수신하여 다시 폭탄 생성
SpawnBombActor();
}
클라이언트 캐릭터가 PlaceBomb()을 호출하면 HasAuthority()가 false이므로 ServerPlaceBomb RPC를 보냅니다. 서버에서 ServerPlaceBomb_Implementation이 실행되면 HasAuthority()==true인 상태이므로 실제 폭탄을 생성하고, 폭탄 Actor는 bReplicates=true이기 때문에 모든 클라이언트에 나타납니다. 이때 폭탄 Actor의 Role은 서버에서는 Authority, 각 클라이언트에서는 SimulatedProxy로 존재하게 됩니다.
정리하면, Net Role과 Authority 개념을 잘 이해하여 언제 서버에서 동작해야 하고, 언제 클라이언트에서만 처리해도 되는지를 구분해야 합니다. 예를 들어 UI 표시나 이펙트 재생 등 즉각적인 피드백은 로컬에서도 처리하되, 게임 플레이에 영향을 주는 상태 변화는 반드시 서버 권한으로 처리하도록 해야합니다.
5. 네트워크 상태 동기화(Network State Synchronization)
멀티플레이어 게임에서는 캐릭터의 위치, 애니메이션, 액션 등의 상태를 모든 참가자에게 일치시켜야 합니다. 언리얼에서는 이 작업을 위해 기본적인 움직임은 자동으로 복제하고(Characters의 Movement Component 등), 그 외 커스텀 상태는 개발자가 RPC나 Replication을 통해 동기화합니다. 핵심은 **클라이언트 측 예측(Client-side Prediction)**과 **서버 측 권한(Server-side Authority)**의 조화를 이루는 것입니다.
- 캐릭터 위치 동기화: ACharacter는 UCharacterMovementComponent를 통해 내장된 네트워크 움직임 예측 시스템을 가집니다. 로컬 플레이어가 움직이면 즉시 자신의 화면에서 이동하고, 동시에 서버에 입력이 전송됩니다. 서버는 권한으로 움직임을 계산하고 정정값(correction)을 클라이언트에 보내 동기화합니다. 이로써 조작중인 캐릭터는 부드럽게 움직이고, 다른 플레이어들에게도 일관된 위치를 보입니다. CharacterMovementComponent는 자동으로 이 과정을 처리하며, 개발자는 이동 속도나 물리 처리 정도만 설정하면 됩니다. 만약 커스텀 Actor를 이동시키는 경우, SetReplicateMovement(true)로 설정하면 Actor의 위치/회전이 일정 간격으로 복제되어 다른 클라이언트에 전파됩니다 (Replication | Unreal Engine Community Wiki). 또는 Tick에서 위치를 업데이트한 후 ForceNetUpdate()를 호출해 즉시 갱신을 요청할 수도 있습니다.
- 애니메이션 동기화: 플레이어 캐릭터의 애니메이션 상태(달리기, 점프, 공격 등)는 각 클라이언트에서 Locally 결정되지만, 그 트리거(발사, 피격 등)는 모두에게 공유되어야 시각적으로 어긋나지 않습니다. 일반적인 방법은 상태 변수의 Replication과 AnimBlueprint 연동입니다. 예를 들어 “공격 중인지”를 나타내는 bool 변수 bIsAttacking을 Replicated로 선언하고 OnRep 함수를 통해 애니 Montages를 재생합니다. 또는 공격 RPC 호출 시 서버에서 NetMulticast로 PlayAttackMontage() 함수를 호출해 모든 캐릭터의 메쉬에 몽타주를 재생하게 할 수 있습니다. 전자의 경우 클라이언트가 변수 변화를 받아 애니메이션을 재생하고, 후자의 경우 서버 지시로 동시에 애니메이션이 재생됩니다. 또한 Anim Notify 이벤트에 RPC를 활용하여 타격 판정 등의 시점을 동기화할 수 있습니다. 중요한 점은 애니메이션 상태는 가급적 서버의 게임플레이 로직과 함께 움직여야 한다는 것입니다. 예를 들어 서버에서 공격 판정을 할 때 공격 애니메이션도 시작되었다는 것을 다른 클라이언트가 알아야 하므로, 한쪽만 일어나지 않도록 해야 합니다.
- 액션 및 상호작용 동기화: 캐릭터의 액션(예: 문 열기, 아이템 획득, 스킬 사용 등)은 해당 액션의 결과가 모든 플레이어에게 보여져야 합니다. 이를 위해 RPC와 리플리케이션을 조합합니다. 예를 들어 문을 여는 행동을 생각해봅시다: 플레이어가 문을 열기 위해 상호작용 키를 누르면,
- 로컬 클라이언트: 즉시 문 열림 애니메이션을 살짝 재생하여 피드백 (선택사항, 예측)
- 클라이언트 → 서버 RPC: ServerOpenDoor() 호출
- 서버: 문 열기 가능 여부를 판단 (잠겨있으면 무시 등), 승인되면 문 상태 변수 bDoorOpen = true로 설정 (Replicated 변수)
- 서버: 승인 시 Multicast RPC MulticastPlayDoorOpenFX()로 문 열리는 소리/이펙트 재생 지시
- 모든 클라이언트: bDoorOpen RepNotify로 문 메쉬 애니메이션을 재생하여 완전히 여는 동작 수행, RPC 통해 소리 재생
- 예측 및 보정: 네트워크 지연이 있을 수밖에 없으므로, 클라이언트는 종종 자기 행동의 결과를 예측하고 먼저 적용합니다. 언리얼의 CharacterMovement가 그 대표적인 예이며, 스킬 사용 등에서도 GAS를 통해 예측 실행이 가능합니다. 예를 들어 총알 발사 시, 총구 섬광 이펙트는 즉시 로컬에서 보이게 하고(Server RPC도 동시에 보냄), 서버가 총알 객체를 생성하여 다른 클라이언트에 복제하면 나중에 약간의 시간차로 나타날 것입니다. 이때 클라이언트에서는 이미 이펙트를 봤으므로 체감상 딜레이가 줄어듭니다. 서버로부터의 공식 객체가 도착하면, 로컬 예측 객체(만약 있었다면)를 파기하거나 위치를 맞추는 식으로 조정합니다. 이러한 패턴은 언리얼이 내부적으로 많이 처리해주지만, 커스텀 로직에서도 필요하다면 적용할 수 있습니다.
정리하면, 중요 게임 상태는 항상 서버에서 결정하고 복제하되, 플레이어 입력에 대한 즉각적인 반응은 로컬에서 예측적으로 처리하여 부드러운 경험을 제공합니다. 클라이언트 개발자는 Replication으로 어떤 상태들을 공유할지, RPC로 어떤 이벤트를 보낼지 잘 설계하고, AnimBlueprint와 연동하여 시각적 동기화가 이루어지도록 해야 합니다.
6. 네트워크 최적화 기법
멀티플레이 게임에서는 네트워크 **대역폭(Bandwidth)**과 **지연(latency)**을 고려한 최적화가 필요합니다. 불필요하게 많은 데이터를 보내거나, 너무 자주 업데이트하면 랙이 생기거나 패킷 손실이 증가할 수 있습니다. Unreal Engine에서는 여러 최적화 기법을 제공하며, 몇 가지 주요 방법은 다음과 같습니다:
- Update Frequency 조정: Actor의 NetUpdateFrequency와 MinNetUpdateFrequency를 조절하여 복제 갱신 빈도를 관리합니다 (Unreal Engine Multiplayer Tips and Tricks - WizardCell) (Unreal Engine Multiplayer Tips and Tricks - WizardCell). 기본적으로 캐릭터 등은 100회/초 정도로 설정되고, PlayerState 등은 더 낮게 설정됩니다. 빈도가 높을수록 더 자주 상태를 전송하지만 그만큼 트래픽이 늘어납니다. 게임 내 중요도가 낮은 Actor(예: 장식 오브젝트나 멀리 있는 AI 등)는 UpdateFrequency를 낮춰도 무방합니다. 반대로 빠르게 움직이거나 중요한 객체는 높게 유지하되, 너무 과도하게 높이지는 않도록 합니다 (엔진 tick rate와 네트워크 한계 내에서).
- Relevancy(관련성) 및 시야 최적화: 기본적으로 언리얼은 거리 기반 관련성을 사용하여 일정 범위 밖의 Actor는 해당 클라이언트에게 replication을 보내지 않습니다. 필요하면 AActor::IsNetRelevantFor()를 오버라이드해 커스텀 관련성 조건을 작성할 수 있습니다 (Replication | Unreal Engine Community Wiki). 예를 들어 방 여러 개가 있는 게임에서, 플레이어가 있는 방의 몬스터들만 그 플레이어에게 복제하고 다른 방의 몬스터는 무시하도록 할 수 있습니다. 또한 bAlwaysRelevant 설정을 통해 중요한 Actor (예: 게임 모드 정보 등)는 거리 상관없이 항상 보내거나, bOnlyRelevantToOwner를 통해 오직 Owner에게만 전송하도록 할 수 있습니다 (Replication | Unreal Engine Community Wiki). 이러한 관련성 관리는 불필요한 데이터 송신을 줄여 네트워크 부하를 낮추는 핵심 기법입니다.
- Dormancy(휴면) 사용: Actor의 상태가 장시간 변하지 않는다면 휴면 상태로 전환해 업데이트를 중단할 수 있습니다. SetNetDormancy(DORM_DormantAll)을 서버에서 호출하면 해당 Actor는 이후 상태 변화가 있을 때까지 클라이언트에 추가 패킷을 보내지 않습니다. 예를 들어 죽은 NPC 시체나 사용되지 않는 오브젝트는 Dormant로 두고, 플레이어가 근처에 와서 상호작용할 때 FlushNetDormancy()로 깨워 다시 복제하게 할 수 있습니다. Dormancy를 잘 활용하면 고정된 상태의 객체들이 불필요하게 동일한 데이터를 계속 보내는 일을 막아 대역폭을 절약합니다.
- 데이터 압축 및 NetSerialize: 복제 데이터의 크기를 줄이는 것도 중요합니다. Unreal은 내장된 NetQuantize 구조체 등을 통해 좌표를 정수 단위로 양자화하여 보낼 수 있습니다. 예를 들어 FVector_NetQuantize10은 소수점 한 자리까지 정밀도로 위치를 압축합니다. 이렇게 하면 일반 FVector(12바이트)를 보내는 것보다 전송 비트를 줄일 수 있습니다. 또한 커스텀 구조체에 대해 bool NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)를 구현하면 직접 바이트 직렬화를 제어할 수 있습니다. 이 기법을 통해 필요한 정보만 압축하여 보내도록 할 수 있습니다 (Unreal Engine Multiplayer Tips and Tricks - WizardCell). 예를 들어 bool 8개를 하나의 바이트 비트필드로 묶어 보내거나, 부동소수점을 정수로 변환해 보내는 식입니다.
- Replication 조건 활용: 앞서 언급한 DOREPLIFETIME_CONDITION을 전략적으로 활용하면 데이터 전송을 최적화할 수 있습니다. 예를 들어 캐릭터의 움직임 정보는 해당 캐릭터를 조종 중인 클라이언트에게는 보낼 필요가 없습니다. 엔진 기본으로도 ReplicatedMovement 등에 COND_SimulatedOnly가 적용되어 있어 AutonomousProxy에는 움직임 복제를 안 보내도록 최적화되어 있습니다 (Network Tips and Tricks - Unreal Engine) (Network Tips and Tricks - Unreal Engine). 이처럼 각 변수마다 어떤 상황에 누구에게 보내야 할지 조건을 명시하면 쓸데없는 전송을 걸러냅니다.
- RPC 사용 최적화: RPC는 남발하면 대역폭과 처리 비용을 크게 차지할 수 있습니다. 특히 Reliable RPC를 Tick마다 보내는 것은 피해야 합니다 (Remote Procedure Calls | An Unreal Engine Blog by Cedric Neukirchen). 대신 상태 변화가 누적되었다가 변화할 때만 보내거나, 여러 이벤트를 하나의 RPC에 묶어 보내는 방법을 고려합니다. 필요 없는 경우 Unreliable로 보내 손실을 허용하고, 클라이언트에서 예측 또는 추측 보완을 하는 것도 방법입니다.
- 패킷 크기 관리: 한 틱에 너무 많은 데이터를 보내면 패킷이 커져서 분할 전송되거나 UDP 특성상 손실 가능성이 높아집니다. Unreal은 한 프레임에 connection당 보낼 수 있는 무언가 한도가 있는데 (엔진 기본 설정), 이를 넘지 않도록 해야 합니다. 즉, 한번에 많은 Actor/변수를 갱신하는 상황을 피하고, 프레임별로 고르게 분배되도록 설계합니다. 예를 들어 50개의 적 AI 상태를 동시에 업데이트하기보다 10개씩 다섯 프레임에 나눠 업데이트 트리거하는 등 간격을 조절합니다.
- Replication Graph 및 Iris: UE4 후반부터 도입된 Replication Graph는 대규모 멀티플레이어에서 replication 대상 그룹을 효율적으로 관리하는 시스템이고, UE5에서는 Iris라는 새로운 네트워크 replication 기술이 개발 중입니다 (Unreal Engine Multiplayer Tips and Tricks - WizardCell). 소규모(2~4인) 게임에서는 기본방식으로도 충분하지만, 향후 플레이어 수가 늘거나 월드 오브젝트가 많아지면 Replication Graph를 사용하는 것을 고려해볼 수 있습니다. 다만 이는 고급 주제이므로 작은 협동 로그라이크에서는 기본 replication과 relevancy 튜닝만으로도 충분한 최적화가 가능합니다.
위의 기법들을 적절히 활용하면 필요한 정보만, 필요한 시점에만 네트워크로 보내도록 조절할 수 있습니다. 네트워크 최적화는 게임 상태에 따라 다르므로, 트래픽 프로파일링을 통해 병목을 찾고 그에 맞게 Frequency나 조건을 조정하는 과정이 필요합니다.
7. PlayerState와 GameState의 활용
언리얼 엔진에서 APlayerState와 AGameState 클래스는 멀티플레이 게임의 정보를 관리하는 중요한 도구입니다. 이들은 모두 AInfo를 상속하며 복제를 지원하도록 만들어져 있습니다.
- PlayerState (플레이어 상태): 각 플레이어마다 하나씩 존재하며, 플레이어별 점수, 이름, 팀, 남은 목숨 등을 저장합니다. PlayerState는 서버에서 생성되어 모든 클라이언트에 복제됩니다 (AGameState | An Unreal Engine Blog by Cedric Neukirchen). 즉, 모든 플레이어의 PlayerState 정보가 모든 클라이언트에 존재하므로, 다른 플레이어의 점수나 상태도 쉽게 조회할 수 있습니다. 예를 들어 4인 멀티플레이에서 각 플레이어의 PlayerState에 Score나 KillCount 변수를 Replicated로 두면, 서버에서 점수를 갱신할 때 자동으로 모두에게 전달되고 UI에 표시될 수 있습니다. PlayerState는 APlayerController::PlayerState로 연결되어 있으며, 게임 도중 플레이어가 죽었다가 다시 살아나도 (재스폰) PlayerState는 유지되어 누적된 정보를 보존합니다. 또한 시ーム리스 트래블(맵 이동) 시에도 PlayerState를 유지할 수 있어, 로비 -> 게임 맵으로 넘어가도 플레이어의 누적 데이터(예: 장비, 능력치)를 잃지 않게 할 수 있습니다.
서버에서 PlayerState->KillCount++처럼 값을 변경하면 자동으로 클라이언트의 PlayerState도 업데이트됩니다. 클라이언트 개발자는 이를 이용해 Scoreboard UI 등을 구성할 수 있습니다. 단, PlayerState는 서버와 해당 클라이언트만 수정 권한을 갖습니다. 예컨대 자신의 이름 변경은 자신의 클라이언트나 서버만 해야 하고, 다른 클라이언트는 임의로 값 변경을 하면 안 됩니다.// 헤더 UCLASS() class AMyPlayerState : public APlayerState { GENERATED_BODY() public: UPROPERTY(Replicated, BlueprintReadOnly, Category="Stats") int32 KillCount; UPROPERTY(Replicated, BlueprintReadOnly, Category="Stats") int32 DeathCount; //... }; void AMyPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutProps) const { Super::GetLifetimeReplicatedProps(OutProps); DOREPLIFETIME(AMyPlayerState, KillCount); DOREPLIFETIME(AMyPlayerState, DeathCount); } - 사용 예시: 커스텀 PlayerState 클래스를 만들어 다음과 같이 사용할 수 있습니다:
- GameState (게임 상태): 게임 전체에 하나만 존재하며, 전체 경기의 상태 정보를 담습니다. GameState도 역시 서버에 생성되고 모든 클라이언트에 복제됩니다 (AGameState | An Unreal Engine Blog by Cedric Neukirchen). 여기에 들어갈 정보는 게임의 진행 상황, 남은 시간, 총 스코어, 현재 라운드, 모든 플레이어의 PlayerState 목록 등이 있습니다 (AGameState | An Unreal Engine Blog by Cedric Neukirchen) (AGameState | An Unreal Engine Blog by Cedric Neukirchen). GameState는 GameMode(서버 전용)의 정보를 클라이언트와 공유하는 다리 역할을 합니다 (Game Mode and Game State in Unreal Engine). 예를 들어 GameMode가 “현재 Wave 번호”를 관리한다면, GameState에 CurrentWave라는 replicated 변수를 추가하여 클라이언트들도 해당 값을 알 수 있게 합니다.사용 예시: 커스텀 GameState 클래스:서버의 GameMode가 Wave를 진행시킬 때 GameState->CurrentWave++로 갱신하면 모든 클라이언트 GameState에 반영되고, 각종 HUD가 업데이트될 수 있습니다.
- UCLASS() class AMyGameState : public AGameStateBase { GENERATED_BODY() public: UPROPERTY(Replicated, BlueprintReadOnly, Category="Game") int32 CurrentWave; UPROPERTY(Replicated, BlueprintReadOnly, Category="Game") float RemainingTime; // ... }; void AMyGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutProps) const { Super::GetLifetimeReplicatedProps(OutProps); DOREPLIFETIME(AMyGameState, CurrentWave); DOREPLIFETIME(AMyGameState, RemainingTime); }
- GameState는 AGameStateBase 기본 기능에 더해 팀 정보나 매치 상태 관리 등을 제공하며, AGameState::PlayerArray 배열에 접속된 모든 PlayerState들을 가지고 있습니다. PlayerArray 자체는 replicated 표시는 없지만, 개별 PlayerState가 복제되어 클라이언트 측에서 PostInitializeComponents 시 자기 자신을 GameState.PlayerArray에 추가하는 로직이 있어 모든 클라이언트에서 PlayerArray를 채웁니다 (AGameState | An Unreal Engine Blog by Cedric Neukirchen) (AGameState | An Unreal Engine Blog by Cedric Neukirchen). 따라서 클라이언트는 GetWorld()->GetGameState()->PlayerArray를 통해 현재 접속자들의 PlayerState에 접근 가능합니다.
멀티플레이 로그라이크 게임에서는 GameState를 사용해 공용 자원(예: 파티 전체의 점수, 진행 단계, 남은 적 수)을 관리하고, PlayerState를 통해 개별 플레이어의 상태(예: 체력, 소지 골드, 개인별 퀘스트 진행)를 관리할 수 있습니다. 이처럼 체계적으로 상태를 분류하면 서버에서도 관리가 용이하고, 클라이언트는 권한에 맞는 데이터를 쉽게 얻어와 표시하거나 활용할 수 있습니다.
추가로, GameMode는 서버에서만 존재하여 게임 규칙을 관리하지만 클라이언트에는 없기 때문에 클라이언트는 반드시 GameState를 통해서만 게임 진행 정보를 조회해야 한다는 점도 기억해야 합니다 (Game Mode and Game State in Unreal Engine). 예를 들어 남은 시간은 GameMode에만 있다면 클라이언트는 모르므로, GameState로 옮겨서 복제해야 합니다.
8. Gameplay Ability System(GAS)
Gameplay Ability System(GAS)은 네트워크 상에서 복잡한 게임플레이 능력(Ability)과 효과(Effect)를 관리하기 위한 프레임워크입니다. RPG나 MOBA 스타일의 스킬 시스템을 염두에 두고 설계되었지만, 일반 액션 게임에도 활용할 수 있습니다 (tranek/GASDocumentation - GitHub). GAS를 사용하면 능력 발동과 취소, 재사용 대기시간, 자원(마나 등) 소모, 버프/디버프 효과 적용 등을 체계적으로 구현할 수 있고, 이 모든 것이 클라이언트/서버 환경에서 잘 동작하도록 자동 지원됩니다. 핵심 구성 요소는 다음과 같습니다:
- Ability: UGameplayAbility를 상속하여 만들며, 하나의 능력(스킬, 아이템 사용 등)을 표현합니다. 능력은 ActivateAbility 함수 내에 스킬의 행동을 구현합니다. 예를 들어 “던지기” 능력은 ActivateAbility에서 투사체를 생성하거나 애니메이션을 재생하는 로직이 있습니다. GAS는 능력의 실행을 서버와 클라이언트에 걸쳐 조정하는데, 로컬 플레이어는 능력을 시전하면 즉시 예측 실행(predict)하고 서버에 승인 요청을 보냅니다. 서버가 승인하면 다른 클라이언트들에게도 능력 효과가 전파됩니다. 이를 통해 플레이어는 입력에 대한 빠른 반응을 얻고, 서버는 권한을 가지고 최종 결과를 결정합니다.
- Ability System Component (ASC): UAbilitySystemComponent는 실제 능력 시스템을 동작시키는 컴포넌트로, 보통 캐릭터나 플레이어 상태에 부착됩니다. ASC는 어떤 능력들을 보유하고 있는지, 현재 어떤 버프/디버프(Effect)가 적용되었는지 추적하며, 서버-클라이언트 간 상태 동기화를 담당합니다. 일반적인 패턴은 서버 권한이 있는 Actor(예: Character나 PlayerState)에 ASC를 넣고 SetIsReplicated(true)로 설정하여 모든 클라이언트에 ASC 상태가 동기화되도록 합니다. 또한 ASC는 **예측 키(Prediction Key)**를 활용하여 클라이언트의 예측 실행과 서버의 실제 실행을 매칭시켜줍니다.
- Attribute: UAttributeSet 클래스를 상속하여 캐릭터의 능력치(체력, 마나, 힘 등)를 정의합니다. AttributeSet 내 변수들은 FGameplayAttributeData 타입으로 정의되고, GAS를 통해 자동으로 Replication 및 변경 통지가 이루어집니다. 예를 들어 Health 속성을 변경하는 GameplayEffect를 적용하면 서버에서 Health 값이 바뀌고 ASC가 이를 복제하여 클라이언트의 Health도 바뀝니다.
- Gameplay Effect: 능력 또는 게임 내 이벤트에 의해 적용되는 효과를 뜻합니다. UGameplayEffect로 정의하며, 지속시간, 즉시 효과, 스택 등 다양한 형태를 가질 수 있습니다. 예를 들어 “화상 디버프” 효과는 5초 동안 매 초 체력을 5 감소시키는 효과로 정의할 수 있고, GAS가 초마다 Health 속성을 감소시키도록 합니다. 효과 적용/소멸도 모두 네트워크 상에서 자동 동기화되므로, 버프/디버프의 상태가 모든 플레이어에게 일관되게 유지됩니다.
GAS의 장점은 이런 시스템을 일일이 만들지 않고도, 프레임워크가 능력 실행의 권한 위임과 예측, 효과의 복제, 상태변화 통지를 알아서 처리해준다는 것입니다. 예를 들어 클라이언트가 “대시” 능력을 사용하면, 클라이언트 자신의 캐릭터는 바로 앞으로 돌진하지만 서버에도 요청이 가고, 서버는 권한으로 이동을 처리한 뒤 위치를 조정하거나 실패 시 롤백시키는 식입니다. 또한 다른 클라이언트들은 해당 플레이어가 대시했다는 것을 서버를 통해 알게 되어 자신의 화면에서도 그 캐릭터가 빠르게 움직이는 것을 봅니다.
실전 적용: C++ 클라이언트 개발자는 GAS를 세팅하기 위해 캐릭터 또는 플레이어 상태에 AbilitySystemComponent를 추가해야 합니다. 일반적으로는 PlayerState에 두는 방식도 있지만, 최근에는 Character(Pawn) 자체에 ASC를 붙이는 경우도 많습니다. 코드 예시로 캐릭터에 ASC를 붙이고 능력을 주는 과정은 아래와 같습니다:
// Character header
UPROPERTY()
UAbilitySystemComponent* AbilitySystemComp;
UPROPERTY()
TSubclassOf<UGameplayAbility> FireballAbility; // 불 능력 클래스
// Character cpp (예: BeginPlay)
AbilitySystemComp = CreateDefaultSubobject<UAbilitySystemComponent>("AbilitySystemComp");
AbilitySystemComp->SetIsReplicated(true);
AbilitySystemComp->SetReplicationMode(EGameplayEffectReplicationMode::Mixed); // 또는 Full
if (HasAuthority() && AbilitySystemComp) {
// 능력 부여: FireballAbility를 레벨1로 ID 0 슬롯에 추가
AbilitySystemComp->GiveAbility(FGameplayAbilitySpec(FireballAbility, 1, 0));
}
AbilitySystemComp->InitAbilityActorInfo(this, this);
위와 같이 설정하면 서버는 해당 캐릭터가 FireballAbility를 갖고 있음을 알고, 클라이언트에게도 그 정보가 전달됩니다. 이제 Input 시스템과 연계하여 특정 키에 Ability를 Activation 시키면, GAS가 알아서 서버 RPC 등을 사용해 능력을 발동시킵니다. 클라이언트에서 AbilitySystemComp->TryActivateAbilityByClass(FireballAbility);를 호출하면, 로컬에서 능력이 즉시 발동(예: 불꽃 이펙트 재생)되고 서버에서도 실행되어 적절히 처리되는 식입니다.
GAS는 또한 GameplayCue라는 개념을 통해 능력이나 효과 발생시 시각/청각 효과를 관리할 수 있습니다. GameplayCue는 주로 클라이언트에서만 실행되는 이펙트 표현으로, 예를 들어 “피해 입음” 효과에 연계된 Cue를 재생하면 피격 이펙트와 소리가 나타납니다. 이런 Cue 시스템도 서버가 Cue 이벤트를 보내면 클라이언트에서 실행되는 형태로 동작하므로, 일종의 멀티캐스트 RPC 역할을 합니다.
요약하면, GAS를 활용하면 복잡한 멀티플레이어 능력 구현을 쉽게 하고 중복되는 네트워크 코드를 줄이며, 능력 간의 우선순위, 취소, 재사용 대기시간 등을 통합적으로 다룰 수 있습니다. 로그라이크 게임에서도 플레이어 능력이나 파워업을 GAS로 구현하면 향후 확장과 밸런싱이 수월해지고, 네트워크 예측 기반으로 부드러운 협동 플레이를 만들어낼 수 있습니다. 다만 GAS 자체의 러닝 커브가 있으므로, 사전 공부와 설계가 필요합니다.
9. 네트워크 디버깅 및 테스트 도구
멀티플레이 기능을 개발할 때는 디버깅과 성능 분석을 위해 다양한 도구를 활용해야 합니다. Unreal Engine 5에서는 네트워크 프로파일링과 시뮬레이션 기능이 잘 마련되어 있으며, 이를 통해 로컬 환경에서 문제를 찾고 개선할 수 있습니다. 주요 도구와 방법은 다음과 같습니다:
- 네트워크 통계 보기 (stat net): 게임 실행 중 콘솔에 stat net을 입력하면 현재 네트워크 상태를 실시간으로 볼 수 있는 패널이 나옵니다 ([QUESTION] Display simple networks stats(lag, loss, etc. ) : r/unrealengine). 여기에는 초당 바이트 전송량, 패킷 수, ping, 각 클라이언트별 정보, 대기 중인 RPC/프로퍼티 수 등이 표시됩니다. 이를 통해 특정 순간에 네트워크 사용량이 급증하는지, ping이 비정상적으로 높아지지는 않는지 감시할 수 있습니다. 또한 stat net 패널은 Actors 항목에서 어떤 Actor가 몇 바이트를 보내고 있는지도 보여주므로, 과도하게 트래픽을 일으키는 객체를 식별하는 데 유용합니다.
- Network Profiler (네트워크 프로파일러): 엔진에 내장된 별도 툴로, 세션 중 네트워크 패킷 로그를 수집하여 분석할 수 있습니다. netprofile 콘솔 명령으로 프로파일링을 시작/종료하면 .nprof 파일이 생성되며, 이를 Unreal Developer Network Profiler 툴로 열면 프레임별 네트워크 사용량, RPC 호출 내역, Replicated 재산 전송량 등을 상세히 볼 수 있습니다. 이 도구를 통해 예를 들어 “어떤 RPC가 가장 빈번하게 호출되는지”, “어느 Actor의 어떤 프로퍼티가 대역폭을 많이 차지하는지” 등을 파악하고 최적화 방향을 결정할 수 있습니다. (UE5에서는 Unreal Insights에 네트워크 트래픽 트레이스가 통합되기도 했습니다.)
- 네트워크 에뮬레이션(Network Emulation): 에디터에서 멀티플레이 테스트 시 강제로 지연과 패킷 손실을 발생시켜 열악한 인터넷 환경을 시뮬레이션할 수 있습니다. 방법은 두 가지인데, 하나는 PIE(Play In Editor) 고급 설정에서 Network Emulation 설정을 조정하는 것이고, 다른 하나는 콘솔 명령을 직접 사용하는 것입니다. 예를 들어 콘솔에 Net PktLag=100이라고 입력하면 왕복 100ms의 인위적 지연을 추가하고, Net PktLoss=5라고 하면 5% 패킷 손실을 일으킵니다. Epic 문서에서는 일반적으로 200ms 지연, 5% 손실을 Worst-case 시나리오로 두고 테스트해 볼 것을 권장합니다 (Recommended poor network conditions to test in? - Multiplayer & Networking - Epic Developer Community Forums). 이렇게 네트워크 환경을 가상으로 나쁘게 만들어놓고 게임을 플레이해 보면, 랙이 있을 때 생기는 문제(예: 애니메이션 불일치, 끊김, 예측 오류 등)를 조기에 발견하고 대처할 수 있습니다.
- 패킷 분석(Packet Analysis): 네트워크 레벨에서 패킷을 캡처하여 내용을 분석하는 것은 고급 디버깅에 속하지만, 필요하면 Wireshark와 같은 도구를 활용해 UDP 패킷 흐름을 볼 수 있습니다. Unreal의 패킷은 바이너리 포맷이지만, 패킷 크기, 빈도, 손실 여부를 외부에서 검증할 수 있습니다. 또한 Unreal 엔진의 로그 기능으로 LogNet 카테고리를 Verbose로 설정하면 패킷 송수신에 대한 자세한 로그를 콘솔에서 확인할 수도 있습니다. 이 방법은 특정 RPC가 도달하지 않는다거나, replication이 제대로 안 된다고 의심될 때 활용하면 원인을 파악하는 데 도움이 됩니다.
- 멀티 인스턴스 테스트: 에디터의 멀티플레이어 미리보기 기능(다수의 PIE 창)이나 별도의 스탠드얼론 클라이언트를 실행하여 실제처럼 테스트하는 것도 중요합니다. PIE에서 2~4개의 클라이언트를 띄워놓고 상호작용을 테스트하면, 한쪽에서 발생한 이벤트가 다른 쪽에 잘 보이는지, 지연 시 어떻게 보이는지를 바로 확인할 수 있습니다. 또한 Dedicated Server 모드로 실행하여 클라이언트를 붙이는 식으로도 테스트해야, Listen 서버와 다른 특이점을 잡아낼 수 있습니다.
- 디버그 로그와 브레이크포인트: 네트워크 코드에서는 실행 주체가 서버인지 클라이언트인지 헷갈릴 때가 많습니다. 이때 UE_LOG에 현재 Role이나 HasAuthority(), GetWorld()->IsServer() 등을 출력해서 로그를 남기면 유용합니다. 예컨대 UE_LOG(LogTemp, Log, TEXT("Fire() called on %s (role %d)"), *GetName(), (int32)GetLocalRole()); 식으로 찍어서, 서버에서만 동작해야 할 함수가 클라이언트에서 잘못 불리는 일은 없는지 찾을 수 있습니다. 또한 Visual Studio 디버거로 클라이언트와 서버 인스턴스를 각각 Attach하여 브레이크포인트를 검출하면, 함수 호출이 원하는 범위에서 일어나는지 검증 가능합니다.
- Profiling (CPU, Memory): 네트워크와 직접 관련은 없지만, 멀티플레이 환경에서는 프레임 드롭이나 랙이 네트워크 문제로 오해될 수 있으므로, 성능 프로파일링도 병행해야 합니다. Unreal Insights나 stat unit, stat fps 등으로 서버와 클라이언트 프레임 시간을 모니터링하고, GC나 타이킥 과부하로 인한 지연이 아닌지 구별해야 합니다.
정리하면, Unreal Engine이 제공하는 stat net, Network Profiler, Emulation 등의 도구를 활용하여 네트워크 트래픽을 가시화하고, 다양한 조건에서 테스트함으로써 숨은 버그와 비효율을 찾는 것이 중요합니다. 클라이언트 프로그래머는 이러한 정보를 토대로 RPC 호출을 줄이거나 패킷 크기를 조절하고, 필요한 곳에만 복제가 이루어지도록 코드를 최적화할 수 있습니다. 또한 QA 단계에서 여러 네트워크 시나리오를 시뮬레이션해봄으로써 출시 후 발생할 수 있는 문제를 미연에 방지해야 합니다.
以上のポイントを踏まえて, Unreal Engine 5.5でのマルチプレイヤーローグライクゲームのクライアントネットワークプログラミングに臨めば、堅牢でスムーズな協力プレイ体験を提供できるでしょう. (※ 마지막 문장은 한국어로 번역)
'UnrealCamp' 카테고리의 다른 글
| 언리얼 엔진 5에서 대난투 스타일 게임을 위한 카메라 시스템 확장하기 (1) | 2025.04.11 |
|---|---|
| 프로젝트 기획서 [Smash Raid] (0) | 2025.04.02 |
| 12주차 Day5 (0) | 2025.03.07 |
| 12주차 Day2 (0) | 2025.03.04 |
| 10주차 Day3- Unreal Engine C++로 Enemy AI Base 구현하기 (0) | 2025.02.19 |