LoginSignup
43
44

More than 5 years have passed since last update.

Promiseを使った逐次処理でユーザー入力との待ち合わせができるイベントループを記述する

Last updated at Posted at 2014-07-12

何がしたいか

  • ゲームループがループごとにイベントを複数生成するのだが、それを時系列に沿って順番に捌きたい
  • でもオブザーバでグローバルなイベント投げまくるとすぐクソコード化する
  • どうにかしてピュアなモデルからビューとの待ち合わせ(オブザーバー含む)を切り離したい。なんでもしますから!

というわけでちゃんと設計練って書いてみたら結構いいんじゃねとなったので、解説書く。

設計の概要

  • EventSource#createEvents() は Eventの配列を生成する
    • 実際にはここがユーザーが記述する部分になる
  • EventRunner はループ毎にEventSource#createEvents()を呼び、上から順に処理する
    • event.eventType毎に処理の仕方を記述する
    • この例ではEvent間の時間を100msとなるよ]うにしてるが、実際なんでもいい。

コード

CoffeeScriptで書く。暗黙のリターンがPromiseと相性が良い。詳しくはコードで解説する。
今回はPromiseにはbluebirdを使ってるが、Promiseの規格を満たしているならなんでもいい。nodeでもブラウザでも動く。

Promise = require 'bluebird'

class Event
  constructor: ({@eventType, @log}) ->

class EventSource
  createEvents: ->
    for i in [1..3]
      seed = Math.random()
      if 0 < seed < 0.5
        new Event
          eventType: 'a'
          log: 'eventType A'
      else if 0 < seed < 0.9
        new Event
          eventType: 'b'
          log: 'eventType B'
      else
        new Event
          eventType: 'waitInput'
          log: 'waitInput'

class EventRunner
  constructor: ->
    @source = new EventSource

  start: =>
    do update = =>
      @source.createEvents()
        .reduce @processEvent, Promise.resolve()
        .then update

  processEvent: (p, event) => new Promise (done) => p.then =>
    setTimeout (=>
      switch event.eventType
        when 'a'
          @log event.log
          done()
        when 'b'
          @log event.log
          done()
        when 'waitInput'
          @waitInput().then =>
            done()
        else
          throw 'unknown event type:'+event.eventType
    ), 100

  waitInput: -> new Promise (done) =>
    @log 'wait user input'
    # catch something event for your application
    setTimeout done, 1000

  log: (message) ->
    console.log 'log:', message

runner = new EventRunner
runner.start()

実行するとこうなる

~/s/event-runner (master) $ coffee main.coffee 
log: eventType B
log: eventType B
log: wait user input
log: wait user input
log: eventType A
log: eventType B
log: eventType B
log: eventType B
log: eventType B
log: eventType A
log: eventType A
log: eventType A
log: eventType A
log: eventType B
log: eventType A
log: eventType A
log: eventType A
log: wait user input

ログだとわかりにくいけど、それぞれの実行は前のイベントを待って実行されてる。

解説

EventSourceはEventのArrayを返せばなんでもいい。この例では5割でa, 4割でb, 1割でwaitInputを返す。適当に作ったんでなんでもいい。

class EventSource
  createEvents: ->
    for i in [1..3]
      seed = Math.random()
      if 0 < seed < 0.5
        new Event
          eventType: 'a'
          log: 'eventType A'
      else if 0 < seed < 0.9
        new Event
          eventType: 'b'
          log: 'eventType B'
      else
        new Event
          eventType: 'waitInput'
          log: 'waitInput'

ループ部分

  start: =>
    do update = =>
      @source.createEvents()
        .reduce @processEvent, Promise.resolve()
        .then update

eventの配列をreduceでPromiseチェーンを作り、全てのpromiseが吐けたら再帰する。詳しくは次の関数。

  processEvent: (p, event) => new Promise (done) => p.then =>
    setTimeout (=>
      switch event.eventType
        when 'a'
          @log event.log
          done()
        when 'b'
          @log event.log
          done()
        when 'waitInput'
          @waitInput().then =>
            done()
        else
          throw 'unknow event type:'+event.eventType
    ), 100

具体的にイベント事にどう捌くか記述しているのだが、冒頭部分が肝で、

  processEvent: (p, event) => new Promise (done) => p.then =>

processEventは「Promise化された」関数で(coffeeの暗黙のリターン)、第一引数のpはreduceで集約された前回のpromiseインスタンス。これをthenでチェーンしてやることで、シーケンシャルに実行するPromiseチェーンが作れる。

waitInputもPromise化された関数で、今回は実は関数名が嘘で、1秒待ってdoneを解決するだけ

  waitInput: -> new Promise (done) =>
    @log 'wait user input'
    # catch something event for your application
    setTimeout done, 1000

たとえばボタン押したらこのdoneを解決するように書けば、ユーザーの入力を待って処理を再開することができる。jQueryを使う例を示す。

# 再開するボタンを作る
$resumeButton = $('button.resume')
$resumeButton.hide() # 最初は表示しない

# ... 中略
  waitInput: -> new Promise (done) =>
    $resumeButton.show() # ユーザーにボタンを見せる
    $resumeButton.on 'click', => # click時のコールバックを定義
      $resumeButton.off() # clickが二重定義にならないように剥がす
      $resumeButton.hide() # 再度隠す
      done() # Promiseを解決する

これがユーザーとのインタラクションを定義する方法になる。

なんか昔仕事でみたコードがこんな感じのループ処理が地獄だった記憶があったんだけど、これぐらいだと綺麗にかけて良いのではないか!

43
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
44