커맨드라인 게임 PacVim을 Emscripten으로 웹으로 포팅하기

Vim 키맵을 사용하기 때문에 이런류의 게임1https://github.com/jmoon018/PacVim 좋아라 한다.  Emscripten을 사용한지도 오래됐고 WebAssembly Tool의 현재 상태도 알아보고 싶기도해서 겸사겸사 포팅을 시작했더랬다.  React Hooks를 씌워서 그럴싸한 UI까지 만드는게 목표였는데, ncurses + xterm.js 조합에 해결하기엔 시간이 너무 들어갈 것으로 보이는 문제 때문에 잠시 묵혀두기로 결정한다.

 

컴파일환경
  • Emscripten v1.38.41-upstream2https://emscripten.org/docs/getting_started/downloads.html, Target: WASM
  • Developing OS: Ubuntu
  • ncurses v6.13https://github.com/mirror/ncurses
  • xterm.js4https://xtermjs.org/ 
  • PacVim5https://github.com/jmoon018/PacVim 

 

중단원인

아래 두 그림에서 보듯 iTerm2 에서의 게임내 레벨 표시에 비해 xterm.js 에서 표시는 중간 부분이 완전히 사라져보이는데, 해당 원인은 Emscripten으로 컴파일된 ncurses에서 제어문자열이 도중에 많은 부분이 제거된채  xterm.js으로 보내지기 때문이다. (비교를 위해 이미지 높이를 동일하게 했더니 왼쪽이 좀 좁아 보인다)

Level 8 in iTerm2 Level 8 in xterm.js

ncurses 에서 문자하나를 현재 위치에 출력하는 addch() 함수호출 뒤에 refresh() 호출 여부가 JS 쪽 출력에 영향을 주는 것이 확인됐다. ncurses API에서 내놓는 escape sequence code를 xterm.js 쪽에서 해석을 하지 못하는 게 아닌가 의심스럽지만,  ncurses API쪽에서 적절한 단위로 코드를 나눠 전송하지 않는 문제일수도 있다.  문자열이 아닌 ASCII code 31 이하의 특수문자는  xterm.js 에서 제대로 해석하는 걸로 확인됐다. 따라서 현재 테스트해봐야할 부분은 ncurses API쪽에서 보낸 데이터와 브라우저 JS에서 받은 코드를 비교함으로써 데이터 누락 유무를 확인 하는 것이다.  제대로 전달되지 않는다면 Emscripten으로 컴파일된 ncurses 쪽 문제이고, 제대로 전달된다면  ncurses 쪽에서 내놓는 코드가 xterm.js 에서 제대로 해석되는지 확인해야 한다. 이래저래 시간 엄청 들어가므로 여기서 멈춘다. 

 

PacVim 코드 수정사항

1  게임내 Player 움직임을 제어하기 위해 ncurses getch() 로 입력받는 부분을 제거하고 브라우저 JS 쪽에서 입력받고 C++ API를 호출하는 방식으로 변경했다.

void EMSCRIPTEN_KEEPALIVE player_move(const char *key) {
        if(!READY) return;
        if(*key) {
                onKeystroke(player, *key);
                move(player.getY(), player.getX());
        }
        refresh();
}
 

 

Emscripten으로 포팅된 getch()는 동기함수라서 매번 입력받는 팝업이 게임 흐름을 정지시키므로 도저히 사용할 수가  없다. 앞으로 ncurses API로 데이터 입력받는 일은 없을 듯. 입력된 키 전달은  브라우저 JS에서  C++ API 호출로 전송한다. 

function onKeyUp(event) {
   if(event.key === 'Enter') {
      running = !running;
      Module._toggle_game_state(running);
      return;
   }
   Module._player_move(event.key);
}

output.addEventListener("keyup", onKeyUp, true);

 

2  game.cpp init() 함수에서 while()루프를 돌며 게임이 진행되지만  emscripten_set_main_loop()로 변환하지 않았다. Ghost는 while 루프와 무관하게 pthread로 동작하기 때문이다. pthread상의 Ghost는 READY 변수로 동작을 pause/resume 시킬 수 있다. 이 부분도 브라우저에서 조절하는 게 훨씬 조작성을 좋게한다.  pthreads 때문에  -s ALLOW_MEMORY_GROWTH=1 설정과 -u USE_PTHREADS=1 설정을 동시에 사용하면 귀찮은 메모리 설정 문제가 발생하므로  전체 메모리를 48MB, 스택을 16MB로 지정하고 컴파일했다.

3  게임속 player life 갯수가 0 이하가 될 때까지 루프를 돌게되어 있지만,  단순히 게임 한판을 종료하면 끝내게 해서  main()을 사용하지 않는다.  사용자입력 검증및 후처리를 ncurses API로 받아 하는 것보다 React Hooks 에서 조절하는게 훨씬 보기 좋기 때문이다. 

4  TERM 설정 값 외에는 다른 terminfo 파일이 필요없으므로 embed 방식으로 포함하였다:

   ↳  --embed-file /opt/emscriptened/share/terminfo/x/xterm@/home/web_user/.terminfo/x/xterm

5  PacVim 레벨 데이터 파일은 preload 로 포함시킨다:

    --preload-file maps@/usr/local/share/pacvim-maps

 

ncurses 기반 앱 활용가능성

SDL6https://www.libsdl.org/ 사용하면 이런 종류의 문제는 없겠지만 개인적으로 콘솔형 어플을 선호하기 때문에 가능한 ncurses + xterm.js 조합이 성공하길 기대하고 있다. ncurses를 사용하는 다른 오픈소스가운데 몇가지 Emscripten으로 컴파일해서 살펴보며 추후 수정여부를 새 글로 올릴 예정이다.

 

References   [ + ]

1. https://github.com/jmoon018/PacVim
2. https://emscripten.org/docs/getting_started/downloads.html
3. https://github.com/mirror/ncurses
4. https://xtermjs.org/
5. https://github.com/jmoon018/PacVim
6. https://www.libsdl.org/