본문 바로가기

카테고리 없음

언리얼 엔진 멀티플레이어 개론

멀티플레이어 개발을 위해 알아야할 전반적인 기초 개념을 정리

기본적인 내용을 다루며 너무 디테일한 부분은 접어두었다.

 

서버의 분류

1. Client - Server

모든 클라이언트가 서로 직접 연결되지 않고 중앙에 있는 서버에게만 연결되어 서버가 모든 클라이언트들을 중개하는 역할을 하는 구조를 말한다. 주로 서버의 권한(Authority)이 요구되는 게임에서 사용한다. 예를들어 MMORPG의 경우 아이템 복사와 같은 문제가 일어나지 않게 하기 위해서는 클라이언트에게 아이템 생성 권한을 차단하고 서버에서 직접 생성하여야 한다.

 

Client - Server 모델은 크게 두 가지 방식으로 다시 나뉘어 진다.

1-1. Listen Server

클라이언트중 하나가 서버 역할도 같이 하는 구조이다. 즉 실제 플레이하는 유저 한명이 서버이므로 서버로 선택된 유저의 컴퓨터에서는 클라이언트와 서버가 둘다 실행된다. 위에서 예를 든 아이템 복사와 같이 서버의 권한이 필수로 요구되지 않는 소규모 협동게임(ex. Left 4 Dead, Lethal Company)에서 주로 사용한다. 가까운 친구들과 플레이 하는 경우, 지역적으로 가깝기 때문에 낮은 지연시간으로 게임을 즐길 수 있고 게임사에서 서버 부담을 지지 않는다는 특징도 있다.

 

1-2. Dedicated Server

게임을 진행하는 플레이어가 아닌 외부에서 직접 서버를 운영하는 방식이다. 치트를 방지해야하는 경쟁 게임이나 MMORPG와 같이 서버의  권한이 요구되는 게임이나 일반 유저의 컴퓨터에서 감당하지 못하는 대규모 멀티 게임에서 주로 사용한다. 서버는 게임에 직접 참여하지 않으므로 화면 렌더링이나 사운드 출력 등이 없는 상태에서 게임 로직만 실행된다.

 

2. P2P(Peer-to-Peer)

플레이어들끼리 직접 연결되는 방식으로 모두가 클라이언트이자 서버인 구조이다.

구현이 상대적으로 쉽고 특정 플레이어가 나가더라도 서버 전체가 다운되는 일이 발생하지 않는다. 또한 특정 플레이어에게 네트워크 트래픽이 몰리지 않는다는 장점도 있다. 리슨 서버와 같이 유저가 직접 서버 역할을 하므로 지역적으로 가까우면 낮은 지연시간으로 게임을 즐길 수 있다. 그러나 플레이어 수가 늘어날 수록 받을 데이터 전송량이 급격히 증가한다는 단점이 있다. (n명이 연결되면 총 n(n-1)/2개의 연결이 필요한 구조이다)

또한 어떤 플레이어가 제공하는 데이터가 진실인지 알지 못한다. 이는 모든 플레이어가 서버 역할을 나누어서 실행하므로 어느 하나의 서버가 권한을 가지지 않기 때문이다. 따라서 게임을 진행함에 있어 게임 동기화가 어려울 수 있다.

또한 각 플레이어는 다음 네트워크 프레임을 시뮬레이션 하기 전에 다른 모든 플레이어의 메시지를 기다려야 하므로 전체 네트워크 대기 시간이 연결이 가장 나쁜 플레이어의 대기 시간으로 통일된다.

 

NetMode

언리얼 엔진에서는 여러가지 네트워크 모드를 지원한다. UEngine::GetNetMode()를 통해 현재 NetMode를 알 수 있다.

 

서버 프로그램을 직접 커스터마이징하여 언리얼과 전혀 상관없이 별도의 프로세스로 실행할 수 있다. 그러나 언리얼에서 제공하는 기능으로 멀티플레이를 구현하고자 한다면 클라이언트와 서버가 같은 프로젝트에서 로직이 작성된다는 것을 알아야한다. 따라서 이 둘을 구분해주는 조건을 넣지 않으면 클라이언트와 서버 모두에서 실행되어 이상해질 수 있는데, 서버에서 실행하기 위해서는 HasAuthority()로 서버인지 확인한 후 로직을 작성해야 한다.

enum ENetMode
{
	 NM_Standalone, // 싱글 게임
    NM_DedicatedServer, // 데디케이티드 서버
    NM_ListenServer, // 리슨 서버
    NM_Client, // 클라이언트
    NM_MAX // NetMode의 총 개수를 나타냄. 게임 인스턴스의 넷모드 값이 NM_MAX가 될 일 없음
}

ENetMode NetMode = GetWorld()->GetNetMode(); // NetMode 확인

if(HasAuthority()) 
{
	// 서버에서 실행시킬 로직
}

 

에디터 상에서 쉽게 리슨 서버로 바꿀 수 있다. 데디케이티드 서버로 변경하려면 몇 가지 추가적인 작업이 필요하다.

 

1. Play Standalone(NM_Standalone)

말 그대로 싱글 게임이다. 모든 게임 로직이 클라이언트에서 실행된다.

 

2. Play As Listen Server(NM_ListenServer)

하나의 클라이언트가 서버 역할을 같이 하는 모드를 의미한다. 

 

3. Play As Client(NM_Client)

서버의 역할을 하지 않고 클라이언트의 역할만 하는 플레이어(리슨 서버의 경우 서버로 선택되지 않은 클라이언트, 데디케이티드 서버에서의 클라이언트)를 의미한다. 


NetRole

네트워크 상에서 클라이언트, 서버 두 가지 방식의 역할만 있다고 생각할 수 있지만 더 나아가 클라이언트 끼리도 구분할 수 있다. 예를들어 내가 조종할 수 있는 캐릭터가 있는 반면 그렇지 않은 캐릭터도 존재할 수 있다. 나중에 밑에서 설명할 RPC 통신에서 중요하게 사용되는 소유권(Ownership) 개념과도 연관된다. 클라이언트끼리 무슨 기준으로 나누어 지는지와 코드 상에서 그들을 어떻게 구별하는지를 알아두면 된다. 

 

UENUM(BlueprintType)
enum ENetRole : int
{
	ROLE_None UMETA(DisplayName = "None"),
	ROLE_SimulatedProxy UMETA(DisplayName = "Simulated Proxy"),
	ROLE_AutonomousProxy UMETA(DisplayName = "Autonomous Proxy"),
	ROLE_Authority UMETA(DisplayName = "Authority"),
	ROLE_MAX UMETA(Hidden),
};

// 현재 실행중인 로컬 머신에서 해당 Pawn이 어떤 네트워킹 역할을 가지고 있는지를 반환
ENetRole NetRole = Pawn->GetLocalRole();

// 이 함수를 호출하는 머신의 반대편 네트워크 머신(주로 서버)에서 해당 액터가 어떤 네트워킹 역할을 가지고 있는지를 반환
ENetRole NetRole2 = Pawn->GetRemoteRole();

if(Pawn->IsLocallyControlled())
{
	// Pawn이 현재 코드가 실행되고 있는 머신에 연결된 로컬 플레이어에 의해 제어되고 있는지 확인
}

 

  • Simulated Proxy
    내가 조종 할 수 없는 Actor들을 의미한다. 100명의 플레이어가 있다고 가정하면 나를 제외한 99명의 플레이어들은 나에게 Simulated Proxy이다. 상대 입장에서도 내가 Simulated Proxy로 보일 것이다.
  • Autonomous Proxy
    내가 실제 조종할 수 있는 Actor들을 의미한다. 내 클라이언트에서 Autonomous Proxy는 오로지 나 자신이다.
  • Authority
    서버를 의미한다.

 

LocalRole, RemoteRole

서버의 경우 모든 권한을 가지고 있으므로 모든 플레이어의 LocalRole은 Authority이고 RemoteRole은 Simulated Proxy이다.

 

클라이언트의 경우 위에서 설명했듯이 내 캐릭터의 LocalRole은 Autonomous Proxy, 나머지 캐릭터의 LocalRole은 Simulated Proxy이다.RemoteRole은 서버이므로 모두 Authority이다. 

 

이렇게 NetRole에 따라 나의 클라이언트와 다른 클라이언트를 구분할 수 있다.

이제 서버에서는 구체적으로 어떻게 플레이어의 NetRole을 알아낼 수 있는지 알아보자.

 

1. 고유 네트워크 연결

각 클라이언트가 서버에 접속할 때마다 서버는 해당 클라이언트와 고유한 네트워크 연결 객체(UNetConnection)을 생성한다. 이 객체가 해당 클라이언트와의 통신 채널 역할을 하며 서버는 이 객체를 통해 특정 클라이언트로 데이터를 보내거나 해당 클라이언트로부터 데이터를 받는다.

 

2. PlayerController

서버는 성공적으로 접속한 각 클라이언트를 위해 APlayerController 액터를 하나씩 생성한다.

이 APlayerController는 해당 클라이언트를 대표하는 객체이며 UNetConnection과 연결된다.

따라서 서버 입장에서는 각 클라이언트의 APlayerController 객체가 특정 클라이언트를 구별하는 주요 식별자 역할을 수행한다. 

 

3. 소유권(Ownership)

서버에서 생성되고 관리되는 Actor들은 특정 PlayerController의 소유가 될 수 있다. 서버는 Actor의 소유권 기반으로 해당 Actor의 상태 변화(Replication)을 어떤 클라이언트에게 보내야 할지를 결정하고 클라이언트로부터 받은 입력이나 RPC 호출이 유효한 소유자에 의해 시작되었는지 확인한다.

 

GameStateBase 클래스에서는 현재 게임에 연결된 모든 플레이어들의 APlayerState 목록 PlayerArray를 가지고 있으며, PlayerArray는 각 플레이어의 APlayerController와 연결되어 있다. 따라서 PlayerArray를 통해 APlayerController에 접근할 수 있다.

 

	if (const UWorld* World = GetWorld())
	{
		if (AGameStateBase* GameState = World->GetGameState<AGameStateBase>())
		{
			for (const APlayerState* PlayerState : GameState->PlayerArray)
			{
				if (APlayerController* PlayerController = PlayerState->GetPlayerController())
				{
					// Logic ...
				}
			}
		}
	}

 


통신 방식

서버와 클라이언트가 통신하는 방법에는 리플리케이션RPC 두 가지 방식이 있다.

 

1. 리플리케이션(Replication)

서버에서 클라이언트에게 데이터를 복제할 때 사용한다. 클라이언트에서 서버 방향으로는 불가능하다는 점을 알아 둘 것.

리플리케이션은 또 다시 두 가지 방식으로 세분화 된다.

1-1. Replicated

변수 값 자체의 동기화가 중요하고 변경되었을 때 클라이언트의 반응이 즉각적으로 필요하지 않은 경우에 사용한다.

클라이언트에서 해당 변수 값이 변경되었다고 해서 어떤 함수가 자동으로 실행되지 않고 필요하다면 해당 변수를 직접 사용자가 만든 함수에서 이용해야한다.

UPROPERTY(Replicated)
AActor* Owner;

 

1-2. RepNotify

Replicated 방식에 더해 변수 값이 서버로부터 복제되어 클라이언트에서 변경될 때 지정된 함수를 자동으로 호출하도록 설정한다. 서버에서 복제된 변수 값이 변경되었을 때, 클라이언트에서 즉각적인 피드백이 필요하거나 상태 변화에 따른 추가 로직을 실행해야 하는 경우에 주로 사용된다.

UPROPERTY(ReplicatedUsing = OnRep_Func)
AActor* Owner;
    
UFUNCTION()
void OnRep_Func(); // 매개변수를 아무것도 안받아도 되고 기존 타입(지금 같은 경우는 AActor)을 받아도 됨

 

주의사항

OnRep 함수는 기본적으로 리플리케이트된 변수 값이 바뀌었음을 수신하고 실행하는 함수이다. 그러나 서버에서는 수신받은 적이 없으므로 리플리케이트된 변수 값이 바뀌어도 OnRep 함수가 실행되지 않는다. 즉 일반적으로 Replicated된 변수가 값이 바뀔때 클라이언트에서는 자동적으로 OnRep 함수를 호출하지만 Server에서는 따로 실행해줘야 한다.

 

void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const

 

GetLifetimeReplicatedProps()은 리플리케이션 대상 프로퍼티 목록들을 정의하기 위해 필요한 함수로 런타임에 네트워킹 시스템에 의해 호출된다. 엔진은 각 액터, 오브젝트 인스턴스가 생성되거나 네트워크 관련성이 변경될 때 해당 객체의 복제 설정을 동적으로 파악할 수 있다. 리플리케이션이 필요한 변수들을 DOREPLIFETIME 계열의 특정 매크로를 사용하여 설정한다.

#include "Net/UnrealNetwork.h"

void AActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(AActor, Owner); // 리플리케이션 대상 목록에 추가
}

 

DOREPLIFETIME 매크로는 언리얼 엔진에서 특정 변수를 네트워크 리플리케이션 대상 목록에 추가할 때 사용한다.

 

블루프린트에서 Details - Replication - Replicate Condition을 설정하는 것 처럼 C++에서도 변수의 리플리케이션 조건을 설정할 수 있다. 아래의 DOREPLIFETIME_CONDITION()와 DOREPLIFETIME_CONDITION_NOTIFY()를 사용하면 된다.

  • DOREPLIFETIME(ClassName, PropertyName)
    조건 없이 리플리케이션 목록에 추가만 하는 경우
  • DOREPLIFETIME_CONDITION(ClassName, PropertyName, Condition)
    Replicated + 조건이 필요할 때 사용한다. Condition에 ELifetimeCondition 이라는 ENUM 값이 사용된다.
  • DOREPLIFETIME_CONDITION_NOTIFY(ClassName, PropertyName, Condition, RepCondition)
    RepNotify + 조건이 필요할때 사용한다. RepCondition에는 ELifetimeRepNotifyCondition Enum 값이 사용된다.

 

ELifetimeCondition

위에서 설명한 DOREPLIFETIME_CONDITION() 계열 매크로에서 Condition에 사용되는 Enum 값으로 리플리케이션의 조건을 추가할 때 사용한다.

더보기
  • COND_None: 특정 조건 없이 항상 복제됨을 의미. DOREPLIFETIME()을 사용했을 때와 동일하다.
  • COND_InitialOnly: 액터가 클라이언트에게 처음 복제될 때 한 번만 복제 됨. 그 이후의 변경 사항은 복제되지 않음
  • COND_OwnerOnly: 해당 액터를 소유한 클라이언트에게만 복제됨(플레이어 인벤토리 같은 경우 다른 클라이언트에 복제할 필요 없음)
  • COND_SkipOwner: 해당 액터를 소유한 클라이언트를 제외한 모든 클라이언트에게만 복제된다. (플레이어 캐릭터의 애니메이션 상태등은 소유한 클라이언트는 직접 제어하므로 복제받을 필요가 없거나 다른 방식으로 동기화할 수 있음)
  • COND_SimulatedOnly: Simulated Proxy(네트워크 상의 다른 플레이어 캐릭터, 소유하지 않은 클라이언트의 액터)에게만 복제됨
  • COND_AutonomousOnly: Autonomous Proxy(소유한 클라이언트 액터)에게만 복제 된다.
  • COND_InitialOrOwner: 처음 복제될 때 해당 클라이언트가 액터의 소유자일 경우에 복제 된다.
  • COND_Custom: 사용자 지정 로직으로 복제 여부를 판단함

ELifetimeRepNotifyCondition

DOREPLIFETIME_CONDITION_NOTIFY()의 RepCondition에 사용되는 Enum 값이다.

더보기
  • REPNOTIFY_OnChanged: 로컬 값에서 변경되는 경우에만 RepNotify 함수를 호출한다.
  • REPNOTIFY_Always: 서버에서 수신되면 항상 RepNotify 함수를 호출한다.

 

추가로 온전히 리플리케이션을 사용하려면 Actor 생성자에서 bReplicates 플래그를 true로 설정해야한다.

일반적으로 Movement(위치, 회전, 스케일)를 동기화할 일이 잦기 때문에 Movement에 대해서는 언리얼이 기본 함수를 만들어 놓았다. 다른 프로퍼티를 복제하려면 위에서 처럼 직접 DOREPLIFETIME  매크로로 등록해주어야함

AActor::AActor(const class FPostConstructInitializeProperties & PCIP) : Super(PCIP)
{
	SetReplicates(true); //bReplicates = true; 동일
    SetReplicateMovement(true);
}

 

심화 내용

더보기

Q. 리플리케이션 하는 주기는?

A. 리플리케이션 하는 주기는 언리얼 네트워크가 관리한다. 네트워크 관련성과 네트워크 우선순위 등을 종합적으로 판단하여 동적으로 리플리케이션을 업데이트한다. 따라서 일반적인 게임 로직처럼 매 프레임마다 업데이트 되는 것이 아니라는 점을 알아둘 것

 

네트워크 업데이트 빈도(NetUpdateFrequency)

AActor 클래스가 가지는 속성 값으로 서버가 해당 액터의 상태 변경을 감지하고 클라이언트에게 업데이트를 시도하려는 초당 횟수를 나타냄. 해당 값은 희망 값이며 언리얼 네트워크 시스템은 다른 요소들을 고려하여 실제 업데이트 빈도를 결정한다.

 

네트워크 우선순위(NetPriority)

AActor 클래스가 가지는 속성 값으로 네트워크 대역폭이 제한적일 때, 어떤 액터의 업데이트를 먼저 보낼지, 얼마나 자주 보낼지를 결정하는 우선순위를 나타낸다. 중요하게 생각되는 액터는 우선순위가 높게 설정되어 더 자주 업데이트 될 가능성이 있다. 액터의 리플리케이션 우선순위를 정할 때 거리와 함께 사용되며, 가까운 액터일수록 더 자주 업데이트되지만, 동일하거나 비슷한 거리일 경우 NetPriority 값이 높은 액터가 먼저 업데이트된다.

 

네트워크 관련성(Network Relevancy)

액터가 특정 클라이언트 시점이나 관심 범위 내에 있는지 판단하는 기준이다. 해당 클라이언트와 관련성이 높은 액터일 수록 더 자주 업데이트가 고려된다.

 

변경 여부

Replicated으로 마킹된 프로퍼티들이 실제로 변경되었을 때만 해당 변경 사항이 복제 대상으로 고려되고 변경되지 않았다면 업데이트 빈도를 높게 설정해도 복제 데이터가 발생하지 않음

 

네트워크 대역폭(Network Bandwidth)

전체 네트워크가 혼잡하거나 사용 가능한 대역폭이 부족하다고 판단되면 업데이트 빈도와 우선순위를 높게 설정해두어도 업데이트 빈도가 제한될 수 있음

 

절전모드(Dormancy)

움직임이 없거나 상태 변화가 없는 액터는 절전 모드에 진입하여 리플리케이션 업데이트가 거의/전혀 발생하지 않도록 최적화 된다. 변화가 생기거나 관련성이 높아지면 절전 모드에서 깨어나 리플리케이션 업데이트가 재개된다.

 


추가 기능

 

적응형 네트워크 업데이트 빈도(Adaptive Network Update Frequency)

해당 기능은 비활성화가 디폴트. 사용하면 실제로 변한 것이 없는 액터를 중복해서 리플리케이트 하느라 낭비되는 CPU 사이클을 절약할 수 있다. console에서 net.UseAdaptiveNetUpdateFrequency = 1으로 설정하면 활성화 된다.

 

2. RPC(Remote Procedure Call)

말 그대로 원격 프로시져 호출로 다른 머신에서 원격으로 실행되는 함수를 의미한다. 서버에서 클라이언트 방향으로만 전달하는 리플리케이션과 다르게 클라이언트에서 서버쪽으로 요청할 수 있다.

 

RPC의 정상 작동을 위해 충족시켜야 하는 요건이 몇 가지 있다.

 

  1. Actor에서 호출되어야 한다.
  2. Actor는 반드시 replicated여야 한다.
  3. 서버에서 호출되고 클라이언트에서 실행되는 RPC의 경우, 해당 Actor가 실제 소유하고 있는 클라이언트에서만 함수가 실행된다.
  4. 클라이언트에서 호출되고 서버에서 실행되는 RPC의 경우, 클라이언트는 RPC가 호출되는 Actor를 소유해야한다.

 

Multicast RPC는 예외(소유권 기반인 RPC 통신에서 소유권을 따지지 않고 전체 클라이언트를 타깃으로 한다.)

  • 서버에서 호출되는 경우, 서버에서는 로컬에서 실행될 뿐만 아니라 현재 연결된 모든 클라이언트에서도 실행된다.
  • 클라이언트에서 호출되는 경우, 로컬에서만 실행되며, 서버에서는 실행되지 않는다.
  • 현재는 Multicast 이벤트에 대해 단순한 스로틀 조절 메커니즘이 있다. Multicast 함수는 주어진 액터의 네트워크 업데이트 기간 동안 두 번 이상 리플리케이트되지 않는다. 앞으로 개선된다고 함

2-1. Multicast

UFUNCTION(NetMulticast, Reliable)
void FuncMulticast();

서버가 모든 클라이언트에서 함수를 실행하도록 요청 방식으로 반드시 서버에서 호출해야한다. 주로 게임 핵심 로직은 2-2의 Server RPC로 처리하고 Multicast는 시각적 피드백(VFX, 애니메이션, 사운드)등에 주로 사용하는 경우가 많다.

 

더보기

Multicast vs Broadcast 용어정리 (현재 주제에서 약간 벗어나는 내용으로 접어놓음)

 

Multicast: 특정 그룹에 속한 다수의 장치에게만 데이터를 전파하는 방식

Broadcast: 네트워크 내의 모든 장치에게 데이터를 전파하는 방식

 

언리얼 Multicast RPC는 모든 클라이언트에 전파하니 Broadcast RPC가 아닌가 싶지만 개념상으로는 클라이언트들은 서버 입장에서 관리되는 그룹 멤버 범위이므로 그룹 단위를 강조하는 Multicast로 일반적인 네트워크 용어인 Broadcast보다 좁은 범위의 의미로 사용하는 것 같다.

Delegate에서의 Broadcast()도 모두 전파한다는 관용적인 의미이지 네트워크 용어 Broadcast와는 다르다. 실제 DECLARE하는 부분에서는 MULTICAST라는 매크로를 사용한다. Broadcast()도 바인딩된 함수들에게만 전파한다.

 

2-2. Run on Server

UFUNCTION(Server, Reliable)
void FuncServer();

클라이언트가 서버에서 함수를 실행하도록 서버에게 요청하는 방식이다. 단 실행하는 Actor를 소유해야한다(Ownership).

 

SpawnActor() 함수에서 인자로 Owner를 지정할 수 있는데 이것이 이러한 이유이다.

갑자기 소유권 개념이 나와 뜬금없다고 느낄 수 있지만 A 플레이어의 인벤토리를 B 플레이어가 업데이트 요청을 하는 경우와 같이 문제가 될 법한 것들을 사전에 차단하는 개념이다. 서버는 보안 및 권한 문제로 특정 클라이언트가 소유권이 없는 Actor를 업데이트 요청한다면 무시하여 실행하지 않을 수 있다. 소유권은 게임 도중 동적으로 변경될 수 있다. 플레이어의 캐릭터가 차량에 탑승하여 차량의 소유권을 얻거나 잃는 경우, 캐릭터를 교체하는 경우를 생각해 볼 수 있다.

 

Q. 그렇다면 RPC 통신을 위하여 모든 Actor는 소유권을 가져야 하는가?

A. 그렇지 않다. 위에서 든 예시처럼 아무도 조종하지 않던 차량, 혹은 열고 닫을 수 있는 문같이 소유권 없이 존재하다가 게임이 진행되면서 플레이어가 상호작용함에 따라 소유권이 동적으로 변경될 수 있으므로 모든 Actor가 소유권을 미리 가지고 있을 필요는 없다.

 

2-3. Run on owning Client

UFUNCTION(Client, Reliable)
void FuncClient();

 

Run on owning Client RPC는 Multicast와 동일하게 서버에서 클라이언트쪽으로 실행하지만 해당 Actor의 소유권이 있는 클라이언트에서만 함수를 호출한다는 차이점이 있다.

 

 

지속적인 상태 유지가 필요하면 리플리케이션을, 함수를 실행시켜야 하는 경우에는 RPC를 사용하면 된다. 일반적인 예로 탄창 정보는 계속 유지해야하므로 리플리케이션, 발사 같은경우는 RPC를 사용한다. 또 다른 예로 게임 실행 도중 랜덤 값을 생성해야하는 경우처럼 결과 값을 예측할 수 없을 때 RPC로 각각의 클라이언트에서 랜덤 값을 생성하면 모두 다른 값이 나타나게 되므로 이런 경우에서는 서버에서만 실행하고 리플리케이션 하는게 맞다.

리플리케이션, RPC 두 가지 방식으로 모두 구현이 가능한 게임 로직의 경우, 일반적으로 리플리케이션이 비용이 적기 때문에 리플리케이션을 사용하는 것이 좋다. 리플리케이션은 언리얼 네트워크 시스템이 최적화 주기를 관리하므로 특정 시간에 독립적으로 실행하는 RPC보다 일반적으로 비용이 낮다.

 

UFUNCTION() - Network 관련한 지정자들

더보기
  • Server: 클라이언트가 서버에 RPC 요청할 때 사용. 해당 함수가 서버에서 실행됨을 의미
  • Reliable: 네트워크 전송 보장 옵션. 네트워크가 불안정하거나 패킷 손실이 발생할 수 있는 환경에서도 이 함수 호출이 반드시 전달되어야 함을 알리는 역할.
    • Reliable이 붙은 RPC 함수는 서버에 도달하도록 재시도 함. 반대로 Unreliable은 전달이 보장되지 않고 패킷이 손실되면 호출이 사라짐. Reliable과 Unreliable을 적절하게 분배하여 네트워크 흐름, 혼잡 제어를 커스터마이징 할 수 있게 해놓음.
    • RPC는 UDP가 기반이므로 비신뢰성이다. Reliable은 TCP처럼 신뢰성 있는 통신이 필요해서 만든 것이다. 그러나 기본적으로 UDP 기반이므로 신뢰성은 TCP > Reliable RPC(UDP) > RPC(UDP)
    • 너무 많은 Reliable은 네트워크 큐가 넘칠 수 있고 트래픽 부하가 발생할 여지가 있어 틱처럼 반복적으로 자주 실행되는 곳에서 Reliable RPC가 실행되면 좋지 않다. 반드시 꼭 필요한 곳(공격 판정, 아이템 획득 등등)에만 사용할 것
    • 게임이 Reliable을 보장한다 하더라도 네트워크는 외부 상황에 따라 크게 달라진다. Reliable이 보장하는 범위는 게임 수준까지(패킷 재전송)
  • Unreliable: 네트워크를 통해 복제되지만 대역폭 제한이나 네트워크 오류로 실패할 수 있음
  • Client: 객체를 소유한 클라이언트에서만 실행, 기본 함수와 동일한 이름의 추가 함수를 선언하지만 끝에 _Implementation을 추가한다. 자동 생성된 코드는 필요한 경우 _Implementation 메서드를 호출
  • Remote: RPC는 클라이언트가 소유한 액터에서 호출되어야함.
  • NetMulticast: 서버에서 로컬로 실행되고 Actor의 NetOwner와 관계 없이 모든 클라이언트에 복제함
  • BlueprintAuthorityOnly: 네트워크 권한이 있는 머신에서 실행되는 경우에만 Blueprint 코드에서 실행
  • WithValidation: 악성 데이터/입력 감지를 위한 관문 역할을 위해 인증(Validation)을 추가할 수 있다. RPC에 대한 인증 함수가 악성 파라미터를 감지한 경우, 해당 RPC를 호출한 클라이언트와 서버의 연결을 끊도록 시스템에 알리는 개념이다. 메인 함수와 동일한 이름의 추가 함수를 선언하지만 끝에 _Validate를 추가함. 이 함수는 동일한 매개변수를 사용하며 메인 함수 호출을 진행할지 여부를 나타내는 bool 값을 반환함
  • BlueprintCosmetic: 함수가 클라이언트 측에서만 실행되어야 하며, 게임에 실제 논리에 영향을 주지 않는 '미용적' 함수를 나타냄. 시각 효과, 파티클, 사운드 재생 등에 사용된다.
  • ServiceRequest: 함수가 RPC 서비스 요청으로 사용됨을 나타낸다. 이는 NetMulticast 및 Reliable 속성을 포함함.

    Q. 그렇다면 UFUNCTION(ServiceRequest)이랑 FUNCTION(NetMulticast, Reliable)은 동일한가?
    A. 기술적으로 동일한 네트워크 복제 동작을 하지만 ServiceRequest는 의도와 목적에 대한 추가적인 정보를 전달한다. 함수가 단순히 모든 클라이언트에 안정적으로 복제되는 함수가 아니라 특정 서비스에 대한 요청으로 설계되었음을 나타낸다. 이와 쌍을 이루는 ServiceResponse 지정자가 있으므로 특정 서비스에 대한 요청-응답 패턴을 따르는 함수들을 구분하기 위해 사용된다.
  • ServiceResponse: 함수가 RPC 서비스 응답으로 사용됨을 나타낸다. 이는 NetMulticast 및 Reliable 속성을 포함함.

 

 

Q. RPC는 Reliable하게 전달하는게 가능한데 Replication은 Reliable하게 전달할 수 있는가?

 

A. 불가능하다. RPC는 특정 시점에 발생하는 이벤트나 명령의 성격이 강하므로 실행하지 못한다면 게임 로직에 아주 중대한 영향을 줄 수 있으므로 재전송을 요청할 수 있도록 설계되었으나 리플리케이션의 경우 최신 상태를 효율적으로 동기화 하는 것이 중요하지 바뀌기 전의 이전의 값을 여러번 재전송하는건 현재 상태를 최신의 상태로 업데이트하는 것과도 맞지 않다. 프로퍼티는 위치, 체력, 애니메이션 상태 등등 엄청나게 많은 값들이므로 이것들이 유실되었을때 지난 값들을 계속해서 재전송한다면 엄청난 네트워크 과부하가 발생한다. 또한 단적인 예로 네트워크 상태가 좋지 않아 중간에 캐릭터의 위치 값이 일시적으로 유실되더라도 네트워크 상태가 복구되면 다음 프레임에 위치 정보가 갱신되므로 게임 진행에 큰 문제가 있다고 보진 않는다.