· No.12
튕김이 최초 투척보다 느렸다 — 체감 수치는 절대값이 아니라 직전 동작과의 비율이다
폭발한 자리에서 폭탄이 옮겨가 다시 폭발한다"는 기능을 넣었는데 "느리다"는 반려가 왔다. 속도 필드가 0이라 이동이 0.5초 고정 시간으로 처리되고 있었고, 실효 약 4m/s는 최초 투척 9m/s의 절반도 안 됐다. 고친 것은 속도 한 칸이 아니라 관계였다 — 튕길수록 빨라지게(14m/s, 회차당 ×1.35) 잡고 거리는 반대로 줄였다(2.7m 고정, ×0.65). 그리고 같은 수치가 문서 세 곳에서 두 세대 어긋난 채 살아 있었다.
폭탄 계열에 튕김을 넣었다. 폭발한 자리에서 폭탄이 한 번 더 옮겨가 다시 터진다. 투척 개수 3발 × (튕김 2회 + 1) = 한 볼리에 폭발 9회가 성립한다.
기능은 들어갔고 테스트는 초록불이었다. 그런데 실기에서 "느리다"는 반려가 왔다.
숫자를 재 보니 반려가 맞았다. 튕김의 실효 속도가 약 4 m/s, 최초 투척이 9 m/s였다. 증폭 연출로 넣은 동작이 직전 동작보다 느렸다.
이 글은 그 한 칸을 고친 기록이 아니다. 고쳐야 했던 것은 속도값이 아니라 "튕김이 무엇과 비교되어 읽히는가"였고, 같은 수치가 문서 세 곳에서 서로 다른 세대로 살아 있던 것을 정리한 기록이다.
1. 신고 문장과 코드가 서로 다른 것을 가리키고 있었다
반려는 두 문장으로 왔다.
"첫 튕김 사거리는 처음 날아간 사거리 비례가 아닌 항상 고정된 거리를 날아간다"
읽는 대로면 현재 구현이 투척 거리에 비례한다는 뜻이다. 그래서 비례 계수를 찾으러 갔다.
없었다. 튕김 거리는 Random.Range(2.2, 3.2), 즉 투척과 무관한 산포였다.
여기서 한 번 멈출 필요가 있었다. 신고자는 코드를 읽지 않는다. 화면에서 본 것을 자기 언어로 옮긴다. 같은 세기로 던졌는데 튕김이 어떤 판에서는 2.2m, 어떤 판에서는 3.2m를 갔다. 폭 1.0m의 산포가 "내 투척에 따라 달라진다"로 읽힌 것이다.
이 오독은 신고자의 실수가 아니라 설계가 만든 것이다. 플레이어에게 통제권이 없는 난수를, 플레이어는 자기 입력의 결과로 읽는다. 그래서 확정안은 계수를 찾는 것이 아니라 산포를 없애는 것이 됐다.
| 이전 | 이후 | |
|---|---|---|
| 첫 튕김 거리 | Random.Range(2.2, 3.2) (폭 1.0m) | 2.7m 고정 |
| 필드 구성 | BounceMinDistance / BounceMaxDistance 2칸 | BounceDistance 1칸 |
⚠ 두 칸을 하나로 줄인 것은 리팩터링이 아니라 사양이다. Min/Max 두 칸을 남긴 채 같은 값을 넣어 두면, 다음에 이 파일을 여는 사람은 "랜덤인데 지금만 좁혀 놨나"로 읽는다. 그러면 언젠가 누가 다시 벌린다. 폐기한 설계는 값이 아니라 자리를 지워야 한다.
2. "느리다"의 정체 — 속도 필드가 0이었다
거리를 고정한 것과 별개로, "느리다"는 남아 있었다.
원인은 단순했다. 튕김 속도 필드가 0이었다. 0이면 속도로 이동을 계산할 수 없으므로, 구현은 0.5초라는 고정 시간 동안 목적지까지 이동하는 방식으로 떨어져 있었다. 거리가 얼마든 항상 0.5초다.
여기서 나온 실효 속도가 약 4 m/s다. 그리고 최초 투척은 9 m/s다.
| 구간 | 속도 | 최초 투척 대비 |
|---|---|---|
| 최초 투척 | 9 m/s | 1.00× |
| 튕김 1회차 (수정 전) | 약 4 m/s | 0.44× |
절대값 4 m/s는 느린 속도가 아니다. 이 게임의 지상 적 보행보다 빠르다. 느린 것은 9 다음에 오는 4다.
플레이어가 튕김을 보는 순서는 고정돼 있다 — 던지고, 터지고, 그다음에 튕긴다. 직전 프레임에 9 m/s를 봤기 때문에 4 m/s가 감속으로 읽힌다. 연출 의도는 "한 번 더 터진다"는 증폭인데, 화면은 잔여물이 굴러떨어지는 것을 그리고 있었다.
체감 수치는 절대값이 아니라 직전 동작과의 비율로 읽힌다. 그래서 튕김·연쇄·후속타처럼 앞 동작이 반드시 존재하는 기능은 단독으로 튜닝하면 거의 항상 틀린다.
3. 그래서 고친 것은 한 칸이 아니라 두 축이다
"느리니까 빠르게"로 끝내면 안 되는 이유가 하나 더 있었다. 속도만 올리면 사거리가 같이 늘어난다. 튕김은 군중 제어용이지 사거리 연장용이 아니다.
그래서 속도와 거리를 서로 반대 방향으로 걸었다.
거리: d(n) = 2.7 × 0.65^(n-1) n = 튕김 회차 (1 ~ 2)
속도: v(n) = 14 × 1.35^(n-1) (m/s)
지연: 회차마다 0.18초| 회차 | 거리 | 속도 | 최초 투척 대비 속도 | 이동 시간(계산) |
|---|---|---|---|---|
| 최초 투척 | — | 9 m/s | 1.00× | — |
| 튕김 1 | 2.700 m | 14.0 m/s | 1.56× | 0.193초 |
| 튕김 2 | 1.755 m | 18.9 m/s | 2.10× | 0.093초 |
튕길수록 빨라지고, 튕길수록 가까워진다. 속도가 오르니 감속으로 안 읽히고, 거리가 줄어드니 폭발이 한 지점에 모인다. 지연 0.18초를 더하면 폭발 간격이 0.373초 → 0.273초로 가속되며 좁혀지는 리듬이 나온다 (이동 시간과 간격은 위 상수로 계산한 값이다).
총 이동 거리는 착탄점에서 2.7 + 1.755 = 4.455m로 상한된다. 회차 상한이 2이므로 이 값은 어떤 성장 조합에서도 넘지 않는다.
실측 검산
부트스트랩한 실제 게임에서 OnExploded 좌표를 그대로 받았다.
폭발 1 (6.00)
폭발 2 (8.70) 간격 2.700
폭발 3 (10.46) 간격 1.755 ← 좌표가 소수 둘째 자리라 1.76 으로 읽힌다간격이 2.700 / 1.755, 즉 2.7과 2.7 × 0.65와 정확히 일치한다. 방향 유지도 확인했다.
⚠ 그리고 돌연변이 테스트를 돌렸다 — 상수를 일부러 틀린 값으로 바꿔 놓고 그 테스트가 실제로 빨간불을 내는지를 봤다. 초록불이 나오는 이유가 "맞아서"가 아니라 "안 재고 있어서"인 경우를 이 저장소에서 이미 여러 번 겪었기 때문이다.
구현 비용은 소였다 — 상수 4개와 필드 1개. 비싼 것은 값을 정하는 판단이었지 코드가 아니다. 참고로 이 기능 전체(볼리 관리·튕김·중복 상한)의 비용은 중이고, 소 비용 대안은 튕김을 빼고 투척 개수만 넣어 폭발 3회로 끝내는 것이었다.
4. 상한은 수치가 아니라 규칙으로 걸었다
폭발이 9회로 늘면 가장 먼저 무너지는 것은 보스전이다. 폭발 횟수를 곱셈으로 늘리면 단일 표적 DPS가 그대로 곱해지기 때문이다.
여기서 흔한 처방은 회차마다 피해 감쇠를 거는 것이다. 실제로 검토했고 기각했다. 감쇠 계수를 걸면 조절해야 할 수치가 횟수·피해·반경 셋이 되고, 다음에 랭크를 하나 더 팔면 또 터진다.
확정안은 수치가 아니라 규칙이었다.
한 적은 한 볼리에 한 번만 폭발 피해를 받는다.
볼리 id를 튕김 자식에게 상속시켜 중복을 판정한다. 결과는 이렇다.
| 단일 표적 DPS | 군중 출력 | |
|---|---|---|
| 튕김 도입 전후 | ×1.00 (고정) | 증가 |
상속이 빠지면 튕김이 같은 적을 다시 때리므로, 이 규칙은 상속 자체가 계약이다. 수치 상한은 랭크가 늘면 다시 검증해야 하지만, 규칙 상한은 랭크와 무관하게 성립한다.
5. 같은 수치가 문서 세 곳에서 두 세대 어긋나 있었다
작업을 닫으면서 참조 문서를 훑다가, 설계 문서에 이 값들이 옛 세대 그대로 살아 있는 것을 발견했다.
| 세대 | 첫 튕김 이동 | 지연 |
|---|---|---|
| 1세대 (최초 설계) | 1.5 ~ 2.5m 무작위 | 0.25초 |
| 2세대 (중간 조정) | 2.2 ~ 3.2m 무작위 | — |
| 3세대 (확정) | 2.7m 고정 | 0.18초 |
설계 문서가 1세대를 적고 있었다. 두 세대 어긋난 값이다. 그리고 이런 값은 조용히 죽지 않는다 — 다음에 누가 이 문서를 근거로 표를 만들면 폐기값이 그대로 되살아난다. 같은 날 취소선과 교체 경로를 함께 박아 넣었다.
⚠ 하위 호환도 끊겼다. 구버전 데이터 파일의 BounceMinDistance는 이제 무시되고, 코드 폴백 2.7로 조용히 떨어진다. 예외도 경고도 없다. 데이터로 값을 바꾸려던 사람은 자기 수정이 먹히지 않는다는 사실조차 모른다. 필드 이름을 바꿀 때는 이 침묵이 기본 동작이라는 것을 알고 바꿔야 한다.
오늘의 정리
- 신고 문장은 증상이지 원인이 아니다. "투척 거리 비례"라는 신고에 비례 계수는 없었고, 실제로는 폭 1.0m의 난수였다. 플레이어는 자기가 통제 못 하는 난수를 자기 입력의 결과로 읽는다.
- 앞 동작이 있는 기능은 단독으로 튜닝하면 틀린다. 4 m/s가 느린 게 아니라 9 다음의 4가 느렸다. 고칠 대상은 한 칸이 아니라 비율이었다.
- 속도와 거리를 반대로 걸면 두 목적을 동시에 만족한다. 빨라져서 증폭으로 읽히고, 가까워져서 사거리 연장이 안 된다.
- 상한은 가능하면 수치가 아니라 규칙으로 건다. 수치 상한은 랭크가 늘 때마다 다시 계산해야 하고, 규칙 상한은 안 그렇다.
- 폐기한 값은 자리까지 지운다. 필드 두 칸, 문서 한 줄이 남아 있으면 그것이 되살아나는 경로다.