운반 가구 벽 통과 트러블슈팅
차단이 없던 게 아니라 검사에서 빠지는 자리가 있었다
가구를 들고 벽으로 걸어 들어가면 가구가 벽을 파고들었다. 벽 차단 코드는 이미 있었고, 낮게 든 가구를 통째로 건너뛰는 조건과 접점 높이로 히트를 버리는 필터가 구멍이었다. 이어서 반동이 흔들림으로 남는 문제와 벽 앞에서 가구가 떠오르는 문제를 함께 고쳤다.
가구를 든 채 벽으로 걸어 들어가면 가구가 벽을 파고들었다. 제보를 받고 처음 확인한 것은 “차단 코드가 있는가”였는데, 있었다. ApplyWallGuard가 가구 OBB를 스윕해 운반자 속도의 법선 성분을 깎는 장치가 이미 돌고 있었고, 플레이 로그에도 [벽막힘]이 8건 찍혀 있었다.
그러니 질문이 바뀐다. 있는 차단이 왜 어떤 가구에서는 안 걸리는가.
요구사항
- 운반 중인 가구가 벽·바닥·차단 볼륨을 뚫으면 안 된다
- 계단·문턱은 그대로 오를 수 있어야 한다 — 벽 차단을 조이다 등반을 죽이면 안 된다
- 소파·화분 같은 다른 가구는 차단 대상이 아니다 (가구끼리는 물리 밀어내기 소관)
먼저 이 시스템의 구조적 한계를 하나 알아 둬야 한다. 운반 중인 가구는 운반자에 부착돼 따라가고, 어태치 이동에는 스윕이 없다. 즉 벽 가드는 언제나 사후 교정이다 — 가구가 들어간 뒤에 밀어낸다. 로그에 찍힌 겹침 깊이 2.6~9.2cm가 눈에는 “벽을 파고듦”으로 보인다. 이 글의 수정은 그 사후 교정을 놓치지 않고 걸리게 하는 쪽이고, 사후 교정 자체를 없애려면 이동을 스윕으로 바꿔야 한다(자동 검사 371항목을 다시 쓰는 규모라 이번 범위 밖이다).
트러블슈팅 ① — 낮게 든 가구는 검사 자체를 건너뛴다
원인 — 계단 픽스에서 들어온 면제 조건
가구 상단이 운반자의 스텝 높이 이하면 박스 스윕을 통째로 건너뛰는 플래그(bSkipBoxSweep)가 있었다. 계단을 오를 수 있게 만들 때 들어온 조건이다 — CMC 스텝업이 알아서 넘길 낮은 단차를 벽으로 읽지 않게 하려던 것.
문제는 그 기준이 가구 쪽에도 걸린다는 것이다. 발이 102, 스텝 상한이 152인 상황에서 램프·전자레인지 같은 1인 소품은 상단이 그 아래라 검사에서 통째로 빠졌다.
해결 — 스텝 위에 얇은 판을 남겨 잰다
플래그를 없애고, 스윕 박스의 세로 구간 계산을 함수 하나로 뺐다.
1
2
3
4
5
6
void UTCGrabHandleComponent::ComputeWallSweepSpan(float CenterZ, float ExtZ, float StepZ, ...)
{
const float FloorZ = StepZ + WallSlabLiftCm;
const float SweepTopZ = FMath::Max(TopZ, FloorZ + WallMinSlabCm);
...
}
| 가구의 세로 위치 | 스윕 구간 |
|---|---|
| 스텝 위에 통째로 있다 | 자기 박스 그대로 |
| 스텝에 걸쳐 있다 | 바닥만 스텝 높이로 올림 (종전과 동일) |
| 스텝 아래에 통째로 있다 | 스텝 위로 6cm(WallMinSlabCm) 판을 남겨 잰다 |
계단 통과가 유지되는 근거는 한 줄로 정리된다. 스윕 박스의 바닥이 절대 스텝 아래로 내려가지 않는다. 계단 챌면과 문턱은 그 아래에 있으니 이 박스에 안 걸린다. 이 불변식은 말로 두지 않고 검사로 세웠다 — 신설한 스모크 5종 중 하나가 “박스 바닥이 스텝 아래로 안 내려간다”를 직접 잰다.
트러블슈팅 ② — 맞은 히트를 필터가 버렸다
①을 넣고도 통과가 남았다. 그런데 로그에는 아무것도 없다. 통과는 로그를 안 남기기 때문에 무엇이 빠지는지 눈으로 셀 수가 없었고, 코드에서 감지가 사라지는 경로를 갈라야 했다.
원인 — 접점 높이가 정확히 필터 경계였다
벽 가드는 운반자별로 “발 + 스텝 높이 이하의 접촉”을 면제한다(ImpactPoint.Z <= CStepZ). 낮은 단차를 벽으로 읽지 않기 위한 필터다.
①에서 낮게 든 가구의 스윕 박스 바닥을 정확히 StepZ로 맞춘 것이 여기서 자충수가 됐다. 그 박스가 벽에 닿으면 접점은 늘 박스 바닥, 즉 정확히 StepZ다. 부등호가 <=니까 스윕은 맞았는데 판정에서 버려진다.
해결 — 판을 2cm 띄우고, 걸러진 히트를 남긴다
바닥을 WallSlabLiftCm(2cm)만큼 띄웠다. 계단·문턱은 여전히 이 판 아래라 안 걸린다.
같이 넣은 것이 더 중요하다. 걸러진 히트에 [벽막힘거름] 로그를 남겼다. 이번 진단이 오래 걸린 이유가 “빠지는 것은 아무 흔적이 없다”였으니, 다음 제보 때는 스윕이 맞았는데 버려진 것인지 애초에 안 맞은 것인지가 로그 한 줄로 갈린다.
트러블슈팅 ③ — 벽이 아닌 것까지 벽으로 세고 있었다
실측 — 막은 쪽이 소파와 화분이었다
플레이 로그의 [벽막힘] 28건을 ‘막은 쪽’으로 분류했다.
| 막은 쪽 | 건수 |
|---|---|
| 벽 (StaticMeshActor) | 22 |
| BlockingVolume | 2 |
| 소파·화분·수리 더미 | 4 |
그리고 [벽충돌피해] 24건 중 4건이 그 소품들에서 나왔다. 전자레인지 하나는 체력이 52 → 44 → … → 0으로 깎여 파괴됐다. 가구를 옆 가구에 문지른 것이 벽에 박은 것과 같은 취급을 받고 있었다.
해결 — 스윕 대상을 정적 지오메트리만으로
스윕 채널에서 WorldDynamic·PhysicsBody를 빼고 WorldStatic만 남겼다. 벽·바닥·BlockingVolume은 전부 WorldStatic이라 그대로 걸린다.
이 한 줄이 세 가지를 동시에 정리한다. 스윕 하나가 차단·반동·내구도 감소의 공통 입구라, 대상을 좁히면 셋이 함께 정적 전용이 된다. 반대로 말하면 종전에는 “소파에 문질러 전진이 막히고, 반동으로 튕기고, 내구도가 깎이는” 것이 한 자리에서 동시에 나오고 있었다.
트러블슈팅 ④ — 반동이 자세를 영영 틀어 놓았다
벽에 박으면 반동이 나가야 게임필이 산다. 그런데 두 가지가 어긋났다.
증상과 원인
| 증상 | 원인 |
|---|---|
| 가구만 돌아가 몸과의 상대각이 틀어진다 | 반동 회전을 자세 정본 SpinYaw에 누적했다 |
| 벽에 밀고 있으면 달달거린다 (같은 벽 0.6~1.5초 간격 12건) | 벽에 붙어 있는 동안 매 프레임 히트가 나는데 그때마다 반동을 줬다 |
SpinYaw는 목표 요를 만드는 항이라 한 번 더해지면 영영 남는다. 잠깐 흔들리라고 준 값이 자세의 정의를 바꿔 버린 것이다.
해결 ① — 흔들림을 정본 밖에 둔다
WallWobbleYaw를 따로 뒀다. 목표 요에 더해지기만 하고 매 틱 0으로 감쇠한다.
1
TargetYaw = 몸요 + GrabYawOffset + SpinYaw + WallWobbleYaw;
정본(SpinYaw)은 건드리지 않으므로 흔들리고 나면 몸과의 상대각이 그대로 돌아온다. 부착 블렌드가 끝난 뒤의 조기 반환에는 흔들림 조건을 더해, 감쇠가 도는 동안만 구동이 살아 있게 했다.
해결 ② — 접촉과 충돌을 가른다
여기가 이 회차의 핵심이다. 벽에 닿아 있는 것은 상태고, 벽에 박는 것은 사건이다. 같은 히트 스트림에서 둘을 갈라야 한다.
1
2
// '떨어졌다 다시 박은 것'만 새 충돌로 본다
bool UpdateWallContact(const FHitResult& Hit, const FVector& N);
접촉 유지 판정은 셋이 동시에 성립할 때다.
- 같은 컴포넌트에 닿았다
- 법선 내적 > 0.85 (약 32도 이내 — 같은 면이다)
- 마지막 접촉에서
WallContactResetSec(0.35초) 이내다
접촉이 유지되는 동안에는 속도 깎기만 남는다. 반동도, 흔들림도, 내구도 감소도 새 충돌에만 발화한다. 내구도는 여기에 속도 임계(250cm/s)와 쿨다운(0.6초)이 더 붙어, 벽에 대고 밀어도 연속 차감이 나지 않는다.
| CVar | 기본 | 뜻 |
|---|---|---|
TC.GrabV2.WallBounce | 0.35 | 되밀기 = 파고든 속도 × 이 값 (새 충돌에만) |
TC.GrabV2.WallBounceSpin | 12 | 충격 순간 기우는 각도(도), 감쇠로 복귀 |
TC.GrabV2.WallWobbleDecay | 6 | 흔들림이 0으로 돌아오는 속도(1/초) |
TC.GrabV2.WallContactResetSec | 0.35 | 이보다 오래 떨어져야 새 충돌 |
TC.GrabV2.WallDamageSpeed / WallDamage / WallDamageCooldown | 250 / 8 / 0.6 | 내구도 |
트러블슈팅 ⑤ — 벽 앞에서 가구가 떠올랐다
벽 가드를 조이자 반대편 장치가 드러났다. 벽을 향해 전진 입력을 유지하면 가구가 주기적으로 들썩이며 떠올랐다.
원인 — 두 판정이 같은 지형을 다르게 읽었다
계단 등반용 리프트는 바닥 클램프의 아래방향 박스 스윕이 내는 ‘필요 올림’으로 발동한다. 그런데 그 스윕이 벽 위 모서리를 법선 Z 1.0인 딛는 면으로 읽어 Rise = 73.1을 냈다.
리프트 분기는 그 값이 상한(MaxRise 60)을 넘는데도 발동했고, 목표는 min(73.1, 60) = 60이 되어 그쪽으로 계속 올렸다. 못 넘으니 들썩임이 반복된다.
같은 순간 벽 가드는 단차 98 > 상한 45로 그 접촉을 거부하고 있었다. 한 지형을 놓고 한쪽은 “오를 계단”이라 하고 다른 쪽은 “너무 높은 벽”이라 하고 있었던 것이다.
해결 — 발동 조건을 상한과 일치시킨다
1
2
3
4
bool UTCGrabHandleComponent::ShouldClimbLift(float Rise, float MaxRise, bool bLatched)
{
return bLatched && Rise <= MaxRise;
}
호출부는 이것만 쓴다. 필요량이 상한을 넘으면 리프트가 안 걸리고 여유가 발 기준으로 떨어져 부분 올림 통로까지 함께 닫힌다. 벽 가드의 ‘높이 초과’ 거부와 기준이 같아진다.
한 가지는 일부러 안 건드렸다. 필요량이 상한을 넘을 때 막힘 래치 갱신까지 막을까 했으나, 계단에서 필요량이 한 프레임 튀는 순간 래치가 풀리면 기존 계단 등반이 흔들릴 수 있다. 한 번 넣었다가 되돌리고 발동만 막았다.
검증 — 검사가 못 만드는 것과 만들 수 있는 것
기존 기능 자동 검사 5종 371항목은 세 회차 모두 통과 상태로 넣었다(그랩 120 · 스핀 120 유지). 다만 앞선 계단 회차와 마찬가지로 이 표는 “다른 걸 깨지 않았다”만 말한다.
이번 축에서 실제로 결함을 잡은 것은 새로 세운 헤드리스 스모크였다.
| 스모크 묶음 | 신설 | 무엇을 고정하나 |
|---|---|---|
| 벽 스윕 기하 | 5종 | 높이 든 가구 / 걸친 가구 / 낮게 든 가구 / 박스 바닥이 스텝 아래로 안 내려감 / 판 두께 |
| 반동 | 5종 | 첫 충돌은 새 충돌 / 같은 벽 계속 닿으면 무발화 / 미는 동안 무발화 / 다른 면은 새 충돌 / 흔들림 무누적 |
| 리프트 발동 | 4종 | 상한 이내 발동 / 딱 상한도 발동 / 필요 73.1 > 상한 60 무발동 / 래치 없으면 무발동 |
세 번째 묶음의 굵은 줄이 ⑤의 실측값을 그대로 단언으로 옮긴 것이다. 로그에서 읽은 숫자를 검사에 박아 두면 같은 지형을 다시 밟았을 때 판정이 뒤집혔는지가 바로 나온다.
반대로 스모크로 못 덮는 것도 분명했다. ‘가구끼리 무반동’과 벽 반동의 손맛은 살아 있는 운반이 필요해 헤드리스로 못 만든다. 그쪽 확인은 플레이 로그 두 가지로 갈랐다 — 막은쪽에 BP_Furniture_*가 안 나오는지, 같은 벽에서 [벽충돌피해]가 연속으로 안 뜨는지.
남은 천장
정직하게 남겨 둔 것이 셋 있다.
- 몸을 돌려서 생기는 회전에는 검사가 없다. 벽 가드는 평행이동만 스윕하고, 축 회전에는 스핀 벽 가드가 따로 있다. 그런데 부착 블렌드가 끝난 뒤 가구는 캐릭터의 자식이라 몸이 돌면 같이 돈다 — 벽 옆 제자리 회전이면 가구가 벽을 쓸고 지나간다. 막았을 때 몸 요를 되돌릴지 가구만 둘지가 갈리고 스핀 자동 검사 120항목이 걸린 영역이라 임의로 손대지 않았다.
- 고속 충돌에서 수 cm 파고듦은 구조적으로 남는다 (사후 교정).
- 이후 고강도 코드 리뷰에서 이 축의 지적이 셋 더 나왔다 — 얇은 판이 가구가 점유하지 않는 높이 대역을 항상 검사해 낮은 가구로 카운터·선반 밑을 지날 때 미접촉인데 막히는 것, 반동 기울기 방향을 정하는 이동 벡터가 이미 덮어써진 값을 읽어 부호가 사실상 무작위인 것, 벽 데미지가 가구 데미지 게이트를 우회해 뽁뽁이 감쇠가 안 걸리는 것. 셋 다 다음 배치 안건으로 적어 뒀다.
결과
벽·바닥·차단 볼륨은 낮게 든 소품까지 포함해 막힌다. 계단과 문턱은 그대로 오르고, 소파·화분은 차단·반동·내구도 어느 쪽으로도 잡히지 않는다. 벽에 박으면 한 번 톡 튀고 자세가 제자리로 돌아온다.
핵심 요약 — “차단이 안 된다”는 제보에서 먼저 확인할 것은 차단 코드의 유무가 아니라 검사에서 빠지는 경로다. 여기서는 면제 플래그 하나와 접점 높이 필터 하나가 그 자리였고, 둘 다 원래는 계단을 오르게 하려고 넣은 조건이었다. 그리고 걸러진 것에도 로그를 남겨야 한다 — 통과는 흔적을 안 남기므로, 안 걸린 것과 걸렸다가 버려진 것을 구분할 수단이 없으면 진단이 코드 리딩으로만 가능해진다. 마지막으로 히트 스트림에서 접촉(상태)과 충돌(사건)을 가르면 반동·내구도처럼 “한 번만 일어나야 하는 것”이 매 프레임 발화하는 문제가 한 번에 정리된다.