tmlib-rpg tmlib.js でテスト/ゲームイベントの Interpreter を作成中


はじめに

最近の事とか

久々の投稿。何もしてなかったわけじゃなく、特に記事にすることがないのです><

好きな位置にウィンドウが表示できて、文字や画像を好きに表示できるようになると、もうあとはRPGのシステムを作るだけ…なので、記事になりそうな、あまり困ることが無いんですよね。

RPGのシステム設計の話といっても素人だし、RGSSでしか触ってないし…
tmlib.js のおかげでアニメーションとかもいろいろ楽だし。

いろいろなシーンとかデータを作っていくだけ。
戦闘シーンとか売買のシーンとか基本的なところから、キャラクターデータとかアイテムデータとか魔法とかスキルとか。
(なんかもう、ブラウザがどうのとか、CoffeeScript がどうのとかHTMLが~CSSが~みたいな話はさっぱり)

残っている課題ぽいのは、セーブとロードかな~?要検討…

ってことで、最近は、ゲーム内のイベントを実行するインタプリターとかを作成中です。

気になったのが、Ruby の Fiber と同じようなノンプリエンプティブなスレッド機能が JavaScript にはあるのかな?
ってところ、どうもES6からは、それぽいのがあるらしいのだけど今は無いみたい。要調査…
だけど、まずは、無いものとして作ってます。
(実際、RGSS1では、Fiber が無くて使ってなかった。Fiber を使ってるのは、RGSS2からだったと思う)

インタプリタの作成中

インタプリタ(interpreter)とは、プログラミング言語で書かれたソースコードないし中間表現にある命令列を逐次解釈しながら実行するプログラムのこと。
Wikipedia

だそうです。今作ってるのは、要するにゲーム内のイベントを随時処理してゲームを進めるための機能なのでインタプリタです。

ここで言う「イベント」はRPGのイベントのことで、キーボードとかマウスとかのイベントとは別。
「プレイヤーがNPCに話しかけた時に、文章を表示する」とか言うのがイベントです。

このイベントを膨大に準備することでストーリーとかを作成してRPGになる…と。
イベントの種類がとても重要(奥の手でJavaScriptを実行できるとかすれば何でもできるけど)

いろんなフラグを立てたり、分岐したりループしたりと、構造化プログラミングと同じ様なことになるってことで
イベントを実行するインタプリタを作る必要がある…。
JavaScript も分類的にはインタプリタだと思うのだけど、その上でさらにインタプリタを実装とか骨が折れる…けど仕方がない。

どうやって記述するか?は別にして、まずは内部の実行形式から

commands = [
  {type:'message',params:['Hello! World']}
  {type:'message',params:['OK']}
]
interpreter = rpg.Interpreter()
interpreter.start(commands)

実際には interpreter を使うシーンは、scene オブジェクト に interpreter を作っておいて
interpreter.update を毎フレーム呼んでもらう。
start メソッドに渡したイベントコマンドのリストがあれば、update で随時実行と、言う感じ。

イベントコマンドは、type 属性に対応した、Interpreter のメソッドを params 属性の値を引数にして呼ぶので
例えば、type:'message'ならInterpreter#command_messsage が呼ばれる。

ここまで組めれば、あとはコマンドの type を考えて、対応する Interpreter のメソッドを実装するだけなので
どんどん実装していくのみ…RGSSで言うとたぶん20~30個くらい?あった気がする。

実行形式は、これでたぶん十分、これで作っていくのは良いのだけど、テストが問題でした。
だいぶ形になってきたので、今回は、その辺のお話です。

(前置き長くなった…)

tmlib.js + mocha + chai でテストする

内部で使う、ブラウザに直接依存しないクラスとかは、CoffeeScript の流儀でクラスを作って grunt-simple-mocha でテストしてました。

Interpreter も内部処理がほとんどなのですが、本当に確認するには、ブラウザで実際にゲームのイベントが実行されるかどうかテストしないとならないので、最初から内部クラスでは無く、tmlib.js の流儀でクラスを作ります。
キャラクター操作関連は特に…

ある程度操作が必要になる部分があるにしても、イベントコマンドをゲーム内で1つ1つ書いて、毎回実行しつつ(操作しつつ)…と言うのは、後で大変そう。
出来るだけ自動で、勝手にテストしてくれるようにしたい。
mocha にはブラウザ上でのテストにも対応してる…と、いうことで、以下のような感じにしてみた。

mocha のテストケース表示に tmlib.js を無理やり追加^^

これで、ほぼ内部クラスと同様の記述で、tmlib.js のクラスに対して、テストコードが書けるように!ただし、実行はブラウザ上です。

通常は、以下のような感じで、ウィンドウサイズに fit させてるので

# アプリケーション作成
app = tm.display.CanvasApp '#' + @canvasId

# リサイズ
app.resize @screen.width, @screen.height

# 自動フィット
app.fitWindow()

# 実行
app.run()

fitWindow のコードを参考に…

# 自動フィット
app.fitDebugWindow = ->
  _fitFunc = (->
    e = @element
    s = e.style

    s.position = 'fixed'
    s.margin = '0'
    s.top  = '55px'
    s.right = '10px'
    s.zIndex = 10

    rateWidth = e.width / window.innerWidth
    rateHeight= e.height / window.innerHeight
    rate = e.height / e.width

    if (rateWidth > rateHeight)
      s.width  = innerWidth / 2 + 'px'
      s.height = innerWidth / 2 * rate + 'px'
    else
      s.width  = innerHeight / 2 / rate + 'px'
      s.height = innerHeight / 2 + 'px'
  ).bind(@)

  # 一度実行しておく
  _fitFunc()
  # リサイズ時のリスナとして登録しておく
  window.addEventListener('resize', _fitFunc, false)

こんなメソッドを作成。
html上で fit させる位置を、mocha のテストケースの右にとりあえず配置。

デバック時には、fitWindow ではなく fitDebugWindowを呼ぶように~
なんとなく、fit させるサイズとか位置を引数かなにかでコントロールしたほうが色々できそうだけど
とりあえずデバック用です。(今のところ)

これで、テストコードと tmlib-rpg が同じ場所で動くので、触りたい放題。

describe '文章表示', ->
  interpreter = null
  commands = [
    {type:'message',params:['TEST1']}
    {type:'message',params:['TEST2']}
  ]
  it 'マップシーンへ移動', (done) ->
    loadTestMap(done)
  it 'コマンド実行', ->
    interpreter = rpg.system.scene.interpreter
    interpreter.start commands

今のところ、rpg.system.scene に、現在のシーンクラスのインスタンスがあるので interpreter を取得して、コマンドを開始と、いう感じ。
(ただし、interpreter を用意してるのは、SceneMap なので、マップシーンに移動しないとダメ)

最初読み込まれると、タイトルシーンが表示されるので、マップシーンへ移動させてから実行してます。

ここで使ってるのが、非同期(async処理)で、it のテスト関数に第1引数がある場合(doneのこと)
その引数に、非同期実行用の function が渡されるので、これを呼ぶまで、テスト処理が止まる。

なので、マップシーンに切り替えるまで(マップをロードするまで)テストを止めて、マップがロードされたら
その続きが実行される。
なかなか便利。

このテストでは、メッセージが表示されるけど、文章表示イベントが2つなので2回に分けて表示されます。
2回の間には、(まだ実装してないけど)ウィンドウにポーズサインが出て、次のメッセージがあることをしめして
プレイヤーが何かボタンを押したら、メッセージが続く、という感じ。

テストコードでもそれを再現します。

  it 'メッセージ表示待ち1', (done) ->
    setTimeout(done,2000)
  it '次のメッセージ表示1', (done) ->
    emulate_key('enter',done)
  it 'メッセージ表示待ち2', (done) ->
    setTimeout(done,2000)
  it '次のメッセージ表示2', (done) ->
    emulate_key('enter',done)

2回表示されるので、2回ボタンを押す感じ。
メッセージは、1文字づつ表示されて、文章ごとにスクロールするので、すこし表示待ち。
なので、全部、非同期実行(async処理)です。

あまりやると、テスト時間が長くなるけど…毎回自分で操作するよりは良いかな?と。

本当は、表示されてるのが正しいかとかチェックが必要だけど、さすがにそれは見た方が早いので、今のところパス。

とりあえず実行に失敗して例外とかでたらテストにも失敗するので動作確認程度には、見てなくてもOK。

テストサンプル

テスト対象