기본 콘텐츠로 건너뛰기

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 별로 파일을 나누면 더욱 보기 좋을 것이다.

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

댓글

이 블로그의 인기 게시물

세상 간단한 https(+secured websocket): Caddy

nginx, apache2 같은 걸로 매번 certbot 연동을 통해 https 하는게 지겨워서 알아보니 Caddy라는게 있더라. 설치법은 강하게 크기 위해 알아서 해본다. # cat /etc/caddy/Caddyfile your.shitty.site:8123 {   proxy / localhost:9123 {     websocket     transparent   } } 이건 외부에서 8123으로 들어오는 걸 내부적으로 9123로 맞춰서 https 맞춰주는 reverse proxy. 만일 port를 생략하면 # cat /etc/caddy/Caddyfile your.shitty.site {   proxy / localhost:9123 {     websocket     transparent   } } 기본포트인 443으로 되어 https://your.shitty.site 로 접속이 된다. 요새 말썽이던 cloudflare 버리고 netlify domain이랑 caddy를 쓰니까 앓던 이가 빠진 느낌. 이 조합은 당분간 계속 써야겠다 싶다.

느려터진 안드로이드 에뮬은 버리고 VM을 쓰자.

iOS개발 환경이 안드로이드보다 우월점은 여러가지가 있겠지만 개인적으로 가장 큰부분이라고 생각하는 점이 iOS Simulator 의 넘사벽 속도다. 사실 iOS 의 경우 Emulator 가 아니라 Simulator 라는 훼이크를 써서 그런건데. 하드웨어+소프트웨어를 같이 하는 회사만이 쓸 수 있는 필살기라 볼 수 있다. 반면 안드로이드의 경우 ARM 에뮬레이터를 사용하는데 이게 참 못만들었다. 플스에뮬이나 GBA에뮬 반정도만 만들어도 써줄텐데 아직 갈길이 멀다. 그래서 구시렁 거리면서 하드웨어를 연결해서 테스트를 하고 있는데 역시 USB연결하는 건 불편하고 apk 를 전송하는 과정도 그다지 빠르지 않아서 개발 생산성이 월등히 나아지지는 않는다. 루팅을 하면 wifi 를 통해 apk 를 인스톨 할 수 있다고 해서 몇 가지를 해보았으나 잘 모르겠지만 인스톨까진 잘 되었는데 디버깅 모드로 실행이 되지 않아 그만두었다. 게다가 전송속도도 USB보다 wifi가 느리고 맘에 들지 않더라. 그러던중 stackoverflow.com(늘 신세지고 있습니다) 에서 "VM으로 안드로이드를 띄워서 adb connect 하면 좋아!" 라는 글에 눈이 번쩍. 시행착오를 몇번 했지만 의외로 간단하더라. 1. VMWare건 VirtualBox건 상관없다. VM호스트를 준비하자. 2. http://www.android-x86.org/download 로 가서  Deprecated x86 2.2 generic  을 받자. Q) 왜 Deprecated 인 2.2 generic 을 받나요. Deprecated는 쓰면 안되는 거 아님? A) http://mariuz.android-dev.ro/vm.iso.7z 도 있다고 한다. http://www.android-x86.org/download 에 있는 요즘 것들은 죄다 안된다. 3. 죄다 일단 Default 설정에 yes yes 하고 설치한다. 한글 문서가 필요한 분은 ...

LG G Pro 2 Custom Rom +Rooting 기록

선 루팅 https://forum.xda-developers.com/lg-g3/general/guide-root-lg-firmwares-kitkat-lollipop-t3056951 1.2버전 스크립트 모드로. echo Looking for LG serial port.. echo. adb.exe wait-for-device 여기에서 정체 상태여서 강제로 adb kill-server 했더니 알아서 재부팅하심. 폰에선 "펌웨어 업데이트"화면이 나오고 Phone found at COM5! Rooting phone.. If you don't see the SuperSu installer script runs within about a minute, then the root failed. Author : blog.lvu.kr SPECIAL COMMAND : ENTER, LEAVE ## 이런 상황이 됨. #이 느린 속도로 하나씩 증가하고 있음. 한 시간 기다려도 성과없음. 뭐 몇번해봐도 안되고 결국 불안하긴 하지만 kingroot.net  로 설치 롤리팝이니까  https://drive.google.com/file/d/0B4kfGU1z_PClQ3AyM2lKOGR1S3M/view  이걸 받아서 압축 풀기. d838recovery.bat 만 있어서 당황스럽긴한데 실행하고 1번 하니까 리커버리 모드 진입까진 잘 되는 듯. adb sideload 라는게 있어서 해봄. Advance에서 adb sideload 선택하고 USB연결한 PC에서 adb sideload RR-N-v5.8.5-20171010-d838-Final.zip 하니까 착착 잘 진행됨. 이제 흉측한 U+LTE 가 없어짐. ㅇㅋ 군더더기 없고 깔끔한데 남은 건 gapps 를 설치해야함. http://opengapps.org/  를 권해줘서 갔는데 지옥 느림. ARM + 7.1 + stock 이 기본이...