TIL 2026-04-30
2026-04-30 NBC 드론 바닥 충돌 디버깅 + VoidUnreal 전체 주석화 + 게임물리 적분 퀴즈 + 과제8 제출 폼
목차
- 2026-04-30 NBC 드론 바닥 충돌 디버깅 + VoidUnreal 전체 주석화 + 게임물리 적분 퀴즈 + 과제8 제출 폼
오늘 한 일 요약
- NBC 과제7 드론 Pawn 바닥 뚫기 버그 수정 — LineTrace 시작점이 표면 위라 UE 가 “내부 시작” 으로 인식 →
bIsGrounded=false→ 매 프레임 중력 가속 → 바닥 뚫기 → plane 이 천장처럼 동작 - CheckGround 시작점 3cm 위로 —
WorldBottom + (0,0,3)→WorldBottom + (0,0,-5), 표면 침범 회피 - CCD (Continuous Collision Detection) 적용 — 예측 지점만 단일 trace 하던 방식을 현재 위치(WorldBottom) → 예측 위치(PredictedBottom) 까지 전 구간 trace 로 교체, 빠른 낙하에서도 바닥 검출 보장
- VoidUnreal 과제8 전체 주석 작업 — 11 개 cpp 파일 함수 레벨 + 헤더 UPROPERTY/UFUNCTION 멤버 변수 주석
- VOIDGameMode 스타일 통일 — 함수 위 한 줄
// 한 줄 설명패턴, 데코(// --- ... ---) 신규 추가 금지, 기존 데코는 보존 - 게임물리 강의 — 수치 적분 퀴즈 풀이 — Symplectic Euler 두 줄 (
v += a*dt,p += v*dt), 초기값 (p=(0,0), v=(2,0), a=(0,-10), dt=0.1) 3-스텝 손계산 - 뉴턴 2법칙 + 감쇠력 —
a = F/m, 감쇠-kv→ 종단속도v_term = mg/k수렴 원리 - 질량 다른 두 물체 시뮬레이션 — 같은 힘 적용 시 가속도가 질량에 반비례 → 위치 차이가 시간 제곱에 비례
- 과제8 제출 폼 작성 — 필수/도전 기능 정리, 나만의 기능 4종 (차량 부품 / 무게 / 소음+AI / 무기), 기술적 어려움 5가지 정리
NBC 과제7 — 드론 Pawn 바닥 뚫기 버그 수정
증상 — 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 패턴이 사실상 필수.
VoidUnreal 과제8 — 전체 소스 주석 작업
작업 범위 — 11개 cpp + 헤더 멤버
8번 과제 마감 D-1 회귀 sweep 의 일환으로 전체 소스에 가독성 주석을 추가. 11개 cpp 파일 함수 레벨, 그리고 그에 대응하는 헤더의 UPROPERTY / UFUNCTION 멤버 변수 주석.
1
2
3
4
5
6
7
8
9
10
11
VOIDPlayerCharacter.cpp / .h
VOIDPlayerController.cpp / .h
VOIDGameMode.cpp / .h ← 스타일 기준
VOIDGameState.cpp / .h
VOIDHealthComponent.cpp / .h
VOIDInventoryComponent.cpp / .h
VOIDDebuffComponent.cpp / .h
VOIDVehicle.cpp / .h
VOIDPartSlot.cpp / .h
VOIDZombieCharacter.cpp / .h
VOIDHUDWidget.cpp / .h
주석 스타일 — VOIDGameMode 패턴
이미 잘 정리되어 있던 VOIDGameMode.cpp 의 주석 스타일을 표준으로 채택. 함수 정의 바로 위에 한 줄 설명.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 좀비 처치 시 점수 가산. ScoreReward 디폴트 10
void AVOIDZombieCharacter::HandleDeath()
{
if (auto* GS = GetWorld()->GetGameState<AVOIDGameState>())
{
GS->AddScore(ScoreReward);
}
}
// HP 0 도달 시 PlayerController → GameMode 위임 체인 시작점
void UVOIDHealthComponent::ApplyDamage(float Damage)
{
// ...
}
헤더 멤버 변수 주석은 한 줄 설명 + 단위 / 디폴트값을 명시.
1
2
3
4
5
6
7
// 무게 비율 임계값. 이 이상이면 Overweight 디버프 ON (디폴트 0.5)
UPROPERTY(EditDefaultsOnly, Category="Inventory")
float OverweightRatioThreshold = 0.5f;
// 시작 무기 우선순위 1 — 샷건 (디버프 발동 체감 윈도우 확보용)
UPROPERTY(EditDefaultsOnly, Category="Loadout")
TObjectPtr<UVOIDWeaponConfig> ShotgunConfig;
데코 패턴 신규 금지 / 기존 보존 원칙
전역 메모리 주석 스타일 규칙:
- 신규 주석은 데코 패턴 (
// --- ... ---) 금지 — 한 줄 간결 - 기존 데코 주석은 보존 — 굳이 제거하지 않음
1
2
3
4
5
6
// (X) 신규 추가 금지
// --- Engine Start ---
// 시동 분기 진입
// (O) 신규 추가 허용
// 시동 분기 진입. bRepairComplete 일 때만 GameMode 에 위임
이유: 데코 주석은 시각적 구분에는 좋지만 IDE outline 에서 노이즈가 되고 diff 에서 의미 없는 라인이 늘어남. 한 줄 설명이 가장 정보 밀도가 높다.
게임물리 강의 — 수치 적분 퀴즈
오늘 강의에서 가장 핵심은 물리 시뮬레이션의 두 줄. 모든 게임 물리는 결국 이 두 줄로 환원된다.
핵심 두 줄 — Symplectic Euler
1
2
3
// 매 프레임 (dt 시간 동안)
v += a * dt; // (1) 속도 갱신 — 가속도 적분
p += v * dt; // (2) 위치 갱신 — 속도 적분
순서가 핵심. 속도를 먼저 갱신하고 그 새 속도로 위치를 갱신 (Symplectic Euler). 반대로 하면 (p += v*dt; v += a*dt) 에너지가 매 프레임 누설돼 발산하거나 진동이 점점 커진다.
| 적분기 | 식 | 안정성 |
|---|---|---|
| Explicit Euler | p += v*dt; v += a*dt | 에너지 누설, 발산 가능 |
| Symplectic Euler | v += a*dt; p += v*dt | 에너지 보존 근사, 안정 |
| Verlet | p += v*dt + 0.5*a*dt²; v += a*dt | 더 정확, 충돌 처리 까다로움 |
게임에서는 거의 항상 Symplectic Euler — 한 줄 추가 비용 0, 안정성 큰 이득.
3-스텝 손계산 (포물선 운동)
초기값:
- 위치
p₀ = (0, 0) - 속도
v₀ = (2, 0)— 수평 우측 - 가속도
a = (0, -10)— 중력 (아래) dt = 0.1s
3 스텝 (t=0.1, 0.2, 0.3) 진행:
| step | v 갱신 | p 갱신 |
|---|---|---|
| 1 | v = (2, 0) + (0,-10)*0.1 = (2, -1) | p = (0,0) + (2,-1)*0.1 = (0.2, -0.1) |
| 2 | v = (2,-1) + (0,-10)*0.1 = (2, -2) | p = (0.2,-0.1) + (2,-2)*0.1 = (0.4, -0.3) |
| 3 | v = (2,-2) + (0,-10)*0.1 = (2, -3) | p = (0.4,-0.3) + (2,-3)*0.1 = (0.6, -0.6) |
관찰:
- 수평 속도 는 2 로 일정 (가속도 X 성분 = 0)
- 수직 속도 는 -1, -2, -3 으로 등차 (중력 누적)
- 수평 위치 는 0.2, 0.4, 0.6 으로 등차 (속도가 일정하니 변위도 일정)
- 수직 위치 는 -0.1, -0.3, -0.6 으로 차이가 0.2, 0.3 → 등차의 누적, 즉 시간 제곱 비례 (포물선)
해석적 해 y(t) = -½ g t² 와 비교:
- t=0.1 → 해석: -0.05, 수치: -0.1 (Symplectic Euler 는 약간 빠르게 떨어짐)
- 누적 오차는
dt에 비례.dt를 작게 (0.01) 하면 해석 해에 수렴.
뉴턴 2법칙 + 감쇠력 → 종단속도
1
F = m·a → a = F / m
낙하하는 물체에 공기 저항 (감쇠력 -kv) 추가하면:
1
2
F_total = mg - kv
a = (mg - kv) / m
속도가 커질수록 kv 가 커져 가속도가 0 에 수렴. 종단속도 v_term = mg / k (a=0 일 때).
1
2
3
4
5
6
// 매 프레임
const float Drag = -k * Velocity;
const float NetF = Mass * Gravity + Drag;
const float Accel = NetF / Mass;
Velocity += Accel * dt;
Position += Velocity * dt;
게임에서 물에 들어갔을 때 빨리 멈추기 같은 표현은 정확히 이 패턴. k (drag coefficient) 만 키우면 종단속도가 줄어 자연스럽게 감속.
질량 다른 두 물체 — 같은 힘, 다른 가속도
1
2
3
F = 10 N (같은 힘)
m1 = 1 kg → a1 = 10 m/s²
m2 = 2 kg → a2 = 5 m/s²
같은 시간 t 동안 위치 차이는:
1
2
3
p1(t) = ½ · 10 · t²
p2(t) = ½ · 5 · t²
Δp(t) = 2.5 · t²
시간 제곱에 비례하는 격차. 같은 힘으로 밀어도 가벼운 물체가 점점 더 멀리 간다는 직관과 일치. 게임에서 폭발 임펄스 적용 시 가벼운 박스가 더 멀리 날아가는 이유.
과제8 제출 폼 작성
필수 / 도전 기능
| 분류 | 항목 | 상태 |
|---|---|---|
| 필수 | TPS 캐릭터 (이동 / 점프 / 카메라) | 완료 |
| 필수 | 좀비 AI (가시선 추적 / 공격) | 완료 |
| 필수 | 무기 시스템 (라이플 / 샷건) | 완료 |
| 필수 | 인벤토리 시스템 (픽업 / 드롭 / 슬롯) | 완료 |
| 필수 | 웨이브 / 스폰 시스템 | 완료 |
| 필수 | HUD (HP / Score / Wave / Time) | 완료 |
| 도전 | 디버프 시스템 (Bleeding / Fracture / Overweight) | 완료 |
| 도전 | 차량 부품 슬롯 + 시동 분기 | 완료 |
| 도전 | 레벨 전환 (Lv_Main / Lv_VoidProto / Lv_Escape) | 완료 |
| 도전 | 게임오버 / 게임클리어 분기 | 완료 |
| 도전 | 10 분 탈출 타이머 | 완료 |
나만의 기능 4종
- 차량 부품 시스템 —
AVOIDPartSlotChildActorComponent 3 개를 차량 본체에 부착, 부품 데이터 (UVOIDPartData) 와 슬롯의PartType매칭 검증, 3 개 모두 채워야bRepairComplete=true→ 시동 분기 진입 가능 - 무게 시스템 → 이동 / 소음 영향 — 인벤토리 누적 무게 비율이 50% 이상이면 Overweight 디버프 ON, 이동 속도 곱셈 감소 + 발걸음 소음 반경 증가 → 좀비 감지 거리에 영향
- 소음 + AI 감지 시스템 — 플레이어 이동 / 사격 시
MakeNoise가EQS의PerceptionComponent에 전달, 좀비 AI 가 시선 외 영역에서도 소음 발생지로 이동 - 무기 시스템 (라이플 / 샷건 + ADS + 반동) — 라이플은 단발 정확도, 샷건은 산탄 패턴 + 광역 데미지, 우클릭 ADS 시 카메라 FOV 축소 + 반동 감소 곱셈
기술적 어려움 5가지
- 차량 슬롯 PIE 검증 블로커 6 개 연쇄 — 콜리전 / Mobility /
AddIgnoredActor루프 / Sweep 차단 /OwnerVehicleBP 자식 설정 /Slot변수명 가림 — 한 페이즈 내 전부 해소 (어제 4/29 Day 5 작업) - HUD 4 페이지 단일 위젯 통합 —
MainMenu / InGame / GameOver / GameClear4 화면을 위젯 4 개로 만드는 대신Lv_1 + Lv_2두 컨테이너 + enum 토글 → swap 로직 0 개 - 델리게이트 바인딩 이전 Broadcast 손실 —
NativeConstruct시점은 이미 BeginPlay 가 끝나서 초기 Broadcast 가 지나간 후 → 바인딩 직후 현재값으로 핸들러 1 회 직접 호출 패턴 표준화 - GameOver = 레벨 전환 X —
OpenLevel호출 시 시체 / 좀비 / 인벤토리 배경이 다 사라져 몰입 깨짐 → 같은 레벨에서SetHUDMode(GameOver)만 토글, ReStart 시점에야 레벨 리셋 - HealthComponent 누락 BP 인스턴스 fallback — BP 측에서
HealthComponent가 빠진 인스턴스가 있어nullptr크래시 →BeginPlay에서NewObject+RegisterComponent런타임 복구
오늘 배운 것 정리
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 에서 새 기능 짜지 말고 주석 + 통독으로 안정화하는 전략이 위험 대비 가장 큰 이득.
제출 폼 작성은 “내가 한 일 회고” 의 강제 장치. 필수 / 도전 / 나만의 기능 / 기술적 어려움 4 분류로 나눠 적으면 5 일간 흐릿했던 작업이 명확히 정리됨. 다음 팀플 (05-01 시작) 에서 어떤 모듈을 가져올지 판단 근거가 됨. TIL 만으론 부족하고 제출 폼처럼 “외부 시각으로 다시 적기” 가 회고를 압축하는 도구.
마감 D-1 = 본 기능 동결 + 회귀만. 본체 종결 (D 0 조기 마감) 한 다음 날에 새 기능 욕심 내면 회귀 위험만 키운다. 오늘은 의도적으로 (1) 회귀 sweep, (2) 주석 작업, (3) 제출 폼, (4) 다른 학습 (NBC 드론 / 게임물리) 으로 시간을 분산. 마감 안전성 + 다른 진도 동시 확보.