Game Programming/Unreal Engine

Unreal Engine에서 기본 요소의 초기화 순서 (Host-Client)

타자치는 문돌이

각 요소를 초기화하고 의존성을 묶는 시작 지점에서 연결이 안되는 문제가 발생했다.
그래서 GameMode, PlayerController, PlayerState, DefaultPawn, HUD 클래스의 초기화 순서에 대해 알아보기로 하였다.

실험 방법

TEST 클래스를 만든 뒤, 클래스를 상속 받는 BP를 만들고, GameMode에 적용했다.

ATESTGameModeBase::ATESTGameModeBase()  
{  
    NET_LOG(Log, TEXT("GameMode::Constructor"));  
}

TEST 클래스는 NET_LOG 매크로를 구현해 로그를 출력했다.

LogNetRole: [PIE0][Standalone][SID=-1] [Server] GameMode::Constructor
  • [PIE0] : PlayInEditor ID. 구분하기 제일 좋은 기준
  • [SID] : PlayerState의 PlayerId로 없으면 -1
  • [Server] : HasAuthority면 Server, 아니면 Client. HUD와 Standalone은 Client가 권한이 있어 Server로 나오는 한계가 있다.

 

Standalone

LogNetRole: [PIE0][Standalone][SID=-1] [Server] GameMode::Constructor
LogNetRole: [PIE0][Standalone][SID=-1] [Server] PlayerController::Constructor
LogNetRole: [PIE0][Standalone][SID=-1] [Server] PlayerState::Constructor
LogNetRole: [PIE0][Standalone][SID=-1] [Server] PlayerState::PostInitializeComponents
LogNetRole: [PIE0][Standalone][SID=262] [Server] HUD::Constuctor
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerCharacter::Constructor
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerCharacter::PossessedBy
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerCharacter::SetupPlayerInputComponent
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerController::OnPossess
LogNetRole: [PIE0][Standalone][SID=262] [Server] GameMode::BeginPlay
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerController::BeginReplication
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerController::BeginPlay
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerState::BeginPlay
LogNetRole: [PIE0][Standalone][SID=262] [Server] HUD::BeginPlay
LogNetRole: [PIE0][Standalone][SID=262] [Server] PlayerCharacter::BeginPlay

 

GameMode->PlayerController->PlayerState->HUD->DefaultPawn 순서로 생성되고, BeginPlay도 같은 순서로 실행된다. HUD가 PlayerCharacter보다 먼저 실행되므로 PlayerCharacter의 BeginPlay에서 HUD가 참조해야 할 요소가 초기화된다면 문제가 있을 수 있다.

 

Listen Server (Host-Client)

LogNetRole: [PIE0][ListenServer][SID=-1] [Server] GameMode::Constructor
LogNetRole: [PIE0][ListenServer][SID=-1] [Server] PlayerController::Constructor
LogNetRole: [PIE0][ListenServer][SID=-1] [Server] PlayerState::Constructor
LogNetRole: [PIE0][ListenServer][SID=-1] [Server] PlayerState::PostInitializeComponents
LogNetRole: [PIE0][ListenServer][SID=264] [Server] HUD::Constuctor
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::Constructor
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::PossessedBy
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::SetupPlayerInputComponent
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::OnPossess
LogNetRole: [PIE0][ListenServer][SID=264] [Server] GameMode::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::BeginReplication
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerState::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] HUD::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::Constructor
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerState::Constructor
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerState::PostInitializeComponents
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerState::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::BeginReplication
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::BeginReplication
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::Constructor
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::BeginPlay
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerCharacter::PossessedBy
LogNetRole: [PIE0][ListenServer][SID=264] [Server] PlayerController::OnPossess
LogNetRole: [PIE1][Client][SID=-1] [Server] PlayerController::Constructor
LogNetRole: [PIE1][Client][SID=-1] [Server] HUD::Constuctor
LogNetRole: [PIE1][Client][SID=-1] [Server] PlayerCharacter::Constructor
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerController::OnRep_Pawn
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerCharacter::OnRep_Controller
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerController::BeginReplication
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerController::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Server] HUD::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerCharacter::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Server] PlayerCharacter::Constructor
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerCharacter::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerController::OnRep_Pawn
LogNetRole: [PIE1][Client][SID=-1] [Server] PlayerState::Constructor
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerState::PostInitializeComponents
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerState::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Server] PlayerState::Constructor
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerState::PostInitializeComponents
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerState::BeginPlay
LogNetRole: [PIE1][Client][SID=-1] [Client] PlayerCharacter::OnRep_PlayerState
LogNetRole: [PIE1][Client][SID=265] [Client] PlayerState::ClientInitialize
LogNetRole: [PIE1][Client][SID=265] [Client] PlayerController::OnRep_PlayerState
LogNetRole: [PIE1][Client][SID=265] [Client] PlayerCharacter::OnRep_PlayerState
LogNetRole: [PIE1][Client][SID=265] [Client] PlayerCharacter::OnRep_Controller
LogNetRole: [PIE1][Client][SID=265] [Client] PlayerCharacter::SetupPlayerInputComponent

 

Host 쪽에서의 순서는 Host 생성 -> 순차적으로 Client 생성으로 예측 가능한 범주지만, Client 쪽은 아무것도 보장되지 않는다. GPT에 따르면,Connection 이후에 액터 채널이 먼저 열린 것부터 생성한 뒤 데이터를 받는다고 한다. 그리고 생성 직후 BeginPlay가 실행되지만, 이 시점에 데이터 복사가 완료되지 않아 OnRep은 더 늦게 호출된다. 즉, 클라이언트는 BeginPlay 때 의존성 있는 요소가 모두 생성되었는 지 보장이 안되고, 생성되었어도 서버 데이터가 적용되었는 지 보장할 수 없다.


오류가 났던 상황을 검토해보자.

  1. PlayerState는 AbilitySystemComponent를 가진다.
  2. AbilitySystemComponent는 InitAbilityActorInfo라는 함수를 써서 초기화하는 과정이 서버와 클라이언트 모두에서 한번 필요한데, 이때 PlayerCharacter가 필요하다.
  3. HUD는 AbilitySystemComponent를 통해 Player의 스탯을 출력한다.

서버의 경우, PlayerState->HUD->PlayerCharacter 순서대로 생성되므로 PlayerCharacter 생성 후 PossessedBy에서 InitAbilityActorInfo를 실행한 뒤, HUD의 BeginPlay에서 데이터를 읽도록 할 수 있다.
클라이언트의 경우, OnRep_PlayerState에서 InitAbilityActorInfo와 HUD의 데이터 읽기가 가능하다. 그런데 상황에 따라 순서가 바뀔 수도 있다.

 

생각해보니 로비에서부터 PlayerState를 만들고 넘기면 될 것 같기도...

반응형