[UE5] 언리얼 엔진의 멀티플레이어 환경 알아보기

kkado·2024년 5월 29일
0

UE5

목록 보기
40/61
post-thumbnail

네트워크를 이용한 멀티플레이어 환경 구축을 시도하기 위해 공식 문서를 정독해 봤을 때 꽤 핵심이 되는 내용들이 많은 것 같아 나름대로 이해한 내용을 재정리하고자 한다.

https://dev.epicgames.com/documentation/en-us/unreal-engine/networking-overview-for-unreal-engine?application_version=5.1


1. Plan for multiplayer early

가능하다면, 만드는 게임이 멀티플레이 환경을 지원하도록 할 생각이라면 처음부터 멀티플레이어 환경을 염두에 두고 프로젝트를 시작하길 권장한다.

  • 처음부터 멀티플레이어 환경을 구축하는 것은 싱글플레이어 환경을 구축하는 것보다 큰 시간이 소요되지 않는다.
  • 멀티플레이어 환경을 염두하고 개발한 게임은 싱글플레이어 환경에서도 잘 동작한다.
  • 만약 싱글플레이어 환경의 게임이 어느정도 개발되어 있고 이를 멀티플레이어 환경으로 확장하느 것은 많은 시간과 노력이 소요될 것이다.
  • 따라서 언리얼 엔진 측에서는 절대 멀티플레이어를 지원할 생각이 없는 경우가 아니라면 항상 멀티플레이어 환경으로 개발을 시작하기를 권장한다.

2. The Client-Server Model

싱글플레이어 또는 로컬 멀티플레이어 게임은 '독립형' 게임으로 작동한다.
플레이어들은 하나의 컴퓨터에 인풋을 연결하고, 액터, 월드, UI 등 모든 것을 '직접' 조종한다.

네트워크 멀티플레이어 게임의 경우, 언리얼 엔진은 클라이언트-서버 모델을 사용한다.
즉 네트워크 내의 하나의 컴퓨터는 서버 로서 작동하고, 멀티플레이어 게임을 위한 세션을 호스트한다.
한편, 다른 모든 플레이어의 컴퓨터들은 이 서버에 클라이언트 로서 연결된다.

서버는 연결된 모든 클라이언트들에게 게임 상태 정보를 전달함으로써, 그들이 서로 상호작용할 수 있게 한다.

서버는 하나의 'authoritative' 한 (권위, 권한이 있는) 게임 상태를 가지고 있다.
다르게 말하면, 멀티플레이어 게임이 실질적으로 돌아가고 있는 곳은 바로 서버다.
클라이언트는 그들 각자가 소유하고 있는 Pawn들을 원격 조종하고, 특정한 기능을 수행하도록 procedure call을 보낸다.

그리고 서버는 클라이언트의 모니터에 시각적 요소들을 직접 스트림해주지 않는다.
대신, 서버는 정보를 복제(replicate)하여 각 클라이언트들에게 알려준다. '정보' 라고 함은, 어떤 액터가 존재하는지, 어떤 액터는 어떻게 동작하는지, 어떤 변수들이 어떤 값을 가지고 있는지 등 게임플레이와 연관된 정보들을 의미한다.
클라이언트들은 이 정보를 가지고 서버에서 일어나고 있는 것과 비슷하게 시뮬레이팅 한다.

2.1. Client-Server Gameplay Example

이해를 돕기 위해 플레이어 1, 2가 존재하는 멀티플레이 환경을 상정하고, 각 프로세스가 어떻게 진행되는지 살펴보도록 한다.

2.1.1. Local Gameplay

플레이어 1이 무기를 발사하려고 키를 입력함

  • 플레이어 1의 폰이 이 입력에 반응하여 무기를 발사한다.
  • 플레이어 1의 무기에서 발사체가 스폰되고, 소리와 시각 효과가 재생된다.

플레이어 1의 발사체가 앞으로 전진한다
플레이어 1의 발사체가 플레이어 2와 충돌한다

  • 충돌은 함수를 트리거하고, 이 함수에서는 플레이어 1의 발사체를 destroy하고, 플레이어 2에게 데미지를 입히고, 소리 및 시각 효과가 재생된다.
  • 플레이어 2의 화면에 데미지를 받은 화면 시각 효과가 재생된다.

2.1.2. Network Gameplay

플레이어 1이 무기를 발사하려고 키를 입력함

  • 플레이어 1의 로컬 폰이 이 입력을 서버 측에 있는 폰에게 중계한다.
  • 서버 측에 있는 플레이어 1의 무기에서 발사체가 스폰된다.
  • 서버는 서버에 연결돼 있는 모든 클라이언트에게 이 발사체의 '복사본' 을 만드라고 알린다.
  • 서버 측에 있는 플레이어 1의 무기는 각 클라이언트들에게 소리, 시각 효과를 재생하라고 알린다.

플레이어 1의 발사체가 앞으로 전진한다

  • 서버는 각 클라이언트들에게 이 발사체의 움직임을 복제하라고 알리고, 따라서 각 클라이언트의 화면에서도 발사체가 이동한다.

플레이어 1의 발사체가 플레이어 2와 충돌한다

  • 충돌은 서버에 있는 플레이어 1의 발사체를 파괴하는 함수를 트리거한다.
  • 서버는 자동으로 각 클라이언트들에게 발사체 1의 복사본을 파괴하라고 알린다.
  • 충돌은 소리와 시각 효과를 출력하는 함수를 트리거한다.
  • 서버 측에 있는 플레이어 2의 폰이 발사체 충돌에 의해 데미지를 입는다.
  • 서버 측에 있는 플레이어 2의 폰이 플레이어 2의 클라이언트에 데미지를 받은 화면 시각 효과를 출력하라고 알린다.

독립형 게임에서는, 모든 상호작용이 같은 기기 안의 '같은 월드' 에서 이루어진다. 그래서 문자 그대로 이해하고, 프로그래밍하기 쉽다. 예를 들어 어떤 오브젝트를 생성할 때, 모든 플레이어가 그 물체를 볼 수 있다고 가정할 수 있다.

네트워크 게임에서는, 이러한 상호작용들이 여러 다른 월드에서 이뤄진다:
하나는 서버에 존재하는 월드이고, 나머지는 각 플레이어의 클라이언트에 존재하는 월드들이다.
서버는 게임이 실질적으로 플레이되는 공간이다. 그러나 우리는 각 클라이언트들의 월드에도 똑같이 보이게끔 할 필요가 있다. 따라서 서버는 각 클라이언트들에게 선택적으로 시각 정보를 생성하라고 정보를 전달한다.

이 프로세스는 필수 게임 플레이 상호 작용(충돌, 움직임, 데미지), 시각 효과(시각 효과 및 소리) 및 개인 플레이어 정보(HUD 업데이트) 간의 구분을 만든다. 그러나 이러한 프로세스들은 완전 자동으로 이루어지지는 않는다. 우리가 어떤 정보가 어떤 기기에 복제되어야 하는지 프로그래밍으로서 정해야 할 필요가 있다.

핵심 이슈는 어떤 정보를 복제할 것인가이며, 모든 플레이어에게 일관된 경험을 제공할 수 있을지이다. 또한 복제할 정보의 양을 최소화함으로써, 네트워크 대역폭을 최소화하는 작업도 동시에 필요하다.

3. Fundamental Networking Concepts

3.1. Network Modes and Server Types

Network Mode는 멀티플레이어 세션 내에 존재하는 컴퓨터들 간의 관계를 나타낸다.
게임 인스턴스는 아래 중 하나의 모드를 갖는다.

3.1.1. Standalone

  • 게임이 서버로서 동작하고, 원격 클라이언트로부터의 연결을 허용하지 않음
  • 게임에 참여하고 있는 플레이어들은 모두 철저히 로컬 플레이어
  • 싱글플레이어 또는 로컬 멀티플레이어 게임에 사용됨
  • 서버사이드 로직과 클라이언트사이드 로직을 모두 실행함

3.1.2. Client

  • 게임이 클라이언트로서 동작하고, 네트워크 멀티플레이어 세션에 있는 서버에 연결되어 있음
  • 서버사이드 로직을 일체 동작하지 않음

3.1.3. Listen Server

  • 게임이 네트워크 멀티플레이 세션을 호스팅하는 서버로서 동작
  • 원격 클라이언트들의 연결을 허용함과 동시에 직접 연결된 로컬 플레이어도 존재함
  • 캐주얼한 협력 또는 경쟁 게임에 적합함

3.1.4. Dedicated Server

  • 게임이 네트워크 멀티플레이 세션을 호스팅하는 서버로서 동작
  • 원격 클라이언트들의 연결을 허용. 그러나 로컬 플레이어는 없음
  • 따라서 효율적인 동작을 위해 그래픽, 사운드, 입력 또는 그 외 플레이어로부터의 기능들을 폐기함
  • 더 지속적이고 안전하거나 대규모 멀티플레이어가 필요한 게임에 적합

아마 Listen Server와 Dedicated Server의 차이가 중요할 것으로 생각된다.
Listen server를 지원하는 게임은 서버를 시작하거나 참여할 서버를 검색할 수 있다.
<어몽 어스>나 <구스 구스 덕>처럼 사용자가 게임을 생성하고, 여기에 참여하는 식의 게임이 여기에 해당한다.
서버를 호스팅하는 플레이어는 서버에서 직접 플레이할 수 있다는 점에서 다소 이점이 있을 수 있다.

Dedicated Server는 전용 서버라는 그 이름 그대로 서버로서만 동작한다.
게임에 참여하는 모든 플레이어가 각기 다른 별도의 컴퓨터가 필요하고, 이들 각각과 네트워크 연결을 맺음으로써 게임에 참여할 수 있다. 모든 플레이어가 동일한 연결 조건 하에서 게임이 진행되기 때문에 공정한 환경이라 할 수 있다. 전용 서버는 그래픽 렌더링과 같은 로직을 수행하지 않기 때문에, 게임 플레이를 더 효율적으로 처리할 수 있다. 많은 수의 플레이어가 참여하는 MMO 게임, 신뢰성 있고 고성능의 온라인 슈팅 게임 등이 여기 해당한다.

3.2. Actor Replication

Replication (복제)는 네트워크 세션의 서로 다른 기기들 간에 게임 상태 정보를 재생산하는 과정이다.
복제가 성공적으로 이루어지면 서로 다른 기기 간의 게임 인스턴스가 동기화된다.

기본적으로는, 대부분의 액터는 복제 기능이 비활성화되어 있으며, 함수를 로컬에서만 실행하도록 되어 있다.
C++에서는 bReplicates 변수를 true로 만들어주거나, 블루프린트에서는 Replicates 노드를 true로 만들어 줌으로써 액터의 복제를 활성화할 수 있다.

아래는 네트워크 게임플레이에서 가장 흔히 사용되는 복제 기능들이다:

Creation and Destruction

  • 복제된 액터의 'authoritative' 버전이 서버에 생성되면, 모든 연결된 클라이언트들의 원격 프록시들에서도 자동으로 생성된다.
  • 그리고 그러한 원격 프록시들에게 정보를 복제해 준다.
  • 만약 권위 있는 액터를 파괴하면, 다른 모든 클라이언트들의 원격 프록시에서도 자동으로 파괴된다.

Movement Replication

  • 만약 권위 있는 액터가 'Replicate Movement' 가 활성화되어 있거나 C++에서 bReplicateMovement가 true라면, 자동으로 그 액터의 location, rotation, velocity가 복제된다.

Variable Replication

  • 복제되도록 지정된 모든 변수들은, 그 값이 바뀔 때마다 권위 있는 액터로부터 복제된다.

Component Replication

  • 액터 컴포넌트들은 액터의 일부분으로서 복제된다.
  • 액터 컴포넌트가 가지고 있는 변수들 중 복제되도록 지정된 변수들은 복제된다.
  • 컴포넌트에서 호출된 모든 RPC들은 액터 클래스에서 일관되게 동작한다.

Remote Procedure Calls (RPCs)

  • RPC는 네트워크 게임에서 특정 기기에 전달되는 특수한 함수이다.
  • RPC을 호출한 기기가 어디든, 그 구현은 의도한 특정 기기에서만 실행된다.
  • 서버에서만 동작하도록 할 수도 있고, 클라이언트에서 동작하게 할 수도 있고, NetMulticast (세션에 접속해 있는 서버를 포함한 모든 기기에서 동작하도록) 로 동작하도록 지정할 수 있다.

생성, 파괴, 이동 등과 같은 흔한 사례는 자동으로 핸들링될 수 있지만 다른 게임플레이 기능들은 자동으로 복제되지 않는다.

UPROPERTY, UFUNCTION 프로퍼티를 통해 어떤 함수, 어떤 변수가 복제되어야 하는지 정확히 지정해줘야 한다.

그러나 아래에 해당하는 요소들은 복제되지 않는다.

  • 스켈레탈 메시, 스태틱 메시 컴포넌트
  • 머티리얼
  • 애니메이션 블루프린트
  • 파티클 시스템
  • 사운드 이미터
  • 물리 오브젝트

이것들은 모든 클라이언트들에서 개별적으로 동작한다. 이러한 시각적 요소들을 유발하는 변수가 바뀌면 이 변수는 복제되므로 결과적으로 거의 동일한 방식으로 시뮬레이트 될 수 있다.

3.2.1. Network Role and Authority

액터의 Network Role 은 이 액터를 런타임에서 어느 기기가 컨트롤 할 것인가를 결정한다.
권한 있는 액터는 그 액터의 상태를 컨트롤하는 것으로 간주되고, 세션 상의 다른 기기들에게 정보를 복제할 것이다.

원격 프록시 (remote proxy)는 그 액터의 원격 기기 상의 복제본이다. 그리고 프록시는 원본 액터 (권한 있는 액터)로부터 정보를 복제받는다. 이것은 다음과 같은 값을 가질 수 있는 Local roleRemote role로 추적될 수 있다.

None
액터는 네트워크 게임에서 아무 역할이 없고, 복제를 진행하지 않는다.

Authority
액터는 권한이 있고, 이 액터의 원격 프록시들에게 정보를 복제해 전달한다.

Simulated Proxy
이 액터는 다른 기기에 권한이 있는 액터로부터 전적으로 컨트롤되는 원격 프록시이다. 발사체, 상호 작용 가능한 오브젝트 등 게임플레의 대부분의 액터는 다른 원격 클라이언트들에게 Simulated Proxy로 나타난다.

Autonomous Proxy
이 액터는 '특정 함수를 로컬에서 실행할 수 있는' 원격 프록시이다. 그러는 한편, 동시에 다른 권한 있는 액터로부터 정정 받는다.


언리얼 엔진에서 디폴트로 사용되는 모델은 server-authoritative 방식이다. 그말인즉 서버는 게임 상태를 관리할 권한을 가지고 있으며, 정보들은 모두 서버 -> 클라이언트의 방향으로 복제된다.

서버 쪽에 존재하는 액터가 로컬 권한을 가지고, 원격 클라이언트 쪽에 존재하는 액터는 Simulated 또는 Autonomous 프록시의 local role을 갖는 것이 일반적이다.

3.2.2. Client Ownership

네트워크 게임 안에서 폰들은 특정한 클라이언트의 기기 상에서 PlayerController 에 의해 소유된다. 폰이 client-only 함수를 실행하면 그 플레이어의 기기로만 전달된다.

Owner 변수가 특정한 폰으로 설정된 액터는 그 폰을 소유한 클라이언트에 속한다. C++의 IsLocallyControlled() 함수 또는 블루프린트의 Is Locally Controlled 노드를 통해 이 폰이 이 클라이언트에 속하는지 여부를 결정할 수 있다.

커스텀 폰 클래스의 생성자에 IsLocallyControlled 를 사용하는 것은 지양해야 한다. 생성되는 과정에서 어떠한 컨트롤러도 할당되지 않을 가능성이 있다.

3.2.3. Relevance and Priority

Relevance(관련성) 은 특정 액터를 복제하는 것이 게임 내에서 유의미한지 를 결정할 때 사용한다.
'관련이 없다' 고 간주되는 액터의 경우 후순위로 밀려나게 된다.

이것은 대역폭을 절약해 주며, 따라서 '관련 있는' 액터들을 효율적으로 복제할 수 있다.

만약 어떤 액터가 아무 플레이어에게도 소유되지 않고, 물리적으로 가까이 있는 액터가 없을 경우, 이 액터는 '관련 없는' 액터로 간주되고, 복제되지 않는다. 이러한 액터들은 서버 상에는 계속해서 존재하고, 게임 상태에도 영향을 미칠 수 있지만, 클라이언트가 접근하지 않는 한 구태여 그 클라이언트에게 복제하지 않음으로써 대역폭을 절약한다.

IsNetRelevantFor 함수를 오버라이딩함으로써 관련성을 직접 설정할 수 있고, NetCullDistanceSquared 프로퍼티를 통해 클라이언트가 '접근' 했다고 간주하는 반경을 설정할 수 있다.

때에 따라서, 한 프레임만에 관련 있는 모든 액터들을 복제하기에 대역폭이 모자라는 경우가 발생할 수 있다. 이런 상황을 위해 액터들은 Priority (우선순위)를 가지고 있다. 기본적으로 폰과 PlayerController는 3.0NetPriority 를 가지고 있고 이는 가장 높은 우선순위이다. 기본적인 액터의 경우 1.0의 값을 가지고 있다. 그리고 액터가 복제되지 않은 지 오랜 시간이 지나면 starvation이 발생할 수 있어 priority 값이 점점 올라간다.


3.3. Variable Replication

C++에서 변수에 Replicated 또는 ReplicateUsing UPROPERTY를 명시함으로써 변수를 복제되게끔 할 수 있다.

권한 있는 액터 상에서 변수의 값이 변경될 때마다 그 정보가 원격 프록시들에게 전달된다.

3.3.1. RepNotifies

RepNotify 라는 함수를 지정할 수도 있는데, 액터가 복제된 정보를 '성공적으로 잘 받았으면' 그에 대한 응답으로 호출되는 함수이다. 변수가 업데이트되면 로컬에서만 트리거한다. 즉 권한 있는 액터의 변수 변경에 대한 응답으로, 낮은 오버헤드로 게임 플레이 로직을 트리거할 수 있다.


3.4. Remote Procedure Calls (RPCs)

replicated functions 이라고도 한다. 어떤 기기에서도 호출될 수 있고, 특정한 기기에만 영향이 가도록 implementation을 보낼 수 있다.

RPC에는 3가지 타입이 있다.

Server
(게임을 호스팅하고 있는) 서버에서만 호출된다.

Client
함수가 속한 액터를 소유하고 있는 클라이언트에서만 호출된다.
만약 액터가 연결되어 있지 않다면 실행되지 않는다.

NetMulticast
서버 자신을 포함하여 서버에 연결되어 있는 모든 클라이언트들에서 호출된다.

블루프린트에서는 디테일 패널의 Replicates 선택 상자를 통해 설정할 수 있고, C++에서는 UPROPERTY에 Server, Client, NetMulticast 를 명시함으로써 설정할 수 있다. 그리고 이러한 함수들의 구현은 접미사 _Implementation 을 붙여서 구현한다.

//Declaration of Server RPC MyFunction.
UFUNCTION(Server, Reliable, WithValidation)
void MyFunction(int myInt);
//Implementation of Server RPC MyFunction.
void AExampleClass::MyFunction_Implementation(int myInt)
{
	//Gameplay code goes here.
}

3.4.1. Reliability

RPC를 지정할 때는 reliable 또는 unreliable을 꼭 함께 지정해 주어야 한다. reliable과 unreliable의 차이는 마치 네트워크 통신에서의 TCP, UDP와 매우 흡사하다.

먼저 unreliable RPC 경우 의도한 목적지에 도착하는 것이 보장되지 않는다. 그러나 이들은 빠르게 전달될 수 있다. 매우 자주 호출되어서 하나쯤 누락되는 것이 게임플레이에 큰 지장이 없는 기능에 주로 사용한다. 예컨대 캐릭터의 이동은 매우 자주 호출되어 위치를 갱신해 줘야 하므로 빠른 전송이 요구되고, 다음 프레임에서 다시 전송받으면 되므로 하나하나의 중요도가 크지 않다. 이러한 기능에 적합하다.

다음으로는 reliable RPC이다. RPC는 의도한 목적지에 도착하는 것이 보장된다. 큐에 삽입되는 방법을 통해 구현되는데, 이 함수가 목적지까지 성공적으로 도달할 때까지 큐에 남아 있는다. 다만 unreliable보다 속도는 느리다. 게임플레이 내에서 실행 빈도가 비교적 많지 않은 대신 하나하나의 성공적 실행이 매우 중요한 충돌, 공격, 액터 스폰 등의 기능에 적합하다.

3.4.2. Validation

WithValidation 을 명시해 줌으로써, 이 함수의 구현에 더해서 추가적으로 '그 함수 호출에 들어온 데이터의 유효성을 검증하는 함수를 추가할 수 있다. 검증 함수는 함수 그 자체와 같은 시그니처를 갖고 있으며, 리턴 값은 불리언이다. true 일때만 그 함수의 구현부 (_Implementation) 을 실행하도록 허가한다.


마무리

  • 언리얼 엔진의 멀티플레이어 환경은 서버-클라이언트 구조를 기반으로 하고 있다는 점
  • 서버 환경에서만 동작하고 있는 게임플레이, state 정보들을 복제받아 똑같은 환경으로 동기화하는 클라이언트의 구조
  • Listen server와 Dedicated server의 차이점
  • 변수와 함수의 복제 및 RPC, 관련성, 우선순위, 신뢰성, 검증

멀티플레이어 환경의 다양한 요소들을 알아보았고, 앞으로도 기반 지식으로서 매우 중요하게 다뤄질 내용들일 것 같다.

https://dev.epicgames.com/documentation/en-us/unreal-engine/networking-overview-for-unreal-engine?application_version=5.1

profile
울면안돼 쫄면안돼 냉면됩니다

0개의 댓글