TECH NOTE · AUTOMATION OPS
상태를 바꿀 때 캐릭터가 위아래로 튀는 원인은 셀 앵커 불일치입니다. 위치에 맞는 자세를 고르는 매핑을 양방향으로 두는 법입니다.
말을 걸면 캐릭터가 위로 튄다
바탕화면 캐릭터가 창틀 위에 앉아 있다가 말을 하면, 순간적으로 위치가 어긋났다. 40픽셀쯤 위로 올라갔다 내려온다.
걷다가 말할 때는 안 그랬다. 창 위에 있을 때만 났다.
원인은 애니메이션도 이동 로직도 아니었다. 스프라이트 시트의 셀 구조였다.
셀 안에서 캐릭터 위치가 다르다
스프라이트 시트는 격자다. 이 프로젝트는 512×512 셀에 상태 하나씩 들어 있다.
그런데 셀 안에서 캐릭터가 어디 있는지는 상태마다 다르다.
셀 (512×512)
┌──────────────┐ ┌──────────────┐
│ │ │ ○ │ ← 앉은 자세: 캐릭터가 위쪽
│ ○ │ │ /|\ │
│ /|\ │ │ / \ │
│ / \ │ │ │
│ │ │ │ ← 선 자세: 아래까지 씀
└──────────────┘ └──────────────┘
선 자세는 셀 아래쪽까지 다리가 내려온다. 앉은 자세는 다리를 접으니 위쪽에 몰려 있다.
셀을 그냥 같은 자리에 그리면, 상태가 바뀔 때 캐릭터의 발이 다른 높이에 온다. 그게 튐이다.
앵커로 맞춘다
해결 자체는 알려진 방식이다. 셀마다 기준점(앵커) 을 정해 두고, 그 점이 화면의 같은 자리에 오도록 그린다.
발이 바닥에 닿는 캐릭터면 앵커는 발 접지점이다.
셀 기준 그리기: 셀 왼쪽 위를 (x, y)에 맞춘다 → 발 높이가 제각각
앵커 기준 그리기: 발 접지점을 (x, y)에 맞춘다 → 발 높이가 고정
각 상태의 앵커 좌표를 미리 재서 표로 갖고 있어야 한다.
이것까지는 돼 있었다. 그런데도 튀었다.
말할 때 다른 셀로 바뀌고 있었다
진짜 원인은 상태를 고르는 쪽이었다.
말풍선이 뜨면 캐릭터를 “말하는 자세”로 바꾼다. 그런데 말하는 자세가 하나뿐이었다. 지상에서 서서 말하는 그림이다.
창 위에 앉아 있다가 말을 하면 앉은 셀에서 선 셀로 바뀐다. 두 셀의 앵커 높이가 다르니 그 차이만큼 튄다.
측정해 보니 39.5픽셀이었다.
window_top_sleepy_sit (앉음) → talking (지상 서기)
cellY 앵커 차이: 39.5px
지금 어디 있는지에 따라 고른다
고치는 방법은 말하는 자세를 상태별로 나누는 것이다. 창 위 전용 말하기 그림이 이미 있었으므로, 고를 때 위치를 보게 했다.
상태를 고르는 함수에 규칙을 넣었다.
// 창 위에서 말할 때 generic "talking"은 window_top_chatty_talking으로 매핑
assert.equal(
pickRenderState("window_top_sleepy_sit", "idle", "talking"),
"window_top_chatty_talking",
);
// 바닥에서 window_top_* 대사 발화 시 지상 talking으로 매핑
assert.equal(
pickRenderState("idle", "idle", "window_top_chatty_talking"),
"talking",
);
양방향이라는 게 중요하다. 창 위에서 지상 자세를 요청하면 창 위 자세로 바꾸고, 바닥에서 창 위 자세를 요청하면 지상 자세로 바꾼다.
두 번째가 없으면 반대 증상이 난다. 바닥에 있는데 창 위 자세가 뜨면 캐릭터가 공중에 떠 보인다.
왜 함수를 따로 뒀나
이 판정이 별도 함수(pickRenderState)로 나와 있어서 테스트가 붙었다.
인자가 셋이다.
- 지금 이동 상태(어디 있나)
- 자율 행동이 요청한 상태
- 발화가 요청한 상태
이 셋으로 실제 그릴 상태를 정한다. DOM도 창 API도 안 쓴다. 순수 함수다.
그래서 이 버그의 회귀 테스트를 두 줄로 쓸 수 있었다. 창을 띄우고 말을 걸어서 확인하는 대신, 인자를 넣고 결과를 비교한다.
이 형태가 아니었으면 39.5픽셀 튐을 테스트로 고정하는 게 사실상 불가능했을 것이다.
같은 계열이 계속 나왔다
이 버그를 고친 뒤에도 앵커 문제가 몇 번 더 나왔다.
- 등반 중간에 자세가 바뀔 때 (전환 구간에서 앵커를 섞어야 했다)
- 등반을 마치고 설 때 (착지 위치가 튐)
- 서 있다 걷기 시작할 때
전부 “어떤 상태에서 어떤 상태로 갈 때 앵커가 안 맞는다”였다.
셀마다 앵커가 다르면 조합 수만큼 경우가 생긴다. 상태가 35개면 전이가 1000가지가 넘는다. 하나씩 잡는 방식으로는 안 끝난다.
그래서 나중에는 규칙을 뒤집었다. 앵커가 다른 셀로는 직접 전환하지 않는다를 기본으로 두고, 필요하면 같은 앵커 계열 안에서 고르거나 전환 구간을 보간했다.
정리
- 스프라이트 셀 안에서 캐릭터 위치는 상태마다 다르다. 셀 기준으로 그리면 전환 때 튄다
- 앵커(발 접지점)를 기준으로 그리면 해결되지만, 상태를 고르는 쪽이 틀리면 소용없다
- 지상 자세와 창 위 자세처럼 앵커가 다른 계열이 있으면, 지금 위치에 맞는 계열을 골라야 한다
- 매핑은 양방향이어야 한다. 한쪽만 하면 반대 증상(공중에 뜸)이 난다
- 상태 선택을 순수 함수로 빼 두면 39.5픽셀 튐도 두 줄짜리 테스트가 된다
- 상태가 늘면 전이 조합이 폭발한다. 하나씩 잡지 말고 앵커가 다른 셀로는 직접 전환하지 않는다를 규칙으로 둔다
애니메이션이 튀면 그림부터 의심하게 되는데, 대개는 어떤 그림을 고르느냐의 문제였다.