목표
1.네트워크 멀티플레이어 게임에서 서버와 클라이언트에 위치한 액터의 역할들을 이해하는 것
2.클라이언트와 서버 간의 커넥션이 맺어지는 과정에 대해서 엔진 소스코드를 보면서 심층적으로 이해하는 것
액터의 역할
우리가 네트워크로 멀티플레이어 게임을 만들면은 서버와 클라이언트 간의 다양한 액터가 존재하게 된다.
이런 액터들은 신뢰할 수 있는가를 기준으로 Authority, Proxy로 구분할 수 있다. (Question : 신뢰 기준이 맞나?)
Authority : 클라이언트-서버 모델에서는 항상 서버에 있는 신뢰되는 액터가 가지는 것
Proxy : 서버에서 클라이언트로 복제된 액터. Proxy(허상)이름 그대로 허상에 불과함.
클라이언트에 있는 액터는 대부분 서버 액터를 복제한 허상에 불과해 Authority를 가지지 않는다. ( Question : 대부분 즉 클라에도 어써리티를 가지는 액터가 존재?)
그림으로 정리하면 다음과 같다. 대부분 서버에서 클라이언트로 복제되는 방향성을 가진다.
로컬 역할과 리모트 역할
리슨 서버의 경우 플레이어로서 게임에 참여하므로, 어플리케이션의 동일한 게임로직을 사용한다.
>( Question : 직접 정의) 게임 로직 : 플레이어 조작, 아이템 생성, 캐릭터의 죽음 등 게임 플레이에 능동적으로 영향을 주는 것.
게임 로직을 수행하는 액터는 플레이어 컨트롤러, 게임 모드, 서버에 존재하는 폰 등이 있다.
반대로 게임 로직을 수행하지 않는 액터는 배경 액터, 플레이어 컨트롤러에 빙의되어 있지 않는 폰 등이 있다.
> Question : 리슨 서버 뿐만아니라 데디케이티드도 서버와 클라이언트가 동일한 어플리케이션을 사용한다고 볼 수있지 않은가? 왜냐하면 서버와 클라이언트가 같은 코드를 사용하고 HasAuthority, IsLocallyControlled 같은 API로 서버인지 클라인지 판단하기 때문
어플리케이션의 게임 로직의 대부분이 서버 액터에서 수행된다.
> 클라이언트에서도 AutonomusProxy 같은 액터는 게임 로직을 일부 수행한다. 아래에서 다루겠다.
따라서 로직을 수행하려는 액터가 서버에 있는지, 혹은 클라이언트 구분하기 위해 언리얼은 Role(역할) 기능을 제공한다.
커넥션을 기준으로 Local역할, Remote 역할로 구분할 수 있다.
Local 역할 : 현재 동작하는 어플리케이션에서의 역할
Remote 역할 : 커넥션으로 연결된 어플리케이션에서의 역할
또 그 안에서 액터의 역할을 기준으로 None, Authority, AutonomousProxy, SimulatedProxy로 구분된다.
로컬 - 리모트 역할은 현재 동작하는 어플리케이션이 어디냐는 기준에 따라 달라지는 상대적인 구조다.
현재 동작하는 어플리케이션이 서버일 경우 서버가 로컬 역할이고 클라이언트가 리모트 역할이다.
반대로 동작하는 어플리케이션이 클라이언트일 경우 클라이언트가 로컬 역할이고 서버가 리모트 역할이 된다.
이부분이 이해가 조금 어려울 수 있는데, 액터 역할의 종류를 알고 몇가지 예를 알면 이해하기 쉽다.
액터 역할의 종류
액터의 역할은 네 가지 종류가 있다.
None : 액터가 존재하지 않음
ex ) 게임모드처럼 액터가 클라이언트에는 없고 서버에만 존재한다면, 서버 어플리케이션을 기준으로 로컬 역할은 Authority, 리모트 역할은 None이다.
Authority : 신뢰할 수 있는 역할. 게임 로직을 수행한다. 보통 서버에 있는 액터의 역할에 해당한다.
ex) 서버에 존재하는 대부분의 액터는 Authority다.
AutonomousProxy : Authority 역할의 액터의 복제된 허상. 일부 게임 로직을 수행한다.
ex) 클라이언트 어플리케이션 기준으로 내가 플레이하는 캐릭터의 로컬 역할은 AutonomusProxy, 리모트 역할은 Authority다.
SimulatedProxy : Authority 역할의 액터의 복제된 허상. 게임 로직을 전혀 수행하지 않는다.
ex) 클라이언트 어플리케이션 기준으로 내 캐릭터가 아닌 다른 캐릭터의 로컬 역할은 SimulatedProxy, 리모트 역할은 Authority다.
AutonomousProxy와 SimulatedProxy
프록시 액터는 AotonomousProxy와 SimulatedProxy로 구분된다.
AutonomousProxy : 클라이언트의 입력 정보를 서버에 보내는 능동적인 역할을 일부 수행한다. 즉 게임 로직을 일부 수행할 수 있다.
> ex) 클라이언트의 플레이어 컨트롤러(Role : AutonomousProxy)는 사용자의 조작정보에 관련된 입력 데이터들을 서버에 보내는 능동적인 역할을 수행한다.
> ex) 클라이언트가 빙의한 폰이 공격하려 하면 공격 관련 데이터를 서버에 보내서 공격을 요청한다.
SimulatedProxy : 일방적으로 서버로부터 데이터를 수신하고 이를 반영한다.
> 배경 액터, 빙의되지 않은 폰이 이에 해당한다.
액터의 역할을 파악하는 API
액터가 가지고 있는 다양한 역할을 파악할 수 있도록 언리얼 엔진은 몇 가지 API 를 제공한다.
우선 이 액터를 신뢰할 수 있는 액터인지 확인하는 기능인AActor::HasAuthority라고 하는 함수 API를 제공한다.
> 보통 이 함수를 이용해서 서버 액터인지 클라이언트 액터인지 확인한다.
로컬에서 액터의 역할을 확인 하는 GetLocalRole, 리모트에서 액터의 역할을 확인하는 GetRemoteRole 함수가 있다.
> 서버 어플리케이션을 기준으로 폰의 경우 GetLocalRole = Authority, GetRemoteRole = AutonomousProxy다.
> 클라이언트 기준으로 NPC 캐릭터일 경우 GetLocalRole = SimulatedProxy , GetRemoteRole = Authority 다.
좀 더 파고들면
현재 구동중인 어플리케이션에서 조작하고 있는 폰과 컨트롤러를 확인할 때
bool AController::IsLocalController , bool APawn::IsLocallyControlled 함수를 사용한다.
사실 내가 조작하고 있는 컨트롤러랑 폰을 찾는 것은 어렵다. 분기가 너무 많기 때문이다.
넷모드에 따른 오브젝트 배치
이러한 역할 정보를 기반으로 네트워크 모드에 따라 게임을 구성하는 언리얼 오브젝트가 클라이언트와 서버에 어떻게 배치되는지 파악하는 것이 중요하다.
대부분은 액터인 언리얼 오브젝트가 배치되는 유형에는 크게 네 가지로 볼수 있다.
1. 서버에만 있는 유형
>이 경우에는 클라이언트에는 아예 존재하지 않는다. 게임 모드가 이에 해당한다.
2. 서버와 모든클라이언트에 존재하는 유형
>구조물, 배경에 해당하는 시뮬레이티드 프록시, 폰이 해당한다고 할 수 있다.
3. 서버와 자기가 컨트롤하는 클라이언트에만 존재하는 유형
>플레이어 컨트롤러같이 오너십을 가지고 클라이언트에만 배치되는 액터가 이에 해당함
4. 클라이언트에만 존재하고 서버에 없는 유형
>주로 시각적인 표현을 담당하는 언리얼 오브젝트가 이에 해당함. 예를 들어 애니메이션 재생을 담당하는 애니메이션 블루프린트, HUD같은 UI가 이에 해당함. 보통 AController::IsLocalController API를 이용해서 이 유형인지 판단함.
오브젝트 배치에 따른 API의 사용
게임 모드는 서버에만 존재하기 때문에 HasAuthority 함수를 호출해서 이 액터가 서버에 있는지 확인할 필요가 없다.
게임 모드와 반대로 폰은 AutonomousProxy와 SimulatedProxy가 혼재되어있다.
따라서 IsLocallyControlled, GetLocalRole과 같은 API를 사용해 이 폰이 오토노머스인지 시뮬레이티드인지 판단해야함.
직접 로그를 찍어보자.
// --------------------- 프로젝트명.h ---------------------------------
#pragma once
#include "CoreMinimal.h"
// 로컬 역할 Get
#define LOG_LOCALROLEINFO *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"),GetLocalRole()))
// 리모트 역할 Get
#define LOG_REMOTEROLEINFO *(UEnum::GetValueAsString(TEXT("Engine.ENetRole"),GetRemoteRole()))
// 넷모드 Get
#define LOG_NETMODEINFO ((GetNetMode()==NM_Client)?*FString::Printf(TEXT("CLIENT%d"),GPlayInEditorID):(GetNetMode()==NM_Standalone)?TEXT("STANDALONE"):TEXT("SERVER"))
// 호출한 함수명 Get
#define LOG_CALLINFO ANSI_TO_TCHAR(__FUNCTION__)
// [네트워크 모드][로컬 역할/리모트 역할] 호출함수 : 로그내용 서식
#define Grizzly_LOG(Format, ...) UE_LOG(LogGrizzly, Warning, TEXT("[%s][%s/%s] %s : %s"),LOG_NETMODEINFO,LOG_LOCALROLEINFO,LOG_REMOTEROLEINFO,LOG_CALLINFO,*FString::Printf(Format,##__VA_ARGS__))
//매크로 카테고리 생성
DECLARE_LOG_CATEGORY_EXTERN(LogGrizzly,Log,All)
// 컨텐츠의 폰의 Tick
void ACPP_Player::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
//내가 조작하고 있는 폰이 아니라면 아래 로직을 실행하지 않는다.
if (!IsLocallyControlled())
{
return;
}
UpdateCameraCrouch(DeltaTime);
}
// 컨텐츠의 폰이 앉을 때 호출되는 가상함수
// 이 함수를 이용해서 각 어플리케이션에서 로그를 찍어보자
void ACPP_Player::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
{
Grizzly_LOG(TEXT("Begin"));
Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
FVector CurrentCameraLocation = StandingCameraLocation;
float Padding = 10.f;
CurrentCameraLocation.Z += ScaledHalfHeightAdjust + Padding;
GetCamera()->SetRelativeLocation(CurrentCameraLocation);
}
// 액터가 신뢰할 수 있는 액터인지 확인하는 함수
// 보통 서버 액터인지 확인할 때 사용
FORCEINLINE_DEBUGGABLE bool AActor::HasAuthority() const
{
return (GetLocalRole() == ROLE_Authority);
}
// 구동중인 어플리케이션에서 조작하는 폰인지 확인하는 함수
bool APawn::IsLocallyControlled() const
{
return ( Controller && Controller->IsLocalController() );
}
// 구동중인 어플리케이션에서 조작하는 플레이어 컨트롤러인지 확인하는 함수
bool AController::IsLocalController() const
{
const ENetMode NetMode = GetNetMode();
//스탠드얼론일경우 플레이어 컨트롤러가 유일하고 무조건 내가 조작하는 플레이어 컨트롤러다.
if (NetMode == NM_Standalone)
{
// Not networked.
return true;
}
//데디케이티드일경우 구동중인 어플리케이션이 클라이언트일 때 내가 조작하는 플레이어 컨트롤러다.
if (NetMode == NM_Client && GetLocalRole() == ROLE_AutonomousProxy)
{
// Networked client in control.
return true;
}
// 리슨 서버일경우 리슨 서버의 플레이어 컨트롤러에 대한 예외처리
if (GetRemoteRole() != ROLE_AutonomousProxy && GetLocalRole() == ROLE_Authority)
{
// Local authority in control.
return true;
}
return false;
}
결과
Question
1.리슨 서버 네트워크 모드에서 서버 액터의 리모트 역할은 Simulated다... 왜지?
Question
2. 서버와 클라 1, 클라 2가 있다고 가정하자.
서버에서 클라1의 리모트 역할은 AutonomousProxy, 클라2의 리모트 역할도 AotonomousProxy다.
사실 서버에서 클라 폰 액터가 리모트 역할이 SimulatedProxy이 될 수 없는데, 왜냐하면 커넥션을 기준으로 로컬-리모트 역할을 정하기 때문이다.
서버에서 클라1의 커넥션에서 클라1 폰 액터의 리모트 역할은 AutonomousProxy라는것이다.
언리얼 엔진의 커넥션 핸드셰이킹
핸드셰이킹 : 네트워크로 접속하는 두 컴퓨터가 잘 연결됐는지 확인하는 과정
언리얼 네트워크 멀티플레이 접속을 위한 핸드셰이킹 과정은 다음과 같다
게임의 준비
커넥션을 허용하면 게임을 시작할 수 있도록 클라이언트와 서버는 준비 과정을 거친다.
클라이언트 : 레벨에 대한 정보를 서버로 부터 받아 맵을 로딩. 로딩이 완료되면 Join 패킷을 보낸다.
서버 : Join 패킷을 받으면 Login 과정을 거치면서 클라이언트를 대표하는 플레이어 컨트롤러의 생성
커넥션 핸드셰이킹의 확인
1. DataChannel.h
2. UWorld::NotifyControlMessage (서버)
3. UPendingNetGame::NotifyControlMessage (클라이언트)
알아두면 좋은 언리얼 네트워크 시스템 구성
번치, 패킷같은 개념을 이해하는데 NetDriver.h 를 참고하는 것이 도움된다.
용도에 따라 패킷을 처리하는 다양한 넷드라이버 클래스를 제공한다.
GameNetDriver : 게임 데이터를 처리하는데 사용하는 네트워크 드라이버
> 언리얼 엔진은 게임넷드라이버로 IpNetDriver을 사용한다.
DemoNetDriver : 게임 리플레이 데이터를 처리하는데 사용하는 네트워크 드라이버
BeaconNetDriver : 게임 외 데이터를 처리하는데 사용하는 네트워크 드라이버
언리얼 엔진에서 패킷은 채널을 통해서 분석된다. (이전 글에서 다뤘던 내용)
언리얼 엔진에서 번치를 처리하는데 사용하는데 사용하는 주요 채널은 다음과 같다
ControlChannel : 클라이언트 서버 간의 커넥션을 다룰 때 사용하는 채널. 초기 접속에 관련된 데이터 패킷은 이 채널을 통해 분석된다.
ActorChannel : 액터 리플리케이션 작업을 다룰 때 사용하는 채널
VoiceChannel : 음성 데이터를 전달할 때 사용
정리
1. 게임 로직을 구현하기 위해 알하야 하는 액터의 역할
> 커넥션을 기준으로하는 Local 역할, Remote 역할
> 역할의 종류는 None, Authority, AutonomousProxy, SimulatedProxy
2. 리슨 서버에서 액터 역할을 구분하기 위한 API와 이의 활용 방법
> HasAuthority ,GetLocalRole, GetRemoteRole, IsLocallyControlled, IsLocalController
3. 클라이언트와 서버간의 접속이 맺어지고 게임이 준비되는 과정의 이해
> 커넥션과 관련된 클래스와 함수, 전달되는 패킷 등
'언리얼 > 이득우 네트워크 멀티플레이' 카테고리의 다른 글
[이득우 네트워크 멀티플레이] 06. 액터 리플리케이션 빈도와 연관성 (0) | 2024.08.19 |
---|---|
[이득우 네트워크 멀티플레이] 05. 액터 리플리케이션 기초 (0) | 2024.08.16 |
[이득우 네트워크 멀티플레이] 03. 커넥션과 오너십 (0) | 2024.08.11 |
[이득우 네트워크 멀티플레이] 02. 게임 모드와 로그인 (0) | 2024.08.08 |
[이득우 네트워크 멀티플레이] 01. 언리얼 네트워크 프레임워크 개요 (0) | 2024.08.06 |