언리얼엔진/구현·실습

Unreal Engine TPS 캐릭터 구현 정리

write76465 2026. 7. 1. 17:37

무기별 로코모션, 장착 메시, 조준 상태, 카메라 전환까지

이번 작업에서는 TPS 캐릭터의 기본 이동 구조 위에 무기별 로코모션과 조준 시스템을 추가했다. 처음에는 피스톨 로코모션만 있는 상태였고, 이후 라이플과 샷건을 같은 구조로 확장했다. 최종적으로는 무기 타입에 따라 캐릭터 애니메이션, 실제 장착 무기 메시, 조준 포즈, 카메라 시점이 함께 바뀌도록 구성했다.


1. 무기별 로코모션 분리

기존에는 피스톨 기준의 로코모션만 사용하고 있었다. 이후 라이플과 샷건을 추가하기 위해 AnimGraph에 무기별 State Machine을 구성했다.

구조는 다음과 같다.

Pistol_Locomotion
Rifle_Locomotion
Shotgun_Locomotion
→ Blend Poses by EWeaponType
→ Orientation Warping
→ Output Pose

무기 타입은 EWeaponType enum으로 관리했다.

Pistol
Rifle
Shotgun

AnimBP에서는 AnimWeaponType 변수를 만들고, 캐릭터 블루프린트의 CurrentWeaponType 값을 매 프레임 받아오도록 했다.

BP_PlayerCharacter.CurrentWeaponType
→ ABP_PlayerCharacter.AnimWeaponType
→ Blend Poses by EWeaponType

이 구조 덕분에 무기 타입만 바꾸면 해당 무기에 맞는 로코모션이 자동으로 선택된다.


2. 무기 메시 장착

처음에는 스켈레톤 에디터에서 보이던 총이 실제 게임에서도 붙어 있는 줄 알았지만, 확인해보니 그것은 Preview Asset이었다. Preview Asset은 에디터에서 소켓 위치를 맞추기 위한 미리보기용 메시이며, 실제 게임에서는 보이지 않는다.

따라서 실제 게임에서 무기를 보이게 하기 위해 BP_PlayerCharacter에 무기별 Skeletal Mesh Component를 추가했다.

BP_PlayerCharacter
→ Mesh
   → PistolMesh
   → RifleMesh
   → ShotgunMesh

각 무기 메시 컴포넌트는 캐릭터 스켈레톤의 WeaponSocket에 붙였다.

Parent Socket = WeaponSocket

처음에는 하나의 WeaponMesh 컴포넌트에 무기 메시를 교체하는 방식도 고려했지만, 피스톨, 라이플, 샷건의 위치와 회전 보정값이 다르기 때문에 각각의 메시 컴포넌트를 따로 두는 방식이 더 직관적이었다.


3. 무기 타입에 따른 메시 전환

무기 타입에 따라 실제 보이는 무기 메시도 바뀌어야 했다. 이를 위해 ApplyWeaponType 함수를 만들었다.

함수의 역할은 다음과 같다.

ApplyWeaponType(NewWeaponType)
→ CurrentWeaponType 설정
→ 무기 타입에 따라 메시 Visibility 변경

예를 들어 피스톨을 장착하면:

CurrentWeaponType = Pistol
PistolMesh = Visible
RifleMesh = Hidden
ShotgunMesh = Hidden

라이플을 장착하면:

CurrentWeaponType = Rifle
PistolMesh = Hidden
RifleMesh = Visible
ShotgunMesh = Hidden

샷건도 같은 방식으로 처리했다.

이 구조의 핵심은 CurrentWeaponType 하나로 두 가지를 동시에 제어한다는 점이다.

CurrentWeaponType
→ AnimBP 로코모션 선택
→ 캐릭터 무기 메시 선택

덕분에 애니메이션과 실제 무기 모델이 따로 놀지 않게 되었다.


4. 무기 교체 테스트

테스트를 위해 키 입력을 임시로 연결했다.

1 → ApplyWeaponType(Pistol)
2 → ApplyWeaponType(Rifle)
3 → ApplyWeaponType(Shotgun)

이렇게 구성하니 키 입력에 따라 총기 메시와 로코모션이 함께 바뀌는 것을 확인할 수 있었다. 이후 실제 무기 교체 시스템이 들어오더라도, 내부적으로는 ApplyWeaponType만 호출하면 되기 때문에 확장하기 쉬운 구조가 되었다.


5. 로코모션 전환 보간

무기 작업 중 이전에 해결했던 이동 애니메이션 문제도 함께 정리했다. 걷기에서 달리기로 갈 때는 자연스러웠지만, 달리기에서 걷기로 돌아올 때 애니메이션이 끊기는 문제가 있었다.

Blend Space Preview에서는 자연스러웠기 때문에 Blend Space 자체의 문제는 아니었다. 실제 게임에서만 끊긴다는 점을 보면, Speed 값이 급격하게 변하는 것이 원인이었다.

이를 해결하기 위해 AnimInstance에 실제 속도와 애니메이션용 속도를 분리했다.

Speed = 실제 캐릭터 속도
AnimSpeed = 보간된 애니메이션용 속도

NativeUpdateAnimation에서 FMath::FInterpTo를 사용해 AnimSpeed를 보간했다.

AnimSpeed = FMath::FInterpTo(
    AnimSpeed,
    Speed,
    DeltaSeconds,
    SpeedInterpRate
);

그리고 Blend Space에는 Speed가 아니라 AnimSpeed를 연결했다.

Blend Space Speed 입력 = AnimSpeed

이후 달리기에서 걷기로 전환될 때 애니메이션이 훨씬 부드러워졌다.


6. 몽타주와 레이어링 시도

무기 교체 시 총을 꺼내는 모션도 넣어보려 했다. 처음에는 Play Montage를 사용해 무기별 Equip Montage를 재생했다.

구조는 다음과 같았다.

키 입력
→ ApplyWeaponType
→ Play Montage

총기 메시가 애니메이션 에셋 안에 포함된 것이 아니라 실제 캐릭터 손에 붙어 있는 구조였기 때문에, 먼저 ApplyWeaponType으로 총을 보이게 한 뒤 몽타주를 재생하는 방식이 맞았다.

이후 장전이나 사격까지 고려해 상체만 몽타주를 적용하는 Layered Blend Per Bone 구조도 시도했다. 그러나 Equip 모션은 하체와 골반 움직임에 의존하는 전신 모션 성격이 강해 상체 레이어로 잘라 쓰기에는 적합하지 않았다.

결론적으로 Equip 모션은 나중에 FullBody Slot으로 처리하고, 장전과 사격 같은 동작은 UpperBody Slot으로 분리하는 방향이 더 적절하다고 판단했다.

Equip / Draw → FullBody
Reload / Fire → UpperBody

7. 조준 상태 구현

다음으로 조준 기능을 만들었다. 조준은 한 번 재생되고 끝나는 액션이 아니라, 버튼을 누르고 있는 동안 유지되는 상태다. 따라서 몽타주가 아니라 bool 상태로 구현했다.

캐릭터에 bIsAiming 변수를 추가했다.

UPROPERTY(BlueprintReadOnly, Category = "Aim")
bool bIsAiming = false;

그리고 입력 바인딩을 통해 조준 시작과 종료를 처리했다.

void APlayerCharacter::StartAim()
{
    bIsAiming = true;
}

void APlayerCharacter::StopAim()
{
    bIsAiming = false;
}

Enhanced Input에서는 Aim Action을 추가하고 다음처럼 바인딩했다.

EnhancedInputComponent->BindAction(
    AimAction,
    ETriggerEvent::Started,
    this,
    &APlayerCharacter::StartAim
);

EnhancedInputComponent->BindAction(
    AimAction,
    ETriggerEvent::Completed,
    this,
    &APlayerCharacter::StopAim
);

AnimBP에서는 캐릭터의 bIsAiming 값을 읽어와 조준 포즈 블렌딩에 사용했다.


8. 조준 포즈 블렌딩

조준 상태에서는 일반 로코모션 위에 ADS 포즈를 섞었다.

처음에는 Layered Blend Per Bone에서 Blend Depth를 크게 잡았지만, 오히려 포즈가 제대로 들어가지 않았다. 스켈레톤에 IK나 무기 보조 본이 많아서 너무 깊게 섞으면 의도하지 않은 본까지 영향을 받는 것으로 보였다.

최종적으로는 Blend Depth를 낮추니 정상적으로 동작했다.

Bone Name = spine_01
Blend Depth = 3
Blend Weight = 1.0

이렇게 설정하니 하체 로코모션은 유지하면서 상체에 조준 포즈가 자연스럽게 적용되었다.

조준 그래프의 개념은 다음과 같다.

Locomotion
→ Layered Blend Per Bone
   Base Pose = Locomotion
   Blend Pose = ADS Pose
→ Blend Poses by Bool
→ Output Pose

IsAiming이 true일 때 ADS 포즈가 적용되고, false일 때는 일반 로코모션이 출력된다.


9. 조준 카메라 전환

조준 상태에서는 애니메이션뿐 아니라 카메라 시점도 바뀌어야 한다. 단순히 Tick에서 계속 카메라 값을 보간하는 방식도 가능하지만, 매 프레임 불필요하게 Tick을 돌리는 것은 아깝다고 판단했다.

그래서 FTimeline과 CurveFloat를 사용해 조준 전환 중에만 Tick을 켜는 방식으로 구현했다.

구조는 다음과 같다.

StartAim
→ Tick 활성화
→ Timeline Play
→ 카메라 줌 인
→ Timeline 종료
→ Tick 비활성화

StopAim
→ Tick 활성화
→ Timeline Reverse
→ 카메라 원복
→ Timeline 종료
→ Tick 비활성화

카메라 보간에는 CurveFloat를 사용했다.

커브는 다음처럼 만들었다.

Time 0.0 → Value 0.0
Time 0.2 → Value 1.0

이 값은 Alpha로 사용된다.

CameraBoom->TargetArmLength = FMath::Lerp(
    DefaultArmLength,
    AimArmLength,
    Alpha
);

CameraBoom->SocketOffset = FMath::Lerp(
    DefaultSocketOffset,
    AimSocketOffset,
    Alpha
);

즉 Alpha가 0이면 기본 카메라, 1이면 조준 카메라가 된다.


10. 조준 카메라 값

조준 시 카메라가 가까워지고 어깨 쪽으로 이동하도록 다음 값을 사용했다.

AimArmLength = 180.0f;
AimSocketOffset = FVector(0.0f, 45.0f, 15.0f);

AimArmLength는 카메라 거리, AimSocketOffset은 카메라 위치 오프셋이다.

AimArmLength 감소 → 카메라가 가까워짐
SocketOffset Y 변경 → 어깨 시점 이동
SocketOffset Z 변경 → 카메라 높이 조정

이 값들은 EditAnywhere로 열어두어 에디터에서 직접 조절할 수 있게 했다.


마무리

이번 작업에서는 단순히 애니메이션을 하나 추가하는 수준이 아니라, 캐릭터의 무기 상태와 애니메이션, 실제 무기 메시, 조준 상태, 카메라 시점을 하나의 흐름으로 연결했다.

핵심 구조는 다음과 같다.

CurrentWeaponType
→ 무기별 로코모션 선택
→ 무기 메시 Visibility 전환

bIsAiming
→ 조준 포즈 블렌딩
→ 조준 카메라 전환

무기별 로코모션은 Blend Poses by EWeaponType으로 선택하고, 조준 상태는 bIsAiming을 통해 AnimBP와 카메라 시스템에 전달했다. 카메라 전환은 CurveFloat와 FTimeline을 사용해 부드럽게 처리하되, 전환 중에만 Tick을 켜도록 만들어 불필요한 Tick 사용을 줄였다.

아직 장전, 사격, 총 꺼내기 모션의 최종 레이어링은 남아 있지만, 이번 작업으로 TPS 캐릭터의 핵심 기반은 어느 정도 잡혔다. 앞으로는 이 구조 위에 발사 로직, 장전 몽타주, 탄약 시스템, 크로스헤어 UI를 연결하면 더 완성도 있는 전투 시스템으로 확장할 수 있을 것 같다.