메모내용
Nav

Connection Flow ( 세상 접속 과정 )

Connection Flow ( 접속 플로우 )

NetworkMode
(네트워크모드)
Login
(로그인)
Connection
(커넥션)
Ownership
(오너십)
Role
(역할)

서버가 게임을 시작한 상태에서 클라이언트가 접속하는 과정을 커넥션 플로우라고 이야기를 하는데, 이 멀티플레이 게임을 만들기 위해서는 먼저 콘텐츠를 만드는 것이 아니라 클라이언트가 접속할 때 어떤 식으로 서버가 이것을 처리하고 이 클라이언트는 이 정보를 받아서 처리하는지 이러한 흐름들을 정확하게 이해하는 것이 무엇보다 중요하다.

Actor 의 초기화

  1. 생성자 초기화 ( Constructor )
  2. 액터 생성 초기화 ( PostInitializeComponents )
  3. 네트워크 초기화 ( PostNetInit )
  4. 게임플레이 초기화 ( BeginPlay )

접속과정에 대한 World 의 주요 함수

네트워크 모드는 현제 World 에 NetDriver 가 있는지 여부에 따라 판단한다.

                        

class UWorld : public UObject, public FNetworkNotify
{
    TObjectPtr<class UNetDriver> NetDriver;

    TObjectPtr<class AGameNetworkManager> NetworkManager;

private:
	/** List of all the controllers in the world. */
	TArray<TWeakObjectPtr<class AController> > ControllerList;

	/** List of all the player controllers in the world. */
	TArray<TWeakObjectPtr<class APlayerController> > PlayerControllerList;

public:

    // Start listening for connections.
    bool UWorld::Listen( FURL& InURL ){
        
        // Create net driver.
        // NetDriver 생성 . Network  기능의 핵심. 통신담당
        if (GEngine->CreateNamedNetDriver(this, NAME_GameNetDriver, NAME_GameNetDriver))
        {
            NetDriver = GEngine->FindNamedNetDriver(this, NAME_GameNetDriver);

            NetDriver->SetWorld(this);
            /*...*/
        }
        
        if( !NetDriver->InitListen( this, InURL, false, Error ) ){
            /*...*/
        }
    }

    /**
        * Returns the current Game Mode instance, which is always valid during gameplay on the server.
        * This will only return a valid pointer on the server. Will always return null on a client.
        * 서버에서 게임을 플레이하는 동안 항상 유효한 현재 게임 모드 인스턴스를 반환합니다.
        * 이는 서버에서 유효한 포인터만 반환합니다. 클라이언트에서는 항상 null을 반환합니다.
    */
    AGameModeBase* UWorld::GetAuthGameMode() const { return AuthorityGameMode; }

    /**
    * Start gameplay. This will cause the game mode to transition to the correct state and call BeginPlay on all actors
    게임 플레이를 시작합니다. 그러면 게임 모드가 올바른 상태로 전환되고 모든 액터에서 BeginPlay를 호출하게 됩니다.
    */
    void UWorld::BeginPlay();


    /** Welcome a new player joining this server. */
    void UWorld::WelcomePlayer(UNetConnection* Connection);


    /**
    * Used to get a net driver object.
    * @return a pointer to the net driver or NULL if no driver is available.
    */
    FORCEINLINE_DEBUGGABLE UNetDriver* UWorld::GetNetDriver() const
    {
        return NetDriver;
    }

    /**
    * Sets the net driver to use for this world
    * @param NewDriver the new net driver to use
    */
    void UWorld::SetNetDriver(UNetDriver* NewDriver)
    {
    NetDriver = NewDriver;
    }

    /**
    * Test whether net mode is the given mode.
    * In optimized non-editor builds this can be more efficient than GetNetMode()
    * because it can check the static build flags without considering PIE.
    */
    bool UWorld::IsNetMode(ENetMode Mode) const;

    /**
    *  Returns the net mode this world is running under.
        이 세계가 실행되고 있는 넷 모드를 반환합니다.
    * @see IsNetMode()
    */
    ENetMode UWword::GetNetMode() const
    {
        // IsRunningDedicatedServer() is a compile-time check in optimized non-editor builds.
        if (IsRunningDedicatedServer())
        {
            return NM_DedicatedServer;
        }
    
        return InternalGetNetMode();
    }

    /** Private version without inlining that does *not* check Dedicated server build flags (which should already have been done). */
    ENetMode UWorld::InternalGetNetMode() const
    {
        // NetDriver 가 있다면, 통신기능을사용한다는것, 즉 Server 아니면 Client 다.
        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;        
    }

    void UWorld::WelcomePlayer(UNetConnection* Connection)
    {
        /.../
    
        FNetControlMessage<NMT_Welcome>::Send(Connection, LevelName, GameName, RedirectURL);
    
        /.../
    }

    void UWorld::NotifyControlMessage(UNetConnection* Connection, uint8 MessageType, class FInBunch& Bunch)
    {

        if(NetDriver->ServerConnection)
        {
            // 클라이언트
            
            switch(MessageType)
            {
                case NMT_Failer:{

                    break;
                }
                case NMT_DebugText:{

                    break;
                }
                case NMT_NetGUIDAssign:{

                    break;
                }
            }


        }
        else{
            // 서버

            switch(MessageType)
            {
                case NMT_Hello:{

                    uint8 IsLittleEndian = 0;
                    uint32 RemoteNetworkVersion = 0;
                    uint32 LocalNetworkVersion = FNetworkVersion::GetLocalNetworkVersion();
                    FString EncryptionToken;
    
                    EEngineNetworkRuntimeFeatures LocalNetworkFeatures = NetDriver->GetNetworkRuntimeFeatures();
                    EEngineNetworkRuntimeFeatures RemoteNetworkFeatures = EEngineNetworkRuntimeFeatures::None;
    
                    if (FNetControlMessage<NMT_Hello>::Receive(Bunch, IsLittleEndian, RemoteNetworkVersion, EncryptionToken, RemoteNetworkFeatures))
                    {
                        const bool bIsCompatible = FNetworkVersion::IsNetworkCompatible(LocalNetworkVersion, RemoteNetworkVersion) && FNetworkVersion::AreNetworkRuntimeFeaturesCompatible(LocalNetworkFeatures, RemoteNetworkFeatures);
                        if (!bIsCompatible) // 호환 불가능
                        {
                            /.../

                            FNetControlMessage<NMT_Upgrade>::Send(Connection, LocalNetworkVersion, LocalNetworkFeatures);
                            Connection->FlushNet(true);
                            Connection->Close(ENetCloseResult::Upgrade);

                        }
                        else
                        {
                            if (EncryptionToken.IsEmpty())
                            {
                                EEncryptionFailureAction FailureResult = EEncryptionFailureAction::Default;
                                
                                if (FNetDelegates::OnReceivedNetworkEncryptionFailure.IsBound())
                                {
                                    FailureResult = FNetDelegates::OnReceivedNetworkEncryptionFailure.Execute(Connection);
                                }
    
                                const bool bGameplayDisableEncryptionCheck = FailureResult == EEncryptionFailureAction::AllowConnection;
                                const bool bEncryptionRequired = NetDriver->IsEncryptionRequired() && !bGameplayDisableEncryptionCheck;
    
                                if (!bEncryptionRequired)
                                {
                                    Connection->SendChallengeControlMessage();
                                    
                                    // 함수를 타고 들어가면, 결국 NMT_Challenge 함수를 보낸다.
                                    void UNetConnection::SendChallengeControlMessage(){
                                        if (GetConnectionState() != USOCK_Invalid && GetConnectionState() != USOCK_Closed && Driver)
                                        {
                                            Challenge = FString::Printf(TEXT("%08X"), FPlatformTime::Cycles());
                                            SetExpectedClientLoginMsgType(NMT_Login);
                                            FNetControlMessage<NMT_Challenge>::Send(this, Challenge);
                                            FlushNet();
                                        }
                                        else
                                        {
                                            UE_LOG(LogNet, Log, TEXT("UWorld::SendChallengeControlMessage: connection in invalid state. %s"), *Describe());
                                        }
                                    }
                                }
                                else
                                {
                                    FString FailureMsg(TEXT("Encryption token missing"));
    
                                    UE_LOG(LogNet, Warning, TEXT("%s: No EncryptionToken specified, disconnecting."), ToCStr(Connection->GetName()));
    
                                    Connection->SendCloseReason(ENetCloseResult::EncryptionTokenMissing);
                                    FNetControlMessage<NMT_Failure>::Send(Connection, FailureMsg);
                                    Connection->FlushNet(true);
                                    Connection->Close(ENetCloseResult::EncryptionTokenMissing);
                                }
                            }
                            else
                            {
                                if (FNetDelegates::OnReceivedNetworkEncryptionToken.IsBound())
                                {
                                    FNetDelegates::OnReceivedNetworkEncryptionToken.Execute(EncryptionToken,
                                        FOnEncryptionKeyResponse::CreateUObject(Connection, &UNetConnection::SendChallengeControlMessage));
                                }
                                else
                                {
                                    FString FailureMsg(TEXT("Encryption failure"));
    
                                    UE_LOG(LogNet, Warning, TEXT("%s: No delegate available to handle encryption token, disconnecting."),
                                            ToCStr(Connection->GetName()));
    
                                    Connection->SendCloseReason(ENetCloseResult::EncryptionFailure);
                                    FNetControlMessage<NMT_Failure>::Send(Connection, FailureMsg);
                                    Connection->FlushNet(true);
                                    Connection->Close(ENetCloseResult::EncryptionFailure);
                                }
                            }
                        }
                    }
    
                    break;    

                }
                case NMT_NetSpeed:{

                }
                case NMT_Abort:{

                }
                case NMT_Skip:{

                }
                case NMT_Login:{

					// ask the game code if this player can join
					FString ErrorMsg;
					AGameModeBase* GameMode = GetAuthGameMode();

					if (GameMode)
					{
						GameMode->PreLogin(Tmp, Connection->LowLevelGetRemoteAddress(), Connection->PlayerId, ErrorMsg);
					}
					if (!ErrorMsg.IsEmpty())
					{
						UE_LOG(LogNet, Log, TEXT("PreLogin failure: %s"), *ErrorMsg);
						NETWORK_PROFILER(GNetworkProfiler.TrackEvent(TEXT("PRELOGIN FAILURE"), *ErrorMsg, Connection));

						Connection->SendCloseReason(ENetCloseResult::PreLoginFailure);
						FNetControlMessage<NMT_Failure>::Send(Connection, ErrorMsg);
						Connection->FlushNet(true);
						Connection->Close(ENetCloseResult::PreLoginFailure);
					}
					else
					{
						WelcomePlayer(Connection);
					}                    

                }
                case NMT_Join:{
					
                    if (Connection->PlayerController == NULL)
                    {

                        Connection->PlayerController = SpawnPlayActor( Connection, ROLE_AutonomousProxy, InURL, Connection->PlayerId, ErrorMsg );
                        if (Connection->PlayerController == NULL)
                        {
                            // Failed to connect.
                            UE_LOG(LogNet, Log, TEXT("Join failure: %s"), *ErrorMsg);
                            NETWORK_PROFILER(GNetworkProfiler.TrackEvent(TEXT("JOIN FAILURE"), *ErrorMsg, Connection));
    
                            Connection->SendCloseReason(ENetCloseResult::JoinFailure);
                            FNetControlMessage<NMT_Failure>::Send(Connection, ErrorMsg);
                            Connection->FlushNet(true);
                            Connection->Close(ENetCloseResult::JoinFailure);
                        }
                        else
                        {
                            // Successfully in game.
                            UE_LOG(LogNet, Log, TEXT("Join succeeded: %s"), *Connection->PlayerController->PlayerState->GetPlayerName());
    
                            Connection->SetClientLoginState(EClientLoginState::ReceivedJoin);
    
                            // if we're in the middle of a transition or the client is in the wrong world, tell it to travel
                            FString LevelName;
                            FSeamlessTravelHandler &SeamlessTravelHandler = GEngine->SeamlessTravelHandlerForWorld( this );
    
                            if (SeamlessTravelHandler.IsInTransition())
                            {
                                // tell the client to go to the destination map
                                LevelName = SeamlessTravelHandler.GetDestinationMapName();
                            }
                            else if (!Connection->PlayerController->HasClientLoadedCurrentWorld())
                            {
                                // tell the client to go to our current map
                                FString NewLevelName = GetOutermost()->GetName();
                                UE_LOG(LogNet, Log, TEXT("Client joined but was sent to another level. Asking client to travel to: '%s'"), *NewLevelName);
                                LevelName = NewLevelName;
                            }

                            if (LevelName != TEXT(""))
                            {
                                Connection->PlayerController->ClientTravel(LevelName, TRAVEL_Relative, true);
                            }
    
                        }
    
                    }

                }
                case NMT_JoinSplit:{

                    AGameModeBase* GameMode = GetAuthGameMode();
					if (GameMode)
					{
						GameMode->PreLogin(Tmp, Connection->LowLevelGetRemoteAddress(), SplitRequestUniqueIdRepl, ErrorMsg);
					}

                    if (!ErrorMsg.IsEmpty())
                    {

                    }
                    else
                    {
                        APlayerController* PC = SpawnPlayActor(ChildConn, ROLE_AutonomousProxy, JoinSplitURL, ChildConn->PlayerId, ErrorMsg, uint8(Connection->Children.Num()));
                    }

                }
                case NMT_PCSwap:{

                }
                case NMT_DebugText:{

                }
            }
        }
    }
}

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);        
}


                        
                    

접속과정에 대한 GameMode 의 주요함수

                        
// In Server

AGameMode::Login(){

    APlayerController ServerPlayerController;
    ServerPlayerController::PostInitializeComponents(){



    }
}    

AGameMode::PostLogin(){

    ServerPlayerController.OnPossess(){  // 빙의 시작

    }

    ServerPlayerController.PossessedBy(){
        Owner = ServerPlayerController
    }

}

// Start listening for connections.
bool UWorld::Listen( FURL& InURL ){
{
    /*
    Unreal Engine에서 Listen 함수는 네트워크 통신에서 중요한 역할을 하는 함수입니다. 
    Listen 함수는 일반적으로 서버에서 호출되며, 
    클라이언트의 연결 요청을 수락하고, 해당 클라이언트와의 통신을 설정합니다.

    보통 게임 서버에서 Listen 함수는 다음과 같은 순서로 호출됩니다:
    
    1. 서버 초기화
        : 게임 서버가 초기화될 때 (예: 레벨이 로드되거나 게임이 시작될 때) Listen 함수가 호출됩니다.
    
    2. 소켓 생성 및 설정
        : Listen 함수 내부에서는 네트워크 소켓을 생성하고, 서버의 IP 주소 및 포트를 설정합니다. 이로써 클라이언트가 서버에 연결할 수 있게 됩니다.
    
    3. 연결 대기
        : 서버는 이제 클라이언트의 연결 요청을 기다립니다. 클라이언트가 서버에 연결을 시도하면, 서버는 이를 수락하고 해당 클라이언트와의 통신을 설정합니다.
    
    4. 클라이언트 핸들링
        : 클라이언트와의 연결이 설정되면, 이후에는 해당 클라이언트와의 통신을 관리하게 됩니다. 이 과정에서는 데이터를 주고받거나, 게임 상태를 동기화하는 등의 작업을 수행합니다.
    
    Listen 함수는 서버에서만 호출되며, 클라이언트에서는 호출되지 않습니다. 
    클라이언트는 서버에 연결 요청을 보내고, 서버가 이를 수락하여 연결이 이루어지면 클라이언트 측에서도 통신이 가능해집니다.    
    */

    // Create net driver.
    // NetDriver 생성 . Network  기능의 핵심. 통신담당
    if (GEngine->CreateNamedNetDriver(this, NAME_GameNetDriver, NAME_GameNetDriver))
    {
        NetDriver = GEngine->FindNamedNetDriver(this, NAME_GameNetDriver);

        NetDriver->SetWorld(this);
        /*...*/
    }
    
    if( !NetDriver->InitListen( this, InURL, false, Error ) ){
        /*...*/
    }
}

AGameMode::StartPlay()
{

    // 만약 함수를 호출하지 않으면 게임이 시작되지 않는다.
    Super::StartPlay(){

        void AGameStateBase::HandleBeginPlay(){
    
            bReplicatedHasBegunPlay = true;
        
            GetWorldSetting()->NotifyBeginPlay();
            AWorldSetting::NotifyBeginPlay(){

                UWorld* World = GetWorld();
                if(!World->bBeginPlay){ // World 의 bBeginPlay 가 false 라면,

                    // 레벨상의 모든 액터에게 BeginPlay 수행하라고 알리기
                    for( FActorIterator It(World); It ; ++It)
                    {
                        cont bool bFromLevelLoad = true;
                        It->DispatchBeginPlau(bFromLevelLoad);

                        {
                            // ... 여기서 ServerPlayerController 도 BeginPlay 가 호출된다.
                            ServerPlayerController->BeginPlay();
                        }
                    }
                }

                World->bBeginPlay = true;
            }

            GetWorldSettings()->NotifyMatchStarted();
        }
    }

}

// <--- 클라이언트 접속 요청

AGameMode::PreLogin(){

    // Client 접속 가능여부 체크
    if(ErrorMessage != Null)
    {
        // Server 로직 끝.
        // Client 는 Standalone 모드로 실행
    
    }
}

AGameMode::Login(){

    APlayerController ClientPlayerController_1;

    ClientPlayerController_1->PostInitializeComponents(){

    }

    ClientPlayerController_1->BeginPlay(){

    }

}

AGameMode::PostLogin(){

    /*
        이미 ClientConnection 이 생겨있다.
        APlayerController 만들때 생겼나?
    */

}
                        
                    
                        
// In Client

APlayerController ClientPlayerController_0;

APlayerController::PostInitializeComponents()
{

}

/*
    서버로부터 값을 넘겨받는 시점
*/

APlayerController::PostNetInit(){

}

/* 주요 기능 변수
    AGameState.bReplicatedHasBegunPlay
    UWorld.bBeginPlay
*/

/* 만약 BeginPlay 관련 값이 설정이 안되어 있다면 호출되지 않는다. */
AGameStateBase::OnRep_ReplicatedHasBegunPlay(){

    /*  서버에서 StartPlay 이미 호출했었다면 호출
        즉 ServerApp 이 아니고, bReplicatedHasBegunPlay 가 True 인경우 */
    if( bReplicatedHasBegunPlay && GetLocalRole() != ROLE_Authority)
    {
         GetWorldSetting()->NotifyBeginPlay(){

            AWorldSetting::NotifyBeginPlay(){

                UWorld* World = GetWorld();
                if(!World->bBeginPlay){ // World 의 bBeginPlay 가 false 라면,

                    // 레벨상의 모든 액터에게 BeginPlay 수행하라고 알리기
                    for( FActorIterator It(World); It ; ++It)
                    {
                        cont bool bFromLevelLoad = true;
                        It->DispatchBeginPlau(bFromLevelLoad);
                    }
                }

                World->bBeginPlay = true;
            }

         };
         GetWorldSetting()->NotifyMatchStarted();
    }

}

/*
    스스로 bBegunPlay 를 true 설정하는 것을 보아, Server 또는 Standalone 의 경우 호출되는 함수 같다. (확인필요!)
*/
bool AGameStateBase::HasBegunPlay() const{

    UWorld* World = GetWorld();
    if(World)
    {
        return World->bBegunPlay;
    }

    return false;
}