[UE5] GameState의 PlayerArray가 동기화되는 방법
게임 스테이트는 클라이언트에 레플리케이트 되어서 서버와 클라이언트에 모두 존재하며, 게임의 상태를 동기화 해서 클라이언트에서도 알수있게 된다.
예를 들면 AGameState
에서 다루는 변수인 MatchState
와 ElapsedTime
은 서버와 동기화 되므로, 이 값이 변함에 따라서 클라이언트에서도 상황에 맞는 작업을 할 수 있다.
이렇게 동기화 된다고 알려진 정보중에 AGameStateBase
의 PlayerArray
도 있다.
PlayerArray
는 현재 존재하는 모든 플레이어들의 플레이어 스테이트를 담아둔 배열이다.
플레이어 스테이트는 서버와 클라이언트에 모두 존재하고, 존재하는 모든 플레이어 스테이트가 각 클라이언트에 레플리케이트 되므로 게임 스테이트의 PlayerArray
를 통해 클라이언트에서도 원할때마다 접속된 모든 플레이어들을 확인할 수 있다.
이를 통해 새로운 플레이어가 접속하거나 기존의 플레이어가 접속을 끊은 상황을 이 PlayerArray
배열을 이용해서 클라이언트 입장에서도 RPC 없이 알아낼 수 있다.
보통 레플리케이트 되는 변수는 RepNotify를 통해 변화가 생겼음을 알려주는 함수가 호출되도록 설정된 경우가 많으므로, 이 함수를 활용하면 실시간으로 플레이어의 접속 상황을 확인할 수 있을 것이다.
(참고로 비슷한 작업을 게임 모드의 Login
과 PostLogin
함수를 통해 확인할 수 있지만, 게임 모드는 서버에만 존재하고 레플리케이트 되지 않으니 클라이언트에서는 사용할 수 없다.)
마침 이런 이벤트를 받아야하는 상황이 생겨서 엔진 코드를 확인해봤지만, 결과는 예상과 달랐다.
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;
}
}
}
만약 클라이언트에서 다른 플레이어의 접속과 접속 해제 이벤트를 알고싶다면, 위의 두 함수를 활용하면 된다.
추가로 이 두 함수가 어디에서 호출되는지도 확인해보면, APlayerState
의 PostInitializeComponents()
와 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
가 유지되는 방식을 응용해서 멀티플레이 게임에서 여러 팀들의 팀원 목록을 관리할 수도 있다.
이 팀원 목록도 레플리케이트 될 필요는 없으며, 대신 플레이어 스테이트에서 자신의 팀 정보를 레플리케이트 받아서, 업데이트가 된 경우 게임 스테이트에게 알리면 된다.