1일1알

언리얼 네트워크 멀티플레이 프레임워크 본문

언리얼/언리얼 네트워크 프레임워크

언리얼 네트워크 멀티플레이 프레임워크

영춘권의달인 2024. 4. 9. 19:55

언리얼 네트워크 멀티플레이 프레임워크 학습 기록용

 

멀티플레이 게임은 싱글플레이 게임과 구조가 완전히 다르기 때문에 만약 멀티플레이 게임을 제작할 것이라면 처음부터 멀티플레이를 고려해야 한다.

만약 처음에 싱글플레이 게임을 제작하다가 나중에 멀티플레이로 확장하려고 하면 거의 모든 기능을 다시 프로그래밍 해야한다.

 

클라이언트-서버 모델을 사용, 하나의 컴퓨터가 서버 역할을 하고 나머지는 클라이언트

게임 로직이 실제로 진행되는 곳은 서버, 클라이이언트는 서버로부터 정보를 받아서 서버에서 일어나는 일의 근사치를 시뮬레이션한다.

 

네트워크 모드

  1. Listen Server
    • 게임이 네트워크 멀티플레이어 세션을 호스팅하는 서버로 실행중, 원격 클라이언트의 연결을 수락하고 로컬 플레이어를 서버에 배치한다. 자기 자신도 하나의 플레이어로서 게임에 참여한다.
    • 리슨 서버를 호스팅하는 플레이어는 서버에서 직접 플레이하기 때문에 네트워크 연결을 통해 플레이하는 다른 플레이어보다 유리한 위치에 있다.
    • 소규모 멀티플레이 게임에 적합
  2. Dedicated Server
    •  Listen Server와 같이 원격 클라이언트의 연결을 허용하지만 로컬 플레이어가 없다.
    • 그래픽 렌더링, 사운드 등의 기능이 없다. 
    • MMO, 온라인 슈팅 게임 등에 사용됨

Listen Server 

클라이언트가 접속을 시도하면 서버의 GameMode에서 로그인 과정을 거침

  1. PreLogin : 클라이언트의 접속 요청을 처리
  2. Login : 접속을 허용한 클라이언트에 대응하는 플레이어 컨트롤러를 만들어줌
  3. PostLogin : 플레이어 입장을 위해 플레이어에 필요한 기본 설정을 모두 마무리
  4. StartPlay : 게임의 시작을 지시
  5. BeginPlay : 게임모드의 StartPlay를 통해 게임이 시작될 때 모든 액터에서 호출됨

GameMode의 StartPlay 함수가 호출되면 GameState에 월드의 액터들에게 BeginPlay를 호출하라고 지시한다.

서버의 액터들은 BeginPlay를 호출하고, 이후에 생성되는 액터들은 BeginPlay가 자동으로 실행된다. 

클라이언트에는 복제된 GameState를 서버에서 받고, GameState는 서버의 GameMode의 명령을 받아서 월드의 액터들에게 BeginPlay를 호출하라고 지시한다.

 

  • (Server) - GameMode::StartPlay -> GameState::HandleBeginPlay() -> Actor::BeginPlay()
  • (Client) - 서버의 GameState::HandleBeginPlay 호출시 bReplicatedHasBegun 프로퍼티 변경, 변경시 이를 감지하여 클라이언트의 GameState::OnRep_ReplicatedHasBegunPlay() -> Actor::BeginPlay()
  • PostNetInit : 원격 클라이언트가 네트워크로 초기화에 필요한 정보를 전달받은 것이 마무리가 되면 호출 (PostLogin 이후에 호출)

네트워크 통신을 위해 액터와 속성값들 등에 대한 정보를 규칙에 맞춰서 일련된 숫자 흐름으로 만들어주는 것을 담당하는 클래스가 필요하다.

큰 틀에서 세가지 클래스로 나누면

  • PlayerController : 네트워크 통신에 접근가능한 게임 내 대표 액터, 네크워크 커넥션을 관리
  • UNetConnection : 주고받는 패킷 데이터의 인코딩, 디코딩, 네트워크 통신량 조절, 채널 관리
  • UNetDriver : 로우레벨에서의 소켓 관리와 패킷 처리, 네트워크 통신 설정

PlayerController에서 전달된 명령은 UNetConnection 객체에 의해서 관리됨, 상위 명령어를 로우레벨 데이터로 변환할 수 있는 기초 작업 진행

NetDriver에서는 NetConnection에서 정돈된 데이터를 실제 네트워크상으로 보내는 로우레벨 기능 수행

클라이언트의 NetDriver에서 서버로부터 전송된 데이터를 수신하면 NetConnection을 거쳐서 액터에 명령을 내리게 됨

 

서버는 월드의 Listen함수를 호출해 NetDriver를 생성하고 네트워크 기능 시작

서버의 넷드라이버는 다수의 클라이언트 커넥션을 가지고, 클라이언트의 넷드라이버는 하나의 서버 커넥션을 가진다.

 

언리얼 엔진에서의 데이터 관리

네트워크에서 주고 받는 데이터들은 다음과 같은 고도화 작업을 거침

  • 커넥션(Connection) : 모든 데이터를 전달하는 네트워크 통로
  • 패킷(Packet) : 네트워크를 통해 전달되는 단위 데이터. 숫자 혹은 문자로 구성
  • 채널(Channel) : 언리얼 엔진 아키텍쳐에 따라 구분된 데이터를 전달하는 논리적인 통로
  • 번치(Bunch) : 언리얼 엔진의 아키텍쳐에 사용되는 데이터. 하나의 명령에 대응하는 데이터 묶음

하나의 커넥션은 다수의 채널을 가짐

데이터 통신을 관리하기 위한 액터로 플레이어 컨트롤러가 주로 사용됨

커넥션을 담당하는 대표 액터는 커넥션에 대한 오너십을 가진다고 표현한다.

플레이어 컨트롤러는 현재 어플리케이션이 관리하고 있는 커넥션의 수만큼 만들어진다.

 

어떤 액터가 통신을 하기 위해서는 자신을 소유한 액터가 커넥션을 소유하고 있어야 한다.

일반적으로 플레이어 컨트롤러는 커넥션을 소유하고 있다.

넷커넥션 역시 플레이어 컨트롤러를 소유하고 있다.

 

액터의 역할

클라이언트-서버 모델에서는 항상 서버에 있는 액터만이 신뢰되고, 이를 Authority를 가진다고 표현

클라이언트의 액터는 대부분 서버 액터를 복제한 허상에 불과하고, 이를 Proxy라고 표현

 

리슨서버의 경우 플레이어로서 게임에 참여하므로 어플리케이션의 게임 로직을 사용한다.

어플리케이션의 게임 로직은 서버 액터에 대해서만 게임에 관련된 작업을 수행해야 한다.

이를 구분하기 위해 현재 동작하는 어플리케이션에서의 역할을 로컬 역할(Local Role), 커넥션으로 연결된 어플리케이션에서의 역할을 리모트 역할(Remote Role)이라고 한다.

 

액터 역할의 종류

  • None : 액터가 존재하지 않음
  • Authority : 서비스를 대표하는 신뢰할 수 있는 역할, 게임 로직 수행 AActor::HasAuthority()
  • AutonomousProxy : Authority를 가진 오브젝트의 복제품, 일부 게임 로직 수행
  • SimulatedProxy : Authority를 가진 오브젝트의 복제품, 게임 로직 수행하지 않음

클라이언트의 Proxy는 크게 Autonomous와 Simulated로 구분된다.

Autonomous는 클라이언트의 입력 정보를 서버에 보내는 능동적인 역할을 일부 수행한다.

Simulated는 일방적으로 서버로부터 데이터를 수신하고 이를 반영한다.

Autonomous 역할을 하는 액터로는 플레이어 컨트롤러와 폰이 있다.

 

서버에만 존재하는 액터 : 게임모드

서버와 모든 클라이언트에 존재하는 액터 : 배경 액터, 폰

서버와 소유하는 클라이언트에만 존재하는 액터 : 플레이어 컨트롤러

클라이언트에만 존재하는 오브젝트 : 애니메이션 블루프린트 및 HUD

 

커넥션 핸드셰이킹

핸드셰이킹 : 네트워크로 접속하는 두 컴퓨터가 잘 연결되었는지 확인하는 과정

언리얼 네트워크 멀티플레이 접속을 위한 핸드셰이킹 과정

  1. Client : UpendingNetGame::SendInitialJoin (서버에 Hello 패킷 전송)
  2. Server : UWorld::NotifyControlMessage (Hello 패킷이 확인되면 클라에 Challenge 패킷 전송)
  3. Client : UPendingNetGame::NotifyControlMessage (Challenge 패킷이 확인되면 서버에 Login패킷 전송)
  4. Server : AGameModeBase::PreLogin에서 접속요청 처리 후 AGameModeBase::WelcomePlayer (클라에 Welcome패킷 전송
  5. Client : UPendingNetGame::NotifyControlMessage (서버에 NetSpeed 패킷 전송)
  6. Server : SetConnectionSpeed

커넥션이 완료되면 게임을 시작할 수 있도록 클라이언트와 서버는 준비 과정을 거침

클라이언트 : 맵의 로딩

서버 : 클라이언트를 대표하는 플레이어 컨트롤러 생성

  1. Client : 맵 로딩 후 UPendingNetGame::SendJoin (서버에 Join패킷 전송)
  2. Server : UWorld::SpawnPlayActor -> AGameModeBase::Login, AGameModeBase::PostLogin

액터 리플리케이션

특정 플레이어에 속한 액터의 정보를 네트워크 내 다른 플레이어에게 복제하는 작업

클라이언트-서버 모델에서는 대부분 서버에서 클라이언트로 전달함

리플리케이션의 방법에는 크게 두 가지가 있음

  • 프로퍼티 리플리케이션
  • RPC(Remote Procedure Call)

고정 액터(배경 액터 등)와 동적 액터(플레이어 컨트롤러, 폰 등)가 있음, 고정 액터애는 NetLoadOnClient 속성을 체크해야 함. 기본값이 체크되어있고, 체크되어있을경우 서버와 통신없이 클라이언트가 초기화될 때 자체적으로 로딩함으로써 서버에서 콘텐츠를 배포한 것과 동일한 효과를 냄

 

프로퍼티 리플리케이션

  • 고정으로 보여지는 액터 중, 게임 중 변경 사항이 발생하는 액터는 그 값을 전달해야 함
  • 네트워크 데이터를 최소화하기 위해 변경 사항을 보내기보다, 변경을 유발한 속성 값을 전달
  • 이를 위해 액터의 Replicates 옵션 체크

프로퍼티 리플리케이션 과정1 (서버 액터의 속성값이 변경되면 클라이언트로 복제되고, 클라이언트에서는 알아서 사용)

  1. 액터의 리플리케이션 속성을 참으로 지정함 (bReplicates 속성을 true로 설정)
  2. 네트워크로 복제할 액터의 속성을 키워드로 지정 (UPROPERTY에 Replicated 키워드 설정)
  3. GetLifdtimeReplicatedProps함수에 네트워크로 복제할 속성을 추가 (Net/UnrealNetwork 헤더파일 지정, DOREPLIFETIME 매크로를 사용해 복제할 속성을 명시)

프로퍼티 리플리케이션2 (서버 액터의 속성값이 변경되면 변경된 값이 클라이언트에 복제될 때 콜백 함수 호출)

  1. UPROPERTY의 키워드를 ReplicatedUsing으로 변경
  2. ReplicatedUsing에 호출할 콜백 함수 지정
  3. 호출될 콜백 함수는 UFUNCTION으로 선언해야 함
  4. 콜백 함수는 OnRep_의 접두사를 가지는 이름 규칙을 가지고, 클라이언트에서만 호출됨. 서버에서는 명시적으로 호출 가능

콜백 함수를 이용하는 방법이 필요한 타이밍에만 해당 로직을 처리할 수 있어서 효율적인 구현이 가능함.

 

액터 리플리케이션의 빈도

  • 클라이언트와 서버간에 진행되는 통신 빈도
  • NetUpdateFrequency : 리플리케이션 빈도의 최대치 설정 (1초당 몇번 리플리케이션을 시도할지 지정한 값, 기본값은 100, 액터마다 기본값이 다를 수 있음)
  • 네트워크 빈도는 최대치일 뿐 이를 보장하지는 않음, 서버에 성능이 네트워크 빈도보다 낮은 경우, 서버의 성능으로

 

연관성(Relevancy)

  • 서버의 관점에서 현재 액터가 클라이언트의 커넥션에 관련된 액터인지 확인하는 작업
  • 대형 레벨에 존재하는 모든 액터 정보를 클라이언트에게 보내는 것은 불필요함
  • 클라이언트와 연관있는 액터만 체계적으로 모아 통신 데이터를 최소화하는 방법

클라이언트에 영향을 끼칠 수 있는 것으로 여겨지는 액터 세트를 모으는 것이 핵심

AActor::IsNetRelevantFor() 함수를 통해 연관성이 있는지 확인할 수 있음 (완벽하지는 않음)

 

연관성에 관련된 다양한 속성

 

연관성 판별을 위한 특별한 액터

  • 뷰어(Viewer) : 클라이언트의 커넥션을 담당하는 플레이어 컨트롤러를 가리킴
  • 뷰 타겟(View Target) : 플레이어 컨트롤러가 빙의한 폰
  • 가해자(Instigator) : 나에게 데미지를 가한 액터
  • 오너(Owner) : 액터를 소유하는 액터, 최상단의 소유 액터

연관성 검사 과정

  1. 서버에서 틱마다 모든 커넥션과 액터에 대해 연관성을 점검
  2. 클라이언트의 뷰어와 관련있고 뷰어와의 일정 거리 내에 있는 액터를 파악
  3. 해당 액터 묶음의 정보를 클라이언트에게 전송

액터 속성에 따른 연관성 판정을 위한 속성

  • AlwaysRelevant : 항상 커넥션에 대해 연관성을 가짐 (GameState, PlayerState)
  • NetUseOwnerRelevancy : 자신의 연관성은 오너의 연관성으로 판정함
  • OnlyRelevancyToOwner : 오너에 대해서만 연관성을 가짐 (뷰어/뷰 타겟을 오너로 하지 않으면 연관성에서 제외)
  • Net Cull Distance : 뷰어와의 거리에 따라 연관성 여부를 결정함

액터 리플리케이션에서 우선권(Priority)

 

클라이언트에 보내는 대역폭(NetBandwith)은 한정되어있음, 클라이언트에 보낼 액터 중, 우선권이 높은 액터의 데이터를 우선 전달하도록 설계되어있음. 액터에 설정된 NetPriority 우선권 값을 활용해 전송 순서를 결정함.

네트워크 트래픽 포화(Saturation)가 일어났을 경우, 우선권이 낮은 액터는 현재 틱에 전송되지 못하고 다음 틱에 우선권을 높혀줌, 우선권을 높혀줄 뿐, 다음 틱에 반드시 전송된다는 보장은 없음. GetNetPriority 함수로 액터의 우선권을 최종 계산

 

우선권 설정 로직

  1. 마지막으로 패킷을 보낸 후의 경과 시간과 최초 우선권 값을 곱해 최종 우선권 값을 생성
  2. 최종 우선권 값을 사용해 클라이언트에 보낼 액터 목록을 정렬함
  3. 네트워크가 포화될때까지 정렬된 순서대로 리플리케이션을 수행
  4. 네트워크가 포화되면 해당 액터는 다음 서버 틱으로 넘김.

액터의 휴면(Dormancy)

 

액터의 전송을 최소화 하기 위해 연관성과 더불어 제공하는 속성

액터가 휴면 상태라면 연관성이 있더라고 액터 리플리케이션(RPC)을 수행하지 않음

언리얼 엔진에서 지정한 휴면 상태

  • DORM_Never : 액터는 휴면이 없음
  • DORM_Awake : 액터는 깨어나 있음
  • DORM_DormantAll : 액터는 언제나 휴면 상태, 필요시에 깨울 수 있음
  • DORM_DormantPartial : 특정 조건을 만족할 경우에만 리플리케이션 수행
  • DORM_Initial : 액터를 휴면 상태로 시작하고 필요할 때 깨우도록 설정할 수 있음

프로퍼티 리플리케이션에 사용시에는 DORM_Initial만 고려하는 것이 좋음

 

자세한 액터 리플리케이션 흐름(로우레벨 액터 리플리케이션)

 

액터 리플리케이션 대부분은 UNetDriver::ServerReplicateActors 함수 안에서 일어남. 서버가 각 클라이언트에 연관성이 있다고 결정내린 액터 전부를 수집하고, 접속된 각 클라이언트가 지난번 업데이트된 이후 변경된 프로퍼티가 있으면 전송하는 곳

  1. 현재 리플리케이션중인 액터 각각에 대해 루프를 돌림 (AActor::SetReplicates)
  2. NetUpdateFrequency 값, 오너십, 휴면상태 등을 검사해서 클라이언트에 보내는 조건을 만족하는 액터들만 골라서 ConsiderList에 모아둠
  3. 서버에 접속한 클라이언트마다 ConsiderList에 있는 액터들과 연관성 검사를 하여 각각 별도의 액터 묶음을 모아둠
  4. 별도의 액터 묶음에 있는 액터들의 우선권을 계산하여 정렬하고 우선권 리스트를 만듦
  5. 우선권 리스트에 따라 클라이언트에 전송함 (Saturation으로 전송되지 못한 정보는 다음 틱에 우선권을 올려줌)

서버는 위의 과정을 매 틱마다 수행함.

 

RPC(Remote Procedure Call)

  • 원격 컴퓨터에 있는 함수를 호출할 수 있도록 만든 통신 프로토콜
  • 네트워크 멀티플레이에서 서버와 클라이언트간에 빠르게 행동을 명령하고 정보를 주고받는데 사용
  • 언리얼 엔진에서 클라이언트에서 서버로 통신하는 유일한 수단 제공

RPC 사용시 오너십 작동 방식을 이해하는 것이 중요.

함수를 RPC로 선언하려면 UFUNCTION 선언에 Server, Client, NetMulticast 키워드와 Reliable or UnReliable를 붙여주면 된다.

  • Client : 서버에서 호출, 클라에서 실행
  • Server : 클라에서 호출, 서버에서 실행
  • NetMulticast : 서버에서 호출, 서버와 모든 클라에서 실행

RPC의 정상 작동을 위해 충족시켜야 하는 요건

  1. Actor에서 호출되어야 한다.
  2. Actor는 반드시 replicated여야 한다.
  3. Client RPC의 경우 해당 액터를 소유하고 있는 클라이언트에서만 함수가 실행된다.
  4. Server RPC의 경우 클라이언트는 RPC가 호출되는 Actor를 소유해야 한다.
  5. Multicast RPC는 예외
    • 서버에서 호출되면 서버의 로컬+연결된 모든 클라이언트에서 실행
    • 클라에서 호출되면 로컬에서만 실행, 서버에서는 실행되지 않음
    • Multicast는 소유권이 아닌 연관성 기반으로 동작

Server RPC는 UFUNCTION에 withCalidation 키워드를 붙여서 해당 RPC를 수행할지 안할지 정할 수 있다.

 

Client RPC

  • 서버에서 클라이언트로 호출하는 RPC
  • 특정 클라이언트에게만 명령을 보낼 수 있음
  • 호출하는 액터에 오너십이 없다면 서버에서 실행됨
  • 서버에서 명령을 보낼 클라이언트의 커넥션을 소유한 액터를 사용해야 함 (AActor::GetNetConnection)

Server RPC

  • 클라이언트에서 서버로 호출하는 RPC
  • 언리얼 엔진 구조에서 유일하게 클라이언트가 서버의 함수를 호출할 수 있는 기능
  • 서버쪽에서 클라이언트의 명령을 검증할 수 있는 함수를 구현 가능
  • Client RPC와 동일하게 서버와의 커넥션을 소유한 액터를 사용해야 함.

NetMulticast RPC

  • 서버를 포함해 모든 플레이어에게 명령을 보내는 RPC
  • 프로퍼티 리플리케이션과 유사하게 연관성 기반으로 동작
  • 프로퍼티 리플리케이션과 유사하지만 다른 용도로 사용
  • 서버에서 호출해야 기대하는 결과를 얻을 수 있음

RPC 선언에 관련된 키워드

  • Unreliable : RPC 호출을 보장하지 않는 옵션, 빠름
  • Reliable : RPC 호출을 보장해주는 옵션, 정말 필요할 때만 호출
  • WithValidation : 서버에서 검증 로직을 추가로 구현할 때 추가하는 옵션

Unreliable RPC는 게임플레이에 중요하지 않고 매우 자주 호출되는 함수에 적합

Reliable RPC는 게임플레이에 매우 중요하지만 자주 호출되지 않는 함수에 적합

 

함수를 만들때는 구현부에 _Implemetation을 붙여줘야 한다.

Client RPC 구현 예시 : 서버 - ClientRPCFunction() , 클라 - ClientRPCFunction_Implementation()

 

액터를 네트워크로 복제하고 싶을 때 따라야 하는 과정

  • 액터의 Replicated는 True
  • 리플리케이티드된 액터가 움직여야 하는 경우, ReplicatesMovement를 True로 설정
  • 리플리케이트된 액터를 스폰 또는 소멸할 때는 반드시 서버에서
  • 리플리케이트할 머신 간에 공유해야 하는 변수 설정
  • 무브먼트 컴포넌트는 리플리케이트용으로 빌드되어있음, 가급적 사용하는게 좋음

RPC 사용시 주의할 점

  • 각 RPC 종류마다 올바르게 사용 (Client, NetMulticast는 서버에서만 호출)
  • Server는 클라이언트에서 호출하지만, 플레이어로 참여하는 리슨서버의 경우 호출 가능
  • Client, Server는 오너십을 가지고 있는 액터에서 호출
    • 컨트롤러 : IsLocalControll
    • 폰 : IsLocallyControlled
  • 게임 플레이 및 액터 상태에 영향을 미치는 경우 RPC보다 프로퍼티 리플리케이션을 사용

 

프로퍼티 리플리케이션 vs NetMulticast RPC

  • 유사점
    • 서버와 모든 클라이언트의 지정한 함수를 호출할 수 있음
    • 지정한 데이터 전송을 보장할 수 있음
    • 액터의 오너십과 무관하게 연관성으로 동작
  • 차이점
    • 프로퍼티 리플리케이션으로 설정한 데이터는 클라이언트에 반드시 동기화됨 (클라이언트가 접속하기 전에 변경된 수치도 자동으로 적용, PostNetInit에서 변경된 기본 속성값들을 받음)
    • NetMulticast RPC를 호출한 타이밍에 클라이언트가 없으면 해당 데이터를 받을 방법이 없음
  • 정리
    • 프로퍼티 리플리케이션은 게임에 영향을 미치는 데이터에 사용
    • NetMulticast RPC는 게임과 무관한 휘발성 데이터에 사용

액터 컴포넌트 리플리케이션

  • 언리얼에서 리플리케이션의 주체는 액터
  • 액터가 소유하는 언리얼 오브젝트에 대해 리플리케이션 진행 가능(이를 통틀어 서브오브젝트라고도 함)
  • 액터 컴포넌트의 리플리케이션 설정
    • 리플리케이션을 지정 : SetIsReplicated(true)
    • 리플리케이션이 준비되면 호출되는 함수 : ReadyForReplication
      • InitializeComponent -> ReadyForReplication -> BeginPlay

 

추가 작성 예정