언리얼에서 PlayerCharacter의 위치를 지정하는 내부 코드 둘러보기

sangmiha·2024년 9월 6일

Unreal

목록 보기
4/4

언리얼을 처음 시작하면 스스로 캐릭터를 생성하지 않았는데 캐릭터가 PlayerStart의 위치에 생성되어 Controller와 연결되어 시용할 수 있는 것을 확인할 수 있습니다.

해당 포스팅에서는 '위치'를 어떻게 지정하는지에 대한 언리얼 코드 살펴보겠습니다.

간단한 흐름만 알고싶으셨던 분은 '요약' 부분만 확인하셔도 좋습니다.

언리얼 코드

AGameModeBase::PostLogin

void AGameModeBase::PostLogin(APlayerController* NewPlayer)
{
	// Runs shared initialization that can happen during seamless travel as well

	GenericPlayerInitialization(NewPlayer);

	// Perform initialization that only happens on initially joining a server

	UWorld* World = GetWorld();

	NewPlayer->ClientCapBandwidth(NewPlayer->Player->CurrentNetSpeed);

	if (MustSpectate(NewPlayer))
	{
		NewPlayer->ClientGotoState(NAME_Spectating);
	}
	else
	{
		// If NewPlayer is not only a spectator and has a valid ID, add it as a user to the replay.
		const FUniqueNetIdRepl& NewPlayerStateUniqueId = NewPlayer->PlayerState->GetUniqueId();
		if (NewPlayerStateUniqueId.IsValid() && NewPlayerStateUniqueId.IsV1())
		{
			GetGameInstance()->AddUserToReplay(NewPlayerStateUniqueId.ToString());
		}
	}

	if (GameSession)
	{
		GameSession->PostLogin(NewPlayer);
	}

	DispatchPostLogin(NewPlayer);

	// Now that initialization is done, try to spawn the player's pawn and start match
	HandleStartingNewPlayer(NewPlayer);
}

위 코드는 언리얼의 AGameModeBase::PostLogin에 대한 구현입니다. 다양한 설정이 있지만 포스팅의 목적대로 캐릭터의 생성부분을 살펴보기 위해서 HandleStartingNewPlayer메소드의 구현을 살펴보겠습니다.

AGameModeBase::HandleStartingNewPlayer

/** Signals that a player is ready to enter the game, which may start it up */
	UFUNCTION(BlueprintNativeEvent, Category=Game)
	ENGINE_API void HandleStartingNewPlayer(APlayerController* NewPlayer);
void AGameModeBase::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
	// If players should start as spectators, leave them in the spectator state
	if (!bStartPlayersAsSpectators && !MustSpectate(NewPlayer) && PlayerCanRestart(NewPlayer))
	{
		// Otherwise spawn their pawn immediately
		RestartPlayer(NewPlayer);
	}
}

위 코드에서 조건문으로 '관전자' 또는 '캐릭터를 스폰할 수 있는지'를 확인한 후에 RestartPlayer를 호출합니다.

bool APlayerController::CanRestartPlayer()
{
	return PlayerState && !PlayerState->IsOnlyASpectator() && HasClientLoadedCurrentWorld() && PendingSwapConnection == NULL;
}

CanRestartPlayer는 위와 같은 조건문을 가지는데, 관전자인지 확인한다거나, Client와 Server의 World가 같은지 확인하거나, Swap이 일어나지 않을 때, True를 반환하게 됩니다.

AGameModeBase::RestartPlayer

void AGameModeBase::RestartPlayer(AController* NewPlayer)
{
	if (NewPlayer == nullptr || NewPlayer->IsPendingKillPending())
	{
		return;
	}

	AActor* StartSpot = FindPlayerStart(NewPlayer);

	// If a start spot wasn't found,
	if (StartSpot == nullptr)
	{
		// Check for a previously assigned spot
		if (NewPlayer->StartSpot != nullptr)
		{
			StartSpot = NewPlayer->StartSpot.Get();
			UE_LOG(LogGameMode, Warning, TEXT("RestartPlayer: Player start not found, using last start spot"));
		}	
	}

	RestartPlayerAtPlayerStart(NewPlayer, StartSpot);
}

생성과 직접적으로 연관이 있는 메소드입니다. FindPlayerStart로 Editor에서 지정한 PlayerStart의 위치를 찾아서 StartSpot에 저장하고 이를 RestartPlayerAtPlayerStart의 인자로 넘기게 됩니다.

간단하게 FindPlayerStart의 구현을 살펴보고 넘어가겠습니다.

AGameModeBase::FindPlayerStart

/**
	 * Return the specific player start actor that should be used for the next spawn
	 * This will either use a previously saved startactor, or calls ChoosePlayerStart
	 * 
	 * @param Player The AController for whom we are choosing a Player Start
	 * @param IncomingName Specifies the tag of a Player Start to use
	 * @returns Actor chosen as player start (usually a PlayerStart)
	 */
	UFUNCTION(BlueprintNativeEvent, Category=Game)
	ENGINE_API AActor* FindPlayerStart(AController* Player, const FString& IncomingName = TEXT(""));
AActor* AGameModeBase::FindPlayerStart_Implementation(AController* Player, const FString& IncomingName)
{
	UWorld* World = GetWorld();

	// If incoming start is specified, then just use it
	if (!IncomingName.IsEmpty())
	{
		const FName IncomingPlayerStartTag = FName(*IncomingName);
		for (TActorIterator<APlayerStart> It(World); It; ++It)
		{
			APlayerStart* Start = *It;
			if (Start && Start->PlayerStartTag == IncomingPlayerStartTag)
			{
				return Start;
			}
		}
	}

	// Always pick StartSpot at start of match
	if (ShouldSpawnAtStartSpot(Player))
	{
		if (AActor* PlayerStartSpot = Player->StartSpot.Get())
		{
			return PlayerStartSpot;
		}
		else
		{
			UE_LOG(LogGameMode, Error, TEXT("FindPlayerStart: ShouldSpawnAtStartSpot returned true but the Player StartSpot was null."));
		}
	}

	AActor* BestStart = ChoosePlayerStart(Player);
	if (BestStart == nullptr)
	{
		// No player start found
		UE_LOG(LogGameMode, Log, TEXT("FindPlayerStart: PATHS NOT DEFINED or NO PLAYERSTART with positive rating"));

		// This is a bit odd, but there was a complex chunk of code that in the end always resulted in this, so we may as well just 
		// short cut it down to this.  Basically we are saying spawn at 0,0,0 if we didn't find a proper player start
		BestStart = World->GetWorldSettings();
	}

	return BestStart;
}

// If incoming start is specified, then just use it
	if (!IncomingName.IsEmpty())
	{
		const FName IncomingPlayerStartTag = FName(*IncomingName);
		for (TActorIterator<APlayerStart> It(World); It; ++It)
		{
			APlayerStart* Start = *It;
			if (Start && Start->PlayerStartTag == IncomingPlayerStartTag)
			{
				return Start;
			}
		}
	}

먼저 IncomingName를 확인하여 원하는 태그가 지정된 경우 해당 태그가 지정된 PlayerStart Start로 저장하여 반환합니다. 하지만 기본 Find에서는 인자를 넣지 않도록 구현되어 있기에 'Override' 또는 '직접 위치를 얻고자 호출'하는 경우가 아니라면 해당하지 않습니다.

// Always pick StartSpot at start of match
	if (ShouldSpawnAtStartSpot(Player))
	{
		if (AActor* PlayerStartSpot = Player->StartSpot.Get())
		{
			return PlayerStartSpot;
		}
		else
		{
			UE_LOG(LogGameMode, Error, TEXT("FindPlayerStart: ShouldSpawnAtStartSpot returned true but the Player StartSpot was null."));
		}
	}

두 번째로 확인되는 것이 ShouldSpawnAtStartSpot(Player) 조건을 확인하여 Player->StartSpot이 설정되어 있는지 확인합니다. 이게 설정되어있다면 PlayerStartSpot으로 저장하고 반환합니다.

StartSpot은 코드에서도 볼 수 있듯 Controller에서 설정되어 있고, 또 설정할 수 있습니다.

public:
	...
	/** Actor marking where this controller spawned in. */
	TWeakObjectPtr<class AActor> StartSpot;
	...

추가로 Public으로 설정되어 있기 때문에 외부에서 설정또한 자유롭습니다.

Reset과 같은 메소드 호출로 인해 Null초기화 되버릴 수 있지만, 일반적으로 해당 값을 설정함으로써 시작 위치를 코드로 지정할 수 있을 것 입니다.

AActor* BestStart = ChoosePlayerStart(Player);
	if (BestStart == nullptr)
	{
		// No player start found
		UE_LOG(LogGameMode, Log, TEXT("FindPlayerStart: PATHS NOT DEFINED or NO PLAYERSTART with positive rating"));

		// This is a bit odd, but there was a complex chunk of code that in the end always resulted in this, so we may as well just 
		// short cut it down to this.  Basically we are saying spawn at 0,0,0 if we didn't find a proper player start
		BestStart = World->GetWorldSettings();
	}

마지막으로 Editor에서 설정한 PlayerStart를 탐색하는 과정을 가지게 됩니다.

글이 길어지게 될 것 같아. ChoosePlayerStart 메소드에 대한 내부 구현은 다른 포스팅에서 알아보겠습니다. 일단은 Editor에서 설정한 PlayerStart Actor를 찾아낸다고 아시면 됩니다.

이때, PlayerStart를 통해서 적절한 위치를 찾아낼 수 없었을 경우(설정을 안했거나 막힌경우).
World->GetWorldSettings();를 이용해서 BestStart를 지정하는데 주석에 의하면 일반적으로 (0, 0, 0)의 좌표를 가지게 될 것 입니다.

즉, 흔히 착각할 수 있지만 PlayerStart를 지정하지 않았다고 해서 Player가 생성되지 않는 것이 아닙니다.

요약

  1. PostLogin으로 Controller의 접속이 World에서 확인됩니다.
  2. PostLogin의 기본 구현으로 HandleStartingNewPlayer를 통해서 Player를 생성 및 위치를 지정하게 됩니다.
  3. Controller를 확인하여 StartSpot이 지정된 것이 있는지 확인합니다. 있다면 해당 액터를 반환합니다.
  4. StartSpot이 지정되지 않았다면 PlayerStart를 탐색합니다.

어떤 PlayerStart이 지정되는지와 조정되는지에 관련해서는 다른 포스팅에서 다룰 예정입니다.

0개의 댓글