견인 주입을 리쉬 이동 제한으로 교체
예측 위에 보정을 얹지 않으면 교정 패킷이 사라진다
공동 운반의 러버밴딩은 튜닝 문제가 아니라 이중 보정 구조 자체가 원인이었다. 되끌어오는 견인을 걷어내고 입력 단계에서 이탈 성분만 지우자 서버와 클라가 같은 결론에 도달했다.
공동 운반의 러버밴딩을 이틀에 걸쳐 좇았지만 값을 바꾸는 것으로는 끝내 잡히지 않았다. 원인은 튜닝이 아니라 구조였다 — 캐릭터 무브먼트가 이미 예측·재생·교정을 끝내는 위에 서버가 한 번 더 보정을 얹고 있었기 때문이다. 이 글에서는 그 이중 보정을 걷어낸 과정을 이야기하려 한다. 되끌어오는 견인을 없애고, 소유 클라이언트의 입력 단계에서 대형을 벗어나는 성분만 지우는 리쉬(leash) 방식으로 바꾸자 교정 패킷 자체가 사라졌다. 마지막에는 손으로는 만들 수 없는 상황(양쪽 동시 반대 입력)을 치트로 재현해 검증한다.
증상 — 덜컹거리며 끌려오다 뚝 끊어진다
2인 가구를 들면 파트너가 기차처럼 덜컹거리며 끌려오고(러버밴딩), 벌어짐이 한계를 넘으면 그랩이 뚝 끊어진다. 1인 가구도 충돌 후 캐릭터가 가구 위치로 되끌려오는 스냅백이 어색하다. 호스트에서는 안 나오고 클라이언트에서만, 그것도 운반 중에만 나온다는 것이 단서였다.
기존 견인 방식의 증상 — 서 있는 파트너가 주입 견인으로 덜컹거리며 끌려오다, 벌어지자 그랩이 끊어진다
원인 — 이중 보정이 곧 러버밴딩이다
언리얼 CMC(CharacterMovementComponent)는 이미 완결된 보정 체계를 갖고 있다. 소유 클라이언트가 입력으로 예측 이동하고, ServerMove로 올라온 같은 입력을 서버가 재생하고, 결과가 어긋나면 ClientAdjustPosition으로 클라이언트를 되돌린다 — 이 되돌림이 러버밴딩 체감의 정체다.
기존 견인은 이 체계 위에 서버가 매 틱 CMC->Velocity를 직접 덮어쓰는 층을 하나 더 얹은 것이었다. 서버가 써넣은 속도는 클라이언트가 예측한 적 없는 값이라 서버 재생과 클라 예측이 매 틱 어긋나고, 그 차이만큼 교정 패킷이 날아온다. 이중 보정 = 러버밴딩.
완화도 해봤다 — 같은 견인 속도를 멀티캐스트로 소유 클라이언트에도 주입해 양쪽을 맞추는 방식. 하지만 Unreliable 멀티캐스트는 패킷이 한 번만 빠져도 그 프레임의 시작 속도가 갈라지고, 원격 지연이 있는 한 두 주입 시점이 완전히 일치할 수도 없어 잔여 밴딩이 남았다.
해결 — 되돌리지 말고, 애초에 못 가게 (리쉬)
발상을 뒤집었다. 플레이어가 대형을 이탈한 뒤에 되끌어오는 게 아니라, 이탈하는 입력 자체를 지운다. 운반 중 각 플레이어에게 자기 대형 지점(Att) 중심의 리쉬 반경을 두고, 입력의 원심(대형에서 멀어지는 방향) 성분만 제거한다. 접선 성분은 남기므로 경계를 따라 도는 건 자유다.
1
2
3
4
// 반경 밖: 대형에서 멀어지는(원심) 성분만 제거 — 접선 이동은 유지
const FVector Away = -ToAtt / Dist;
const float Outward = FVector::DotProduct(WorldInput, Away);
return (Outward > 0.0f) ? WorldInput - Away * Outward : WorldInput;
핵심은 이 필터가 소유 클라이언트의 입력 생성 지점에서 돈다는 것. 잘린 입력이 그대로 ServerMove로 올라가면 서버는 같은 가속을 재생하고, 결과가 일치하니 교정 패킷 자체가 발생하지 않는다. 서버에는 안전망으로 “반경 밖 바깥 방향 속도 성분 감쇠”만 남겼는데, 이것도 주입이 아니라 깎기라 예측과 충돌하지 않는다.
함정 ①: 여유 반경의 방향. 클라이언트는 보간된 가구 트랜스폼을 보니 서버와 수 uu 오차가 있다. 처음엔 “오차를 포용한다”는 직관으로 클라 반경을 서버보다 넓게(+8) 줬다. 결과는 정반대 — 클라가 서버 한계 밖까지 걸어갔다가 서버 안전망에 걸려 되끌려오는 밴딩이 재발했다. 여유는 좁게(-8) 줘야 한다. 클라가 서버 한계에 닿기 전에 스스로 멈추면 서버가 개입할 일이 없다.
후속 1 — 축차 클램프가 만든 한쪽 쏠림
플레이어만 묶으면 반쪽이다. 한 명이 걸으면 가구가 따라가고, 가구가 가면 그 사람의 대형 지점도 같이 밀려나서 리쉬 경계가 계속 도망간다 — 서 있는 파트너는 방치된 채 벌어지다 자동 해제. 그래서 가구의 이동 목표도 모든 운반자의 리쉬 안으로 클램프했다(대칭 리쉬).
함정 ②: 축차 클램프의 순서 편향. 첫 구현은 플레이어를 순서대로 돌며 위반량만큼 가구 목표를 당기는 축차 방식이었는데, 줄다리기 테스트에서 가구가 한쪽으로 꾸준히 흘러갔다. 종이에 적어보니 수학이 명확했다. 양쪽 다 한계 L을 초과량 g만큼 넘은 대칭 상황에서:
- P1 보정: 가구가 P1 쪽으로
g−L이동 → P2의 위반이 그만큼 커짐 - P2 보정: 커진 위반 기준으로
2(g−L)이동 - 순이동 = 매 틱
g−L만큼 마지막 순번 쪽으로
보정을 원본 후보 기준으로 동시에 계산해 합산 적용하는 것으로 바꿨다. 반대 방향 위반은 합산에서 상쇄되니 가구가 중간에 머문다. 순서가 결과를 바꾸는 축차 갱신은 이럴 때 늘 의심해야 한다.
운반 디버그(F9) HUD — 능동 Δ/허용 반경, 가구 실속도, 막힘 플래그를 매 틱 수치로 보면서 쏠림·진동을 판별했다
후속 2 — 무입력 파트너는 끌려와야 한다
리쉬로 전부 바꾸고 나니 새 문제가 왔다. 서 있는 파트너를 끌어줄 수단이 없다. 한 명이 걸으면 가구가 파트너의 리쉬 끝에서 멈추고, 걷는 쪽 경계도 함께 동결 — 아무도 못 움직이는 교착이다. “밴딩이 싫다”와 “파트너는 끌려와야 한다”는 둘 다 참이라, 답은 역할 분담이었다.
| 상황 | 담당 |
|---|---|
| 이동 입력 중인 운반자 | 리쉬 (주입 없음 — 밴딩 없음) |
| 2인+ 무입력 파트너 | 기존 견인 (가구를 따라 끌려옴) |
| 서로 반대로 당김 | 둘 다 입력자 → 리쉬 + 대칭 클램프로 중간 정지 |
| 1인 운반 | 항상 리쉬 (스냅백 제거) |
함정 ③: 반경의 순서. 하이브리드를 켰는데 견인이 전혀 발동하지 않았다. 견인 진입 반경(70uu)이 대칭 클램프 한계(48uu)보다 바깥에 있었기 때문이다 — 가구는 48에서 멈추는데 견인은 70부터 걸리니, 발동 조건에 영원히 도달할 수 없다. 무입력 피동의 견인 진입을 25uu로 당겨 클램프보다 안쪽에 두자 자연스러운 견인이 돌아왔다. 문턱값이 여러 개면 반드시 순서를 그려본다 — 견인 진입(25) < 클램프 한계(48) < 리쉬 서버 한계.
검증 — 키보드 하나로 줄다리기 재현하기
“서로 반대 방향 동시 입력”은 PIE 두 창에서 한 키보드로는 테스트할 수 없다(포커스는 하나뿐). 콘솔 치트를 만들었다 — 켜면 가구를 잡은 로컬 폰 전원이 가구 반대 방향으로 자동 입력을 넣는다. 자동 입력도 실제 입력 경로처럼 리쉬 필터를 통과시켜서, 진짜 플레이어의 줄다리기와 동일한 코드 경로를 검증한다. 기존 견인 방식과의 A/B 비교는 모든 가구의 리쉬 모드를 일괄 토글하는 명령으로 게임 중 즉시 전환한다.
리쉬 방식의 결과 — 양쪽이 자동으로 반대로 당기지만 가구는 중간에 고정되고, 아무도 튕기지 않고, 그랩도 끊어지지 않는다
자동 줄다리기 진행 중인 디버그 HUD — 가구 실속도와 운반자막힘/가구막힘 플래그가 실시간으로 찍힌다
4장의 축차 클램프 쏠림을 잡아준 게 이 치트다. 눈으로는 “왠지 흘러간다” 수준이던 게 손을 떼고 관찰하니 일정 속도의 드리프트로 명확해졌고, 그래서 수식으로 검산할 생각을 하게 됐다. 재현 장치를 먼저 만들면 관찰이 측정이 된다.
정리 — 이 전환에서 남은 것
- 예측 시스템 위에 보정을 얹지 마라. CMC가 이미 예측-재생-교정을 하고 있다면, 그 위의 두 번째 보정은 곧 밴딩이다. 서버가 손대는 대신 입력을 다듬어 서버·클라가 같은 결론에 도달하게 하라.
- 여유 반경은 좁은 쪽에 준다. 클라 한계를 서버보다 넓게 잡으면 “포용”이 아니라 “위반 후 교정”이 된다.
- 축차 갱신은 순서 편향을 만든다. 여러 제약을 순서대로 적용해 결과가 흐르면, 원본 기준 동시 계산·합산부터 시도한다.
- 문턱값들의 순서를 그린다. 발동 반경이 정지 반경보다 바깥이면 그 기능은 조용히 죽는다.
- 재현 장치가 먼저다. 손으로 못 만드는 상황(동시 반대 입력)은 치트로 만들어라. 관찰이 측정이 되고, 측정이 수식이 된다.
핵심 요약 — CMC가 이미 예측-재생-교정을 하는 위에 서버가 속도를 덮어쓰는 두 번째 보정을 얹으면 그 자체가 러버밴딩이 된다. 되끌어오는 대신 입력 단계에서 원심 성분만 지우면, 서버와 클라의 재생 결과가 일치해 교정 패킷 자체가 발생하지 않는다.