2016년 9월 30일 금요일

RxJS 에서 GameScene 을 구현하고 있다.

결과물은 여기에
http://jsbin.com/wuhega/edit?js,output

Reactive Programming 의 장점+효용이 극대화되는 분야는 아무래도 게임이지 않을까 싶어서 짬을 내어 RxJS로 슈팅게임을 만들고 있다.

  1. 배경+플레이어+총알: https://jsbin.com/worewow/edit?js,output
  2. 1+적+폭파에니메이션: https://jsbin.com/modedic/edit?js,output
  3. 2+coffee+적 움직임등 : https://jsbin.com/yobuka/
대략, 이런 흐름으로 살을 조금씩 붙여가면서 올리고 있는데
어느정도 튜토리얼을 만들려는 생각으로 ECMA6를 가지고 시작을 했지만 {}, (), [] 괄호노동을 통한 손목터널증후근과 코딩하는 시간 만큼 쌍 맞추는 시간이 소모되는 자신을 발견하고 개인의 행복을 위해(...) 다시 coffeescript로 바꿔서 진행하고 있다. 살 것 같다. 아효.

아마도 곧 합쳐질 것 같지만 어느정도 내용물이 만들어졌으니 이것들을 감싸는 껍데기를 어떻게 만들지 코드를 짜면서 고민해보았다.

일단 크게 나누어 보기를
  1. Splash Scene - 한번만 노출. 게임명과 제작자에 대해 알리는 화면
  2. Title Scene - 게임시작을 위해 대기하는 화면
  3. Game Scene - 실제 게임 화면
  4. GameOver Scene - 게임을 더 이상할 수 없는 상황.
이정도로 잡고 1>2>3>4>2>... 의 반복 구조로 설계를 해보기로 했다.

아무런 레퍼런스도 없고 검색해 본 것도 없어서 매우 삽질이 예상되지만 그래도 FRP(Functional Reactive Programming)원칙에 충실하려고 노력했다.

먼저, 화면을 준비한다.
'use strict'
### setup canvas ###
c = document.createElement 'canvas'
document.body.style.margin = 0
document.body.style.height = '100%'
document.body.style.overflow = 'hidden'
document.body.appendChild c
c.width = window.innerWidth
c.height = window.innerHeight
ctx = c.getContext '2d'

캔버스를 만들고 scroll이 되지 않게 margin을 0, 높이를 100%, 그리고 스크롤바도 숨겼다.
캔버스의 크기는 윈도우의 크기만큼 꽉 차게.

두번째로 각각의 Scene을 Observable로 준비한다.
FPS = 16
SplashScene = Rx.Observable.interval FPS
TitleScene = Rx.Observable.interval FPS
GameScene = Rx.Observable.interval FPS
GameOverScene = Rx.Observable.interval FPS
FPS(Frame per Second)를 60frame으로 잡기 위해 1000msec/60frame 한 값이 16.666을 거칠게 16으로 주고 각 Scene 들이 16msec 에 한번씩 canvas 를 그리는 구조를 잡기 위해 위와 같이 설정했다.

이번엔 말 그대로 "껍데기"를 만들 것이므로 최대한 각 Scene들을 단순하게 구현하고자 했다.

시작은 Splash부터
paintSplash = (t)->
  int2hex = (i)-> "0#{i.toString(16)}".toString(16).substr(-2)
  q = int2hex (255-t*3)>0 && 255-t*3 || 0
  ctx.fillStyle = "##{q}#{q}#{q}"
  ctx.fillRect 0, 0, c.width, c.height
  ctx.fillStyle = "#eaeaea"
  ctx.font = "3rem arial"
  ctx.textAlign = "center"
  [x,y]=[c.width/2, c.height/2]
  ctx.fillText "Appsoulute", x, y - 25
  ctx.fillText "games", x, y + 25 if t>150
goSplash = ->
  console.log "enter splash"
  SplashScene
  .takeUntil Rx.Observable.timer 5000
  .subscribe paintSplash, (->), goTitle
goSplash()
각 Scene들은 여기에서 크게 다르지 않은 구조다.
  1. 해당 Scene을 subscribe 하도록 하는 함수를 만들어 호출할 것이며 (goSplash())
  2. subscribe 시 canvas에 요소들을 그릴 것이며 (paintSplash)
  3. 각 Scene을 종료하는 제약조건을 가질 것이며 (takeUntil)
  4. 3의 제약조건으로 종료되고 난 뒤 다음 Scene을 생성할 함수를 호출할 것이다. (goTitle)
이게 전부고 큰 뼈대라고 보면 되겠다.
SplashScene 을 Rx.Observable.interval FPS 로 정의했다.
FPS값, 즉 16msec (0.016초) 마다 subscribe에선 주기적으로 증가하는 값을 받을 것이며 이는 paintSplash 에 산술적으로 증가하는 정수값을 전달할 것이다.
Rx.Observable.interval(100).takeUntil(Rx.Observable.timer(5000)).subscribe(t=>console.log(t))
하고 콘솔에 한 번 입력해보자. 5000msec(5초)동안 0부터 100msec 마다 1씩 값이 증가하면서 출력되는 것을 확인할 수 있다.

paint시리즈의 시작은 canvas 영역만큼 페인트를 확 들이 붓는 것부터 시작하는데 이건 매 frame (정확히 말해 각 Stream의 interval)마다 딱 한번씩만 실행한다.
paintSplash의 인자 t를 기억하자. 애니메이션을 할때 키가 되는 값이다.
  int2hex = (i)-> "0#{i.toString(16)}".toString(16).substr(-2)
  q = int2hex (255-t*3)>0 && 255-t*3 || 0
  ctx.fillStyle = "##{q}#{q}#{q}"
  ctx.fillRect 0, 0, c.width, c.height
배경색을 지정할때 "#000000"식으로 RGB를 표현하는데
255부터 거꾸로 세어서 #ffffff , #fefefe , #fcfcfc .... #020202, #010101, #00000 에 이르도록 배경을 처리하였다.
"#{variable}"은 ECMA6의 `${variable}`과 완전히 같다.
fillStyle 에서 색을 지정하고 fillRect로 0,0 에서 canvas 높이와 너비 만큼 칠한다.

폰트의 경우
  ctx.fillStyle = "#eaeaea"
  ctx.font = "3rem arial"
  ctx.textAlign = "center"
의 세가지 속성을 사용했다.
fillStyle로 글씨에 사용할 색을 변경하고
font로 폰트의 크기(3rem: 해당페이지 기본 폰트에 x3배)와 이름(arial: 기본폰트)을 지정한 뒤
textAlign로 가로 정렬을 잡아준다.
textAlign은

이와 같은 특성을 가지고 있어서 매우 유용하다. start, end, left, center, right 속성을 바꿔가면서 비교해보자.
다시, subscribe 쪽으로 돌아와서 보면
.takeUntil Rx.Observable.timer 5000
takeUntil는 인자로 들어오는 Observable에 Stream이 발생할 때 해당 Observable을 종료하는 Operator이다.
subscribe가 시작되고 5000msec(5초가 지나면) takeUntil이 complete 를 발생시켜 Observable을 중단하고 subscribe에 지정한 complete 콜백을 실행한다.

.subscribe paintSplash, (->), goTitle

subscribe는 세개의 인자를 갖는데 각각 next, error, complete 이다. 세번째 인자인 goTitle 을 지정하여 결과적으로 5초 지나고 난 뒤 해당 함수로 이동한다.
https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/subscribe.md
내용을 참조하여 사용예를 익히자.

Splash가 5초 지나고 Title을 부르면 나머지는 다 비슷한데 시간을 기다리는 것이 아니라 클릭을 하면 다음으로 넘어가도록 구현한다.
goTitle = ->
  TitleStream = Rx.Observable.fromEvent c, "mouseup"
  TitleScene
  .takeUntil TitleStream
  .subscribe paintTitle, (->), goGame
goSplash와 거의 같은 데 제약 조건이 canvas에서 mouseup 이벤트를 fromEvent를 통해 스트림으로 받았다.
그러면 Title Scene은 마우스 클릭이 있기까지 Observable이 유효한 상태이고 마우스를 눌렀다 떼는 순간 complete을 하고 goGame으로 향한다.
goGame = ->
  console.log "enter game"
  GameStream = Rx.Observable.fromEvent document, "keydown"
  .bufferCount 3
  GameScene
  .takeUntil GameStream
  .subscribe paintGame, (->), goGameOver
goGame에선 제약 조건을 다르게 해봤는데 bufferCount 라는 Operator를 사용했다.
bufferCount는 buffer의 일종으로 해당 Observable이 3번 일어날 때 활성화가 되어 takeUntil을 실행한다.
그렇다. 이는 Life 카운트를 구현하기 위한 것으로 적과 충돌 같은 사망 스트림이 3번 발생한 경우로 goGameOver로 향하게 하는 장치라고 보면 되겠다.
구현을 단순화 하기 위해 keydown을 통해 세번 아무 키나 누르면 goGameOver 로 가게 했다.

마지막으로 GameOver를 보자.
사실상 GameOver는 Splash랑 거의 같다.

goGameOver = ->
  console.log "enter gameOver"
  GameOverScene
  .takeUntil Rx.Observable.timer 3500
  .subscribe paintGameOver, (->), goTitle

3500msec(3.5초)만큼 기다리고 그저 goTitle로 향한다.

이와 같이 하나의 루프가 완성되었다.
더 좋은 방법이 있을 법도 하지만 거꾸로 하나하나 Scene을 만드는 것도 꽤 재밌는 작업이었다.
import 나 외부 script 참조를 통해 적절하게 Scene 별로 파일을 나누면 더욱 보기 좋을 것이다.

기실 어떤 프로그램이든 좋은 껍데기 구조는 수익+평판이랑 직결된다. 좀 더 예리하게 다듬어보자.