데디케이트 서버 기반의 프로젝트를 진행하면서 인게임 로직을 담당하게 되었다.
캐릭터가 사망하고 리스폰되었을 때를 처리하는 부분을 설계하다가 단순히 다른 컨트롤러에 빙의되는 것이 아니라 격투게임이니만큼 격투를 관전할 수 있는 관전뷰를 만들어 거기에 플레이어들을 빙의시켰다.
트러블슈팅
하다가 서버에서 클라이언트의 플레이어컨트롤러를 관전 카메라에 배치하는건 잘 작동되었다.
void ACharacterController::ClientSpectateCamera_Implementation(ACameraActor* SpectatorCam)
{
if (!SpectatorCam)
{
UE_LOG(LogTemp, Warning, TEXT("ClientSpectateCamera: SpectatorCam is null"));
return;
}
UnPossess();
ChangeState(NAME_Spectating);
SetViewTargetWithBlend(SpectatorCam, 0.f);
UE_LOG(LogTemp, Log, TEXT("ClientSpectateCamera: switched to SpectatorCamera"));
}
void ACharacterController::NotifyPawnDeath()
{
AArenaPlayerState* ArenaPlayerState = GetPlayerState<AArenaPlayerState>();
if (!ArenaPlayerState)
{
UE_LOG(LogTemp, Warning, TEXT("NotifyPawnDeath: PlayerState not invaild"));
return;
}
if (ArenaPlayerState->MaxLives > 0)
{
ArenaPlayerState->MaxLives--;
FTimerHandle RespawnTimerHandle;
GetWorld()->GetTimerManager().SetTimer(RespawnTimerHandle, [this]()
{
if (AArenaGameMode* ArenaGameMode = Cast<AArenaGameMode>(GetWorld()->GetAuthGameMode()))
{
ArenaGameMode->RespawnPlayer(this);
}
}, 3.0f, false);
}
else
{
if (AArenaGameState* ArenaGameState = Cast<AArenaGameState>(GetWorld()->GetGameState()))
{
float CurrentRoundTime = ArenaGameState->GetRemainingTime();
float TotalRountTime = ArenaGameState->GetRoundStartTime();
ArenaPlayerState->SetSurvivalTime(TotalRountTime - CurrentRoundTime);
UE_LOG(LogTemp, Log, TEXT("NotifyPawnDeath: Survivla time is %.2f"), CurrentRoundTime);
}
if (AArenaGameMode* ArenaGameMode = Cast<AArenaGameMode>(GetWorld()->GetAuthGameMode()))
{
if (ACameraActor* SpectatorCam = ArenaGameMode->GetSpectatorCamera())
{
ClientSpectateCamera(SpectatorCam);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("NotifyPawnDeath: SpectatorCamera not invaild"));
}
}
}
}
이건 처음에 작성했던 코드인데, 클라이언트 RPC로 구현했다.
그런데 저 카메라 전환에 대해 0.2초정도는 원하는 카메라로 이동되었다가 갑자기 이상한 뷰로 전환되는 것이 아닌가!
솔직히 로직상으로는 뭐가 문제인지 잘 찾을 수 없었던 것 같다.
하지만 찾아내야했다..
카메라는 블루프린트로 만들어진 카메라 액터였고, 컨트롤러에 Tick 함수에 어떤 카메라가 빙의되는지 찾아봤다.
근데 내 관전카메라로 떴다가 컨트롤러로 뜨는 것이 아닌가..
아마 캐릭터쪽에서 Pawn Destroy()를 타이머로 관리하고, 바로 컨트롤러의 죽음 처리 함수를 호출한다.
컨트롤러 죽음 함수에서 카메라 전환을 위해 컨트롤러를 UnPossess하고, SetViewTarget을 해주게되는데 카메라 빙의 함수 호출 순서가 꼬여서 그런 것 같다.
(디버깅에만 4시간 쓴 것 같다...)
void ACharacterController::NotifyPawnDeath()
{
if (!HasAuthority()) { return; }
AArenaPlayerState* ArenaPlayerState = GetPlayerState<AArenaPlayerState>();
if (!IsValid(ArenaPlayerState)) { return; }
AArenaGameMode* ArenaGameMode = Cast<AArenaGameMode>(GetWorld()->GetAuthGameMode());
if (!IsValid(ArenaGameMode)) { return; }
UnPossess();
DisableInput(this);
FlushPressedKeys();
SetIgnoreMoveInput(true);
SetIgnoreLookInput(true);
ArenaPlayerState->MaxLives--;
// Posses Spectator Camera
if (AArenaGameState* ArenaGameState = Cast<AArenaGameState>(GetWorld()->GetGameState()))
{
FTimerHandle SpectateHandle;
ACameraActor* SpectatorCam = ArenaGameState->GetSpectatorCamera();
if (IsValid(SpectatorCam))
{
GetWorld()->GetTimerManager().SetTimer(SpectateHandle, [this, SpectatorCam]()
{
FViewTargetTransitionParams Params;
Params.BlendTime = 0.f;
Cast<APlayerController>(this)
->ClientSetViewTarget(SpectatorCam, Params);
}, 0.2f, false);
}
if (IsLocalController())
{
GetWorld()->GetTimerManager().SetTimer(SpectateHandle, [this, SpectatorCam]()
{
FViewTargetTransitionParams Params;
Params.BlendTime = 0.f;
Cast<APlayerController>(this)
->ClientSetViewTarget(SpectatorCam, Params);
}, 0.2f, false);
}
}
// Respawn
AArenaPlayerState* PS = GetPlayerState<AArenaPlayerState>();
const bool bHasLives = PS && PS->MaxLives > 0;
if (ArenaGameMode->SelectedArenaSubMode == EArenaSubMode::DeathMatch)
{
ClientStartRespawnCountdown();
FTimerHandle RespawnTimerHandle;
GetWorld()->GetTimerManager().SetTimer(
RespawnTimerHandle,
[this, ArenaGameMode]()
{
ArenaGameMode->SpawnPlayer(this);
},
5.0f,
false
);
}
else if (ArenaGameMode->SelectedArenaSubMode == EArenaSubMode::Elimination)
{
if (bHasLives)
{
ClientStartRespawnCountdown();
FTimerHandle RespawnTimerHandle;
GetWorld()->GetTimerManager().SetTimer(
RespawnTimerHandle,
[this, ArenaGameMode]()
{
ArenaGameMode->SpawnPlayer(this);
},
5.0f,
false
);
}
else
{
if (AArenaGameState* ArenaGameState = Cast<AArenaGameState>(GetWorld()->GetGameState()))
{
float SurvivalTime = ArenaGameState->GetRoundStartTime() - ArenaGameState->GetRemainingTime();
ArenaPlayerState->SetSurvivalTime(SurvivalTime);
ClientShowDieMessage();
}
}
}
}
여기 함수를 보면 ClientSetViewTarget을 호출하는데 이건 언리얼 내장 RPC 함수이고 호출될 때 지연시간이 있다고 한다.
그래서 충분히 UnPossess되고, 빙의되고 이런 과정을들 기다렸다가 호출하기 때문에 순서가 꼬이지 않아 정상적으로 카메라 전환이 되었던 것 같다.
또는 컨트롤러에서 빙의해제가 되기 전에 RPC 함수가 호출되면서 컨트롤러의 빙의 처리와 SetViewTarget의 호출시점이 꼬여서 발생했을 수도 있을 것 같다,
마무리말
아직 언리얼 내장 함수들이 어떻게 동작하는지 명확하게 이해하지 못한 것 같다.
결론도 혼자 함수와 코드를 따라가보면서 유추한것뿐이라, 나중에 시간을 따로 내어서 내장 함수를 깊에 들여다보는 시간을 가져야겠다.
'Unreal Engine' 카테고리의 다른 글
[ UE ] TrainingMode 및 UI 구현 (0) | 2025.04.08 |
---|---|
[ UE ] 주말 작업동안의 트러블 슈팅 정리 (0) | 2025.04.07 |
[ UE ] 위젯과 타이머 연동하기 (0) | 2025.04.04 |
[ UE ] 훈련장모드 베이스 설계 (0) | 2025.04.03 |
[ UE ] 데디케이트 서버 패키징을 해보자 (1) | 2025.03.31 |