← 전체 글로

AI 사용기

AI 활용기: 게임 설정에 '피드백 3종'을 적어놨는데, 실제로 켜졌는지는 아무도 안 재고 있었다

게임을 자동 양산하는 파이프라인에서 config에 적힌 '입력마다 모션+소리+마스코트' 같은 피드백 선언이 실제로 브라우저에서 실행됐는지는 검증 대상이 아니었다. 브라우저를 계측해 실행 여부를 세는 게이트를 만들며, 자동 게임 디자인이 '주스(juice)'를 다루기 어려워하는 이유를 다룬 논문과 겹쳐 읽었다.

이 글의 목적

자동 파이프라인의 설정 파일(config)에 무언가 '적혀 있다'는 사실을 '실제로 작동한다'는 증거로 착각하는 사람에게, 선언과 실행 사이의 간극을 실제 계측 코드로 보여준다.

핵심 내용

게임 스타터를 자동 생산하는 파이프라인에 '스테이지 50개 이상, 스테이지당 의미 있는 입력 3회 이상, 입력·성공·실패·클리어 이벤트마다 모션·소리·마스코트 등 피드백 채널 2개 이상'이라는 수치 계약(contracts/game-quality.json)을 넣었다. 문제는 이 계약을 config 필드(feedbackPolicy)가 선언하고 있는지만 검사하면, 실제로 그 애니메이션이나 오디오가 플레이 중 한 번도 실행되지 않아도 게이트를 통과한다는 것이었다. 해결책은 헤드리스 브라우저에 계측 코드를 주입하는 것 — animationstart 이벤트 카운터, 마스코트 DOM 요소의 class 변경을 감시하는 MutationObserver, AudioContext.createOscillator를 감싸 실제 재생 호출을 세는 래퍼를 넣어, '선언된 채널 수'가 아니라 '한 판 플레이 동안 실제로 발동한 횟수'를 게이트 조건으로 바꿨다. Swift 앱공장에서 이미 쓰던 '기계 계측 우선, 실행 증거 사슬' 원칙을 웹 게임 쪽에 그대로 이식한 것이다.

읽고 나서

자동 생성 파이프라인에 '~해야 한다'는 계약을 넣을 때는 그 계약이 선언 필드만 확인하는지, 실제 실행 결과를 계측하는지부터 구분하고, 가능하면 후자로 만든다. 선언만 검사하는 게이트가 있다면 '선언은 있는데 실행은 0번'인 가짜 사례를 직접 만들어 게이트가 실제로 잡아내는지 먼저 시험해본다.

목차

먼저 답하면

게임을 자동으로 찍어내는 파이프라인에 “입력마다 모션+소리+마스코트 중 2개 이상 반응한다”는 규칙을 넣었는데, 그 규칙을 config 파일에 적어뒀는지만 검사하면 실제로 한 번도 실행되지 않은 애니메이션도 통과해버렸다. 브라우저에 실제 실행 여부를 세는 계측 코드를 심어야 했다.

재현 방법

게임 스타터를 자동 생산하는 사이드 프로젝트에 새 품질 계약을 넣었다. contracts/game-quality.json 하나에 숫자 기준을 모으고, 공개 가능한 스타터는 스테이지 50개 이상, 스테이지당 의미 있는 입력 3회 이상(단일 선택 자체가 장르인 틀린그림찾기류만 single-decision 예외), 입력·성공·실패·클리어 이벤트마다 모션·소리·마스코트·파티클·플래시·흔들림·텍스트 중 2개 이상을 결합해야 한다는 규칙을 박았다.

1차 구현은 단순했다. 각 게임의 game.config.jsonfeedbackPolicy: { onSuccess: ["motion", "mascot"], onFail: [...] }처럼 채널을 선언하게 하고, 게이트 스크립트가 그 배열의 길이가 2 이상인지만 확인했다. 테스트도 통과하고 레지스트리 검사도 통과했다.

그런데 이 방식엔 구멍이 있었다. feedbackPolicy"motion", "audio"를 적어놓기만 하면 되지, 그 애니메이션 클래스가 실제 CSS에 정의돼 있는지, 그 오디오 함수가 실제로 재생을 호출하는지는 아무도 확인하지 않는다. 선언과 실행 사이에 아무 연결도 없는 것이다. 이건 앞서 같은 파이프라인에서 겪은 “픽셀 diff가 스킨만 바꿔도 통과시키는” 문제와는 다른 종류의 구멍이다 — 그때는 측정 축이 틀렸고, 이번엔 측정 대상 자체가 선언이지 실행이 아니었다.

검증 결과

해결책은 헤드리스 브라우저(Playwright)의 verify-playable.mjs에 실행 계측을 심는 것이었다. 페이지 로드 전에 세 가지를 주입한다.

  1. animationstart 이벤트에 전역 리스너를 걸어 발생 횟수를 센다.
  2. 마스코트 DOM 요소(#mascot)를 MutationObserver로 감시해 class 속성이 몇 번 바뀌는지 기록한다.
  3. window.AudioContextcreateOscillator를 감싸서, 실제로 oscillator.start()가 호출된 횟수를 센다.

한 판을 완주시키는 시험 흐름(클릭 → 성공/실패 판정 → 클리어) 뒤에 window.__forgeFeedback을 읽어, 애니메이션 발생 횟수와 마스코트 상태 변경 횟수가 최소 기준을 넘었는지 확인한다. 넘지 못하면 게이트는 그 스타터를 프로덕션 승격 대상에서 뺀다. feedbackPolicy에 뭐라고 적혀 있든, 실제로 한 판 플레이하는 동안 화면과 소리가 움직이지 않으면 게이트를 통과할 수 없게 됐다.

이 설계는 즉흥이 아니라, 같은 운영자가 iOS 앱 자동 생산 파이프라인(Swift 앱공장)에서 이미 검증한 원칙 — 문서나 제작자의 자기보고보다 기계 계측과 실행 증거 사슬을 우선한다 — 을 웹 게임 쪽에 그대로 옮긴 것이다. 스테이지 개수·입력 횟수 같은 정적 계약은 config 파일 검사로 충분하지만, “피드백이 실제로 발동하는가”는 정적 검사로는 원천적으로 잡을 수 없는 종류의 질문이었다.

한계와 다음 개선

이 문제를 정리하면서 비슷한 어려움을 다룬 논문을 찾아봤다. Johansen과 Cook의 “Challenges in Generating Juice Effects for Automatically Designed Games”(AAAI AIIDE 2021)는 자동 게임 디자인 연구가 대개 게임 규칙(mechanics)에만 집중하고 “주스(juice)” — 입력에 반응하는 과장된 시청각 피드백 — 는 미학의 영역이라며 건너뛴다고 지적한다. 저자들은 자동 피드백 생성 도구(Squeezer)와 자동 게임 디자이너(Puck)를 결합해, 자동으로 디자인된 피드백과 사람이 디자인한 피드백에 대한 플레이어 반응을 비교하는 사용자 연구를 했고, 두 시스템을 통합하는 과정에서 겪은 엔지니어링적 어려움을 함께 보고했다.

이 논문이 짚는 지점은 이번 사례와 정확히 겹친다 — 피드백(주스)은 “규칙에 맞는가”처럼 선언적으로 검증하기 쉬운 속성이 아니라서, 자동화 파이프라인에서 가장 먼저 생략되거나 형식적으로만 흉내 내기 쉬운 부분이라는 것이다. “Designing Game Feel: A Survey”(arXiv 2011.09201)도 게임 피드(feel)를 정의하고 측정하는 시도들을 정리하면서, 피드백의 존재 여부보다 그것이 실제로 플레이어의 입력-반응 루프 안에서 작동하는지가 핵심이라는 점을 반복해서 강조한다.

두 자료를 같이 보면, 이번에 브라우저 계측으로 풀었던 문제는 게임 개발 고유의 어려움이기도 하다는 걸 알 수 있다. “적혀 있다”와 “작동한다” 사이의 간극은 소프트웨어 테스트 일반의 문제이면서, 동시에 자동 생성 게임이 유독 자주 걸려 넘어지는 지점이기도 하다. 다음에 비슷한 계약을 넣을 때는 선언 필드를 추가하기 전에 “이걸 실행 증거로 확인할 방법이 있는가”부터 먼저 물어야 한다는 게 이번에 남은 결론이다.