네트워크를 이용한 멀티플레이어 환경 구축을 시도하기 위해 공식 문서를 정독해 봤을 때 꽤 핵심이 되는 내용들이 많은 것 같아 나름대로 이해한 내용을 재정리하고자 한다.
가능하다면, 만드는 게임이 멀티플레이 환경을 지원하도록 할 생각이라면 처음부터 멀티플레이어 환경을 염두에 두고 프로젝트를 시작하길 권장한다.
싱글플레이어 또는 로컬 멀티플레이어 게임은 '독립형' 게임으로 작동한다.
플레이어들은 하나의 컴퓨터에 인풋을 연결하고, 액터, 월드, UI 등 모든 것을 '직접' 조종한다.
네트워크 멀티플레이어 게임의 경우, 언리얼 엔진은 클라이언트-서버
모델을 사용한다.
즉 네트워크 내의 하나의 컴퓨터는 서버
로서 작동하고, 멀티플레이어 게임을 위한 세션을 호스트한다.
한편, 다른 모든 플레이어의 컴퓨터들은 이 서버에 클라이언트
로서 연결된다.
서버는 연결된 모든 클라이언트들에게 게임 상태 정보를 전달함으로써, 그들이 서로 상호작용할 수 있게 한다.
서버는 하나의 'authoritative'
한 (권위, 권한이 있는) 게임 상태를 가지고 있다.
다르게 말하면, 멀티플레이어 게임이 실질적으로 돌아가고 있는 곳은 바로 서버다.
클라이언트는 그들 각자가 소유하고 있는 Pawn들을 원격 조종하고, 특정한 기능을 수행하도록 procedure call을 보낸다.
그리고 서버는 클라이언트의 모니터에 시각적 요소들을 직접 스트림해주지 않는다.
대신, 서버는 정보를 복제(replicate)
하여 각 클라이언트들에게 알려준다. '정보' 라고 함은, 어떤 액터가 존재하는지, 어떤 액터는 어떻게 동작하는지, 어떤 변수들이 어떤 값을 가지고 있는지 등 게임플레이와 연관된 정보들을 의미한다.
클라이언트들은 이 정보를 가지고 서버에서 일어나고 있는 것과 비슷하게 시뮬레이팅 한다.
이해를 돕기 위해 플레이어 1, 2가 존재하는 멀티플레이 환경을 상정하고, 각 프로세스가 어떻게 진행되는지 살펴보도록 한다.
플레이어 1이 무기를 발사하려고 키를 입력함
플레이어 1의 발사체가 앞으로 전진한다
플레이어 1의 발사체가 플레이어 2와 충돌한다
플레이어 1이 무기를 발사하려고 키를 입력함
서버 측에 있는 플레이어 1의 무기
에서 발사체가 스폰된다.서버 측에 있는 플레이어 1의 무기
는 각 클라이언트들에게 소리, 시각 효과를 재생하라고 알린다.플레이어 1의 발사체가 앞으로 전진한다
플레이어 1의 발사체가 플레이어 2와 충돌한다
독립형 게임에서는, 모든 상호작용이 같은 기기 안의 '같은 월드' 에서 이루어진다. 그래서 문자 그대로 이해하고, 프로그래밍하기 쉽다. 예를 들어 어떤 오브젝트를 생성할 때, 모든 플레이어가 그 물체를 볼 수 있다고 가정할 수 있다.
네트워크 게임에서는, 이러한 상호작용들이 여러 다른 월드에서 이뤄진다:
하나는 서버에 존재하는 월드이고, 나머지는 각 플레이어의 클라이언트에 존재하는 월드들이다.
서버는 게임이 실질적으로 플레이되는 공간이다. 그러나 우리는 각 클라이언트들의 월드에도 똑같이 보이게끔 할 필요가 있다. 따라서 서버는 각 클라이언트들에게 선택적으로 시각 정보를 생성하라고 정보를 전달한다.
이 프로세스는 필수 게임 플레이 상호 작용(충돌, 움직임, 데미지), 시각 효과(시각 효과 및 소리) 및 개인 플레이어 정보(HUD 업데이트) 간의 구분을 만든다. 그러나 이러한 프로세스들은 완전 자동으로 이루어지지는 않는다. 우리가 어떤 정보가 어떤 기기에 복제되어야 하는지 프로그래밍으로서 정해야 할 필요가 있다.
핵심 이슈는 어떤 정보를 복제할 것인가이며, 모든 플레이어에게 일관된 경험을 제공할 수 있을지이다. 또한 복제할 정보의 양을 최소화함으로써, 네트워크 대역폭을 최소화하는 작업도 동시에 필요하다.
Network Mode
는 멀티플레이어 세션 내에 존재하는 컴퓨터들 간의 관계를 나타낸다.
게임 인스턴스는 아래 중 하나의 모드를 갖는다.
아마 Listen Server와 Dedicated Server의 차이가 중요할 것으로 생각된다.
Listen server를 지원하는 게임은 서버를 시작하거나 참여할 서버를 검색할 수 있다.
<어몽 어스>나 <구스 구스 덕>처럼 사용자가 게임을 생성하고, 여기에 참여하는 식의 게임이 여기에 해당한다.
서버를 호스팅하는 플레이어는 서버에서 직접 플레이할 수 있다는 점에서 다소 이점이 있을 수 있다.
Dedicated Server는 전용 서버라는 그 이름 그대로 서버로서만 동작한다.
게임에 참여하는 모든 플레이어가 각기 다른 별도의 컴퓨터가 필요하고, 이들 각각과 네트워크 연결을 맺음으로써 게임에 참여할 수 있다. 모든 플레이어가 동일한 연결 조건 하에서 게임이 진행되기 때문에 공정한 환경이라 할 수 있다. 전용 서버는 그래픽 렌더링과 같은 로직을 수행하지 않기 때문에, 게임 플레이를 더 효율적으로 처리할 수 있다. 많은 수의 플레이어가 참여하는 MMO 게임, 신뢰성 있고 고성능의 온라인 슈팅 게임 등이 여기 해당한다.
Replication
(복제)는 네트워크 세션의 서로 다른 기기들 간에 게임 상태 정보를 재생산하는 과정이다.
복제가 성공적으로 이루어지면 서로 다른 기기 간의 게임 인스턴스가 동기화된다.
기본적으로는, 대부분의 액터는 복제 기능이 비활성화되어 있으며, 함수를 로컬에서만 실행하도록 되어 있다.
C++에서는 bReplicates
변수를 true로 만들어주거나, 블루프린트에서는 Replicates
노드를 true로 만들어 줌으로써 액터의 복제를 활성화할 수 있다.
아래는 네트워크 게임플레이에서 가장 흔히 사용되는 복제 기능들이다:
Creation and Destruction
Movement Replication
bReplicateMovement
가 true라면, 자동으로 그 액터의 location, rotation, velocity가 복제된다.Variable Replication
Component Replication
Remote Procedure Calls (RPCs)
NetMulticast
(세션에 접속해 있는 서버를 포함한 모든 기기에서 동작하도록) 로 동작하도록 지정할 수 있다.생성, 파괴, 이동 등과 같은 흔한 사례는 자동으로 핸들링될 수 있지만 다른 게임플레이 기능들은 자동으로 복제되지 않는다.
UPROPERTY
, UFUNCTION
프로퍼티를 통해 어떤 함수, 어떤 변수가 복제되어야 하는지 정확히 지정해줘야 한다.
그러나 아래에 해당하는 요소들은 복제되지 않는다.
이것들은 모든 클라이언트들에서 개별적으로 동작한다. 이러한 시각적 요소들을 유발하는 변수가 바뀌면 이 변수는 복제되므로 결과적으로 거의 동일한 방식으로 시뮬레이트 될 수 있다.
액터의 Network Role
은 이 액터를 런타임에서 어느 기기가 컨트롤 할 것인가를 결정한다.
권한 있는 액터는 그 액터의 상태를 컨트롤하는 것으로 간주되고, 세션 상의 다른 기기들에게 정보를 복제할 것이다.
원격 프록시
(remote proxy)는 그 액터의 원격 기기 상의 복제본이다. 그리고 프록시는 원본 액터 (권한 있는 액터)로부터 정보를 복제받는다. 이것은 다음과 같은 값을 가질 수 있는 Local role
과 Remote role
로 추적될 수 있다.
None
액터는 네트워크 게임에서 아무 역할이 없고, 복제를 진행하지 않는다.
Authority
액터는 권한이 있고, 이 액터의 원격 프록시들에게 정보를 복제해 전달한다.
Simulated Proxy
이 액터는 다른 기기에 권한이 있는 액터로부터 전적으로 컨트롤되는 원격 프록시이다. 발사체, 상호 작용 가능한 오브젝트 등 게임플레의 대부분의 액터는 다른 원격 클라이언트들에게 Simulated Proxy로 나타난다.
Autonomous Proxy
이 액터는 '특정 함수를 로컬에서 실행할 수 있는' 원격 프록시이다. 그러는 한편, 동시에 다른 권한 있는 액터로부터 정정 받는다.
언리얼 엔진에서 디폴트로 사용되는 모델은 server-authoritative
방식이다. 그말인즉 서버는 게임 상태를 관리할 권한을 가지고 있으며, 정보들은 모두 서버 -> 클라이언트의 방향으로 복제된다.
서버 쪽에 존재하는 액터가 로컬 권한을 가지고, 원격 클라이언트 쪽에 존재하는 액터는 Simulated 또는 Autonomous 프록시의 local role을 갖는 것이 일반적이다.
네트워크 게임 안에서 폰들은 특정한 클라이언트의 기기 상에서 PlayerController
에 의해 소유된다. 폰이 client-only 함수를 실행하면 그 플레이어의 기기로만 전달된다.
Owner
변수가 특정한 폰으로 설정된 액터는 그 폰을 소유한 클라이언트에 속한다. C++의 IsLocallyControlled()
함수 또는 블루프린트의 Is Locally Controlled
노드를 통해 이 폰이 이 클라이언트에 속하는지 여부를 결정할 수 있다.
커스텀 폰 클래스의 생성자에 IsLocallyControlled
를 사용하는 것은 지양해야 한다. 생성되는 과정에서 어떠한 컨트롤러도 할당되지 않을 가능성이 있다.
Relevance(관련성) 은 특정 액터를 복제하는 것이 게임 내에서 유의미한지
를 결정할 때 사용한다.
'관련이 없다' 고 간주되는 액터의 경우 후순위로 밀려나게 된다.
이것은 대역폭을 절약해 주며, 따라서 '관련 있는' 액터들을 효율적으로 복제할 수 있다.
만약 어떤 액터가 아무 플레이어에게도 소유되지 않고, 물리적으로 가까이 있는 액터가 없을 경우, 이 액터는 '관련 없는' 액터로 간주되고, 복제되지 않는다. 이러한 액터들은 서버 상에는 계속해서 존재하고, 게임 상태에도 영향을 미칠 수 있지만, 클라이언트가 접근하지 않는 한 구태여 그 클라이언트에게 복제하지 않음으로써 대역폭을 절약한다.
IsNetRelevantFor
함수를 오버라이딩함으로써 관련성을 직접 설정할 수 있고, NetCullDistanceSquared
프로퍼티를 통해 클라이언트가 '접근' 했다고 간주하는 반경을 설정할 수 있다.
때에 따라서, 한 프레임만에 관련 있는 모든 액터들을 복제하기에 대역폭이 모자라는 경우가 발생할 수 있다. 이런 상황을 위해 액터들은 Priority
(우선순위)를 가지고 있다. 기본적으로 폰과 PlayerController는 3.0
의 NetPriority
를 가지고 있고 이는 가장 높은 우선순위이다. 기본적인 액터의 경우 1.0의 값을 가지고 있다. 그리고 액터가 복제되지 않은 지 오랜 시간이 지나면 starvation이 발생할 수 있어 priority 값이 점점 올라간다.
C++에서 변수에 Replicated
또는 ReplicateUsing
UPROPERTY를 명시함으로써 변수를 복제되게끔 할 수 있다.
권한 있는 액터 상에서 변수의 값이 변경될 때마다 그 정보가 원격 프록시들에게 전달된다.
RepNotify
라는 함수를 지정할 수도 있는데, 액터가 복제된 정보를 '성공적으로 잘 받았으면' 그에 대한 응답으로 호출되는 함수이다. 변수가 업데이트되면 로컬에서만 트리거한다. 즉 권한 있는 액터의 변수 변경에 대한 응답으로, 낮은 오버헤드로 게임 플레이 로직을 트리거할 수 있다.
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.
}
RPC를 지정할 때는 reliable
또는 unreliable
을 꼭 함께 지정해 주어야 한다. reliable과 unreliable의 차이는 마치 네트워크 통신에서의 TCP, UDP와 매우 흡사하다.
먼저 unreliable RPC
경우 의도한 목적지에 도착하는 것이 보장되지 않는다.
그러나 이들은 빠르게 전달될 수 있다. 매우 자주 호출되어서 하나쯤 누락되는 것이 게임플레이에 큰 지장이 없는 기능에 주로 사용한다. 예컨대 캐릭터의 이동은 매우 자주 호출되어 위치를 갱신해 줘야 하므로 빠른 전송이 요구되고, 다음 프레임에서 다시 전송받으면 되므로 하나하나의 중요도가 크지 않다. 이러한 기능에 적합하다.
다음으로는 reliable RPC
이다. RPC는 의도한 목적지에 도착하는 것이 보장된다.
큐에 삽입되는 방법을 통해 구현되는데, 이 함수가 목적지까지 성공적으로 도달할 때까지 큐에 남아 있는다. 다만 unreliable보다 속도는 느리다. 게임플레이 내에서 실행 빈도가 비교적 많지 않은 대신 하나하나의 성공적 실행이 매우 중요한 충돌, 공격, 액터 스폰 등의 기능에 적합하다.
WithValidation
을 명시해 줌으로써, 이 함수의 구현에 더해서 추가적으로 '그 함수 호출에 들어온 데이터의 유효성을 검증하는 함수를 추가할 수 있다. 검증 함수는 함수 그 자체와 같은 시그니처를 갖고 있으며, 리턴 값은 불리언이다. true
일때만 그 함수의 구현부 (_Implementation
) 을 실행하도록 허가한다.
멀티플레이어 환경의 다양한 요소들을 알아보았고, 앞으로도 기반 지식으로서 매우 중요하게 다뤄질 내용들일 것 같다.