[GAS] Unreal Engine 죽음 시네마틱 카메라 시스템

2026. 1. 14. 22:54·Dev./UE 언리얼 엔진

캐릭터가 죽는 순간, 시간이 느려지고 카메라가 천천히 얼굴 주위를 공전한다.

수많은 게임에서 봐왔던 익숙한 연출이다. 구현은 간단해 보인다. 이것을 구현해보고 싶었다...!

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
'Dev./UE 언리얼 엔진' 카테고리의 다른 글
  • [GAS] Unreal Engine 데미지 팝업 시스템 개선: 타격감을 위한 Dev Log
  • [GAS] 1월 초 구현한 시스템 정리
  • [GAS] 언리얼 엔진 피격 인디케이터 개발
  • [GAS] Wwise 오디오 오클루전 시스템 구축
raindrovvv
raindrovvv
raindrovvv 님의 블로그 입니다.
  • raindrovvv
    raindrovvv 님의 블로그
    raindrovvv
  • 전체
    오늘
    어제
    • 분류 전체보기 (170) N
      • Dev. (163) N
        • AI 인공지능 (27)
        • UE 언리얼 엔진 (81) N
        • Unity 유니티 (0)
        • Wwise 와이즈 (7)
        • 게임 네트워크 (8)
        • 그래픽스 Graphics (22)
        • 프로젝트 (8)
        • 기타 개발 관련 (10)
      • Computer Science (0)
        • 하드웨어 HW (0)
        • 소프트웨어 SW (0)
        • 통신 (0)
        • 데이터 (0)
      • 블로그 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    TA
    언리얼엔진
    Git
    UE
    devlog
    생산성
    unrealengine
    게임개발
    언리얼
    깃
    트러블슈팅
    Unreal
    Wwise
    에이전트
    인디게임
    그래픽스
    네트워크
    dev
    바이브코딩
    AI
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
raindrovvv
[GAS] Unreal Engine 죽음 시네마틱 카메라 시스템
상단으로

티스토리툴바