1P by neo 4달전 | favorite | 댓글 1개

xkcd의 "Machine" 개발 노트

초기 구상

  • 3월 말까지 아이디어를 고민하다가 4월 초에 결정한 아이디어
  • "Something Awful 사용자들이 만든 블루볼 GIF처럼 타일형 거대 장치를 만들 수 있을까? 모두가 작은 사각형 하나씩 기여하는 거야"
  • 처음에는 아이디어가 온전히 형성된 것처럼 느껴졌지만, 실제로 이야기를 나누다 보니 아직 많은 결정을 해야 함을 깨달음
  • 공 어디서 나오는지, 모두가 같은 기계를 보는지, 목적은 무엇인지, 플레이어는 어떻게 상호작용하는지 등 핵심적인 부분에서 서로 다른 생각을 가지고 있었음

이전 시도에서 배운 점

  • 사용자 제작 콘텐츠 중심의 인터랙티브 만화 제작 경험이 있었음
    • Lorenz: 독자들이 패널 텍스트를 작성해 농담과 스토리를 발전시키는 엑스퀴짓 코퍼스 (매우 재미있었음)
    • Collector's Edition: 독자들이 xkcd 아카이브에 숨겨진 스티커를 찾아 공유 캔버스에 영구적으로 붙이는 게임 (의도한 결과를 얻지 못함)
      • 초기에 빈 중앙 지도에서 시작하면 혼란으로 빠짐
      • 스티커 배치에 대한 인센티브가 부족해서 개인 행동으로 플롯을 진전시키기 어려웠고 단순한 패턴만 생김
      • 전체 스토리나 목표가 부재했고 스티커들의 관계도 불분명했음
  • 집단 캔버스가 성공하려면 무엇을 만들 수 있는지 예시로 가르쳐주고, 공유된 맥락과 목적이 있어야 함

제약 조건 설계

  • 대형 구슬 낙하 기계를 만들기로 결정한 후 너무 많은 선택지에 직면
  • 100x100 크기의 그리드로 구성하기로 결정
    • 클라이언트에서 1만개 타일을 실시간으로 시뮬레이션하는 것은 위험해 보임
    • 플레이어들이 직접 소통없이 복잡한 기계의 하위 구획을 만드는 방법, 분리된 타일이 통합될 때 작동할지 확신할 수 없었음
  • 여러 사고 실험 끝에 3가지 핵심 원칙을 세움:

1. 정확성을 희생하더라도 플레이어의 표현력 극대화

  • 기계가 얼마나 예측 가능해야 하는가?
    • 전체를 서버에서 실행하거나 개별 타일을 검증하는 방안도 고려했지만, 프로토타입 에디터에서 혼돈스러운 구슬 충돌 패턴을 쉽게 만들 수 있음을 확인
    • 구슬이 방해없이 직선으로 움직이지 않으면 예측 불가능한 기계를 만들기 쉬웠음
  • 기계의 예측 가능성을 높이는 것은 플레이어의 자유도와 상충됨
    • 촉박한 개발 기한도 예측/시뮬레이션이 적은 접근법을 선호하게 만듦
  • 극도로 비결정론적이거나 고장난 기계까지 포함해 플레이어에게 매우 유연한 제작 자유도를 주기로 결정
    • 능동적인 검수를 통해 제약 조건 충족 여부와 부적절한 콘텐츠 제거 필요

2. 호환성 있고 교체 가능한 기계를 장려하는 엄격한 제약 조건 제공

  • 검수 수용과 예측 불가능한 플레이어 기계로 인해 오히려 더 많은 질서를 요구하게 됨
  • 초기에는 입출력을 완전히 자유 형식으로 고려했으나, 검수 과정에서 초기 타일 교체가 필요한 경우 대규모 장애를 일으킬 수 있음을 인지
  • 같은 타일 공간에서 여러 플레이어가 호환 가능한 디자인을 만들 수 있도록 충분히 강력한 제약 조건 설계
    • Robustness 원칙 적용: "보내는 데이터는 보수적으로, 받는 데이터는 관용적으로"
  • 입출력 제약을 주기 위해 시작부터 전체 기계 지도 필요
    • 지도 생성을 통해 기계의 난이도 조절 (단순한 1입력1출력에서 복잡한 4입력4출력 병합까지)
  • 실시간 피드백을 주기 위해 타일이 받은 것과 비슷한 속도로 구슬을 배출하도록 제한
    • 구슬을 삼키거나 지연시키는 기계 제한
    • 무작위 입력 속도로 타일을 혼돈 테스트
  • "기계를 한동안 실행하고 평균적으로 제약 조건을 충족하는지 확인" 원칙 확립

3. 기계는 처음 30초 이내에 안정 상태에 도달해야 함

  • 검수자가 얼마나 오래 지켜봐야 하는지에 대한 질문 제기
    • 전체 기계 검수에 걸리는 시간 계산 (1만개 타일 기준 83.3시간)
    • 30초 이내에 안정 상태 진입하도록 임의 결정
  • 구슬이 30초 후 사라지도록 설정
    • 초기에는 만료 시간이 없어서 플레이어들이 게임을 배우는 동안 구슬이 쌓이고 화면을 가득 채움
    • 활성화된 강체가 많아지면서 물리 시뮬레이션 속도 저하
    • 재미보다는 구슬이 방해가 되는 상황
  • 구슬 만료로 기계가 시간이 지남에 따라 오류를 축적하지 않게 됨
    • 검수자는 30초만 지켜보면 대부분의 구슬이 어디로 갈 수 있는지 파악 가능

시뮬레이션과 초현실

  • Machine 아키텍처의 두 가지 큰 도전:
    1. 위 설계 제약 조건으로 이질적인 타일을 연결해 전체 기계로 만드는 게 작동할까?
      • 몇 개의 작은 지도를 생성하고 해결해 검증
    2. 거대한 기계를 서버나 클라이언트에서 실시간으로 실행할 수 없다면 어떻게 표시할까?

스크롤하며 단일 구슬을 추적하는 것이 가능하도록 하는 것이 목표

  • 전체 기계가 시뮬레이션되지 않더라도 플레이어가 보는 영역 주변은 시뮬레이션되어야 함
  • 초기에 무한 지도에서 보이는 영역만 시뮬레이션하는 것을 테스트
    • 꽤 잘 작동했지만 스크롤 시 타일이 빈 초기 상태로 시뮬레이션에 들어와서 흐름에 공백이 생김
  • 빈 타일 대신 이미 활동이 있는 것처럼 보여야 했음

두 번째 도전: 타일의 스냅샷을 안정 상태에 도달한 후에만 찍어서 스크롤로 보이기 직전에만 존재하게 하기

  • 최종 만화에서 디스플레이 클리핑을 끈 뷰 (CSS overflow:hidden, contain:paint 비활성화):
    • 스냅샷을 눈치챘는가? 특별히 주의 깊게 보지 않으면 눈치채기 어려움
    • 렌더링된 타일만 물리 시뮬레이션에 존재
    • 디스플레이 최적화: 뷰 영역 안의 구슬만 보이지만 전체 타일 범위에서 시뮬레이션됨
    • 기계의 상단을 가장하기 위해 시뮬레이션 최상단 행에서 구슬을 생성하고 공급 (입력 제약의 예상 속도 기반)
  • 검수 UI에 스냅샷 생성 연동
    • 검수자는 타일 승인 전 최소 30초 대기해야 함
    • 승인 버튼 클릭 시 스냅샷 생성
    • 검수자 재량으로 기계가 좋아 보이는 상태가 될 때까지 조금 더 기다릴 수 있음
  • 스냅샷 방식이 기대 이상으로 잘 작동
    • 기계에 축적된 오류를 재설정하는 좋은 결과
    • 스크롤하면서 보는 타일의 첫인상은 검수자가 마음에 든 깨끗하고 좋은 상태
    • 실제로 오래 지켜보면 많은 기계가 고장나거나 망가질 수 있지만, 계속 탐색하면 새로운 스냅샷으로 들어가기 때문에 볼 일 없음
  • 만화에서 스크롤하는 기계는 실제가 아님. 초현실임
    • 전체가 한 번에 시뮬레이션되지 않지만 오히려 더 좋은 결과를 낳음

React와 DOM으로 수천 개의 공 렌더링

  • Rapier 물리 엔진 기반으로 구축
    • 훌륭한 문서, 유용한 기본 요소의 깔끔한 API, Rust 구현 (브라우저에서 WASM으로 실행) 덕분에 인상적인 성능
    • 초기에는 Rapier의 결정론 보장에 끌렸지만, 서버 측 시뮬레이션은 하지 않음
  • Rapier 위에 사용자 정의 React 컨텍스트 <PhysicsContext> 작성
    • Rapier 물리 객체를 생성하고 React 컴포넌트 라이프사이클 내에서 관리
    • 물리나 충돌 표면이 있는 배치 가능한 각 객체에 대한 "위젯" 컴포넌트 개발 용이
    • React가 간단하고 더러운 scene graph 역할
    • 뷰 스크롤 시 타일 로딩/언로딩 단순화: 타일 마운트 해제 시 모든 물리와 DOM이 정리됨
    • 보너스로 hot reloading을 fast refresh와 연결하기 쉬워짐 (충돌 모양 튜닝에 정말 좋음)
  • React 컨텍스트 접근 방식의 또 다른 장점:
    • 물리 훅이 <PhysicsContext> 내부에 없으면 noop이 됨
    • 검수 UI에서 정적 타일 미리보기 렌더링에 사용
  • Rapier 객체 생성에 훅 대신 컴포넌트를 사용했으면 좋았을 것 (react-three-rapier가 취한 방식)
    • React diffing에 더 잘 맞음 (의존성 변경 시 useEffect는 이전 인스턴스 제거 후 재생성)
  • Machine은 전적으로 DOM을 사용해 렌더링됨
    • 초기 개발 중에는 성능 면에서 DOM 렌더링의 한계에 다다를까 우려
    • 너무 느려지면 PixiJS나 canvas로 전환할 것으로 예상했지만, DOM을 어디까지 활용할 수 있을지 보고 싶었음
  • 렌더링 성능 최적화:
    • 프레임 루프가 물리 시뮬레이션이 있는 위젯에 직접 스타일 적용
      • React의 diff는 scene graph에 구조적 변경이 있을 때만 실행
    • 초기에는 공을 React로
Hacker News 의견

여러 의견을 종합해보면 다음과 같은 내용으로 요약할 수 있음:

  • XKCD의 만우절 이벤트였던 "The Incredible Machine"은 4월 1일 하루 동안 진행된 협력형 퍼즐 게임이었음
    • 유저들은 물리 엔진으로 구현된 기계 요소들을 활용해 퍼즐을 풀고, 자신만의 퍼즐을 제작해 제출할 수 있었음
    • 하지만 진행 방식에 대한 설명이 부족해서 어리둥절해 하는 유저들도 많았던 것으로 보임
  • 퍼즐 게임의 형식은 오래된 DOS 게임인 "The Incredible Machine"과 유사함
    • 제한된 기계 부품들을 이용해 특정 목표를 달성하는 방식
  • 개발 과정에서는 Rapier 물리 엔진을 사용했는데, 재귀적 오류로 인해 크래시가 발생하기도 했음
  • 이벤트가 끝난 후에도 자신이 제작한 퍼즐을 공유할 수 있는 퍼머링크 기능이 있으면 좋겠다는 의견이 제시됨
    • 저장 공간 문제로 어려울 수 있으니 JSON을 Base64로 인코딩해서 URL 파라미터로 전달하는 방법을 제안
  • 단 3주 만에 이런 규모의 프로젝트를 완성한 것은 대단한 성과라는 평가
  • XKCD가 랜달 먼로 한 사람이 운영하는 줄 알았는데, 여러 사람이 참여한 것 같아 의아하다는 의견도 있었음
  • 이벤트에 대한 더 자세한 정보는 Reddit, Explain XKCD, Github 저장소 등에서 확인할 수 있음