움직임 엔진
사람이 걷고 앉고 일어서는 모습이 어떻게 계산되는지. 앱을 만드는 데는 몰라도 되지만, 에셋을 만들거나 다른 렌더러에 붙이거나 이상한 움직임을 고칠 때 필요하다.
흐름
계획 (틱마다 늘려 감, 결정적) 그리기 (매 프레임)
createBehaviorWorld ── frameAt(t) ─┬─ overrides 자리·방향·자세 ─┐
루틴·명령·길 찾기·예약 ├─ transitions 앉기·일어서기 진행 ├─ behaviorActors ─ actorPartBoxes ─ composeRender ─ SVG
└─ strides 걸어온 거리 ─┘ (파츠 상자) (깊이 정렬)- 계획은 누가 언제 어디로 가서 무엇을 할지다. 세그먼트(
idle·walk·wait·stand-up·sit-down·act·away)를 이어 붙인다. 시각으로만 정해지므로 아무 t 나 물어도 된다. - 그리기는 그 시각의 파츠 상자를 월드 공간에서 계산하고 깊이 정렬한다. 화면 좌표로 미끄러뜨리지 않으므로
- 걷는 사람이 떨리지 않는다 (틱 사이를 잇느라 속도가 들쭉날쭉하지 않다).
- 가구를 뚫고 지나가 보이지 않는다 (파츠마다 실제 자리에서 앞뒤를 가린다).
걷기
길은 0.25칸 격자 위의 가로·세로 A* 다 (buildNavGrid·findPath). 걷는 사람끼리는 시간 칸 단위로 노드를 예약해 돌아가거나 기다린다.
다리·팔은 걸어온 거리에 맞춰 흔든다. 배속을 올려도 발이 미끄러지지 않는다.
값 (GAIT) | ||
|---|---|---|
cycle | 0.9칸 | 왼발·오른발을 한 번씩 내딛는 거리 |
swing | 0.06 | 다리가 앞뒤로 흔들리는 폭 |
arm | 0.05 | 팔 흔들림 (다리와 반대) |
bob | 0.02 | 두 발이 모일 때 몸이 오르는 높이 |
ease | 0.15칸 | 걷기 시작·멈추기 전에 흔들림을 키우고 줄이는 거리 |
- 흔드는 파츠는 이름으로 정한다:
leg|shoe|shorts-l/r는 다리,arm|forearm|hand-l/r는 팔 (gaitLimb). - 속도:
WALK_SPEED1.6칸/초.createBehaviorWorld(…, { speed }),useBehaviorPlayback(…, { walkSpeed })로 바꾼다. - 방향은 90° 단위로, 꺾는 점에서 바로 돈다.
앉기·일어서기
앉는 데 POSE_SECONDS(0.45초)가 걸린다. 두 단계다.
① 접기 (0 ~ 45%) ② 옮기기 (45 ~ 100%)
선 자리에서 앉은 모양으로 몸을 접는다 그 모양 그대로 자리로 옮겨 앉는다
- 앉을 방향으로 먼저 돌아선다 - 모양이 바뀌지 않으므로, 앉은 모습이 가구와 겹치지 않고
- 높이(무릎 들기·손 올리기)를 먼저, 옮기는 길이 비어 있으면 가구를 뚫지 않는다
앞으로 뻗기(다리·손)는 나중에 - 높은 자리로 올라앉으면 높이를 먼저 맞추고 옮긴다
- 허벅지·정강이는 다리 위·아래 절반, - 낮은 자리로 내려앉으면 옮긴 뒤 내린다
발은 신발에서 빚어낸다- 일어서기는 같은 길을 거꾸로 간다: 자리에서 빠져나온 뒤 몸을 편다.
- 눕기(침대)는 누울 면 높이에서 몸을 눕힌다. 파츠가 발치를 축으로 호를 그리며 뒤로 넘어가고(90°), 그 모양 그대로 침대 위로 옮긴다. 설 곳은 침대 옆(허리 옆)에서 바깥으로 0.25 씩 물러나며 찾고, 막혔으면 발치다. 둘이 쓰는 침대는 각자 제 쪽에서 올라간다.
- 선 모습에만 있는 파츠(손에 든 짐, 치마 자락)는 접는 동안 줄어들어 사라지고, 앉은 모습에만 있는 파츠(앞으로 내민 아래팔)는 점에서 자라난다.
- 앞에서 들어가면 접은 모습의 발끝이 선 자리 앞끝을 넘지 않도록 자리 쪽으로 당겨 접는다 (소파 앞 커피 테이블에 발이 닿지 않게).
actorPartBoxes(object, asset, { transition: { from, progress } })가 이 모습을 준다.from은 전환 전 모습(자리·방향·자세)이다.
들어가는 쪽
앉을 자리마다 사람이 설 곳 넷을 본다 (standUpCorners): 앞 → 왼쪽 → 오른쪽 → 뒤.
- 설 수 있어야 한다 (
canStand: 가구·벽과 겹치지 않고 바닥재 위). - 그중 앉는 동작 전체가 가구를 뚫지 않는 첫 곳을 고른다 (
sitPathClear). 씬에 있는 모든 사람 에셋으로 재 본다. - 그런 곳이 없으면 설 수 있는 첫 곳이다.
그래서 책상에 붙인 의자는 옆에서(앞은 책상, 뒤는 등받이), 소파는 앞에서 들어간다. 편집기의 '일어서기'도 같은 규칙으로 설 곳을 고른다.
알려진 한계: 식탁 다리에 바짝 붙은 의자나 옆이 바퀴인 휠체어처럼 뚫지 않는 쪽이 없으면, 앉는 순간 발이 다리·바퀴를 잠깐 스친다.
충돌 검사 API
| 함수 | 뜻 |
|---|---|
transitionClear(seated, asset, from, obstacles) | 한 사람이 from 에서 seated 로 앉는 동안 obstacles(상자)를 뚫지 않는지. 접는 동안은 잘라 재고, 옮기는 동안은 지나가는 자리 전체를 잰다. 처음·끝 모습에서 이미 닿은 곳은 치지 않는다 |
sitPathClear(seated, standing, sitters, obstacles) | 여러 사람 에셋으로 재고, 가까운 장애물만 골라 쓴다 |
seatObstacles(scene, registry, ignore?) | 씬의 장애물 상자 (바닥재·러그는 빼고, ignore 의 사람도 뺀다) |
flatOnFloor(asset) | 바닥재·러그처럼 무엇보다 먼저 그리는 얇은 깔개인지 (장애물이 아니다) |
그리기
prepareRender가 정지한 것을 한 번 그리고 파츠끼리 앞뒤 간선을 재 둔다. 화면 x 로 줄 세워 x 구간이 겹치는 쌍만 재므로 파츠 600개 씬에서 약 1.7ms 다.composeRender는 매 프레임 움직이는 사람의 파츠만 그 간선들 사이에 끼워 넣고 위상 정렬한다.- 사람 실루엣과 겹치는 정지 파츠만 새로 재므로 사람 8명 기준 1ms 안팎이다.
- 앞뒤 판정은 파츠 상자끼리다: 화면에서 겹치는 두 상자는 어느 한 축에서 갈라지고, 그 축이 앞뒤를 정한다 (
isBehind). 바닥재·깔개는 따로 먼저 그린다. SceneLayer는 React 자식 순서를 늘 같게 두고, 그리는 순서는 DOM 에서 최소한만 옮긴다(가장 긴 증가 부분열).moveBefore를 쓸 수 있는 브라우저에서는 옮겨도 화면·타자 CSS 애니메이션이 처음부터 다시 돌지 않는다.- 시계의
timeAt(wall)은 같은 프레임이면 어디서 읽어도 같은 값이다. 여러 source 를 한 프레임에 그려도 어긋나지 않는다.
숫자
| 걷는 속도 | 1.6칸/초 (WALK_SPEED) |
| 앉기·일어서기 | 0.45초 (POSE_SECONDS) |
| 길 찾기 격자 | 0.25칸 (NAV_STEP), 사람 발자국 0.5×0.5 (PERSON_SIZE) |
| 틱 | 80ms (TIMELINE_TICK_MS) — 계획·활동·말풍선이 바뀌는 간격 |
| 앉은 자세 틀 | 발자국 0.5×0.75, 등 y 0.1, 무릎 y 0.46, 엉덩이 z 0.49 (SITTING) |
| 누운 자세 틀 | 발자국 0.5×1.4, 머리맡 y 0, 등 z 0 (LYING) |
| world 만들기 | 스타트업 오피스(8명, 자리 20곳) 약 6ms — 들어가는 쪽 충돌 검사 포함 |