본문 바로가기

공부/이득우의 언리얼 프로그래밍

[Study] Part 3 - 커넥션과 오너십 (3/15)

728x90
반응형

 

 

 

  • 정리
    • 커넥션과 오너십
      1. 네트웍 멀티플레이어 게임에서 진행되는 초기화 과정의 학습
      2. 언리얼 엔진에서 네트웍 데이터가 로우레벨에서 전달되는 과정의 이해
      3. 커넥션을 담당하는 액터의 오너십을 필요성과 이를 설정하는 방법의 이해

 

  • 원격 액터의 초기화 과정

액터의 준비와 게임의 시작

 

// GameModeBase에 있는 로그인 관련 함수들
// PreLogin : 클라이언트의 접속 요청을 처리하는 함수
void AABGameMode::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("=========================================================================================="));
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	Super::PreLogin(Options, Address, UniqueId, ErrorMessage);

	// ErrorMessage를 할당하지 않으면 접속 허용이지만, ErrorMessage를 할당하게 되면 에러로 간주하고 접속을 제한 시켜버린다.
	ErrorMessage = TEXT("Server is full");

	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
}
  • ABGameMode.cpp

 

  • 다음과 같이 ErrorMessage를 할당하지 않으면 접속 허용이지만, ErrorMessage를 할당하게 되면 에러로 간주하고 접속을 제한시켜 버린다.

 

Client의 경우 접속이 제한됨

 

  • ErrorMessage가 할당되었기 때문에 Server에서는 PreLogin을 하지만, 다음 실행한 Client의 경우 Client가 아닌 Standalone으로 동작하는 것을 볼 수 있다.

 

// PostInitializeComponents는 네트워크와 무관하게 액터를 초기화할 때 사용하는 것
void AABPlayerController::PostInitializeComponents()
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	Super::PostInitializeComponents();

	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
}

// PostNetInit은 원격 클라이언트에서 네트워크로 초기화에 필요한 정보를 전달받은 것이 모두 마무리가 되면 호출된다.
// 서버가 아닌 클라이언트에서만 호출된다.
void AABPlayerController::PostNetInit()
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	Super::PostNetInit();

	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
}
  • ABPlayerController.cpp

 

  • ABPlayerControllerPostInitializeComponentsPostNetInit을 만들어 호출 타이밍을 확인해보자.

 

  • PostInitializeComponents는 네트워크와 무관하게 액터를 초기화할 때 호출된다.
  • PostNetInit은 원격 클라이언트에서 네트워크로 초기화에 필요한 정보를 전달받은 것이 모두 마무리가 되면 호출된다.

 

PostInitializeComponents와 PostNetInit의 호출 타이밍

 

  • Server에서는 서버를 열기전 플레이어를 생성하기위해 PostInitializeComponents가 호출되는 모습을 볼 수 있고, 서버를 연 후, 클라이언트를 위한 플레이어를 만들기 위해 PostInitializeComponents가 호출되는 걸 볼 수 있다.
  • 이후, 클라이언트에서 PostInitializeComponents가 호출되고, PostNetInit이 호출된 뒤, BeginPlay 로직이 호출되는 것을 확인할 수 있다.

 

  • 그렇다면 StartPlay를 하지 않으면 어떻게 나올까? ABGameMode에 있는 StartPlay의 로직을 주석처리하고 실행해보자.

 

// StartPlay : 게임의 시작을 지시하는 함수
void AABGameMode::StartPlay()
{
	/*AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	Super::StartPlay();

	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));*/
}
  • ABGameMode.cpp

 

ABGameMode에서 StartPlay에 주석 처리를 했을 때

 

  • ABGameModeStartPlay에서 아무것도 실행되지 않으니, 게임이 시작되지 않는 모습을 볼 수 있다.
  • StartPlay가 실행되어야 BeginPlay가 호출되는데, BeginPlay가 호출되지않아 숄더뷰로 보여지고, 클라이언트를 추가해도 같은 모습인 걸 볼 수 있다.
  • 이를 이용해 게임이 시작하기 전 호출되는 함수들을 바탕으로 로비 시스템등을 만들 수 있다.

 

  • 언리얼 엔진에서의 커넥션 구성

네트웍 통신을 담당하는 주요 클래스

  • PlayerController 클래스 : 네트웍 통신에 접근가능한 게임 내 대표 액터
  • UNetConnection 클래스 : 주고받는 패킷 데이터의 인코딩 디코딩, 네트웍 통신량 조절, 채널 관리
  • UNetDriver 클래스 : 로우레벨에서의 소켓 관리와 패킷 처리, 네트웍 통신 설정
  • 언리얼의 통신은 크게 하이 레벨로우 레벨로 나눌 수 있다.
    • 하이(High) 레벨
      • 게임을 구성하는 단위인 액터, 컴포넌트, 월드와 같이 컨텐츠를 구성하는 오브젝트의 상태와 속성에 관련된 어떤 상위 개념
    • 로우(Low) 레벨
      • 이러한 상태와 속성을 네트워크를 통해 전달하기 위해 만들어진 어떤 데이터 스트림
    • 예시
      • 플레이어가 아이템을 습득했다면 하이 레벨에서는 폰의 속성 값이 변경될 것이고, 이를 클라이언트에 전달하기 위해서는 지정된 규칙에 따라 변경된 데이터를 데이터 스트림을 통해 네트워크로 전달해야 한다.

 

  • 서버의 네트워크 초기화 과정
    • 현재 월드에 넷드라이버가 있으면 클라이언트-서버, 아니면 스탠드얼론으로 판단함
    • 서버는 월드의 Listen 함수를 호출해 넷드라이버를 생성함으로 네트웍 기능을 시작함
    • UWorld::InternalGetNetMode()
      • 넷 모드가 어떻게 파악되는 지 확인 가능
    • UWorld::Listen()
      • 서버가 어떻게 시작되는지 확인 가능

 

Ctrl + Shift + F / 전체 솔루션에서 찾기

 

  • World에 있는 InternalGetNetMode를 찾기

 

ENetMode UWorld::InternalGetNetMode() const
{
	if ( NetDriver != NULL )
	{
		const bool bIsClientOnly = IsRunningClientOnly();
		return bIsClientOnly ? NM_Client : NetDriver->GetNetMode();
	}

	PRAGMA_DISABLE_DEPRECATION_WARNINGS
	if ( DemoNetDriver )
	{
		return DemoNetDriver->GetNetMode();
	}
	PRAGMA_ENABLE_DEPRECATION_WARNINGS

	ENetMode URLNetMode = AttemptDeriveFromURL();
#if WITH_EDITOR
	if (WorldType == EWorldType::PIE && URLNetMode == NM_Standalone && !AreActorsInitialized())
	{
		// If we're too early in startup and it defaults to Standalone, use the mode we were created with
		// Once the world has been initialized the URL will be correct so we will use that
		// This is required for dedicated server worlds so it is correct for InitWorld
		return PlayInEditorNetMode;
	}
#endif
	return URLNetMode;
}
  • World.cpp의 InternalGetNetMode()

 

  • bIsClientOnlytrue이면 NM_Client, false이면 NetDriver->GetNetMode()를 따른다.

 

ENetMode UNetDriver::GetNetMode() const
{
	// Special case for PIE - forcing dedicated server behavior
#if WITH_EDITOR
	if (World && World->WorldType == EWorldType::PIE && IsServer())
	{
		//@todo: world context won't be valid during seamless travel CopyWorldData
		FWorldContext* WorldContext = GEngine->GetWorldContextFromWorld(World);
		if (WorldContext && WorldContext->RunAsDedicated)
		{
			return NM_DedicatedServer;
		}
	}
#endif

	// Normal
	return (IsServer() ? (GIsClient ? NM_ListenServer : NM_DedicatedServer) : NM_Client);
}
  • NetDriver.cpp의 GetNetMode()

 

  • 서버인경우 클라이언트로 참여하면 NM_ListenServer, 그게 아니라면 NM_DedicatedServer. 서버가 아니라면 NM_Client
  • GetNetMode() 함수를 호출해서 네트웍 모드를 파악할 때 중간에 바뀌는 이유가 넷 드라이버의 유무에 따라서 바뀌게 된다.

 

  • 넷드라이버의 커넥션 관리
    • 넷드라이버는 다수의 커넥션을 관리하고 있으며, 서버와 클라이언트에 따라 다르게 동작함
    • 클라이언트에서 넷드라이버는 항상 하나의 서버 커넥션을 가진다.
    • 서버에서 넷드라이버는 다수의 클라이언트 커넥션을 가진다.
    • UNetDriver::IsServer()
      • 현재 어플리케이션이 서버 모드인지 클라이언트 모드인지를 구분

 

// PostLogin : 플레이어 입장을 위해 플레이어에 필요한 기본 설정을 모두 마무리하는 함수
void AABGameMode::PostLogin(APlayerController* NewPlayer)
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	Super::PostLogin(NewPlayer);

	// 넷 드라이버 가져오기
	UNetDriver* NetDriver = GetNetDriver();
	// 넷 드라이버가 있다면
	if (NetDriver)
	{
		// 현재 연결된 클라이언트가 없으면
		if (NetDriver->ClientConnections.Num() == 0)
		{
			AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No Client Connection"));
		}
		// 현재 연결된 클라이언트가 있다면
		else
		{
			for (const auto& Connection : NetDriver->ClientConnections)
			{
				AB_LOG(LogABNetwork, Log, TEXT("Client Connections : %s"), *Connection->GetName());
			}
		}
	}
	// 넷 드라이버가 없다면
	else
	{
		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No NetDriver"));
	}

	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
}
  • ABGameMode.cpp

 

  • 서버의 PostLogin에서 넷 드라이버를 가져와 현재 연결된 클라이언트를 확인할 수 있도록 로그를 찍어준다.

 

// PostNetInit은 원격 클라이언트에서 네트워크로 초기화에 필요한 정보를 전달받은 것이 모두 마무리가 되면 호출된다.
// 서버가 아닌 클라이언트에서만 호출된다.
void AABPlayerController::PostNetInit()
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	Super::PostNetInit();

	// 넷 드라이버 가져오기
	UNetDriver* NetDriver = GetNetDriver();
	// 넷 드라이버가 있다면
	if (NetDriver)
	{
		if (NetDriver->ServerConnection)
		{
			// 클라이언트는 하나의 클라이언트 커넥션만을 가지고 있음
			AB_LOG(LogABNetwork, Log, TEXT("Server Connections : %s"), *NetDriver->ServerConnection->GetName());
		}
	}
	// 넷 드라이버가 없다면
	else
	{
		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No NetDriver"));
	}

	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
}
  • ABPlayerController.cpp

 

  • 클라이언트에서 현재 커넥션된 클라이언트를 로그로 찍어준다.

 

Client Connection과 Server Connection

  • 클라이언트가 추가될 때마다 서버에서는 PostLogin에서 Client Connections가 추가되고, 각 클라이언트는 PostNetInit에서 Server Connection이 호출되는 모습을 확인할 수 있다.

 

  • 커넥션과 오너십
    • 언리얼 엔진에서의 데이터 관리
      • 네트웍에서 주고 받는 데이터들은 다음과 같은 고도화 작업을 거친다.
        • 커넥션(Connection) : 모든 데이터를 전달하는 네트웍 통로
        • 패킷(Packet) : 네트웍을 통해 전달되는 단위 데이터. 숫자 혹은 문자로 구성
        • 채널(Channel) : 언리얼 엔진 아키텍쳐에 따라 구분된 데이터를 전달하는 논리적인 통로
        • 번치(Bunch) : 언리얼 엔진의 아키텍쳐에 사용되는 데이터. 하나의 명령에 대응하는 데이터 묶음
      • 데이터 통신을 관리하기 위한 대표 액터로 플레이어 컨트롤러가 주로 사용
        • 서버의 경우 여러개의 플레이어 컨트롤러가 만들어지고, 클라이언트에서는 딱 하나만 만들어지게 된다.
      • 커넥션을 담당하는 대표 액터는 커넥션에 대한 오너십을 가진다고 표현한다.
        • 소유권

 

  • 접속 소유권의 중요한 이유
    • 접속 소유권을 통해 이후 RPC를 호출할 때 접속 소유권이 있어야 되고, 접속 소유권에 따라서 어떤 클라이언트에 실행될지를 결정해야 된다.
    • 접속 소유권을 바탕으로 모든 액터를 다 동기화 시키는 것이 아닌 소유권만 가지고 있는 액터만 동기화 시킬 수도 있다.
    • 액터 프로퍼티 리플리케이션조건에서도 프로퍼티를 모두 다 보내는 것이 아니고 소유권을 가진 애만 필터링해서 보낼 수 있다.
  • 액터와 플레이어 컨트롤러의 넷커넥션
    • 어떤 액터가 통신을 하기 위해서는 자신을 소유한 액터가 커넥션을 소유하고 있어야 한다.
    • 일반적으로 플레이어 컨트롤러는 넷커넥션을 소유하고 있다.
    • 넷커넥션 역시 플레이어 컨트롤러를 소유하고 있다.
    • APlayerController::GetNetConnection()
    • AActor::GetNetConnection()
UNetConnection* AActor::GetNetConnection() const
{
	return Owner ? Owner->GetNetConnection() : nullptr;
}
  • Actor.cpp

 

  • 이때, OwnerActor가 될수도, Pawn이 될수도, PlayerController가 될수도 있다. 다 각자 오버라이드 되어 있다.
  • Actor에서는 Owner가 있다면 Owner->GetNetConnection()을 리턴하고, 없으면 nullptr
  • PlayerController에서는 Player가 존재한다면 실제 NetConnection 객체를 리턴. 결국에는 플레이어 컨트롤러가 어떤 액터를 소유하게 되면 그 액터를 소유한 오너의 넷 커넥션이 호출
  • PawnController가 있다면 ControllerNetConnection을 가져온다. 그게 아니라면 Super 즉, ActorGetNetConnection 함수를 호출한다고 되어 있다.
  • 어떤 액터가 통신을 진행할 때 오너값이 설정 되어 있지 않으면 통신이 진행 안 되게끔 GetNetConnection 함수가 구성되어 있다.

 

  • 플레이어 컨트롤러를 통한 설정 예시
    • 플레이어 컨트롤러가 캐릭터에 빙의하면 캐릭터의 오너로 설정
      • 서버 : 빙의(Possess) 함수를 호출해 오너십을 설정
      • 클라이언트 : 오너십이 설정된 캐릭터의 속성이 배포되면서 자신이 조종하는 캐릭터에 오너십이 설정됨
    • 이 때 캐릭터가 무기 액터를 소유하면 무기 액터는 통신이 가능한 상태가 됨
      • 플레이어 컨트롤러 : 통신 가능
      • 플레이어 컨트롤러가 소유한 액터 : 통신 가능
      • 플레이어 컨트롤러가 소유한 액터가 소유한 무기 액터 : 통신 가능

 

// 클라이언트에서는 호출되지않음
void AABCharacterPlayer::PossessedBy(AController* NewController)
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	// Super 전
	AActor* OwnerActor = GetOwner();
	if (OwnerActor)
	{
		AB_LOG(LogABNetwork, Log, TEXT("Owner : %s"), *OwnerActor->GetName());
	}
	else
	{
		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No Owner"));
	}

	Super::PossessedBy(NewController);

	// Super 후
	OwnerActor = GetOwner();
	if (OwnerActor)
	{
		AB_LOG(LogABNetwork, Log, TEXT("Owner : %s"), *OwnerActor->GetName());
	}
	else
	{
		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No Owner"));
	}

	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
}

// Owner의 값이 변경이 되면 함수가 호출된다.
// 클라이언트는 OnRep_Owner 함수를 통해 Owner값이 동기화 된다.
void AABCharacterPlayer::OnRep_Owner()
{
	AB_LOG(LogABNetwork, Log, TEXT("%s %s"), *GetName(), TEXT("Begin"));

	Super::OnRep_Owner();

	AActor* OwnerActor = GetOwner();
	if (OwnerActor)
	{
		AB_LOG(LogABNetwork, Log, TEXT("Owner : %s"), *OwnerActor->GetName());
	}
	else
	{
		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No Owner"));
	}

	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
}

// PostNetInit는 네트워크를 통해가지고 모든 데이터가 다 전달을 받았기 때문에 Owner가 들어왔을 것
// 클라이언트에서는 빙의가 일어나지 않는다. (=OnPossess 함수가 호출되지않음)
void AABCharacterPlayer::PostNetInit()
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	Super::PostNetInit();

	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
}
  • ABCharacterPlayer.cpp

 

  • 빙의(PossessedBy)클라이언트에서는 호출되지않는다.
  • OnRep_OwnerOwner의 값이 변경되면 함수가 호출되고, 클라이언트는 이 함수를 통해 Owner값이 동기화된다.

 

클라이언트 추가, OnRep_Owner

  • 클라이언트가 추가되자 OnRep_Owner가 호출되면서 ABCharacterPlayer_0에 대해서만 오너가 복제되면서 오너가 설정되는 것을 확인할 수 있다.

 

 

 

 

 

해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이어 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
 

이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해 강의 | 이득우 - 인프런

이득우 | 또 하나의 언리얼 엔진이라고도 불리는 네트웍 멀티플레이어 프레임웍을 학습합니다. 네트웍 멀티플레이어 게임을 제작할 때 반드시 알아야 하는 주요 개념, 내부 동작 원리, 최적화

www.inflearn.com

 

 

 

 

728x90