3냥 집사이면서 게임 개발자입니다.
언리얼 엔진 멀티플레이 Replication System 에 대하여 본문
언리얼 엔진의 멀티 플레이 시스템을 독학하고 있기에 틀린 내용이 있을 수 있습니다.
이 포스팅은 멀티 플레이에 대해 공부하던 중 좋은 영상을 발견하고 영어로 작성된 내용을 언제든 찾아 볼 수 있게 한글로 정리하고자 작성합니다.
https://www.youtube.com/watch?v=JOJP0CvpB8w&t=952s
멀티플레이란 2명 이상의 유저가 같은 환경에서 플레이하는 것,
언리얼 엔진은 태생이 FPS 게임을 만들기 위한 엔진으로 UDP 기반의 네트워킹 시스템이 제공된다.
Net Mode : World 의 속성
3가지의 간단한 질문을 바탕으로 넷모드를 구별할 수 있다.
1. 플레이가 가능한 환경인가?
2. 권한이 있는가? (서버인가?)
3. 외부의 접속에 열려 있는가?
이 질문으로 4가지의 넷모드로 구분할 수 있다.
1. Stand Alone
2. Dedicated Server
3. Listen Server
4. Client
1. Stand Alone (싱글 플레이)
- 게임 인스턴스가 로컬에 맵을 로드한 경우, Net Mode는 독립 실행형(Stand Alone)이 된다.
단일 게임 인스턴스는 서버이자 클라이언트이나 단일 플레이어 구성이라 클라이언트가 접속할 수 없다.
2. Listen Server
- ?Listen 옵션을 추가해 청취 서버(Listen Server)로 실행할 수 있다.
기본적으로 Stand Alone 과 동일하게 실행되나, 게임의 다른 인스턴스가 클라이언트로 연결될 수 있다.
즉, 싱글 플레이 환경에서 클라이언트 접속이 가능해진다.
호스트가 게임 플레이 연산과 서버 연산을 모두 해야되는 상황이다.
3. Dedicated Server
전용 서버. Local Player나 뷰포트가 없는 게임의 인스턴스다.
플레이어가 클라이언트로 연결할 수 있는 서버 전용 콘솔 응용 프로그램일 뿐이다.
실제로 구축 후 구동할 때 서버.exe를 따로 실행 후 각 클라이언트들이 접속하는 방식
4. Client
서버가 아닌 유일한 모드이다.
로컬 플레이어가 게임을 플레이하나, 서버 요청에 따라 업데이트된다.
따라서 언리얼 게임은 세 가지 다른 네트워킹 시나리오에서 실행될 수 있다.
싱글 플레이
- 하나의 Game Instance 가 있고, Stand Alone NetMode에서 실행된다.
멀티 플레이
- 멀티 플레이의 경우 각각 자체의 Game Instance를 가지고, 자체 월드 사본을 가진 프로세스들이 있다.
그 프로세스 중 하나가 서버(리슨 or 데디케이트)이고, 나머지는 클라이언트가 된다.
서버의 동작 방식에 따라 2가지로 구분된다.
Listen Server
플레이가 가능하면서 원본 월드를 가진 프로세스가 리슨 서버로 호스트를 가진 Session 방식으로 볼 수 있다.
게임 플레이 로직 연산과 렌더링 연산, 서버 연산까지 호스트의 PC에서 이루어진다.
Dedicated Server
원본 월드를 가진 프로세스가 플레이는 불가능한 단순 응용 프로그램으로 동작하는 것이 데디 서버다.
게임 플레이 로직 연산이 아닌, 서버 연산만을 목표로하는 프로세스다.
Replication System Basics
각자의 PC에서 플레이할 때 사용되는 아이템, 캐릭터 등의 데이터를 복제(Replication)을 통해 동기화한다.
멀티 플레이 게임이 실행되면 언리얼 엔진의 Replication 시스템이 동작해, 각 클라이언트의 게임 인스턴스를 동기화합니다.
이를 실현하기 위해 Replication 시스템은 Net Driver, UNetConnenction, UChannel 이라는 3가지 클래스에 의존합니다.
예시로 데디 서버에서 2개의 클라이언트가 있다고 가정합니다.
이 3개의 프로세스는 각자의 고유한 엔진객체를 갖습니다.
서버를 부팅하면 NetDriver가 생성되고 원격 프로세스의 메세지를 수신하기 시작합니다.
클라이언트를 부팅하면 클라이언트 만의 NetDriver가 생성되고, 이것이 서버에 연결 요청(Init Connection())을 보냅니다.
서버와 클라이언트의 NetDriver가 접촉하면 각 NetDriver 내에 NetConnection이 설정됩니다.
서버에는 연결된 원격 플레이어마다 하나의 NetConnection이 있습니다.
각 클라이언트는 서버와의 연결을 나타내는 단일 NetConnection을 갖습니다.
모든 NetConnection 내에는 다양한 채널이 있습니다. 일반적으로 연결에는 ControlChannel, VoiceChannel이 있고,
각 연결에는 일련의 ActorChannel이 있는데, 현재 해당 커넥션을 통해 Replicate되고 있는 각 액터에 대해 하나씩 있습니다.
이는 Replication System의 주요 사실은 복제는 레벨의 액터에서 발생한다는 것입니다.
네트워크 상에서 액터가 동기화 상태를 유지해야 하는 경우, 해당 액터를 복제 대상으로 구성합니다.
복제에 적합한 액터가 특정 플레이어와 관련이 있다고 간주되면,
서버는 해당 플레이어의 NetConnection에서 ActorChannel을 열고,
서버와 클라이언트는 해당 채널을 사용해 해당 액터에 대한 정보를 교환합니다.
Actor Replication
액터가 클라이언트에 복제되는 경우, 결과적으로 세 가지 중요한 일이 발생할 수 있습니다.
1. Life Time
액터의 수명은 서버와 클라이언트 사이에서 동기화됩니다.
서버가 복제된 액터를 생성하면 클라이언트에 알림이 전달되어 자체 사본을 생성할 수 있습니다.
서버에서 액터가 파괴되면 클라이언트에서도 자동으로 파괴됩니다.
2. Property Replication
복제를 위해 플래그가 지정된 속성이 액터에 있는 경우, 해당 속성이 서버에서 변경되면 새 값이 클라이언트로 전파됩니다.
3. RPCs(Remote Procedure Call) 원격 프로시저 호출
함수를 멀티 캐스트 RPC로 지정한 경우, 서버에서 해당 함수를 호출하면 서버는 해당 액터가 현재 복제되고 있는 모든 클라이언트에 메세지를 보내 클라이언트가 해당 복사본에서 해당 함수를 호출해야 함을 나타냅니다.
액터 또한 클라이언트 및 서버 RPC를 선언해 서버와 액터를 소유한 단일 클라이언트 간에 메세지를 주고받을 수 있습니다.
OwnerShip
소유권은 Actor 복제의 또 다른 중요한 개념입니다.
각 액터는 다른 액터를 소유자로 지정할 수 있습니다. 일반적으로 스폰 시 Owner를 설정하지만,
런타임에 SetOwner를 호출할 수도 있습니다.
Player Controller 클래스는 소유권과 관련해 특별한 중요성을 갖습니다.
기본적으로 각 NetConnection은 플레이어를 나타내며 플레이어가 게임에 완전히 로그인하면 해당 플레이어와
연관된 Player Controller 액터를 갖게 됩니다.
서버 관점에서 볼 때, Connection은 해당 Player Controller를 소유하며, 확장하여 소유권을 PlayerController 까지 추적할 수 있는 모든 액터를 소유합니다.
플레이어 컨트롤러는 자신이 소유하고 있는 폰을 자동으로 소유합니다.
그럼 각 플레이어 컨트롤러가 폰을 갖고 있고, 각 폰이 자신이 소유한 Weapon액터를 생성했다고 가정합니다.
서버는 이러한 무기 액터 중 하나를 보고, 소유자 레퍼런스 체인을 따라 (Weapon->GetNetOwningPlayer())
플레이어 컨트롤러로 돌아가서 해당 무기 액터가 특정 클라이언트 연결에 속한다는 것을 알아낼 수 있습니다.
Enabling Actor Replication
액터의 복제를 구성하는 방법
액터가 복제 대상으로 고려되려면 생성자에서 bReplicates 플래그를 true 로 설정하는 것이다.
BP의 경우 Replicates를 체크한다.
런타임에도 설정이 가능하다.
서버는 해당 액터를 해당 클라이언트에 복제하기 위해 주어진 NetConnection 내에서 ActorChannel을 열 수 있다.
Relevancy
액터의 Relevancy(관련성)은 어떤 연결이 언제 발생할지를 결정한다.
액터가 복제에 적합한 경우, 서버의 NetDriver는 때때로 해당 액터를 각 클라이언트 연결과 비교하여
해당 클라이언트와 관련이 있는지 확인한다.
일부 액터는 bAlwaysRelevant 다.
즉, 복제에 적합한 한 서버는 항상 모든 클라이언트에 이들을 복제한다.
예를 들어, GameState와 PlayerState 액터는 항상 관련이 있다.
Ownership은 Relevancy에서 중요한 역할을 한다.
특정 플레이어가 소유하거나 특정 플레이어가 주도한 액터는 항상 클라이언트와 관련이 있는 것으로 간주된다.
플레이어 컨트롤러와 같은 일부 액터는 소유자에게만 관련이 있도록 구성되어 있으므로
소유하지 않은 클라이언트에게 복제되지 않는다.
소유자로부터 관련성을 상속하도록 액터를 구성할 수도 있다.
이러한 특수 플래그가 설정되지 않고 해당 클라이언트가 Actor를 소유하지 않은 경우
기본 동작은 GetNetOwningPlayer() => nullptr 과 같다.
액터가 숨겨져 있고 루트 컴포넌트의 충돌이 비활성화된 경우 관련이 없는 것으로 간주한다.
그렇지 않으면 관련성은 클라이언트 연결에 해당하는 플레이어와의 거리를 기준으로 한다.
액터와 플레이어 간의 제곱 거리의 값이 NetCullDistanceSquared 보다 작으면 액터가 해당 플레이어와 관련이 있다.
IsNetRelevantFor(P0) => true
이런 규칙은 하드코딩 되어 있지 않다. 모든 액터 클래스의 IsNetRelevantFor 함수를 재정의해
사용자 정의 규칙을 제공할 수 있다.
Update Frequency & Priority
액터가 복제되면 업데이트 빈도와 우선 순위에 따라 서버가 해당 액터와 관련된 클라이언트에게
업데이트를 보내는 빈도가 결정된다.
NetUpdateFrequency 를 설정하면 서버가 1초에 몇 번이나 액터를 확인하고,
변경사항이 있으면 클라이언트에 새 데이터를 보낼지 결정합니다.
실제 네트워크는 지연 시간이 매우 가변적이고 대역폭이 빠르게 제한 요소가 될 수 있으므로
액터가 초당 60회 업데이트하도록 설정되어 있어도 다른 쪽에서 완벽하게 원활한 결과를 볼 수는 없다.
서버의 NetDriver는 대역폭 포화를 완화하기 위해 간단한 부하 분산을 사용한다.
NetDriver는 언제든지 작업할 수 있는 한정된 양의 대역폭을 가지고 있으므로
우선순위에 따라 관련 액터를 정렬한 다음, 사용 가능한 대역폭을 모두 사용할 때까지 네트워크 업데이트를 실행한다.
플레이어와 가까운 액터는 더 높은 우선순위를 가지며, 한동안 업데이트되지 않은 액터도 더 높은 우선순위를 가지므로,
결국 모든 액터가 우선순위 목록의 앞으로 올라가게 된다.
Actor의 NetPriority 속성을 설정하면 해당 가중치에 추가적인 크기가 적용된다.
예를 들어, 중요한 Actor를 중요하지 않은 액터보다 5배 더 자주 업데이트 하도록 지정할 수 있다.
RPCs in Detail
주기적이고 대역폭이 제한된 네트워크 업데이트 프로세스는 대부분 Property Replication에 적용된다.
네트워크로 즉시 전송하고 싶은 높은 우선순위 메세지가 있다면 RPCs 에 적용된다.
모든 UFUNCTION을 클라이언트, 서버 또는 NetMulticast로 지정해 RPC로 만들 수 있다.
서버에서 클라이언트 RPC를 호출하면 소유 클라이언트에서 실행된다.
소유 클라이언트에서 서버 RPC를 호출하면 서버에서 실행된다.
서버에서 Multicast RPC를 호출하면 해당 서버에서 실행되고, 그다음 모든 클라이언트에서 실행된다.
서버 및 클라이언트 RPC와 달리 Multicast RPC의 경우 Relevancy가 중요한 요소다.
소유하지 않은 클라이언트는 해당 액터에 대한 오픈 채널이 없을 수 있기 때문이다.
소유하지 않은 클라이언트는 RPC를 수신하지 못한다.
즉, 지속적인 상태 변경 사항을 클라이언트에 복제하기 위해 Multicast RPC에만 의존해서는 안된다.
RPC는 Reliable, UnReliable로 선언할 수 있다.
UnReliable RPC는 대역폭이 포화 상태일 때 삭제될 수 있으며,
도착이 보장되지 않으므로, 순서대로 도착할 것도 보장되지 않는다. 순서가 중요한 경우 Reliable 힌트를 사용해야한다.
Reliable RPC는 도착이 보장되며, 단일 액터 내에서 신뢰할 수 있는 RPC는 호출된 순서대로 도착이 보장된다.
게임 플레이에 함수 호출이 중요한 경우 이러한 Reliable이 필요하지만, 과도하게 사용하면 대역폭 포화가 심화되고,
패킷 손실이 발생하는 경우 병목 현상이 발생할 수 있다.
C++에서는 함수의 실제 본문을 "_Implementation" 접미사로 정의해야 한다.
이는 실제로 원격 프로세스에서 실행되는 함수인 반면, 로컬에서 호출하는 함수는 네트워크를 통해
적절한 메세지를 전송하는 자동 생성된 Thunk이다.
서버 RPC도 WithValidation으로 선언할 수 있으며, 이 경우 모든 동일한 인수를 사용하고
해당 값이 신뢰할 수 있는지 여부를 나타내는 bool값을 반환하는 해당 "_Validate"함수를 구현해야 한다.
이는 서버가 클라이언트로부터 전송된 데이터를 게임 플레이에 영향을 미치는 방식으로
사용하는 경우의 부정 행위를 감지하는 수단이다.
서버 RPC가 검증에 실패하면 해당 RPC를 보낸 클라이언트가 게임에서 바로 추방되도록 구현할 수 있다.
따라서 RPC는 즉시 전송되며 Reliable할 수 있다.
Property Replication이 비교적 제한될 수 있는 특정한 경우에 유용하다.
서버 RPC는 소유 연결을 통해 클라이언트에서 서버로 데이터를 가져오는 유일한 방법이므로 중요한 역할을 한다.
하지만 대체로 RPC는 일반적으로 우선순위가 높고 시간이 중요한 네트워크 코드에 사용된다.
예를 들어, 엔진의 캐릭터 이동 시스템은 RPC를 자유롭게 사용해 위치 업데이트를 서로 전송한다.
이는 이동 예측 및 수정에 가능한 한 짧은 지연시간으로 최신 정보가 필요하기 때문이다.
그 외의 모든 경우에는 가능하면 Property Replication을 사용하는 것이 좋다.
Property Replication in Detail
Property Replication은 언리얼 Replication System 의 핵심이며 앞서 논의한 부하 분산 및
우선 순위 지정 기능은 이를 훨씬 더 확장 가능하게 만들어준다.
RPCs는 NOW!!(즉각적)이지만, Property Replication은 Eventually(결국)의 느낌을 갖고 있다.
서버에서 Property Replication을 변경하면 결국 모든 클라이언트가 서버와 동기화될 것이라고 확신할 수 있다.
플레이어가 너무 멀리 떨어져 있어 관련성이 없을 때 서버에서 액터가 변경되면 변경사항이 계속 적용된다.
결국 액터가 해당 클라이언트와 다시 관련성을 갖게 되면 업데이트된 속성 값을 받게 된다.
Property Replication는 어떠한 경우에도 업데이트 빈도와 대역폭 제한을 존중한다.
서버에서 Property Replication을 매 프레임마다 변경할 수 있으며, 클라이언트는 업데이트될 때마다 가장 최신 값만 받게 된다.
서버는 모든 중간값을 전송할 의무가 없다.
Property Replication을 활성화하려면 Replicated 지정자를 추가하면 된다.
C++ 의 .cpp 파일에 Net/UnrealNetwork.h 를 포함해야하며 GetLifetimeReplicatedProps 함수 구현부를 재정의해야한다.
이 기능은 어떤 속성을 어떤 조건에서 복제해야 하는지 지정하는 기능이다.
가장 간단한 경우, 모든 클라이언트에 항상 속성을 복제하고 싶을 뿐이며, DOREPLIFETIME 매크로에서 속성의 이름을 지정하여 이를 구현할 수 있다.
하지만 복제 조건을 지정할 수도 있다.(Replication Condition)
예를 들어, 소유한 클라이언트에게만 속성을 복제하면 될 수도 있고,
소유하지 않은 클라이언트에게만 업데이트를 받으면 될 수도 있다.
또는 스폰 시 초기화되지만 런타임에는 변경되지 않는 속성이 있을 수도 있다.
복제된 속성이 업데이트 될 때 일부 코드를 실행해야 하는 경우
RepNotify 함수를 선언하고 ReplicatedUsing 지정자를 사용할 수 있다.
Replicate 업데이트로 인해 값이 변경될 때 마다 지정된 알림 함수가 클라이언트에서 호출된다.
블루프린트에서 서버의 속성 값을 변경하면 서버에서 연관된 RepNotify 함수가 자동으로 호출된다.
C++에서는 이런 경우가 없기 때문에 RepNotify 로직을 클라이언트뿐만 아니라 서버에서도 실행하려면
속성 값을 업데이트 한 후 RepNotify 함수를 수동으로 호출해야 한다.
Authority & Role
마지막으로 살펴봐야 할 개념은 액터의 네트워크 Role이다.
액터는 4가지 Role를 사용할 수 있지만 대부분의 경우 훨씬 간단한 질문에만 신경 쓰면 된다.
[ ROLE_None, ROLE_SimulatedProxy, ROLE_AutonomousProxy, ROLE_Authority ]
즉, 내가 이 액터에 대한 권한이 있는가?
액터 클래스에서 코드를 실행할 때마다 권한을 확인할 수 있다.
권한이 있는 경우, 게임이 싱글 플레이어 모드에서 실행중이거나
사용자의 권한이 없는 경우 Actor 상태를 업데이트할 최종 결정권이 있다.
코드가 서버에서 실행되거나, 액터가 클라이언트에만 존재하기 때문이다.
권한이 없으면 코드는 클라이언트에서 실행되고, 액터는 서버에서 복제된다.
이 경우, 클라이언트의 액터 사본은 서버의 권한 있는 버전에 대한 프록시다.
액터에게 권한이 없으면 그 ROLE은 거의 항상 SimulatedProxy다.
일반적으로 AutonomousProxy는 플레이어에 관해 이야기할 때만 등장한다.
PlayerController는 소유 클라이언트에 복제되고 AutonomousProxy가 되며,
연관된 Pawn은 해당 클라이언트에 대한 AutonomousProxy가 된다.
다른 모든 클라이언트의 경우 Pawn은 SinulatedProxy로 복제된다.
Autonomous는 클라이언트가 완전한 권한을 가지고 있지 않더라도, 액터의 움직임과 행동을 직접 제어한다는 사실을 말한다.
하지만 플레이어 캐릭터를 상대하지 않는 한, 문제는 대개 이분법적이다.
"내게 권한이 있는가? "
플레이어 관련 코드를 다루는 경우, 또 다른 중요한 질문은 플레이어가 로컬에서 제어되는지 여부이다.
폰이 로컬에서 제어되는 경우, 해당 플레이어는 코드가 실행되는 GameInstance에 해당한다.
그렇지 않은 경우에는 원격 클라이언트의 플레이어다.
멀티플레이 프로그래밍 팁
액터가 복제된다고 하더라도 그 액터의 모든 부분이 네트워킹과 관련될 필요는 없다는 것을 기억하자.
스태틱 메쉬 컴포넌트와 다이나믹 머테리얼 인스턴스가 있는 액터의 경우, 이러한 객체 중 어느 것도 네트워크를 인식하지 못한다.
네트워킹에 대해 생각하지 않고도 해당 객체를 초기화하고 수정하는 함수를 작성할 수 있다.
이런 함수는 각 게임 인스턴스에서 독립적으로 실행되며 게임의 모든 인스턴스에서 일관된 방식으로
호출되도록 하려면 Replication만 함수에 포함시키면 된다.
대부분의 게임 코드는 기본적으로 멀티 플레이어를 위해 설계된 Unreal GameFramework를 기반으로 구축된다.
이러한 클래스의 멤버 함수를 재정의하여 사용자 고유의 기능을 추가할 때, 해당 함수가 서버에서만 실행되도록 설계되었는지,
클라이언트에서만 실행되도록 설계되었는지, 아니면 모든 곳에서 실행되도록 설계되었는지 알고 있어야한다.
Assertion을 사용하면 코드를 살펴보면 실행되도록 설계된 위치를 명확하게 알 수 있으며,
권한이 있어야만 호출되는 함수에 "Auth"와 같은 접두사를 사용하는 등 도우미 함수에 대한 명명 규칙을 수립하는 데도
유용할 수 있다.
액터의 기능을 일종의 원인-결과 흐름도로 생각할 수 있다.
한쪽에는 사물을 움직이게 하는 이벤트가 있다.
반면에, 그 결과로 발생해야 할 상태 변경이 있다. 네트워크의 다른 장소에서 시작해서 끝나는 프로세스가 있는 경우 일종의 Replication을 사용해야 한다.
프로세스가 클라이언트에서 시작되어 서버에서 종료되는 경우 일반적으로 서버 RPC를 사용한다.
어떤 프로세스가 서버에서 시작되어 결국 클라이언트에 영향을 미치게 되면, 데이터를 클라이언트까지 복제할 위치를 고려해야 한다.
플레이어가 클라이언트 측에서 데이터를 보거나 수정하여 부당한 이점을 얻을 수 있거나, 권한이 있는 게임 상태를 업데이트하는 동안은 코드가 서버에 남아있어야 한다.
하지만 그 후에는 일반적으로 최종 결과를 재구성하는 데 필요한 최소한의 데이터 집합을 복제하고 나머지 프로세스는 클라이언트가 독립적으로 처리하도록 한다.
코드에서 네트워킹을 지원해야 하는 경우, 일반적으로 몇 가지 간단한 사례 중 하나에 해당한다.
'Project_II' 카테고리의 다른 글
리슨 서버와 P2P Super peer 차이 (0) | 2024.12.26 |
---|---|
게임 서버 P2P? CS? (0) | 2024.12.26 |