포스트

UE C++ 6DOF 드론 Pawn 중력·착지 구현

이동 전 예측 트레이스로 통통 튀는 착지 잡기

이동 3축·회전 3축을 전부 플레이어가 쥐는 드론이라 CharacterMovement가 없다. 중력과 착지 스냅을 직접 짜야 했고, 이동 후에 스냅하는 순서 때문에 드론이 바닥에서 튀었다.

UE C++ 6DOF 드론 Pawn 중력·착지 구현

드론은 걷는 캐릭터와 달리 이동 3축 + 회전 3축, 여섯 자유도를 전부 플레이어가 쥔다. 그래서 CharacterMovementComponent가 대신 해 주던 중력과 착지를 직접 짜야 했고, 그 지점에서 문제가 몰려 나왔다. 이 글에서는 C++ Pawn으로 6DOF 드론을 만든 과정을 이야기하려 한다 — 구현은 Enhanced Input 매핑, Pawn 컴포넌트 구조, 중력과 착지 스냅 물리이고, 트러블슈팅은 바닥에서 통통 튀는 현상을 비롯한 네 건이다.

기술 구현 1 — Enhanced Input 세팅 (IMC_Drone)

IA Value Type 변경

IA(Input Action)는 생성 직후 기본값이 Digital(bool) 이므로 먼저 Value Type을 바꿔야 입력값이 제대로 넘어온다.

IAValue Type
IA_MoveAxis2D (Vector2D)
IA_LookAxis2D (Vector2D)
IA_MoveUpAxis1D (float)
IA_RollAxis1D (float)

IMC_Drone 키 매핑

IAModifier
IA_MoveW없음
 SNegate
 DSwizzle (YXZ)
 ASwizzle (YXZ) + Negate
IA_LookMouse XY 2D-Axis없음
IA_MoveUpSpace없음 (+1)
 Left ShiftNegate (-1)
IA_RollMouse Wheel Axis없음

핵심: Triggers / Modifiers는 IA 에셋이 아닌 IMC(Input Mapping Context)에서 키별로 설정한다.
ETriggerEvent::Triggered = 누르는 매 프레임 호출 (기본값, IA에서 따로 설정 안 해도 됨)


기술 구현 2 — C++ Pawn 구조

1
2
3
4
BoxComp (루트, 반크기 XY=50 / Z=15 — 드론 형태에 맞게 납작한 박스)
└─ MeshComp (SM_Drone)
└─ SpringArmComp (400cm)
   └─ CameraComp

AutoPossessPlayer = EAutoReceiveInput::Player0 — PlayerController 없이 바로 빙의
bUsePawnControlRotation = false — 카메라가 Pawn 회전을 따라가지 않음


기술 구현 3 — 중력과 착지 스냅

CheckGround

1
2
3
4
5
6
7
8
9
10
11
// 발밑 (박스 반높이 Z + 1cm) LineTrace로 착지 여부 반환
bool ANBC_AssignmentPawn::CheckGround(FHitResult& OutHit)
{
    const float HH = BoxComp->GetScaledBoxExtent().Z;
    FCollisionQueryParams Params;
    Params.AddIgnoredActor(this);
    return GetWorld()->LineTraceSingleByChannel(
        OutHit, GetActorLocation(),
        GetActorLocation() + FVector(0, 0, -(HH + 1.f)),
        ECC_Visibility, Params);
}

Tick 물리 루프

1
2
3
4
5
6
7
8
9
10
11
12
13
매 프레임:
  CheckGround → bIsGrounded
  if 착지:
    FallVelocity = 0, Z 스냅
  else:
    if bAscending:
      중력 누적 억제 (Space 누르는 동안)
    else:
      FallVelocity += GravityZ * DeltaTime
      예측 위치(PredictedLoc)에서 LineTrace
        충돌 → 이동 전 스냅 (바닥 뚫기 방지)
        없으면 → AddActorWorldOffset
    bAscending = false (매 프레임 리셋)

트러블슈팅

Space 바 통통 튀는 현상

  • 원인: FallVelocity가 한 프레임에 크게 누적돼 드론이 바닥 아래로 터널링(한 프레임 이동량이 커서 콜리전을 뚫고 지나가는 현상) → 스냅이 위로 밀어내면서 반복
  • 해결: 다음 프레임 위치를 먼저 예측(PredictedLoc)하고 그 지점에서 LineTrace → 충돌 시 이동 전에 스냅

착지 중 Shift 하강 / 마우스 Pitch 차단

  • Shift 하강 차단: if (bIsGrounded && V < 0.f) return; — 지상에서 아래로 파고드는 입력 무시
  • Pitch 차단: const float Pitch = bIsGrounded ? 0.f : V.Y; — 착지 중 상하 회전 억제, 좌우(Yaw)는 허용

Space 바 지속 상승 불가

  • 원인: GravityZ = -980MoveSpeed = 600을 이겨서 약 0.6초 후 하강 전환
  • 해결: bAscending 플래그 — Space 입력 프레임에 true 세팅, Tick에서 중력 누적 건너뜀

Space 바 기울기 방향이 아닌 월드 Z로만 이동

  • 원인: AddActorLocalOffset(GetActorUpVector() * ...)GetActorUpVector()는 월드 공간 벡터인데 로컬 오프셋으로 넘겨서 이중 회전 발생. 드론이 수평일 때는 우연히 일치해 보임
  • 해결: AddActorWorldOffset(GetActorUpVector() * V * MoveSpeed * DT, true) 로 변경

WASD 방향이 이상함 (카메라 회전 후)

  • 원인: GetActorForwardVector()가 Pitch 기울기를 포함 → 드론이 기울어지면 W가 비스듬하게 이동
  • 해결: Yaw만 추출한 회전 행렬에서 Forward/Right 계산
1
2
3
4
const FRotator YawOnly = FRotator(0.f, GetActorRotation().Yaw, 0.f);
const FVector Forward = FRotationMatrix(YawOnly).GetScaledAxis(EAxis::X);
const FVector Right   = FRotationMatrix(YawOnly).GetScaledAxis(EAxis::Y);
AddActorWorldOffset((Forward * V.X + Right * V.Y) * MoveSpeed * DeltaTime, true);

CapsuleComponent → BoxComponent 교체

  • 이유: 캡슐은 반지름·반높이만 조절 가능 — 드론처럼 X/Y/Z를 각각 다르게 맞춰야 할 때 부적합
  • 방법: #include "Components/BoxComponent.h", SetBoxExtent(FVector(50, 50, 15)), 반높이 참조는 GetScaledBoxExtent().Z
  • 장점: 에디터 Details 패널에서 XYZ 개별 조정 가능 → 메시에 정확히 피팅

LineTrace 시작점이 액터 중심 → 바닥 뚫기 가능성

  • 원인: CheckGround 트레이스가 GetActorLocation()(중심)에서 시작 — 드론이 기울어지면 실제 박스 바닥이 중심보다 낮은 다른 위치에 있어서 감지가 틀어짐
  • 해결: 박스 로컬 바닥점 (0, 0, -HH)GetActorTransform().TransformPosition() 으로 월드 변환한 위치에서 1cm만 내려서 트레이스
1
2
const FVector WorldBottom = GetActorTransform().TransformPosition(FVector(0.f, 0.f, -BoxComp->GetScaledBoxExtent().Z));
GetWorld()->LineTraceSingleByChannel(OutHit, WorldBottom, WorldBottom + FVector(0, 0, -1.f), ...);
  • 스냅 오프셋도 수정: 기울어진 상태에서는 HH(로컬 Z 반크기)가 실제 월드 Z 거리와 다름 → BottomOffset = Location.Z - WorldBottom.Z 로 계산
1
2
const float BottomOffset = Location.Z - WorldBottom.Z;  // 기울기 반영
SnapLoc.Z = ImpactPoint.Z + BottomOffset;

정리 — 이 구현에서 남은 것

  • LineTrace 충돌 채널: ECC_Visibility 기준으로 바닥이 막혀있어야 감지됨. 콜리전 없는 Static Mesh는 무시된다.
  • 바닥 뚫기 방지는 예측이 핵심: 이동 스냅이 아니라 이동 에 예측 위치를 체크해야 바운스가 없다.
  • bAscending 패턴: 입력 콜백에서 플래그만 세팅하고 실제 처리는 Tick에서 — 입력과 물리를 분리하는 깔끔한 구조.
  • FRotationMatrix(YawOnly): 6DOF 회전 중 수평 이동만 뽑아낼 때 유용. Pitch/Roll 기울어짐과 무관하게 카메라 방향으로 이동 가능.
  • AddActorLocalOffset vs AddActorWorldOffset: GetActorUpVector()는 월드 공간 벡터이므로 AddActorLocalOffset에 넘기면 이중 회전이 적용된다. 액터 Up 방향 이동은 반드시 AddActorWorldOffset(GetActorUpVector() * ...) 으로 써야 한다.
  • CapsuleComponent → BoxComponent: 드론처럼 납작한 형태는 캡슐보다 박스가 적합. SetBoxExtent(FVector(X, Y, Z))로 반크기를 지정하고, 반높이는 GetScaledBoxExtent().Z로 읽는다. 에디터 Details 패널에서 XYZ를 개별 조정 가능해 메시에 정확히 맞출 수 있다.
  • 착지 중 입력 제한: bIsGrounded 플래그를 입력 핸들러에서 체크해 Shift(하강)와 마우스 Pitch(상하 회전)를 차단. 단순한 early return / 조건식으로 자연스러운 지상 동작을 구현할 수 있다.
  • LineTrace는 반드시 월드 변환된 바닥점에서: GetActorLocation()(중심)에서 쏘면 기울어진 액터에서 실제 바닥 접촉점을 놓친다. GetActorTransform().TransformPosition(로컬_바닥점) 으로 월드 바닥 위치를 구한 뒤 트레이스해야 정확하다. 스냅 오프셋도 Location.Z - WorldBottom.Z 로 기울기를 반영해야 한다.

핵심 요약 — 바닥 뚫기(터널링)는 이동 가 아니라 이동 에 예측 위치를 LineTrace로 검사해야 바운스 없이 막을 수 있다. 그리고 GetActorUpVector()는 월드 공간 벡터라 AddActorLocalOffset에 넘기면 이중 회전이 걸린다 — 액터 Up 방향 이동은 AddActorWorldOffset으로 써야 한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.