포스트

TIL 2026-04-30

TIL 2026-04-30

2026-04-30 NBC 드론 바닥 충돌 디버깅 + VoidUnreal 전체 주석화 + 게임물리 적분 퀴즈 + 과제8 제출 폼

목차


오늘 한 일 요약

  1. NBC 과제7 드론 Pawn 바닥 뚫기 버그 수정 — LineTrace 시작점이 표면 위라 UE 가 “내부 시작” 으로 인식 → bIsGrounded=false → 매 프레임 중력 가속 → 바닥 뚫기 → plane 이 천장처럼 동작
  2. CheckGround 시작점 3cm 위로WorldBottom + (0,0,3)WorldBottom + (0,0,-5), 표면 침범 회피
  3. CCD (Continuous Collision Detection) 적용 — 예측 지점만 단일 trace 하던 방식을 현재 위치(WorldBottom) → 예측 위치(PredictedBottom) 까지 전 구간 trace 로 교체, 빠른 낙하에서도 바닥 검출 보장
  4. VoidUnreal 과제8 전체 주석 작업 — 11 개 cpp 파일 함수 레벨 + 헤더 UPROPERTY/UFUNCTION 멤버 변수 주석
  5. VOIDGameMode 스타일 통일 — 함수 위 한 줄 // 한 줄 설명 패턴, 데코(// --- ... ---) 신규 추가 금지, 기존 데코는 보존
  6. 게임물리 강의 — 수치 적분 퀴즈 풀이 — Symplectic Euler 두 줄 (v += a*dt, p += v*dt), 초기값 (p=(0,0), v=(2,0), a=(0,-10), dt=0.1) 3-스텝 손계산
  7. 뉴턴 2법칙 + 감쇠력a = F/m, 감쇠 -kv → 종단속도 v_term = mg/k 수렴 원리
  8. 질량 다른 두 물체 시뮬레이션 — 같은 힘 적용 시 가속도가 질량에 반비례 → 위치 차이가 시간 제곱에 비례
  9. 과제8 제출 폼 작성 — 필수/도전 기능 정리, 나만의 기능 4종 (차량 부품 / 무게 / 소음+AI / 무기), 기술적 어려움 5가지 정리


NBC 과제7 — 드론 Pawn 바닥 뚫기 버그 수정

증상 — plane 이 천장처럼 동작

PIE 시작 직후 드론 Pawn 이 바닥에 안착하지 않고 그대로 뚫고 내려간다. 그 후 다음 PIE 에서 같은 plane 이 천장처럼 동작 — Pawn 이 위에서 못 들어오고 바닥에 막힘. 같은 plane 인데 한 번은 통과하고 한 번은 막는 비대칭 동작이 핵심 단서.

근본 원인 — LineTrace 시작점이 표면 위

AVOIDDronePawn::CheckGroundWorldBottom = 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=0100-1099~95 검사 → 바닥(=0) 못 봄
t=190-10089~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 → 중력 클램프.

항목단일 지점 traceCCD 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 Eulerp += v*dt; v += a*dt에너지 누설, 발산 가능
Symplectic Eulerv += a*dt; p += v*dt에너지 보존 근사, 안정
Verletp += v*dt + 0.5*a*dt²; v += a*dt더 정확, 충돌 처리 까다로움

게임에서는 거의 항상 Symplectic Euler — 한 줄 추가 비용 0, 안정성 큰 이득.

3-스텝 손계산 (포물선 운동)

초기값:

  • 위치 p₀ = (0, 0)
  • 속도 v₀ = (2, 0) — 수평 우측
  • 가속도 a = (0, -10) — 중력 (아래)
  • dt = 0.1 s

3 스텝 (t=0.1, 0.2, 0.3) 진행:

stepv 갱신p 갱신
1v = (2, 0) + (0,-10)*0.1 = (2, -1)p = (0,0) + (2,-1)*0.1 = (0.2, -0.1)
2v = (2,-1) + (0,-10)*0.1 = (2, -2)p = (0.2,-0.1) + (2,-2)*0.1 = (0.4, -0.3)
3v = (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종

  1. 차량 부품 시스템AVOIDPartSlot ChildActorComponent 3 개를 차량 본체에 부착, 부품 데이터 (UVOIDPartData) 와 슬롯의 PartType 매칭 검증, 3 개 모두 채워야 bRepairComplete=true → 시동 분기 진입 가능
  2. 무게 시스템 → 이동 / 소음 영향 — 인벤토리 누적 무게 비율이 50% 이상이면 Overweight 디버프 ON, 이동 속도 곱셈 감소 + 발걸음 소음 반경 증가 → 좀비 감지 거리에 영향
  3. 소음 + AI 감지 시스템 — 플레이어 이동 / 사격 시 MakeNoiseEQSPerceptionComponent 에 전달, 좀비 AI 가 시선 외 영역에서도 소음 발생지로 이동
  4. 무기 시스템 (라이플 / 샷건 + ADS + 반동) — 라이플은 단발 정확도, 샷건은 산탄 패턴 + 광역 데미지, 우클릭 ADS 시 카메라 FOV 축소 + 반동 감소 곱셈

기술적 어려움 5가지

  1. 차량 슬롯 PIE 검증 블로커 6 개 연쇄 — 콜리전 / Mobility / AddIgnoredActor 루프 / Sweep 차단 / OwnerVehicle BP 자식 설정 / Slot 변수명 가림 — 한 페이즈 내 전부 해소 (어제 4/29 Day 5 작업)
  2. HUD 4 페이지 단일 위젯 통합MainMenu / InGame / GameOver / GameClear 4 화면을 위젯 4 개로 만드는 대신 Lv_1 + Lv_2 두 컨테이너 + enum 토글 → swap 로직 0 개
  3. 델리게이트 바인딩 이전 Broadcast 손실NativeConstruct 시점은 이미 BeginPlay 가 끝나서 초기 Broadcast 가 지나간 후 → 바인딩 직후 현재값으로 핸들러 1 회 직접 호출 패턴 표준화
  4. GameOver = 레벨 전환 XOpenLevel 호출 시 시체 / 좀비 / 인벤토리 배경이 다 사라져 몰입 깨짐 → 같은 레벨에서 SetHUDMode(GameOver) 만 토글, ReStart 시점에야 레벨 리셋
  5. HealthComponent 누락 BP 인스턴스 fallback — BP 측에서 HealthComponent 가 빠진 인스턴스가 있어 nullptr 크래시 → BeginPlay 에서 NewObject + RegisterComponent 런타임 복구


오늘 배운 것 정리

  1. LineTrace 시작점은 표면을 침범하면 무시된다. UE 물리 엔진은 trace start 가 콜리전 내부 (또는 표면과 동일 Z) 면 “starting inside” 로 분류해 결과 무시. trace 시작점은 항상 표면에서 살짝 벗어난 위치 (1~5cm) 로 잡아야 한다. WorldBottom 직접 사용 금지. 단순한데 디버깅 시간을 가장 많이 잡아먹는 함정.

  2. 단일 지점 trace 는 빠른 이동에서 터널링한다. 한 프레임 이동량이 trace 길이를 넘으면 그 사이 표면을 그대로 통과. 해결: trace 길이를 “탐색 거리” 가 아니라 “이번 프레임 이동량” 으로 잡는다 = CCD. Start = 현재 위치, End = 예측 위치 가 가장 단순한 CCD 패턴. 비용은 단일 trace 와 동일.

  3. Symplectic Euler 두 줄이 모든 게임 물리의 출발점. v += a*dt; p += v*dt 순서가 핵심. 순서 바꾸면 에너지 누설 → 발산. 한 줄 비용 0 으로 안정성 확보. Tick 안에서 무빙 직접 짜야 할 때 가장 먼저 적용할 패턴.

  4. 종단속도는 게임 감속 표현의 일반화된 도구. F_drag = -kv 만 추가하면 물 / 공기 저항 / 점성 매질 모두 표현. k 가 크면 종단속도 작음 = 빨리 멈춤. 별도 조건문 없이 자연스러운 감속.

  5. 같은 힘, 다른 질량 → 시간 제곱 비례 격차. Δp = ½(F/m₁ - F/m₂)·t². 폭발 임펄스에서 가벼운 박스가 멀리 날아가는 게 정확히 이 식. 질량 차가 시각적으로 드러나는 핵심 메커니즘.

  6. 함수 한 줄 주석이 데코 주석보다 정보 밀도 높다. // --- ... --- 데코는 IDE outline 노이즈 + 의미 없는 diff 라인. 함수 위 한 줄 한국어 설명이 검색·가독성·diff 모두에서 우월. 신규 주석은 데코 금지가 합리적 디폴트.

  7. 헤더 멤버 주석은 단위 + 디폴트값을 명시. // 무게 비율 임계값. 이 이상이면 Overweight ON (디폴트 0.5) 처럼 의미 + 단위 + 디폴트를 한 줄에. BP 측에서 값 조정할 사람도 같은 주석을 보므로 단위 명시 필수.

  8. 회귀 sweep 단계의 주석 작업은 코드를 다시 한 번 통독하는 효과. 11 개 cpp 를 주석 다는 동안 자연스럽게 함수 책임을 재확인 → 어색한 책임 분리 / 중복 호출 / 죽은 코드 발견 가능. 마감 D-1 에서 새 기능 짜지 말고 주석 + 통독으로 안정화하는 전략이 위험 대비 가장 큰 이득.

  9. 제출 폼 작성은 “내가 한 일 회고” 의 강제 장치. 필수 / 도전 / 나만의 기능 / 기술적 어려움 4 분류로 나눠 적으면 5 일간 흐릿했던 작업이 명확히 정리됨. 다음 팀플 (05-01 시작) 에서 어떤 모듈을 가져올지 판단 근거가 됨. TIL 만으론 부족하고 제출 폼처럼 “외부 시각으로 다시 적기” 가 회고를 압축하는 도구.

  10. 마감 D-1 = 본 기능 동결 + 회귀만. 본체 종결 (D 0 조기 마감) 한 다음 날에 새 기능 욕심 내면 회귀 위험만 키운다. 오늘은 의도적으로 (1) 회귀 sweep, (2) 주석 작업, (3) 제출 폼, (4) 다른 학습 (NBC 드론 / 게임물리) 으로 시간을 분산. 마감 안전성 + 다른 진도 동시 확보.

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