[GAS] Unreal Engine 1인칭/3인칭 카메라 전환 시스템

2026. 1. 19. 18:45·Dev./UE 언리얼 엔진

F5 키를 누른다. 3인칭에서 1인칭으로 전환된다.

마우스 좌클릭(활을 조준)을 누른다. 짧은 시간이 지나면 카메라가 갑자기 3인칭으로 전환되어 버린다.

분명 1인칭이었는데, 왜 3인칭 위치로 돌아간 걸까?

 

다른 캐릭터로 바꿔본다. 이번엔 1인칭에서 3인칭으로 복귀가 안 된다.

카메라가 캐릭터 내부에 박혀서 움직이지 않는다.

멀티플레이 테스트를 시작한다. 내 화면에서는 정상인데, 다른 플레이어 화면에서 내 캐릭터가 엉뚱한 방향을 바라보고 있다.

 

시점 전환은 단순해 보이지만, 상태 관리, Tick 최적화, 네트워크 동기화가 복잡하게 얽힌 시스템이다.

 

이 글은 1인칭/3인칭 카메라 전환 시스템을 구현하면서 마주친 다섯 가지 실제 문제와 그 해결 과정을 기록한다.


문제 1: 1인칭에서 줌 효과 후 3인칭으로 점프

증상

메르시(Merci) 캐릭터로 1인칭 시점에서 활을 조준하면,

줌이 끝날 때 카메라가 갑자기 3인칭 위치로 이동한다.

 

재현 조건은 다음과 같다.

  1. F5 키로 1인칭 시점 전환
  2. 마우스 왼쪽 버튼으로 활 조준 (줌 타임라인 실행)
  3. 조준 해제 시 카메라가 뒤로 “점프”

에러 로그는 없다. 논리적 버그다.


원인 분석

메르시의 UpdateZoom() 함수가 줌 타임라인 종료 시...

SpringArmComp->TargetArmLength를 하드코딩된 3인칭 값(350.0f)으로 리셋하고 있었다.

// 문제가 있는 코드
void AGS_Merci::UpdateZoom(float Alpha)
{
    // Alpha가 0이 되면 TargetArmLength가 350으로 설정됨
    float TargetArmLength = FMath::Lerp(350.0f, 180.0f, Alpha);
    SpringArmComp->TargetArmLength = TargetArmLength;
}

핵심 실수는 줌 로직이 현재 시점을 확인하지 않은 것이다.

1인칭에서는 SpringArm 길이가 0이어야 하는데, 줌 함수가 이를 덮어썼다.


해결 과정

// 수정된 코드
void AGS_Merci::UpdateZoom(float Alpha)
{
    if (!SpringArmComp || !CameraComp)
    {
        return;
    }

    // FOV 조정은 1인칭/3인칭 공통으로 적용
    float BaseFOV = IsFirstPerson() ? FirstPersonFOV : SavedTPSFOV;
    float TargetFOV = FMath::Lerp(BaseFOV, BaseFOV - 20.0f, Alpha);
    CameraComp->SetFieldOfView(TargetFOV);

    // 핵심: 1인칭에서는 카메라 거리 조정을 스킵
    if (IsFirstPerson())
    {
        return;
    }

    // 3인칭에서만 카메라 거리 및 오프셋 조정
    float TargetArmLength = FMath::Lerp(350.0f, 180.0f, Alpha);
    SpringArmComp->TargetArmLength = TargetArmLength;
}
변경 전  변경 후
시점 확인 없이 ArmLength 설정 IsFirstPerson() 체크 후 조기 리턴
FOV와 ArmLength를 함께 처리 FOV는 공통, ArmLength는 3인칭 전용

“카메라 관련 함수는 항상 현재 시점 상태를 먼저 확인해야 한다. 이 원칙을 지키지 않으면 예측 불가능한 동작이 발생한다.”


문제 2: 특정 캐릭터에서 3인칭 복귀 실패

증상

찬(Chan), 드라카(Drakhar) 캐릭터에서 F5로 1인칭 전환 후 다시 3인칭으로 돌아오면 카메라가 캐릭터 내부에 고정된다. ArmLength가 0인 상태로 유지된다. 메르시, 아레스에서는 정상적으로 3인칭 복귀가 된다.


원인 분석

시점 전환 보간이 Tick() 함수에서 처리되는데, 캐릭터가 최적화를 위해 Tick을 비활성화하고 있었다.

[GS_Player::TogglePerspective]
    ↓
bIsPerspectiveTransitioning = true
SetActorTickEnabled(true)  ← 부모 클래스에서 Tick 활성화
    ↓
[다음 프레임 - GS_Chan::Tick]
    ↓
??? ← 찬은 bCanEverTick=false라서 Tick 자체가 안 돌아감

 

각 캐릭터의 Tick 설정을 정리하면 다음과 같다.

캐릭터 bCanEverTick bStartWithTickEnabled 결과
메르시 true true ✅ 정상
아레스 true true ✅ 정상
찬 false - ❌ Tick 불가
드라카 true true ⚠️ 비행 로직이 Tick 강제 종료

해결 과정

찬(Chan) 수정:

// 문제가 있는 코드
AGS_Chan::AGS_Chan()
{
    PrimaryActorTick.bCanEverTick = false;
}

// 수정된 코드
AGS_Chan::AGS_Chan()
{
    PrimaryActorTick.bCanEverTick = true;
    PrimaryActorTick.bStartWithTickEnabled = false;
}

드라카(Drakhar) 수정:

// 문제가 있는 코드
void AGS_Drakhar::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    if (!bIsFlying)
    {
        SetActorTickEnabled(false);
    }
}

// 수정된 코드
void AGS_Drakhar::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    if (!bIsFlying)
    {
        if (!bIsPerspectiveTransitioning)
        {
            SetActorTickEnabled(false);
        }
    }
}

bCanEverTick과 bStartWithTickEnabled의 차이를 명확히 이해해야 한다.

bCanEverTick = false로 설정하면 SetActorTickEnabled(true)를 호출해도 Tick이 실행되지 않는다.

반면 bStartWithTickEnabled = false는 Tick 실행 자체는 가능하되, 게임 시작 시에는 비활성화된 상태로 시작한다.

“상속 구조에서 부모 클래스의 Tick 로직이 자식 클래스의 최적화 코드에 의해 방해받을 수 있다. 자식 클래스에서 Tick을 끄기 전에 부모의 진행 중인 작업을 확인해야 한다.”

 


문제 3: 멀티플레이에서 캐릭터 회전 불일치

증상

다른 플레이어 화면에서 내 캐릭터가 바라보는 방향이 실제와 다르게 보인다.

플레이어 A가 1인칭으로 전환하면, 플레이어 B 화면에서 A의 캐릭터가 여전히 이동 방향으로 회전한다.


원인 분석

bUseControllerRotationYaw(캐릭터가 컨트롤러 방향을 따라 회전할지 결정하는 플래그) 설정이

클라이언트에서만 변경되고 서버에 동기화되지 않았다.

// 클라이언트 전용 변경 (다른 플레이어에게 반영 안 됨)
void AGS_Player::TogglePerspective()
{
    if (bIsFirstPerson)
    {
        bUseControllerRotationYaw = true;
    }
}

Unreal Engine의 캐릭터 회전 시스템에서 bUseControllerRotationYaw = true면 캐릭터가 카메라 방향을 바라보고, false면 이동 방향을 바라본다. 서버가 이 값을 모르면 다른 클라이언트에게 잘못된 회전 정보가 전달된다.


해결 과정

// GS_Player.h
UFUNCTION(Server, Reliable)
void Server_SetPerspectiveState(bool bFirstPerson);

// GS_Player.cpp
void AGS_Player::TogglePerspective()
{
    bIsFirstPerson = !bIsFirstPerson;
    
    // 서버에 시점 상태 동기화
    Server_SetPerspectiveState(bIsFirstPerson);
    
    if (bIsFirstPerson)
    {
        bUseControllerRotationYaw = true;
    }
    else
    {
        bUseControllerRotationYaw = bSavedUseControllerRotationYaw;
    }
}

void AGS_Player::Server_SetPerspectiveState_Implementation(bool bFirstPerson)
{
    bUseControllerRotationYaw = bFirstPerson;
}

동기화 흐름은 다음과 같다.

클라이언트 A에서 F5를 누르면 TogglePerspective()가 호출되고, Server_SetPerspectiveState(true) RPC가 서버로 전송된다.

서버에서 bUseControllerRotationYaw = true가 설정되고, 이 값이 다른 클라이언트들에게 복제된다.

“시각적 효과만 영향을 주는 것(FOV, 카메라 거리)은 클라이언트 전용으로 처리해도 된다. 하지만 다른 플레이어에게 보여야 하는 것(회전, 포즈)은 서버 동기화가 필수다.”


문제 4: 디더링 효과로 인한 캐릭터 투명화

증상

1인칭 전환 시 캐릭터 메시가 투명하게 보인다.

머티리얼 구조를 보면 디더링 노드 구조가 확인된다.


원인 분석

디더링(Dithering, 카메라가 캐릭터에 가까워질 때 메시를 점진적으로 투명하게 만드는 기법)은 3인칭에서 벽에 막혔을 때 캐릭터가 안 보이는 문제를 해결하기 위한 기법이다.

 

문제는 1인칭에서 카메라가 캐릭터 내부에 있으므로 디더링이 캐릭터를 투명하게 만든다는 것이다.

 

머티리얼 그래프를 보면 DitherDistance 파라미터가 MF_Dither 함수를 거쳐 DitherTemporalAA로 연결된다.

거리값에 따라 알파 임계값이 조절되는 구조다.


해결 과정

void AGS_Player::UpdateCharacterDither(bool bEnabled)
{
    // 1인칭: 거리 0 (디더링 비활성화)
    // 3인칭: 기본 거리 (디더링 활성화)
    float TargetDistance = bEnabled ? DefaultDitherDistance : 0.0f;

    TArray<UMeshComponent*> MeshComps;
    GetComponents<UMeshComponent>(MeshComps);

    // 부착된 액터(무기 등)의 메시도 포함
    TArray<AActor*> AttachedActors;
    GetAttachedActors(AttachedActors);
    for (AActor* AttachedActor : AttachedActors)
    {
        if (AttachedActor)
        {
            TArray<UMeshComponent*> AttachedMeshComps;
            AttachedActor->GetComponents<UMeshComponent>(AttachedMeshComps);
            MeshComps.Append(AttachedMeshComps);
        }
    }

    for (UMeshComponent* MeshComp : MeshComps)
    {
        if (MeshComp)
        {
            MeshComp->SetScalarParameterValueOnMaterials(DitherParamName, TargetDistance);
        }
    }
}

void AGS_Player::TogglePerspective()
{
    if (bIsFirstPerson)
    {
        UpdateCharacterDither(false);
    }
    else
    {
        UpdateCharacterDither(true);
    }
}

무기나 장비 등 부착된 액터의 메시도 함께 처리해야 한다.

그렇지 않으면 캐릭터는 보이는데 무기만 투명해지는 현상이 발생한다.


문제 5: 메르시 줌 FOV 계산 오류

증상

1인칭에서 조준 시 화면이 갑자기 확대되지 않고 오히려 축소되는 느낌이 든다.


원인 분석

// 잘못된 기준값 사용
float TargetFOV = FMath::Lerp(SavedTPSFOV, SavedTPSFOV - 20.0f, Alpha);
// 1인칭 FOV가 75인데, 줌 시 90→70으로 계산되어 화면이 넓어짐

항상 3인칭 FOV(90)를 기준으로 계산했기 때문에, 1인칭 FOV(75)에서 줌을 시작하면 오히려 시야가 넓어지는 역효과가 발생했다.


해결 과정

// 현재 시점에 맞는 기준값 사용
float BaseFOV = IsFirstPerson() ? FirstPersonFOV : SavedTPSFOV;
float TargetFOV = FMath::Lerp(BaseFOV, BaseFOV - 20.0f, Alpha);
// 1인칭: 75→55, 3인칭: 90→70 으로 올바르게 계산

정리

시점 전환 시스템 체크리스트

  • [ ] 카메라 관련 함수에서 현재 시점 상태 확인
  • [ ] 자식 클래스 Tick 설정이 부모 로직을 방해하지 않는지 확인
  • [ ] 다른 플레이어에게 보여야 하는 값은 Server RPC로 동기화
  • [ ] 디더링 등 거리 기반 머티리얼 효과 처리
  • [ ] FOV 계산 시 현재 시점 기준값 사용

Tick 최적화 패턴

// 생성자
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = false;

// 필요할 때만 활성화
SetActorTickEnabled(true);

// 작업 완료 후 비활성화
SetActorTickEnabled(false);

멀티플레이 동기화 기준

항목 동기화 필요 방법
카메라 FOV ❌ 로컬 전용
카메라 거리 ❌ 로컬 전용
캐릭터 회전 ✅ Server RPC
애니메이션 상태 ✅ Replicated 변수

관련 파일 요약

파일 역할 주요 수정
GS_Player.cpp 시점 전환 기반 클래스 TogglePerspective(), Server RPC 추가
GS_Merci.cpp 메르시 줌 로직 UpdateZoom() 시점 분기 추가
GS_Drakhar.cpp 드라카 비행 카메라 Tick 종료 조건에 보간 체크 추가
GS_Chan.cpp 찬 Tick 설정 bCanEverTick 활성화

 

 

'Dev. > UE 언리얼 엔진' 카테고리의 다른 글

[NodeToCode + 로컬 AI 가이드] 블루프린트를 N초 만에 C++로?  (0) 2026.01.27
언리얼 엔진 GAS(Gameplay Ability System)  (1) 2026.01.21
[GAS] Unreal Engine 데미지 팝업 시스템 개선: 타격감을 위한 Dev Log  (0) 2026.01.16
[GAS] 1월 초 구현한 시스템 정리  (0) 2026.01.16
[GAS] Unreal Engine 죽음 시네마틱 카메라 시스템  (2) 2026.01.14
'Dev./UE 언리얼 엔진' 카테고리의 다른 글
  • [NodeToCode + 로컬 AI 가이드] 블루프린트를 N초 만에 C++로?
  • 언리얼 엔진 GAS(Gameplay Ability System)
  • [GAS] Unreal Engine 데미지 팝업 시스템 개선: 타격감을 위한 Dev Log
  • [GAS] 1월 초 구현한 시스템 정리
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)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

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

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
raindrovvv
[GAS] Unreal Engine 1인칭/3인칭 카메라 전환 시스템
상단으로

티스토리툴바