네트웍 커넥션과 캐릭터 설정 과정의 이해

  • 네트웍 멀티 플레이어에서 원격 액터의 초기화 과정의 이해
  • 로우레벨에서 진행되는 네트웍 처리 방식의 이해
  • 네트웍 멀티플레이어에서 사용하는 오너십 개념의 이해

 

 

인프런 이득우님의 '언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해' 강의를 참고하였습니다. 
😎 [이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해] 강의 들으러 가기!

 

 

 

목차

     

     


     

     

    원격 액터의 초기화 과정

     

    네트웍 커넥션과 캐릭터 설정 과정의 이해


     

     

    학습 주제: Connection + Ownership

     


     

     

    액터의 준비와 게임의 시작

     

    ※ 액터의 초기화 방법 2가지

    • PostInitializeComponents() :  게임과 무관한 액터 초기화 시 사용
      • PostInitializeComponents()함수는 액터를 설정하는 기능이라 네트웍과는 관련이 없다. 그래서 네트워크 기능 사용시에는 PostNetInit() 함수를 추가로 사용해야 한다. 
      •  PostNetInit() 
        •  PostNetInit() 함수는 원격 클라이언트에서 호출된다.
        • 서버가 아닌 클라이언트에서만 호출된다.
        • 서버에서 변경된 내용들(ex.속성, 다양한 값) 모두 전달 받아서 원격 클라이언트의 액터 세팅이 모두 마무리될 때 호출된다. (= 원격 클라이언트에서 네트워크로 초기화에 필요한 정보를 전달받는 것이 모두 마무리 되면 호출됨)

     

    • BeginPlay() :  현재 로딩된 레벨에서 게임 플레이와 관련된 액터 초기화 시 사용
      • BeginPlay()는 GameModeStartPlay() 함수를 통해 호출된다. 이를 위해서는 네트워크 상에 있는 모든 플레이어가 준비가 되어야 하는데 모든 사람이 준비될 때까지 BeginPlay()가 호출되지 않을 수 있다.
      • 클라이언트 입장: 서버 컨텐츠의 허상을 전달받는 형태이기 때문에 게임을 시작하기 전 준비과정에서 서버에세 변경 상항이 발생하면 네트워크를 통해서 변경된 속성을 미리 전달 받아야 한다.
    • 두 함수 사용은 초기화하려는 액터의 데이터가 게임 플레이와 관련이 있는지 없는지로 나눌 수 있다.

     

     

     

    게임과 무관한 액터 설정의 초기화는 PostInitializeComponents를 통해 초기화하는 것이 좋다.

    원격 클라이언트에서 네트워크 관련 설정의 초기화는 PostNetInit

    게임 진행에 필요한 초기화는 BeginPlay

     


     

     

    예제 실습

     


     

     

    예제 1:  GameMode  -   PreLogin 단계에서 접속 제한하기 

     

    GameMode.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/GameModeBase.h"
    #include "Interface/ABGameInterface.h"
    #include "ABGameMode.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABGameMode : public AGameModeBase, public IABGameInterface
    {
    	GENERATED_BODY()
    public:
    	AABGameMode();
    
    	virtual void PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) override;
    	virtual APlayerController* Login(UPlayer* NewPlayer, ENetRole InRemoteRole, const FString& Portal, const FString& Options, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) override;
    	virtual void PostLogin(APlayerController* NewPlayer) override;
    
    	virtual void StartPlay() override;
    };

     

    GameMode.cpp

    더보기
    #include "Game/ABGameMode.h"
    #include "ABGameMode.h"
    #include "Player/ABPlayerController.h"
    #include "ArenaBattle.h"
    #include "ABGameState.h"
    
    AABGameMode::AABGameMode()
    {
    	static ConstructorHelpers::FClassFinder<APawn> DefaultPawnClassRef(TEXT("/Script/Engine.Blueprint'/Game/ArenaBattle/Blueprint/BP_ABCharacterPlayer.BP_ABCharacterPlayer_C'"));
    	if (DefaultPawnClassRef.Class)
    	{
    		DefaultPawnClass = DefaultPawnClassRef.Class;
    	}
    
    	static ConstructorHelpers::FClassFinder<APlayerController> PlayerControllerClassRef(TEXT("/Script/ArenaBattle.ABPlayerController"));
    	if (PlayerControllerClassRef.Class)
    	{
    		PlayerControllerClass = PlayerControllerClassRef.Class;
    	}
    
    	GameStateClass = AABGameState::StaticClass();
    }
    
    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 = TEXT("Server is full");
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    APlayerController* AABGameMode::Login(UPlayer* NewPlayer, ENetRole InRemoteRole, const FString& Portal, const FString& Options, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	APlayerController* NewPlayerController = Super::Login(NewPlayer, InRemoteRole, Portal, Options, UniqueId, ErrorMessage);
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    
    	return NewPlayerController;
    }
    
    void AABGameMode::PostLogin(APlayerController* NewPlayer)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::PostLogin(NewPlayer);
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    void AABGameMode::StartPlay()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::StartPlay();
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }

     

     

    작업한 부분

    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 = TEXT("Server Is Full"); // 다음과 같은 텍스트를 할당하면 에러로 간주하고 접속을 제한 시킨다.
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }

     

     

    실행결과


     

     

     

    예제 2:  PostNetInit 함수 분석하기

     

    PlayerController.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/PlayerController.h"
    #include "ABPlayerController.generated.h"
    
    DECLARE_LOG_CATEGORY_EXTERN(LogABPlayerController, Log, All);
    
    UCLASS()
    class ARENABATTLE_API AABPlayerController : public APlayerController
    {
    	GENERATED_BODY()
    	
    public:
    	AABPlayerController();
    	
    protected:
    	virtual void PostInitializeComponents() override;
    	virtual void PostNetInit() override;
    	virtual void BeginPlay() override;
    	virtual void OnPossess(APawn* InPawn) override;
    
    protected:
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUD)
    	TSubclassOf<class UABHUDWidget> ABHUDWidgetClass;
    
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = HUD)
    	TObjectPtr<class UABHUDWidget> ABHUDWidget;
    };

     

    PlayerController.cpp

     

     

     

    실행결과


     

     

    예제 3:  GameMode - StartPlay() 함수 실행되지 않게 하기

     

    위의 ABGameMode에서 StartPlay() 함수 실행되지 않게 주석 처리한다.

    void AABGameMode::StartPlay()
    {
    	// AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    	// Super::StartPlay();
    	// AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }

     

    상위 클래스의 StartPlay() 함수가 호출이 되지 않으면 게임이 시작이 안 된다.

     

     

    실행화면

     

    쿼터뷰 셋업은 BeginPlay()에서 진행되기 때문에 위와 같이 게임을 시작하지 않아 쿼터뷰 셋업이 되지 않은 화면이 보여지게 된다.

     

    PreLogin

    PostInitializeComponents 

    호출이 되지만 BeginPlay()는 호출되지 않는다.


     

     

    언리얼 엔진의 커넥션 구성


     

     

    엔리얼 엔진의 통신:   High Level  vs.  Low Level

     

    언리얼 엔진의 통신은 크게 하이레벨과 로우레벨로 나눌 수 있다.

    • 하이레벨: 게임을 구성하는 단위인 액터, 컴포넌트, 월드와 같이 컨텐츠 구성 오브젝트의 상태, 속성에 관련된 상위 개념이다.
    • 로우레벨: 상태, 속성을 네트웍을 통해 전달하기 위해 만들어진 데이터 스트림을 의미한다.

     

    • ex. 플레이어가 아이템을 습득했다면,
      • 하이레벨에서는 폰의 속성값이 변경될 것이다.
      • 이를 클라이언트에 전달하기 위해서는 지정된 규칙에 따라 변경된 데이터를 일련된 숫자의 흐름을 통해 네트워크로 전달해야 한다. 이런 역할을 담당하는 클래스가 필요하다.

     

     

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

     

       
     PlayerController 클래스  네트웍 통신에 접근가능한 게임 내 대표 액터
    • 네트웍 커넥션을 관리한다고 해서, 별도로 네트웍 작업을 지시할 필요는 없다.
    • 소유권에 대해서만 신경써주면 된다.
     UNetConnection 클래스  주고받는 패킷 데이터의 인코딩 디코딩, 네트웍 통신량 조절, 채널 관리
    • PlayerController 로부터 전달된 명령이 UNetConnection에서 관리된다.
      UNetConnection 클래스는 상위에서 네트워크를 담당하는 클래스다.
    • 상위 명령을 로우 레벨 데이터로 변환할 수 있는 기초 작업 등을 수행한다.
    • 밴드위스, 대역폭 등 흐름 조절 역할도 담당한다.
     UNetDriver 클래스  로우레벨에서의 소켓 관리와 패킷 처리, 네트웍 통신 설정
    • 커넥션을 관리한다.
    • UNetDriver 클래스가 없으면 스탠드얼론이다.
    • UNetConnection에서 정돈된 데이터를 실제 네트워크 상으로 보내는 로우레벨 기능을 제공한다.
    • UNetDriver 를 통한 데이터가 클라이언트에게 전달되면, 클라이언트는 거꾸로 UNetConnection 을 거쳐서 액터에 명령을 내린다.

     


     

     

    서버의 네트워크 초기화 과정

     

    • 현재 월드에 UNetDriver가 있으면 클라이언트-서버, 아니면 스탠드얼론으로 판단함. (강의에서 스탠드얼론으로 시작한 후 리슨서버로 들어간 것이 대표적인 예이다)
    • (내부적으로 월드가 구동될 때) 서버는 월드의 Listen 함수를 호출되면 월드에 UNetDriver 객체를 생성되고 이로부터 네트웍 통신이 시작된다.

    UWorld:: InternalGetNetMode() 함수

    UWorld::Listen() 함수

     

     

    World.cpp

    UWorld::InternalGetNetMode() 함수

    • 통신을 담당하는 UNetDriver가 설정되어 있다면 Clinet(NM_Client) 또는 Server(NetDriver->GetNetMode())다.

     

    NetDriver.cpp

    먼저 Dedicated Server인 경우 NM_DedicatedServer로 실행

     

    IsServer() : 서버인지 아닌지 판별

     

    GIsClient :  내가 플레이어로서 게임에 참여하는 어플리케이션인지 판별하는 전역변수

    • 게임에 참여하는 경우 NM_ListenServer
    • 게임에 참여하지 않는 경우 NM_DedicatedServer

     

     

    넷드라이버의 커넥션 관리 ( UNetDriver::IsServer() 함수 )

     

    • NetDriver는 네트워크 모드에 따라서 다수의 커넥션을 소유/관리하고 있으며, 서버와 클라이언트에 따라 다르게 동작함.
      • 네트워크 모드가 서버인 경우: 
        • NetDriver는 다수의 Connection을 소유.
      • 네트워크 모드가 클라이언트인 경우:   
        • NetDriver는 하나의 ServerConnection만 소유.
      • 언리얼 엔진은 이러한 특징을 사용해서 현재 어플리케이션이 서버 모드인지, 클라이언트 모드인지 구분
        • 이 때, UNetDriver::IsServer() 함수 사용
    • 클라이언트에서 NetDriver는 항상 하나의 서버 커넥션을 가진다.
    • 서버에서 NetDriver는 다수의 클라이언트 커넥션을 가진다.

     

    UNetDriver::IsServer() 함수

     

    NetDriver.cpp

    ServerConnection == NULL인 경우, 서버로 판단

    ServerConnection != NULL인 경우, 클라이언트로 판단


     

     

    예제실습 :  GameMode - PostLogin에 NetDriver로 접속한 클라이언트 확인하

     

    GameMode.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/GameModeBase.h"
    #include "Interface/ABGameInterface.h"
    #include "ABGameMode.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABGameMode : public AGameModeBase, public IABGameInterface
    {
    	GENERATED_BODY()
    public:
    	AABGameMode();
    
    	virtual void PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) override;
    	virtual APlayerController* Login(UPlayer* NewPlayer, ENetRole InRemoteRole, const FString& Portal, const FString& Options, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) override;
    	virtual void PostLogin(APlayerController* NewPlayer) override;
    
    	virtual void StartPlay() override;
    };

     

     

    GameMode.cpp

    더보기
    #include "Game/ABGameMode.h"
    #include "ABGameMode.h"
    #include "Player/ABPlayerController.h"
    #include "ArenaBattle.h"
    #include "ABGameState.h"
    
    AABGameMode::AABGameMode()
    {
    	static ConstructorHelpers::FClassFinder<APawn> DefaultPawnClassRef(TEXT("/Script/Engine.Blueprint'/Game/ArenaBattle/Blueprint/BP_ABCharacterPlayer.BP_ABCharacterPlayer_C'"));
    	if (DefaultPawnClassRef.Class)
    	{
    		DefaultPawnClass = DefaultPawnClassRef.Class;
    	}
    
    	static ConstructorHelpers::FClassFinder<APlayerController> PlayerControllerClassRef(TEXT("/Script/ArenaBattle.ABPlayerController"));
    	if (PlayerControllerClassRef.Class)
    	{
    		PlayerControllerClass = PlayerControllerClassRef.Class;
    	}
    
    	GameStateClass = AABGameState::StaticClass();
    }
    
    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 = TEXT("Server is full");
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    APlayerController* AABGameMode::Login(UPlayer* NewPlayer, ENetRole InRemoteRole, const FString& Portal, const FString& Options, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	APlayerController* NewPlayerController = Super::Login(NewPlayer, InRemoteRole, Portal, Options, UniqueId, ErrorMessage);
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    
    	return NewPlayerController;
    }
    
    void AABGameMode::PostLogin(APlayerController* NewPlayer)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::PostLogin(NewPlayer);
    
    	UNetDriver* NetDriver = GetNetDriver();
    	if (NetDriver)
    	{
    		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"));
    }
    
    void AABGameMode::StartPlay()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::StartPlay();
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }

     

    ABPlayerController.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/PlayerController.h"
    #include "ABPlayerController.generated.h"
    
    DECLARE_LOG_CATEGORY_EXTERN(LogABPlayerController, Log, All);
    
    UCLASS()
    class ARENABATTLE_API AABPlayerController : public APlayerController
    {
    	GENERATED_BODY()
    public:
    	AABPlayerController();
    	
    protected:
    	virtual void PostInitializeComponents() override;
    	virtual void PostNetInit() override;
    	virtual void BeginPlay() override;
    
    protected:
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUD)
    	TSubclassOf<class UABHUDWidget> ABHUDWidgetClass;
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = HUD)
    	TObjectPtr<class UABHUDWidget> ABHUDWidget;
    };

     

    ABPlayerController.cpp

    더보기
    #include "Player/ABPlayerController.h"
    #include "UI/ABHUDWidget.h"
    #include "Kismet/GameplayStatics.h"
    #include "ABSaveGame.h"
    #include "ArenaBattle.h"
    
    DEFINE_LOG_CATEGORY(LogABPlayerController);
    
    AABPlayerController::AABPlayerController()
    {
    	static ConstructorHelpers::FClassFinder<UABHUDWidget> ABHUDWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_ABHUD.WBP_ABHUD_C"));
    	if (ABHUDWidgetRef.Class)
    	{
    		ABHUDWidgetClass = ABHUDWidgetRef.Class;
    	}
    }
    
    void AABPlayerController::PostInitializeComponents() // 네트워크와 무관하게 액터를 초기화할 때 사용
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::PostInitializeComponents();
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    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 Connection: %s"), *NetDriver->ServerConnection->GetName());
    		}
    	}
    	else
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No NetDriver"));
    	}
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    void AABPlayerController::BeginPlay()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::BeginPlay();
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    
    	FInputModeGameOnly GameOnlyInputMode;
    	SetInputMode(GameOnlyInputMode);
    }

     

     

     

    작업한 부분

    void AABGameMode::PostLogin(APlayerController* NewPlayer)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    	Super::PostLogin(NewPlayer);
    
    	UNetDriver* NetDriver = GetNetDriver();
    	if (NetDriver)
    	{
    		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"));
    }

     

     

    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 Connection: %s"), *NetDriver->ServerConnection->GetName());
    		}
    	}
    	else
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No NetDriver"));
    	}
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }

     

     

    실행화면

     

    클라이언트 입장 후


     

     

     

    커넥션과 오너십 Connection & Ownership

     


     

     

     

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

     

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

     

    "

    각 접속에는 PlayerController 가 있으며, 해당 접속 전용으로 만들어진 것입니다. 그러한 이유로 생성되는 각 PlayerController는 해당 접속에 소유됩니다. 일반적으로 액터가 접속에 소유되었는지 알아보려면, 액터의 Outer 오너에 질의를 하여, 그 오너가 PlayerController 인 경우 그 액터 역시도 그 PlayerController 를 소유하는 동일 접속에 소유된 것입니다.

     

    이에 대한 한 가지 예제는, Pawn 액터가 PlayerController 에 빙의(possess)될 때입니다. 그 오너는 빙의된  PlayerController가 될 것입니다. 이 동안 액터는 PlayerController 의 접속에 소유됩니다. Pawn 은 PlayerController 에 의해 소유/빙의된 기간동안 이 접속에 소유됩니다. 그리고 PlayerController 가 더이상 Pawn 에 빙의하지 않게 되면, Pawn 은 더이상 접속에 소유되지 않습니다.

    또 한가지 예제는 Pawn 에 소유된 인벤토리 아이템입니다. 이 인벤토리 아이템은 Pawn 을 소유한 (것이 있다면) 동일 접속에 소유됩니다.

    "

     

     

     

    https://dev.epicgames.com/documentation/ko-kr/unreal-engine/actor-owner-and-owning-connection-in-unreal-engine

     

    언리얼 엔진의 액터와 그 접속 소유

    멀티플레이어에서 서버의 역할에 대한 개요입니다.

    dev.epicgames.com


     

     

    접속 소유권이 중요한 이유

     

    • RPC는 어느 클라이언트에서 RPC를 실행할지 결정해야 한다.
      • RPC를 호출할 때 접속 소유권이 있어야 한다.
    • 액터 리플리케이션 및 접속 연관성 때문이다.
      • 소유권을 가지고 있는 액터만 동기화 시킬 수 있는 옵션이 있다.
    • 오너가 연관된 경우의 액터 프로퍼티 리플리케이션 조건 때문이다.

     

    접속 소유권을 사용함으로써 옵션이 늘어난다

    = 우리가 네트워크에서 통신하는 데이터양을 최적화할 수 있다!!


     

     

     

    액터와 플레이어 컨트롤러의 넷커넥션

     

    • 어떤 액터가 통신을 하기 위해서는 자신을 소유한 액터(Owner)가 Connection을 소유하고 있어야 한다.
    • 일반적으로 PlayerController는 NetConnection을 소유하고 있다.
      • APlayerController::GetNetConnection()
        • AActor::GetNetConnection()를 override한 것이다.
        • Player가 존재하면, 액터를 소유한 오너의 NetConnection 객체를 반환
        •  
    • NetConnection 역시 자신을 담당하고 있는 플레이어 컨트롤러에 대한 정보를 소유하고 있다.
      • AActor::GetNetConnection() 함수
        • Owner가 있을 때 오너의 GetNetConnection 호출
      • APawn::GetNetConnection() 함수
        • 플레이어 컨트롤러의 GetNetConnection 호출

     

    AActor::GetNetConnection() 함수

    APlayerController::GetNetConnection() 함수

     

     

    Actor.cpp의  AActor::GetNetConnection() 함수

    소유자(Owner)가 있을때 Owner의 GetNetConnection()을 호출한다. 

    소유자(Owner)가 없다면 NetConnection이 없다고 판단하여 nullptr를 반환한다.

     

     

    PlayerController.cpp의  APlayerController::GetNetConnection() 함수

    Player가 존재하면 NetConnection을 반환한다.

     

     

    PlayerController.cpp의  APlayerController::GetNetConnection() 함수

    Controller가 있으면 Controller의 GetNetConnection() 함수를 호출하여 NetConnection을 반환한다.


     

     

    플레이어 컨트롤러를 통한 설정 예시 - 빙의

     

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

     

    예제 코드: Player, Pawn, Actor 간에 Connection에 대한 Ownership 설정

     

    ABPlayerController.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "GameFramework/PlayerController.h"
    #include "ABPlayerController.generated.h"
    
    DECLARE_LOG_CATEGORY_EXTERN(LogABPlayerController, Log, All);
    
    UCLASS()
    class ARENABATTLE_API AABPlayerController : public APlayerController
    {
    	GENERATED_BODY()
    public:
    	AABPlayerController();
    	
    protected:
    	virtual void PostInitializeComponents() override;
    	virtual void PostNetInit() override;
    	virtual void BeginPlay() override;
    	virtual void OnPossess(APawn* InPawn) override;
    
    protected:
    	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUD)
    	TSubclassOf<class UABHUDWidget> ABHUDWidgetClass;
    	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = HUD)
    	TObjectPtr<class UABHUDWidget> ABHUDWidget;
    };

     

    ABPlayerController.cpp

    더보기
    #include "Player/ABPlayerController.h"
    #include "UI/ABHUDWidget.h"
    #include "Kismet/GameplayStatics.h"
    #include "ABSaveGame.h"
    #include "ArenaBattle.h"
    
    DEFINE_LOG_CATEGORY(LogABPlayerController);
    
    AABPlayerController::AABPlayerController()
    {
    	static ConstructorHelpers::FClassFinder<UABHUDWidget> ABHUDWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_ABHUD.WBP_ABHUD_C"));
    	if (ABHUDWidgetRef.Class)
    	{
    		ABHUDWidgetClass = ABHUDWidgetRef.Class;
    	}
    }
    
    void AABPlayerController::PostInitializeComponents() // 네트워크와 무관하게 액터를 초기화할 때 사용
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::PostInitializeComponents();
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    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 Connection: %s"), *NetDriver->ServerConnection->GetName());
    		}
    	}
    	else
    	{
    		AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("No NetDriver"));
    	}
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }
    
    void AABPlayerController::BeginPlay()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::BeginPlay();
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    
    	FInputModeGameOnly GameOnlyInputMode;
    	SetInputMode(GameOnlyInputMode);
    }
    
    void AABPlayerController::OnPossess(APawn* InPawn)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    
    	Super::OnPossess(InPawn);
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }

     


    ABCharacterPlayer.h

    #pragma once
    #include "CoreMinimal.h"
    #include "Character/ABCharacterBase.h"
    #include "ABCharacterPlayer.generated.h"
    
    UCLASS()
    class ARENABATTLE_API AABCharacterPlayer : public AABCharacterBase, public IABCharacterHUDInterface
    {
    	GENERATED_BODY()
    public:
    	AABCharacterPlayer();
    
    protected:
    	virtual void BeginPlay() override;
    	virtual void PossessedBy(AController* NewController) override;
    	virtual void OnRep_Owner() override; // Owner가 변경되면 호출되는 함수
    	virtual void PostNetInit() override;
    	//... 생략 ...
    };

     

    ABCharacterPlayer.cpp

    #include "Character/ABCharacterPlayer.h"
    
    void AABCharacterPlayer::BeginPlay(){
    	생략 }
    
    void AABCharacterPlayer::PossessedBy(AController* NewController)
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));
    	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);
    
    	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"));
    }
    
    void AABCharacterPlayer::OnRep_Owner()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s %s"), *GetName(), TEXT("Begin"));
    
    	Super::PostNetInit();
    
    	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"));
    }
    
    void AABCharacterPlayer::PostNetInit()
    {
    	AB_LOG(LogABNetwork, Log, TEXT("%s %s"), TEXT("Begin"), *GetName());
    
    	Super::PostNetInit();
    
    	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("End"));
    }

     

     

    실행화면 1 - AABCharacterPlayer::PostNetInit()  함수 +  AABCharacterPlayer::OnRep_Owner() 함수 추가 전

     

    클라이언트 접속 후

     

     

     

    실행화면 2 - AABCharacterPlayer::PostNetInit()  함수 +  AABCharacterPlayer::OnRep_Owner() 함수 추가 

     

    클라이언트 접속 후


     

     

    정리

     

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