언리얼엔진/그 외

[UE5] GameState의 PlayerArray가 동기화되는 방법

mstone8370 2024. 9. 9. 14:59

 

게임 스테이트는 클라이언트에 레플리케이트 되어서 서버와 클라이언트에 모두 존재하며, 게임의 상태를 동기화 해서 클라이언트에서도 알수있게 된다.

예를 들면 AGameState에서 다루는 변수인 MatchStateElapsedTime은 서버와 동기화 되므로, 이 값이 변함에 따라서 클라이언트에서도 상황에 맞는 작업을 할 수 있다.

 

이렇게 동기화 된다고 알려진 정보중에 AGameStateBasePlayerArray도 있다.

PlayerArray는 현재 존재하는 모든 플레이어들의 플레이어 스테이트를 담아둔 배열이다.

플레이어 스테이트는 서버와 클라이언트에 모두 존재하고, 존재하는 모든 플레이어 스테이트가 각 클라이언트에 레플리케이트 되므로 게임 스테이트의 PlayerArray를 통해 클라이언트에서도 원할때마다 접속된 모든 플레이어들을 확인할 수 있다.

 

이를 통해 새로운 플레이어가 접속하거나 기존의 플레이어가 접속을 끊은 상황을 이 PlayerArray 배열을 이용해서 클라이언트 입장에서도 RPC 없이 알아낼 수 있다.

보통 레플리케이트 되는 변수는 RepNotify를 통해 변화가 생겼음을 알려주는 함수가 호출되도록 설정된 경우가 많으므로, 이 함수를 활용하면 실시간으로 플레이어의 접속 상황을 확인할 수 있을 것이다.

(참고로 비슷한 작업을 게임 모드의 LoginPostLogin 함수를 통해 확인할 수 있지만, 게임 모드는 서버에만 존재하고 레플리케이트 되지 않으니 클라이언트에서는 사용할 수 없다.)

 

마침 이런 이벤트를 받아야하는 상황이 생겨서 엔진 코드를 확인해봤지만, 결과는 예상과 달랐다.

 


 

GameStateBase.h 를 찾아보면 PlayerArray는 아래와 같이 선언되어있다.

// GameStateBase.h

/** Array of all PlayerStates, maintained on both server and clients (PlayerStates are always relevant) */
UPROPERTY(Transient, BlueprintReadOnly, Category=GameState)
TArray<TObjectPtr<APlayerState>> PlayerArray;

Replicate 변수로 지정되어있지 않으며, 따라서 RepNotify 함수도 없다.

그리고 달려있는 주석을 보면 서버와 클라이언트 모두 maintain 된다고 적혀있다.

레플리케이트 되지는 않지만, 어떠한 방법을 통해 관리되어 상태를 유지하고 있다는걸 확인할 수 있다.

 

PlayerArray가 어떻게 관리되고있는지 확인해보니 아래와 같은 두 함수를 찾을 수 있었다.

// GameStateBase.h

/** Add PlayerState to the PlayerArray */
ENGINE_API virtual void AddPlayerState(APlayerState* PlayerState);

/** Remove PlayerState from the PlayerArray. */
ENGINE_API virtual void RemovePlayerState(APlayerState* PlayerState);



// GameStateBase.cpp

void AGameStateBase::AddPlayerState(APlayerState* PlayerState)
{
    // Determine whether it should go in the active or inactive list
    if (!PlayerState->IsInactive())
    {
        // make sure no duplicates
        PlayerArray.AddUnique(PlayerState);
    }
}

void AGameStateBase::RemovePlayerState(APlayerState* PlayerState)
{
    for (int32 i=0; i<PlayerArray.Num(); i++)
    {
        if (PlayerArray[i] == PlayerState)
        {
            PlayerArray.RemoveAt(i,1);
            return;
        }
    }
}

만약 클라이언트에서 다른 플레이어의 접속과 접속 해제 이벤트를 알고싶다면, 위의 두 함수를 활용하면 된다.

 

추가로 이 두 함수가 어디에서 호출되는지도 확인해보면, APlayerStatePostInitializeComponents()Destroyed() 함수에서 월드의 게임 스테이트를 찾아서 직접 호출해주고 있는걸 알 수 있다.

void APlayerState::PostInitializeComponents()
{
    // ...
    
    UWorld* World = GetWorld();
    AGameStateBase* GameStateBase = World->GetGameState();

    // register this PlayerState with the game state
    if (GameStateBase != nullptr )
    {
        GameStateBase->AddPlayerState(this);
    }

    // ...
}

void APlayerState::Destroyed()
{
    UWorld* World = GetWorld();
    if (World->GetGameState() != nullptr)
    {
        World->GetGameState()->RemovePlayerState(this);
    }

    // ...
}

 

즉 플레이어 스테이트가 레플리케이트 되어서 생성될때와 제거될때에 PlayerArray에 스스로 추가되거나, 제거되는 방식으로 관리되고 있다.

 


 

생각해보면 접속중인 플레이어 배열은 크기가 커질수도 있으니 배열 자체를 레플리케이트 하는건 좋지 않은 선택이다.

 

그 뿐만 아니라 PlayerArray가 레플리케이트 된다고 가정해보면, 클라이언트 입장에서 새로운 플레이어가 접속했을때를 알고자 할 때에는 PlayerArray의 RepNotify를 활용할 것이다.

하지만 플레이어 스테이트 자체도 레플리케이트 되고, PlayerArray도 별도로 레플리케이트 될텐데 이런 경우에는 어떤것이 먼저 레플리케이트 될지는 알 수 없다.

만약 PlayerArray가 먼저 레플리케이트 되어서 플레이어 스테이트가 생성되기 전에 OnRep 함수가 호출된다면, PlayerArray에는 존재하지 않는 플레이어 스테이트를 가리키는 원소가 포함될테니 원하는 정보를 얻을 수 없게된다.

 

 

PlayerArray가 유지되는 방식을 응용해서 멀티플레이 게임에서 여러 팀들의 팀원 목록을 관리할 수도 있다.

이 팀원 목록도 레플리케이트 될 필요는 없으며, 대신 플레이어 스테이트에서 자신의 팀 정보를 레플리케이트 받아서, 업데이트가 된 경우 게임 스테이트에게 알리면 된다.