1P by GN⁺ | ★ favorite | 댓글 1개
  • xkcd의 Machine은 독자들이 만든 타일형 장치를 이어 붙인 거대한 루브 골드버그식 구슬 기계 게임으로, 아이디어를 3주 만에 실제 인터랙티브 만화로 구현함
  • 이전 사용자 참여형 프로젝트 경험은 공유 캔버스가 잘 작동하려면 공통 맥락과 목적이 필요하다는 설계 기준으로 이어짐
  • 플레이어의 표현력을 살리되 타일 호환성을 위해 입력·출력 제약을 강하게 두고, 각 장치가 30초 안에 안정 상태에 도달하도록 제한함
  • 전체 기계를 실시간으로 모두 시뮬레이션하지 않고 보이는 영역만 Rapier로 돌리며, 승인 시점의 스냅샷으로 이미 작동 중인 장치처럼 보이게 만듦
  • React와 DOM 렌더링, Haskell 백엔드, Redis, OpenAPI, TanStack Query, 모더레이션 UI를 조합해 제출물 승인과 배포 흐름을 운영함

Machine의 출발점

  • xkcd는 4월 5일 Machine을 공개함
  • Machine은 고전 게임 The Incredible Machine 스타일의 거대한 Rube Goldberg machine 빌더임
  • 전체 기계는 개별 xkcd 독자가 만든 작은 장치 타일을 이어 붙인 형태로 구성됨
  • 팀은 Machine을 3주 안에 만들었고, 아이디어는 2005년 협업 GIF인 Blue Ball Machine에서 출발함
  • 초기 브레인스토밍의 핵심 질문은 구슬의 출처, 모두가 보는 기계의 동일성, 기계의 목적, 플레이어 상호작용, 참여 동기였음

사용자 참여형 xkcd에서 얻은 교훈

  • 사용자 제작 콘텐츠가 중심인 이전 xkcd 인터랙티브 만화 중 Lorenz는 독자들이 패널 텍스트를 작성해 농담과 이야기를 발전시키는 방식이었고 좋은 경험으로 남음
  • 2020년 Collector’s Edition은 플레이어가 xkcd 아카이브에서 스티커를 찾아 전역 공유 캔버스에 한 번씩 붙이는 방식이었지만 기대만큼 작동하지 않음
    • 모든 플레이어가 비어 있는 지도 중앙에서 시작했고, 곧 혼란스러운 화면이 첫인상이 됨
    • 스티커 위치를 신중히 고를 유인이 부족했고, 개별 행동만으로 줄거리를 진전시키기 어려웠음
    • 공유된 이야기나 목표가 없어 각 스티커가 페이지의 다른 요소와 어떻게 연결되는지 분명하지 않았음
  • 집단 캔버스가 잘 작동하려면 사용자가 무엇을 만들면 멋진지 예시로 학습할 수 있어야 함
  • 창작 결과를 한 방향으로 모으려면 무엇을 만들지 동기화해 주는 공유 맥락과 목적이 필요함

제약 설계: 표현력, 호환성, 30초 안정 상태

  • 큰 협업 구슬 낙하 장치를 만들기로 한 뒤에도 전체 기계 크기, 시뮬레이션 방식, 타일 통합 방식이 문제로 남음
  • 100x100 크기 기계를 가정하면 10,000개 타일을 클라이언트에서 실시간으로 돌리고 각 타일에서 수십 개 구슬을 처리하는 목표는 위험하다고 봄
  • 표현력을 정확성보다 우선

    • 전체 기계를 서버에서 돌리거나 개별 타일을 시뮬레이션해 검증하는 방안이 검토됨
    • 프로토타입 에디터에서 혼돈적인 구슬 충돌 패턴이 쉽게 만들어지면서, 예측 가능한 기계를 요구하면 플레이어의 자유도가 줄어든다는 결론에 도달함
    • 최종 설계는 매우 비결정적이거나 고장 난 장치까지 만들 수 있는 플레이어 유연성을 우선함
    • 이 선택 때문에 타일이 제약을 만족하는지 확인하고 공격적인 콘텐츠를 제거하는 활성 모더레이션이 필요해짐
  • 타일 간 호환성을 위한 입력·출력 제약

    • 초기에는 이전 타일의 출력 위치에 맞춰 다음 플레이어가 자유롭게 확장하는 방식을 고려함
    • 하지만 초기에 배치된 타일을 나중에 교체해야 할 경우, 그 타일에 의존한 큰 영역이 깨질 수 있음
    • 그래서 같은 타일 공간 안에서 여러 플레이어가 호환 가능한 설계를 만들도록 입력·출력 제약을 강하게 둠
    • 이 접근은 Robustness principle의 “보내는 것은 보수적으로, 받는 것은 관대하게”라는 원칙과 맞닿아 있음
    • Kevin의 map generator는 단순한 1입력 1출력 퍼즐에서 시작해 중간의 4입력 4출력 병합으로 복잡해졌다가, 끝에서는 타일당 2출력으로 돌아가게 구성됨
    • 에디터는 플레이어가 타일을 만들 때 실시간 피드백을 제공함
      • 타일은 평균적으로 받은 구슬과 비슷한 속도로 구슬을 내보내야 함
      • 구슬을 먹어버리거나 큰 지연을 만드는 장치를 줄이려 함
      • 상류 입력 변동을 반영하기 위해 에디터에 들어오는 구슬 속도를 무작위화하는 chaos testing을 적용함
  • 30초 안에 안정 상태에 도달해야 함

    • 모더레이터가 얼마나 오래 봐야 하는지를 줄이기 위해, 장치가 30초 안에 안정 상태에 들어가야 한다는 임의 기준을 둠
    • 10,000개 타일을 30초씩 본다면 전체 모더레이션 시간이 약 83.3시간이 된다는 계산이 기준이 됨
    • 구슬도 30초 뒤 만료되도록 바꿈
      • 만료가 없을 때는 초보 플레이어의 첫 경험이 구슬이 화면에 쌓이는 상황이 됨
      • 활성 rigid body 수가 커져 물리 시뮬레이션도 느려짐
    • 구슬 만료는 시간이 지나며 오류가 누적되지 않게 만들고, 30초 관찰만으로 대부분의 구슬 도착 위치를 볼 수 있게 해 모더레이션을 단순화함

전체 기계를 실시간으로 돌리지 않는 방식

  • Machine 아키텍처의 첫 번째 큰 가정은, 위 제약을 지키면 서로 다른 타일을 이어 붙여 전체 기계처럼 보이게 할 수 있다는 점임
  • 작은 맵 몇 개를 생성하고 풀어 보며 이 가정을 확인함
  • 전체 기계를 서버나 클라이언트에서 실시간으로 돌릴 수 없었기 때문에, 사용자가 보는 영역 주변만 시뮬레이션하는 방식이 필요했음
  • 목표는 하나의 구슬을 기계 위에서 아래까지 따라갈 수 있게 만드는 것이었음
  • 보이는 영역만 존재하는 물리 세계

    • 초기 맵 뷰어는 보이는 영역만 시뮬레이션했지만, 스크롤할 때 새로 들어오는 타일이 빈 상태로 시작해 흐름의 빈틈이 보였음
    • 빈 타일 대신 이미 활동 중인 상태로 보이게 하려고, 안정 상태에 도달한 타일의 스냅샷을 저장하고 화면에 들어오기 직전에 불러오는 방식을 선택함
    • 최종 만화에서 실제 물리 시뮬레이션에 존재하는 것은 렌더링되는 타일뿐임
    • 화면 위쪽에 더 많은 기계가 있는 것처럼 보이도록, 시뮬레이션 상단 행 타일에는 입력 제약의 예상 속도에 맞춰 구슬을 생성해 공급함
  • 승인 시점 스냅샷

    • 스냅샷 생성은 모더레이션 UI와 연결됨
    • 모더레이터는 타일 승인 전에 최소 30초를 기다려야 하며, 승인 버튼을 누르는 시점의 상태가 스냅샷으로 저장됨
    • 장치가 보기 좋은 상태가 될 때까지 조금 더 기다릴 재량도 모더레이터에게 있음
    • 스냅샷 방식은 누적 오류를 리셋하는 효과가 있어, 사용자가 스크롤해 새 타일을 처음 볼 때는 모더레이터가 좋다고 판단한 깨끗한 상태를 보게 됨
    • 오래 지켜보면 많은 장치가 멈추거나 깨진 상태가 될 수 있지만, 계속 탐색하면 새 스냅샷을 만나게 됨
    • 전체 기계는 완전히 시뮬레이션되지 않으며, 결과적으로 hyperreality에 가까운 구조가 됨

React, DOM, Rapier 렌더링 구조

  • Machine은 Rapier 물리 엔진 위에 만들어짐
  • Rapier는 문서, API, 유용한 기본 요소, Rust 구현을 통한 WASM 브라우저 성능이 장점으로 쓰임
  • 처음에는 Rapier의 결정성 보장에도 관심이 있었지만, 최종적으로 서버 측 시뮬레이션은 하지 않음
  • Rapier 위에는 커스텀 React context인 <PhysicsContext>를 작성함
    • React 컴포넌트 생명주기 안에서 Rapier 물리 객체를 만들고 관리함
    • 각 배치 가능한 물체와 충돌 표면을 “widget” 컴포넌트로 개발하기 쉬워짐
    • React는 빠르고 거친 scene graph처럼 동작함
    • 타일이 언마운트되면 관련 물리 객체와 DOM이 정리됨
    • 빠른 새로고침을 통한 핫 리로딩으로 충돌 형태를 조정하기 쉬웠음
  • 물리 훅은 <PhysicsContext> 안에 없으면 동작하지 않도록 만들어, 모더레이션 UI의 정적 미리보기에 활용됨
  • 나중에는 Rapier 객체를 훅이 아니라 컴포넌트로 만드는 방식이 더 나았다고 봄
    • react-three-rapier가 이 방식을 쓰며, React diffing에 더 잘 맞음
    • useEffect 기반 방식은 의존성이 바뀔 때 이전 인스턴스를 없애고 새로 만들게 됨
  • DOM 전용 렌더링

    • Machine은 전부 DOM으로 렌더링됨
    • 초기에는 성능 한계가 오면 PixiJS나 canvas로 옮길 수 있다고 생각했지만, 빌드해야 할 것이 적은 DOM 방식을 최대한 밀어붙임
    • 렌더링 성능을 위해 프레임 루프가 물리 시뮬레이션 대상 위젯의 스타일을 직접 적용함
    • React diff는 장면 그래프의 구조가 바뀔 때만 실행됨
    • 처음에는 구슬도 React로 렌더링했지만, 빈번한 생성·삭제가 diff 비용을 늘려 별도 optimized renderer를 만들었음
    • 화면 밖 구슬과 위젯에는 draw culling을 적용함
    • 이 방식은 시뮬레이션 중인 구슬 4,000개와 화면상 수백 개 구슬에서 잘 작동해 DOM 전용 렌더링으로 확정됨

API, 모더레이션, 제출물 운영

  • 백엔드는 davean과 Kevin이 Haskell로 작성했고, 저장소로 Redis를 사용함
  • 코드베이스 간 타입 공유에는 OpenAPI와 OpenAPI fetch를 사용함
    • Haskell 타입에 맞추는 초기 불편함은 있었음
    • 막판 API 변경을 조율하는 데 도움이 됨
  • TanStack Query는 서버 푸시 없이 캐싱과 자동 새로고침을 처리하는 데 유용했음
  • 모더레이션 UI와 우선순위

    • Ed White가 설계한 모더레이션 UI는 모든 제출물이 공개되기 전에 거치는 병목 구간이었음
    • 모더레이터는 특정 타일에 대해 수백 개 후보 디자인 중에서 선택해야 할 수 있음
    • 큐 우선순위는 위젯 종류별 interestingness score를 두고 각 인스턴스를 세어 후보 타일을 정렬하는 방식으로 정함
    • 이 방식은 많은 요소를 넣은 해법에 치우치지만, 모더레이터가 목록 중간을 검토해 더 미니멀한 해법을 보완함
    • 제출된 디자인 수와 실제 기계에 게시된 디자인 수 사이의 큰 불균형은 아쉬운 점으로 남음
    • 출시 전 더 많은 백로그를 공개하는 방안을 찾았지만, 모더레이션 시간 제약 안에서 좋은 절충안을 찾지 못함
    • 라이브 제출이 끝난 뒤 제출 데이터셋을 더 많이 공유하는 방법을 찾고 싶어 함
  • 승인 쿨다운과 속도 조절

    • 타일 스냅샷 품질이 중요했기 때문에, 모더레이터 승인 버튼은 시뮬레이션을 최소 30초 실행하기 전까지 비활성화됨
    • 이 쿨다운은 안정 상태의 스냅샷을 만들고 출력이 예상 속도로 구슬을 받는지 확인하게 함
    • 처음에는 모더레이터에게 귀찮을 것으로 예상했지만, 성급한 결정을 막아 준다는 이유로 긍정적으로 받아들여짐
    • 출시 후에는 모더레이터가 시뮬레이션을 실시간보다 훨씬 빠르게 돌릴 수 있는 슬라이더가 추가됨
    • 이 기능으로 제출물의 첫 30초를 5초 미만에 볼 수 있게 되었고, 더 긴 시간의 동작도 검토하기 쉬워짐

의도하지 않은 타일 상호작용

  • Jamslunt Interfoggle”은 공개 후 첫 몇 시간 안에 올라온 장치로, 팬의 좁은 범위를 이용하는 메커니즘임
  • 이 장치는 파란 구슬을 통로에 모아 무게가 충분해지면 양쪽으로 쏟아지게 함
  • 위쪽에 배치된 “Bouncy”는 세 갈래 교차 경로로 구슬을 발사하는 혼돈 엔진임
  • Bouncy는 가끔 초록 구슬을 잘못된 출력으로 보내고, 이 구슬이 정체된 파란 구슬 더미를 깨뜨려 Interfoggle에 연쇄 흐름을 만듦
  • 에디터에서는 입력을 이해하기 쉽게 하려고 올바른 색만 공급했기 때문에, Interfoggle은 이 초록 구슬 동작을 염두에 두고 설계될 수 없었음
  • 이런 의도하지 않은 조합은 공유 캔버스에서 사람들이 창의적으로 도구를 쓰는 방식이 프로젝트의 큰 즐거움이 됨

코드와 남은 실험

  • Machine의 소스 코드는 GitHub 저장소에서 확인할 수 있음
  • 전체 기계를 전역으로 완전히 시뮬레이션하는 구현은 흥미로운 해킹 과제로 남음
  • Machine에 직접 디자인을 추가하는 링크는 xkcd 2916에 있음

댓글과 토론

Hacker News 의견들
  • 이 글을 읽고 웃겼던 게, 당시엔 이런 일이 벌어지는 줄 전혀 몰랐음
    무슨 상황인지 설명도 없어 보였고, 모두가 같이 하는 경험이라는 것도 몰랐으며, 그냥 여러 무작위 일이 혼란스럽게 일어난다고만 느꼈음
    타일 몇 개를 완성해서 제출했는데, 그게 “다음 단계”로 가는 방법인 줄 알고 “test 1b” 같은 멍청한 이름을 붙였음. 싱글플레이라고 생각해서 이름은 나만 보는 줄 알았기 때문임
    몇 개 만들고는 질렸고, 돌아다니다가 복잡한 것들을 봤지만 그게 제출작인 줄 모르고 그냥 레벨 풀이용 시작점이라고 생각했음. 결국 내가 만우절 장난에 당한 셈임

    • 상호작용 가능한지도 몰랐고, 그냥 보고 “멋지네”라고만 생각했음
    • 처음부터 편집 가능한 예시를 보여주거나, 아니면 최종 결과를 먼저 보여주고 빈 슬롯에 초점을 맞추는 식으로 행동 유도를 했어야 함
    • 참고로 아직 제출을 받고 있음
    • 나도 훨씬 심했음. 2분쯤 만져보다가 무슨 일인지 전혀 몰라서 포기했음
      아마 원본이 된 그 기계 게임을 해본 적이 없어서 그랬을지도 :-)
    • 다음번엔 공개 전에 해설 글부터 올리면 좋겠음
  • “bonk” 요소를 잔뜩 추가하다가 rapier를 죽여버린 것 같음
    Uncaught Error: recursive use of an object detected which would lead to unsafe aliasing in rust
    at jt (rapier_wasm2d_bg.js:4836:11)
    at 4ea5626ea4b1e4145572.module.wasm:0xf061c
    at 4ea5626ea4b1e4145572.module.wasm:0xf0638
    at 4ea5626ea4b1e4145572.module.wasm:0xb5e7b
    at H.remove (rapier_wasm2d_bg.js:1051:14)
    at l.remove (collider_set.js:87:18)
    at y.removeCollider (world.js:343:28)
    at PhysicsContext.tsx:258:15
    그래도 정말 재미있고, 실시간으로 열려 있을 때 알지 못해서 아쉬움. 사람들이 만든 개별 기계에도 영구 링크를 만들 수 있으면 정말 좋겠음
    저장공간 문제가 있을 수는 알지만, JSON을 base64로 인코딩해서 URL 매개변수에 넣는 식은 안 될까? 이상한 맵을 만들어 사람들과 공유하고 싶음

    • 아직 열려 있음. https://xkcd.com/2916/
      전체 공개 버전에 들어간 기계는 영구 링크가 가능하지만, 조정 대기열에서 선택되지 않은 개별 제작물에는 영구 링크가 없음
      만화 도메인에서 검수되지 않은 사용자 생성 콘텐츠를 호스팅하는 위험을 피하려고 의도적으로 그렇게 결정했음
  • 참고로 HN에는 4월 6일에도 이 주제가 올라왔고 댓글 14개가 있었음
    https://news.ycombinator.com/item?id=39953514

  • “스티커를 어디에 붙일지 신중히 고민할 유인이 없었다. 플레이어가 개별 행동으로 줄거리를 진전시킬 충분한 주도권이 없었다. 그래서 창의성이 비슷한 스티커를 타일처럼 반복하거나 선을 만드는 단순한 패턴으로 제한됐다.”
    아, 게임이 대기업 직장생활이 되어버렸군

  • 공개됐을 때 참여해봤음. 올바른 공이 올바른 출력으로 가도록 최대한 신뢰성 있게 만들려고 한 시간쯤 썼던 것 같음
    제출한 뒤 페이지를 새로고침하니 그 자리에 다른 사람의 장치가 있었음. 인정하자면 더 예쁘긴 했지만, 신뢰성은 떨어졌음
    그런 방식이라는 걸 좀 더 앞에서 알려줬으면 좋았겠음. 그리고 건축 블록 목록이 스크롤 가능하다는 걸 몰랐던 사람이 나뿐만은 아닌 듯함

    • 방금 두 개 제출했는데, 목록을 스크롤할 수 있는지 전혀 몰랐음
      이제 다시 돌아가서 확인할 기운은 없음 :(
  • 와, 나와 친구도 2014년에 같은 아이디어를 떠올려서 Ludum Dare용으로 구현했었음. https://nickfa.ro/wiki/CoinSlot
    그 아이디어가 더 다듬어지고 잘 작동하는 형태로 나온 걸 보니 멋짐

  • 젊을 때 이걸 떠올리게 함. 정말 많은 시간을 아주 즐겁게 낭비했음
    https://www.myabandonware.com/game/the-incredible-machine-1m...

    • 글에도 이 프로젝트가 그것에서 영감을 받았다고 나와 있음
  • 뭔가 놓친 것 같은데, 왜 특정 요소들은 기계 안에서 특정 색 공에만 영향을 주는 것처럼 보이는 걸까?
    색이 완전히 뒤섞이는 걸 막기 위한 장치라고 짐작하지만, 이 글에서는 설명되지 않은 것 같음

    • 각 공에 서로 다른 물리적 특성을 줬음
      노란 공은 가볍고 공기 저항이 크며, 초록 공은 무겁고, 빨간 공은 매우 잘 튐
      그래서 물리적인 분류기를 설계할 수 있음
  • 내가 만든 기계 중 최종 버전에 들어간 것이 있는지 쉽게 확인하는 방법이 있었으면 좋겠음
    다음 설계에서는 이전 제출물 제목을 로컬 스토리지 같은 곳에 저장해두고 알림을 보여줄 수 있으면 괜찮을 듯함