UE LineTrace가 무시되는 조건과 CCD 적용
시작점이 표면에 닿으면 결과가 통째로 버려진다
드론이 바닥을 뚫고 내려가고 어떤 각도에서는 바닥이 천장처럼 굴었다. 원인은 LineTrace 시작점이 콜리전 표면과 같은 높이라 엔진이 결과를 버린 것. 빠른 낙하의 터널링은 CCD로 막았다.
6DOF 드론 Pawn을 만들 때 착지 스냅으로 잡았다고 생각한 바닥 문제가 다시 돌아왔다. 이번엔 통통 튀는 게 아니라 바닥을 그냥 뚫고 내려갔고, 어떤 각도에서는 바닥 판이 천장처럼 굴었다. 이 글에서는 그 원인을 추적해 잡은 과정을 이야기하려 한다 — 원인은 LineTrace 시작점이 콜리전 표면과 같은 높이라 trace 자체가 무시된 것이었고, 거기에 빠른 낙하에서의 터널링이 겹쳐 있었다. 별도의 신규 구현 없이 트러블슈팅 한 건을 끝까지 파는 글이다.
트러블슈팅 — 드론이 바닥을 뚫고 내려간다
증상 — plane 이 천장처럼 동작
PIE 시작 직후 드론 Pawn 이 바닥에 안착하지 않고 그대로 뚫고 내려간다. 그 후 다음 PIE 에서 같은 plane 이 천장처럼 동작 — Pawn 이 위에서 못 들어오고 바닥에 막힘. 같은 plane 인데 한 번은 통과하고 한 번은 막는 비대칭 동작이 핵심 단서.
근본 원인 — LineTrace 시작점이 표면 위
AVOIDDronePawn::CheckGround 가 WorldBottom = GetActorLocation() - (0, 0, HalfHeight) 위치에서 LineTrace 를 시작했다. WorldBottom 은 콜리전 박스의 하단 표면과 같은 Z. 언리얼 물리 엔진은 trace 시작점이 콜리전 표면을 침범한 상태로 시작하면 “내부에서 시작” 으로 분류해 그 trace 자체를 무시한다.
1
2
3
4
5
6
[기존] LineTrace start = WorldBottom (= 콜리전 하단 표면 Z)
end = WorldBottom + (0, 0, -ProbeDist)
→ 시작점이 표면과 동일 → UE 내부 판정: "starting inside"
→ Hit 무시 → bIsGrounded = false
→ 매 프레임 중력 가속도 누적 → 속도 폭증 → 바닥 뚫기
bIsGrounded 가 영구 false 라 중력 끄는 분기에 진입 못 함. 결국 무한 가속 낙하.
수정 1 — CheckGround 시작점 3cm 위로
trace 시작점을 표면에서 살짝 들어 올린다. UE 의 “starting inside” 판정 영역에서 벗어나는 게 목적이라 1~5cm 정도면 충분.
1
2
3
4
5
6
7
// VOIDDronePawn.cpp — CheckGround()
const FVector WorldBottom = GetActorLocation() - FVector(0.f, 0.f, HalfHeight);
// (변경 전) const FVector Start = WorldBottom;
// (변경 후) 표면 위 3cm 에서 시작 — "starting inside" 회피
const FVector Start = WorldBottom + FVector(0.f, 0.f, 3.f);
const FVector End = WorldBottom + FVector(0.f, 0.f, -5.f); // 5cm 아래까지 검사
이렇게 하면 trace 가 외부에서 시작 → 표면 도달 시점이 정상 Hit 으로 잡힘 → bIsGrounded=true.
수정 2 — CCD 적용 (현재→예측 전 구간 trace)
위 수정만으로는 “느린 낙하” 만 잡힌다. 빠른 낙하 (한 프레임에 박스 두께를 초과하는 거리 이동) 에서는 여전히 바닥을 통과한다. 이게 이산 시뮬레이션의 터널링 문제 (tunneling) — 단일 지점 검사가 한 프레임 사이의 이동 경로를 보지 못하는 현상.
1
2
3
4
5
6
7
8
9
10
11
12
// (변경 전) 예측 지점만 trace
const FVector PredictedBottom = WorldBottom + Velocity * dt;
const FVector Start = PredictedBottom + FVector(0,0,3);
const FVector End = PredictedBottom + FVector(0,0,-ProbeDist);
// → 현재 위치 ↔ 예측 위치 사이 구간을 전혀 보지 않음
// (변경 후) 현재 위치에서 예측 위치까지 전 구간 trace = CCD
const FVector PredictedBottom = WorldBottom + Velocity * dt;
const FVector Start = WorldBottom; // 현재 위치 (바닥 표면)
const FVector End = PredictedBottom; // 예측 위치 (다음 프레임 바닥)
GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params);
핵심 아이디어: trace 길이를 “탐색 거리” 가 아니라 “이번 프레임 동안 이동할 거리” 로 잡는다. 어떤 속도라도 한 프레임 안에 지나갈 경로가 trace 에 모두 포함되므로 바닥을 절대 건너뛰지 않음.
Start = WorldBottom, End = PredictedBottom 의 의미를 풀면:
- Start — 이번 프레임 시작 시점의 바닥 위치
- End — 이번 프레임 끝 시점의 바닥 위치 (속도 적분 결과)
- 두 점 사이를 선분 으로 trace → 빠른 낙하 (예: dt=0.016 에 100cm 이동) 에서도 그 100cm 구간 어디든 바닥이 있으면 검출
왜 빠른 낙하에서 바닥을 뚫는가 — 단일 지점 검사의 한계
| 프레임 | 위치 (Z) | 속도 (Z) | 단일 지점 trace 결과 |
|---|---|---|---|
| t=0 | 100 | -10 | 99~95 검사 → 바닥(=0) 못 봄 |
| t=1 | 90 | -100 | 89~85 검사 → 못 봄 |
| t=2 | -10 | -200 | -11~-15 검사 → 이미 바닥 통과 |
중력 가속이 누적되면서 매 프레임 이동량이 5cm → 10cm → 100cm 로 폭증. 단일 지점 trace 는 “현재 위치 주변 5cm” 만 검사하므로 한 프레임에 100cm 점프하는 순간 바닥을 건너뛴다.
CCD 는 이 문제를 “이번에 지나갈 길 전체를 trace” 로 풀어낸다. 같은 t=1 → t=2 구간에서 trace 가 90 → -10 선분을 검사 → Z=0 지점에서 정상 Hit → bIsGrounded=true → 중력 클램프.
| 항목 | 단일 지점 trace | CCD trace |
|---|---|---|
| Start | 예측 위치 + 약간 위 | 현재 위치 |
| End | 예측 위치 + 약간 아래 | 예측 위치 |
| 길이 | 고정 (예: 5cm) | 가변 (이번 프레임 이동량) |
| 빠른 낙하 안정성 | 터널링 발생 | 안정 |
| 비용 | 동일 | 동일 (LineTrace 1회) |
비용은 동일한데 정확성이 차원이 다르다. Pawn 의 내장 CharacterMovementComponent 가 안 쓰일 때 (커스텀 무빙) 는 CCD 패턴이 사실상 필수.
정리 — LineTrace에서 다시 안 밟을 함정
LineTrace 시작점은 표면을 침범하면 무시된다. UE 물리 엔진은 trace start 가 콜리전 내부 (또는 표면과 동일 Z) 면 “starting inside” 로 분류해 결과 무시. trace 시작점은 항상 표면에서 살짝 벗어난 위치 (1~5cm) 로 잡아야 한다.
WorldBottom직접 사용 금지. 단순한데 디버깅 시간을 가장 많이 잡아먹는 함정.단일 지점 trace 는 빠른 이동에서 터널링한다. 한 프레임 이동량이 trace 길이를 넘으면 그 사이 표면을 그대로 통과. 해결: trace 길이를 “탐색 거리” 가 아니라 “이번 프레임 이동량” 으로 잡는다 = CCD.
Start = 현재 위치,End = 예측 위치가 가장 단순한 CCD 패턴. 비용은 단일 trace 와 동일.Symplectic Euler 두 줄이 모든 게임 물리의 출발점.
v += a*dt; p += v*dt순서가 핵심. 순서 바꾸면 에너지 누설 → 발산. 한 줄 비용 0 으로 안정성 확보.Tick안에서 무빙 직접 짜야 할 때 가장 먼저 적용할 패턴.종단속도는 게임 감속 표현의 일반화된 도구.
F_drag = -kv만 추가하면 물 / 공기 저항 / 점성 매질 모두 표현.k가 크면 종단속도 작음 = 빨리 멈춤. 별도 조건문 없이 자연스러운 감속.같은 힘, 다른 질량 → 시간 제곱 비례 격차.
Δp = ½(F/m₁ - F/m₂)·t². 폭발 임펄스에서 가벼운 박스가 멀리 날아가는 게 정확히 이 식. 질량 차가 시각적으로 드러나는 핵심 메커니즘.함수 한 줄 주석이 데코 주석보다 정보 밀도 높다.
// --- ... ---데코는 IDE outline 노이즈 + 의미 없는 diff 라인. 함수 위 한 줄 한국어 설명이 검색·가독성·diff 모두에서 우월. 신규 주석은 데코 금지가 합리적 디폴트.헤더 멤버 주석은 단위 + 디폴트값을 명시.
// 무게 비율 임계값. 이 이상이면 Overweight ON (디폴트 0.5)처럼 의미 + 단위 + 디폴트를 한 줄에. BP 측에서 값 조정할 사람도 같은 주석을 보므로 단위 명시 필수.회귀 sweep 단계의 주석 작업은 코드를 다시 한 번 통독하는 효과. 11 개 cpp 를 주석 다는 동안 자연스럽게 함수 책임을 재확인 → 어색한 책임 분리 / 중복 호출 / 죽은 코드 발견 가능. 마감 D-1 에서 새 기능 짜지 말고 주석 + 통독으로 안정화하는 전략이 위험 대비 가장 큰 이득.
핵심 요약 — LineTrace는 시작점이 콜리전 표면과 같은 위치면 “starting inside”로 분류돼 통째로 무시된다. 시작점을 표면에서 3cm 들어 올려 해결했고, 빠른 낙하의 터널링은 trace 구간을 “현재 위치→예측 위치”로 잡는 CCD로 막았다 — 비용은 단일 trace와 동일하다.