본문으로 건너뛰기

움직임 엔진 ​

사람이 걷고 앉고 일어서는 모습이 어떻게 계산되는지. 앱을 만드는 데는 몰라도 되지만, 에셋을 만들거나 다른 렌더러에 붙이거나 이상한 움직임을 고칠 때 필요하다.

흐름 ​

계획 (틱마다 늘려 감, 결정적)                        그리기 (매 프레임)
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)
cycle0.9칸왼발·오른발을 한 번씩 내딛는 거리
swing0.06다리가 앞뒤로 흔들리는 폭
arm0.05팔 흔들림 (다리와 반대)
bob0.02두 발이 모일 때 몸이 오르는 높이
ease0.15칸걷기 시작·멈추기 전에 흔들림을 키우고 줄이는 거리
  • 흔드는 파츠는 이름으로 정한다: leg|shoe|shorts-l/r 는 다리, arm|forearm|hand-l/r 는 팔 (gaitLimb).
  • 속도: WALK_SPEED 1.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): 앞 → 왼쪽 → 오른쪽 → 뒤.

  1. 설 수 있어야 한다 (canStand: 가구·벽과 겹치지 않고 바닥재 위).
  2. 그중 앉는 동작 전체가 가구를 뚫지 않는 첫 곳을 고른다 (sitPathClear). 씬에 있는 모든 사람 에셋으로 재 본다.
  3. 그런 곳이 없으면 설 수 있는 첫 곳이다.

그래서 책상에 붙인 의자는 옆에서(앞은 책상, 뒤는 등받이), 소파는 앞에서 들어간다. 편집기의 '일어서기'도 같은 규칙으로 설 곳을 고른다.

알려진 한계: 식탁 다리에 바짝 붙은 의자나 옆이 바퀴인 휠체어처럼 뚫지 않는 쪽이 없으면, 앉는 순간 발이 다리·바퀴를 잠깐 스친다.

충돌 검사 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 — 들어가는 쪽 충돌 검사 포함

MIT 라이선스로 배포합니다.