Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

enchant.jsのフレーム管理周りを改造した話

この土日は体調回復しつつ国産ゲームエンジンenchant.jsのコードを読んでいました。先日9leapで公開したTowerDefenceゲームを低速端末でもスムーズに動作させたかったのです。
その成果はなんとかforkとして結実しました。ここにはなにをやったか、既存実装にはどのような課題があったのかをまとめておきます。

FPSってなによ?

Frames per second、すなわち1秒あたり何度処理するかを表す数値。enchant.jsではプログラマが期待する処理頻度をGame.fpsプロパティで設定可能であり、この数値がenterframe/exitframeイベントの発火頻度を決定づけています。
大きければ大きいほどほどぬるぬるした描写が可能になりますが、大きすぎるとリフレッシュレートや人間の認知能力を超え、無駄な処理になります。enchant.jsのデフォルトFPSは30であり、拙作TowerDefenceでは18と小さめです。

setIntervalの弱点

定期処理の最も単純な実現方法であるsetIntervalは、端末によっては期待通りの間隔で実行されないことがあります。10msec間隔で実行してほしいのに20~30msec間隔でしか関数が呼び出されなかったりするのです。よってプログラマが期待する更新頻度が得られないことがあります。
enchant.jsの既存実装もsetIntervalを使う単純なものでしたので、iPod touchのような端末ではプログラマが期待する更新頻度が得られないことがありました。

対策・実際の経過時間をもとに実行フレーム数を行う

これを解決する方法として、前回関数実行時からの経過時間(elapsed time)をもとに計算回数を調整する方法があります。例えば20msごと実行を期待している関数が60msごとにしか実行されなかった場合、1実行ごとに3回分の処理をすれば良いというわけです。

setIntervalの実行頻度に依らず一定速度で動かす - jsdo.it - share JavaScript, HTML5 and CSS

しかし「3回分の処理」を行いたいのは移動・アタリ判定・スコア計算といった計算処理がほとんどであり、描画は1度だけで充分です。たとえ3度描きかえても、ユーザにはその変化を目視できないでしょう。ですから実際には、以下のような関数をsetIntervalで定期的に呼び出すことになります。

function scheduled() {
    var now = new Date();
    var elapsedTime = now - lastCalledTime;
    var frames = Math.min(elapsedTime / SPAN, MAX_FRAMES);
    if (frames >= 1) {
        for (var i = 1; i <= frames; ++i) {
            game.update();  // 計算処理はたくさん行うかもしれない
            lastCalledTime += SPAN;
        }
        game.draw();  // 描画処理は1回でいい
        if (frames === MAX_FRAMES) { lastCalledTime = +newDate(); }
    }
}

このあたりの解説は今回参考にしたブログ記事の方が丁寧ですので、詳しく知りたい方はそちらをどうぞ。処理を切り分ける動機は異なりますが、やっていることは同じです。

EventDrivenなenchant.jsに組み込む

enchant.jsでは_tick()関数をsetIntervalに実行させることでフレーム処理を実現しています。このsetIntervalと_tick()関数の間に「何回_tick()を実行するか」を決定づける関数を挟んでやることで、setIntervalの弱点を補うことができます。

問題は描画処理と計算処理を切り分けることです。enchant.jsには描画処理と計算処理という概念がもちろんありませんので、何らかの機構を自作する必要があります。
今回はenterframeイベントに似たenterdrawframeイベントを作成することで対応してみました。計算処理をenterframeイベントで、描画処理をenterdrawframeイベントで行うようにプログラムすることで、細かい話を抜きに端末の性能に影響されにくい快適な動作を実現できます。

ついでにrequestAnimationFrameにも対応しています。安定するまでは面倒を見ていくつもりです。