캐릭터가 죽는 순간, 시간이 느려지고 카메라가 천천히 얼굴 주위를 공전한다.
수많은 게임에서 봐왔던 익숙한 연출이다. 구현은 간단해 보인다. 이것을 구현해보고 싶었다...!
SpringArm을 회전시키고, TimeDilation을 낮추면 끝 아닌가?

.
.
.

그렇게 생각했다면, 이 글이 도움이 될 것이다.
하나의 시네마틱 시스템을 완성하기까지 마주한 문제와 그 해결 과정을 기록한 트러블슈팅 기록이다.
각 문제는 언리얼 엔진의 구조를 이해하지 않으면 절대 해결할 수 없는 것들이었다.
문제 1: 카메라가 공전하지 않고 제자리에서 회전

🔍 문제 정의
카메라가 캐릭터 주위를 "공전"해야 하는데, 제자리에서 아예 움직이지 않았다.
로그를 확인해보니 이상한 점이 있었다.
[DeathCinematic] Camera orbit started: -42.2 -> 137.8
[DeathCinematic] Camera orbit completed
시작과 완료 사이에 중간 로그가 전혀 없다. 2초짜리 연출이 즉시 끝나버린 것이다.
🛠️ 원인 분석
세 가지 원인이 복합적으로 작용하고 있었다.
원인 1: bUsePawnControlRotation 간섭
SpringArm->bUsePawnControlRotation = true; // 플레이어 입력이 카메라를 제어
ManagedSpringArm->SetWorldRotation(NewRot); // 다음 프레임에 덮어씌워짐
bUsePawnControlRotation이 활성화되어 있으면 플레이어의 마우스 입력이 SpringArm 회전을 제어한다.
우리가 아무리 SetWorldRotation을 호출해도 다음 프레임에 플레이어 입력값으로 덮어씌워진다.
원인 2: 상대 회전과 월드 회전의 혼동
ManagedSpringArm->SetRelativeRotation(NewRot); // 부모 기준 상대 회전
상대 회전(Relative Rotation)은 부모 액터를 기준으로 한다.
캐릭터가 래그돌(물리 시뮬레이션으로 쓰러지는 효과)로 넘어지면 SpringArm도 함께 기울어진다.
원인 3: 시간 계산 오류
ElapsedTime += DeltaTime; // TimeDilation이 적용된 시간
CustomTimeDilation이 0.15일 때, 엔진이 제공하는 DeltaTime도 0.15배로 느려진다.
실제 2초를 기다리려면 DeltaTime 기준으로는 약 13초가 필요하다. 하지만 코드는 이를 고려하지 않았다.
✅ 해결 과정
세 가지 원인을 각각 해결했다.
해결책 1: 컨트롤러 제어권 해제
void UGS_DeathCinematicComponent::StartCameraRotation()
{
// 원래 설정 저장
bOriginalUsePawnControlRotation = ManagedSpringArm->bUsePawnControlRotation;
// 플레이어 입력 간섭 제거
ManagedSpringArm->bUsePawnControlRotation = false;
// 월드 기준 절대 회전 활성화
ManagedSpringArm->SetUsingAbsoluteRotation(true);
}
해결책 2: 월드 회전 사용
void UGS_DeathCinematicComponent::UpdateCameraRotation(float RealDeltaTime)
{
FRotator NewRot = OriginalSpringArmRotation;
NewRot.Yaw = NewYaw;
NewRot.Pitch = NewPitch;
ManagedSpringArm->SetWorldRotation(NewRot); // 월드 기준
}
해결책 3: 실제 시간 기준 보정
float CharacterDilation = OwnerCharacter.IsValid()
? FMath::Max(OwnerCharacter->CustomTimeDilation, 0.001f)
: 1.0f;
float RealDeltaTime = DeltaTime / CharacterDilation;
ElapsedTime += RealDeltaTime;
DeltaTime은 이미 CustomTimeDilation이 곱해진 값이다. 실제 시계 기준 시간을 얻으려면 다시 나눠줘야 한다.
⚠️ 주의: SpringArm의 회전 제어에는 명확한 우선순위가 있다. bUsePawnControlRotation이 켜져 있으면 다른 모든 회전 설정을 덮어쓴다. 시네마틱 연출 시에는 반드시 이를 비활성화해야 한다.
문제 2: 관전자 시스템이 카메라를 뺏어감
🔍 문제 정의
카메라 회전 문제를 해결했더니 또 다른 문제가 나타났다.
LogTemp: Warning: [Spectator] No alive seekers to spectate.
캐릭터가 죽으면 게임 시스템이 자동으로 관전자 모드로 전환하려 했다. 그 과정에서 우리의 시네마틱 카메라가 무력화됐다.
🛠️ 원인 분석
문제는 시스템 간의 경합이었다.
1. 캐릭터 사망 → OnDeath() 호출
2. 시네마틱 시작 → ViewTarget을 죽은 캐릭터로 설정
3. 게임 시스템이 사망 감지 → ViewTarget을 관전 대상으로 변경 시도
4. 시네마틱 무효화
시작 시 ViewTarget을 한 번만 설정했기 때문에, 다른 시스템이 이를 덮어쓸 수 있었다.
✅ 해결 과정
단순하지만 확실한 방법을 선택했다. 매 프레임마다 ViewTarget을 강제로 유지한다.
void UGS_DeathCinematicComponent::TickUpdate(float DeltaTime)
{
if (APlayerController* PC = GetWorld()->GetFirstPlayerController())
{
if (PC->IsLocalController() && OwnerCharacter.IsValid())
{
if (PC->GetViewTarget() != OwnerCharacter.Get())
{
PC->SetViewTarget(OwnerCharacter.Get());
}
}
}
// ...
}
💡 게임에는 여러 시스템이 동시에 동작한다. 한 번 설정한다고 끝이 아니다. 중요한 상태는 매 프레임 검증하고 유지해야 할 수 있다. 물론 연출이 끝나면 반드시 원래 상태로 복구해야 한다.
문제 3: 멀티플레이어에서 GlobalTimeDilation 사용 문제
🔍 문제 정의
멀티플레이어 환경에서 테스트했더니 문제가 발생했다.
한 플레이어가 죽으면 모든 플레이어의 게임이 슬로우 모션에 빠졌다.
🛠️ 원인 분석
원인은 시간 조절 방식의 선택이었다.
UGameplayStatics::SetGlobalTimeDilation(World, 0.15f);
GlobalTimeDilation은 이름 그대로 전체 월드에 영향을 미친다.
서버와 모든 클라이언트가 동일한 World를 공유하므로, 한 명이 설정한 슬로우 모션이 모두에게 적용된다.
| 방식 | 영향 범위 | 적합한 용도 |
| GlobalTimeDilation | 전체 월드 | 일시정지, 전체 슬로우 모션 |
| CustomTimeDilation | 개별 액터 | 특정 캐릭터만 슬로우/빠르게 |
✅ 해결 과정
CustomTimeDilation을 사용하도록 변경했다.
void UGS_DeathCinematicComponent::PlayDeathCinematic()
{
if (OwnerCharacter.IsValid())
{
OwnerCharacter->CustomTimeDilation = TimeDilationFactor;
}
}
void UGS_DeathCinematicComponent::StopCinematic()
{
if (OwnerCharacter.IsValid())
{
OwnerCharacter->CustomTimeDilation = 1.0f;
}
}
CustomTimeDilation은 해당 액터에게만 적용된다. 다른 플레이어의 게임에는 전혀 영향을 미치지 않는다.
⚠️ 주의: 멀티플레이어 게임을 개발한다면 GlobalTimeDilation 사용을 자제해야 한다. 대부분의 경우 CustomTimeDilation이 올바른 선택이다.
문제 4: 죽는 순간 IsLocallyControlled()가 false 반환

🔍 문제 정의
분명히 내 캐릭터인데 시네마틱이 재생되지 않았다. 디버깅 결과 IsLocallyControlled()가 false를 반환하고 있었다.
🛠️ 원인 분석
언리얼 엔진에서 캐릭터가 죽으면 일반적으로 AController::UnPossess()가 호출된다.
Controller가 분리되면 GetController()가 nullptr을 반환하고, IsLocallyControlled() 내부에서 Controller를 확인하므로 false가 된다.
// 문제의 코드
void UGS_DeathCinematicComponent::PlayDeathCinematic()
{
if (!OwnerCharacter->IsLocallyControlled()) // 이미 false!
{
return; // 시네마틱 재생 안 됨
}
}
죽는 순간에 확인하면 이미 늦다.
✅ 해결 과정
캐릭터가 살아있을 때 미리 정보를 저장하는 방식으로 변경했다.
void UGS_DeathCinematicComponent::InitializeForOwner(...)
{
OwnerCharacter = Cast<ACharacter>(InOwner);
if (OwnerCharacter.IsValid())
{
bIsOwnedByLocalPlayer = OwnerCharacter->IsLocallyControlled();
}
}
void UGS_DeathCinematicComponent::PlayDeathCinematic()
{
if (!bIsOwnedByLocalPlayer) // 저장된 값 사용
{
return;
}
// 시네마틱 재생
}
TWeakObjectPtr를 사용해 댕글링 포인터(이미 삭제된 객체를 가리키는 포인터) 문제도 방지했다.
💡 실무 팁: 게임 상태는 언제든 변할 수 있다. 나중에 필요한 정보는 안정적인 시점에 미리 저장하는 습관을 들인다. 이는 죽음뿐 아니라 레벨 전환, 빙의 해제 등 다양한 상황에서 유용하다.
시네마틱 카메라 시스템 구현 체크리스트
카메라 시네마틱 구현 패턴
void StartCinematic()
{
OriginalSettings = CurrentSettings; // 원래 설정 저장
DisablePlayerControl(); // 플레이어 입력 차단
EnableAbsoluteRotation(); // 월드 기준 회전
SetComponentTickEnabled(true); // Tick 활성화
}
void TickUpdate(float DeltaTime)
{
EnforceViewTarget(); // ViewTarget 강제 유지
float RealTime = DeltaTime / Dilation; // 시간 보정
UpdateAnimation(RealTime); // 애니메이션 업데이트
}
void StopCinematic()
{
RestoreOriginalSettings(); // 원래 설정 복구
SetComponentTickEnabled(false); // Tick 비활성화
}
문제별 해결책 요약
| 문제 | 원인 | 해결책 |
| 관전자 모드로 전환됨 | 시스템 간 경합 | 매 프레임 ViewTarget 강제 |
| 전체가 슬로우 모션 | GlobalTimeDilation | CustomTimeDilation 사용 |
| 로컬 플레이어 체크 실패 | Controller 분리 | 초기화 시 미리 캐싱 |
| 비정적 멤버 컴파일 오류 | UCLASS/GENERATED_BODY 누락 | 헤더 필수 요소 복구 |
| 함수를 찾을 수 없음 | 시그니처 불일치 | 헤더-소스 동일하게 |
저는 AI를 적극적으로 활용하는 개발자입니다.
코드 구현은 AI 도구와 협업하고, 저는 문제 분석, 기술 설계, 트러블슈팅, 최종 검증에 집중합니다.
모든 기술적 의사결정과 트러블슈팅은 제가 직접 수행한 것이며, AI는 그 과정을 가속화하는 도구였습니다. 이 블로그는 그 판단과 사고의 기록입니다.
"어떤 도구를 쓰느냐"보다 "어떤 문제를 해결하느냐"가 진짜 개발자의 가치라고 믿습니다.
I believe a developer's value lies in "what problems they solve," not "what tools they use."
'Dev. > UE 언리얼 엔진' 카테고리의 다른 글
| [GAS] Unreal Engine 데미지 팝업 시스템 개선: 타격감을 위한 Dev Log (0) | 2026.01.16 |
|---|---|
| [GAS] 1월 초 구현한 시스템 정리 (0) | 2026.01.16 |
| [GAS] 언리얼 엔진 피격 인디케이터 개발 (0) | 2026.01.13 |
| [GAS] Wwise 오디오 오클루전 시스템 구축 (0) | 2026.01.09 |
| [GAS] 나나이트(Nanite) 메시의 거리 컬링 최적화 (0) | 2026.01.07 |