高速化 TIPS

# これは

ゲームの動作を高速化するための検討材料について記述したドキュメントです。 Akashic Engine で開発されたコンテンツに適用できる高速化手法について記述します。

目次

描画の高速化

# g.Pane の利用箇所を減らす

g.Pane は Akashic のゲームに様々な描画効果を与えることができますが、できるだけ利用箇所を減らすほうが動作は軽くなります。g.Pane は内部に描画キャッシュを持ち、描画内容が更新されるたびにキャッシュを更新するためです。 この更新は、 g.Pane 自身には更新が無くとも、子要素が更新された場合にも必要です。頻繁に更新されるエンティティの親や祖先に g.Pane がある場合、頻繁に g.Pane のキャッシュ再描画が必要になります。 もし、ゲーム内で利用している g.Paneg.E でも賄える場合、 g.E へ変更することで、負荷を減らすことができます。 特に、 g.Pane をグループ化の用途のみに使っていて、 CompositeOperation shaerProgram の指定がなく、子孫要素が g.Pane のサイズをはみ出ない場合は、 g.E での代替を検討するべきです。

# エンティティの更新頻度

modified()invalidate() の呼ばれたエンティティはいずれも更新され、再描画されます。 再描画には一定のコストがかかるため、演出上妥協できる場合は、これらのメソッドを呼ぶ回数を減らすことで、負荷を低減することができます。

# カメラ外の描画

ゲーム内の空間が広く、ゲーム画面に収まりきらないほど大きい場合、画面外の要素は描画しても画面に反映されません。 画面外要素のドローコールを省略することで、描画にかかるゲーム全体の負荷を低減できる場合があります。 エンティティが画面に入っているかは、g.Camera でゲーム画面の描画位置を制御している場合、そのカメラの位置・ゲーム画面のサイズ・エンティティの位置と座標から判定することができます。

画面の内外を判定する処理は単純ではありませんが、たとえば次のような条件を満たす場合は、以下のコードで描画を省略させることができます。

  • カメラの回転や拡大縮小がない
  • 各エンティティの描画される範囲が、親エンティティの矩形の範囲をほとんどはみ出さない
  • 描画中のエンティティの回転や拡大縮小による描画範囲の変化が margin の範囲に収まる

例として、以下のようなメソッドを g.E の派生クラスに定義する(オーバーライド)ことで、その派生クラスのエンティティの画面外での描画を省略することができます。

render(renderer: g.Renderer, camera?: g.Camera): void {
    if (camera) {
        const margin = 50;
        const globalOffset = this.localToGlobal(this);

        if (
            globalOffset.x >= camera.x - margin &&
            globalOffset.x <= camera.x + g.game.width + margin &&
            globalOffset.y >= camera.y - margin &&
            globalOffset.y <= camera.y + g.game.height + margin
        ) {
            super.render(renderer, camera);
        } else {
            return;
        }
    }
}

# DynamicFont の生成

Akashic のゲームで文字列を表示する方法の一つとして、ダイナミックフォントがあります。 ダイナミックフォントはグリフのラスタライズ結果をキャッシュしており、一度描画したグリフを保持することで、二度目以降の描画を効率的に行います。 一度目の描画はグリフをラスタライズする必要があるので、二度目以降の描画よりも描画のコストが高くなります。 つまり、初めて描画するグリフを多く含む文字列を描画する場合、瞬間的に通常よりも多くの負荷がかかります。

この対策として、 g.DynamicFont は、生成時にフォント生成のヒント情報を与えることができます。 ヒント情報には、予めキャッシュを生成する文字のセットや、キャッシュを描画するテクスチャアトラスのサイズを指定することができます。 例えば、 g.DynamicFont を以下のように生成することで、0 ~ 9 のグリフのキャッシュを持ったダイナミックフォントを生成できます。

const font = new g.DynamicFont({
    game: g.game,
    fontFamily: g.FontFamily.Serif,
    size: 48,
    hint: {
      presetChars: "0123456789"
    }
});

その他

# 高負荷な処理を探す方法

Akashic ゲームに限らず、作成したゲームの挙動が明らかに重い場合、ゲーム処理のどこかに高い負荷があることが推測されます。これを解決するには、何が高負荷な処理なのか見つける必要があります。 主要なブラウザのプロファイラ機能は、このような調査をする際に有効なツールです。(ここでは例として Google Chrome を用います)

プロファイラは F12 キーで開くことができます。プロファイラの詳しい使い方は下記 URL を参考にしてください。 https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/timeline-tool?hl=ja

プロファイラは、実行中の JavaScript の実行状態を記録します。例として、ある Akashic ゲームの記録は以下のようになります。

performance1

このグラフは多くの箇所に赤い印がついており、高負荷を示しています。 これを拡大すると、何の処理にどの程度の時間をかけたかがグラフで図示されます。

performance2

この図では、 E.render が大きな比率を占めていることから、描画による負荷が大きいことが分かります。個別のグラフを拡大すると、より細かい負荷配分を見ることができます。

performance3

描画による負荷であれば、描画回数を減らす(≒ modified() 呼び出し頻度を下げる)などの対策が考えられます。また、描画に関係のない負荷であれば、計算を簡略化するなどが対策の候補になります。