RPGツクールMVのモバイルブラウザにおける描画・メモリの解説(メモリパッチ付き)
閉じる
閉じる

新しい記事を投稿しました。シェアして読者に伝えましょう

×

RPGツクールMVのモバイルブラウザにおける描画・メモリの解説(メモリパッチ付き)

2016-10-21 19:55

    最初にパッチのリンクを張ります、こちらですー[Gist ZIP]


    この記事は、一般的なWeb系エンジニアで、ツクールMVのプラグインやゲームを作ってみたいな、という方向けです。

    一般的なWebプログラミングと、HTML5ゲームは若干作りが異なります。特に混乱するのは、描画周りではないでしょうか。

    そこで、その描画の手段をみてみましょう。

    #内部で使っているレンダリングエンジン、Pixi
    HTML5で描画を行う場合、選択肢が三つあります。

    * DOM
    みんな大好きDOMを操作する方法です。
    ですが、大量の画像を表示するのに不向きなため、他のの手段が主に使われています。

    * Canvas 2D
    HTML5の目玉の一つである、2Dのレンダリングに特化した要素です。
    ベジェ曲線や画像描画等、基本的な描画機能が搭載されています。
    最悪、ピクセル単位の編集が行えるため、速度に目をつぶればなんでもできます。

    * WebGL
    OpenGLといわれる、3D描画APIのWeb版です。
    機能としては、WebGL1.0はOpenGL ES 2.0相当です。
    2.0が進行中で、こちらはOpenGL ES 3.0相当になります。
    3DAPIですが、Z軸を無視することで、2D描画を行えます。

    機能面、速度面ともにWebGLが一番で、Canvas 2D、DOMと続きます。
    ですが昔、WebGLが今ほど実装状況が良くなかった都合上、基本的にWebGLを使い、使えないプラットフォームではCanvasでとりあえず動かすというレンダリングエンジンがいくつか登場しました。
    Pixiはそんなレンダリングエンジンの一つで、体感ではこれ一強です。

    # WebGL, Canvas2D
    Pixiはこの二つの動作モードを切り替えるのですが、
    この二つのモード、モバイル時の速度面で癖があります。

    * Android: Canvas2D
    とても遅いです。
    * Android: WebGL
    とても速いです。

    Androidの場合、速度差が著しく、とても扱いづらいです。

    * iOS: Canvas2D
    それなりの速度で動きます。
    * iOS: WebGL
    それなりの速度で動きます。

    機能面の差異こそあれ、速度面ではMVを動かすうえで、どちらもあまり変わりません。
    プロファイルをとれないので予想でしかないのですが、JavaScript実行速度がボトルネック
    になっていると推測しています。

    # メモリはどのくらい必要なのか
    ## Canvas2D時
    Chrome以外はCPUメモリが画像の面積分必要です。
    Chromeは特殊で、ある程度の大きさになるまで描画命令をため込む方式……でした。
    Canvasをいくら確保してもメモリ面で圧迫せず、MVの設計上アドバンテージがあったのですが、いつのバージョンからか、ある程度のサイズまで、他のブラウザと同じようにメモリを確保しているようです。
    また、レンダリングにはGPUを経由しているため、少なくても表示している分は、GPUメモリも必要です。

    ## WebGL時
    GPUメモリが画像の面積分必要です。GPUの種類によっては内部で可逆圧縮を行っていますので、正確には面積分よりも少ない可能性がありますが、ともあれ、面積に応じたGPUメモリが必要です。
    ただし、GPUメモリはアプリ起動中であっても揮発性です。消える可能性が常に存在し、画像本体をCPUメモリ上に保持する必要があります。
    この辺りはPixiがよしなにしてくれますが、それとMVとの相性が悪く、場合によってはメモリをためこんで落ちてしまいます。

    # MVはモバイル時、メモリ不足に陥りやすい
    ここまでが前提知識で、いよいよ本題、MVのメモリ事情についてです。

    # 1.3.1でもメモリブロートは変わらず
    1.3.0でTTL(一定時間使われなかったら消える)ベースのキャッシュが搭載されましたが、
    これはどこからも起動されておらず、仮に起動しても、特別扱いすべきSystem系も消えるように読めます。
    よって、単純に起動すると不具合が起きる可能性が高いです。

    # 1.3.x系で新規発生、メモリリークのような挙動
    ではSystem系の画像を特別扱いしたTTLベースのキャッシュが実装されればいいのかというと、それだけでもありません。Pixi4にアップデートしたため、ショートスパンでメモリ確保を繰り返すと、メモリ利用量がどんどん上がります。
    特にステータスウインドウ開閉時に顕著で、開閉するたび、数MBのメモリが増えていきます。

    ## 根本的な原因は、destroyしていないこと
    Pixi4にはTextureGCという、新機能が搭載されています。
    これはあまり使っていないGPUメモリを開放する機能ですが、使う頻度が低いからと言って、全部消してしまうと、次レンダリングするときなにもレンダリングされません。
    アンロードされたTextureを再びレンダリングできるようにするためには、
    前述したように、CPUメモリ上に画像を保持して、なおかつその位置を *** Pixiが知っている必要があります *** 。
    つまり、Textureが有効な限り、CPUメモリを握りっぱなしなのです。

    これを解放する方法は、不要になった場合、Textureに用意されたdestroyメソッドを実行するか、GC可能状態(MVから参照されてない状態)にしたのち、PixiのGCを走らせることです。

    また、PixiのGCは、現段階ではRTTと呼ばれる特殊なテクスチャは解放しません。
    これは確実に手動で開放しないとメモリリークします。

    ## MV固有の問題
    そして、ここで機能してないCache機構が問題になります。Cache機能がため込む限り、CPUメモリはJavaScriptのGCで回収されません。
    また、MVの実装上、Window(メッセージウインドウ等)が作成されるたびにBitmap(Textureを使っているクラス)を作成します。
    Pixi4のTextureGCの回収タイミングは執筆時は3600フレーム以上(つまり一分以上)描かれてない場合です。つまり、一分以内にメモリがあふれるほどステータスウインドウの開閉を繰り返すと、ブラウザごと落ちてしまいます。

    ## MVにおいてどう解決したらよいか
    すべての生成されたBitmapをトラッキングして、シーン遷移時に使っていないBitmapをまとめてdestroy&Cacheから削除する&RTTテクスチャを手動開放するコードを作りました。

    さらにメモリがひっ迫した場合のみ緊急で、現在表示しているBitmap以外はすべてdestroyします。
    ですが、これはプラグインに対しては破壊的変更で、特に表示していないBitmapを内部で保持している場合、表示系プラグインがおかしくなります。

    よって、シーンが生きている間、Bitmapを生かしておくメソッド、usingというメソッドを追加、さらによりきめ細かく制御可能な、retain、releaseメソッドを追加しております。

    #メモリ問題が解決したら何がうれしいか
    モバイルでWebGLが使えることです!!!!!
    WebGLのほうがCanvas2Dにくらべ、圧倒的に表現力が高いため、ツクラーさんたちへのメリットは多大です。
    さらにAndroidでもまともに動きます。

    #現在KADOKAWAさんに提案中です

    今現在パッチプラグインを作り終わってまして、KADOKAWAさんに提案中です。これがうまくマージされれば解決しますので、どうぞよろしくお願いします!

    #Canvas2D、WebGLによる描画
    DOMの操作に慣れている方は、描画したい場合、描画したいモノを宣言するだけで描画される、という概念をお持ちでしょう。

    <div id="game">
    <img src="a.jpg"/>
    <img src="b.jpg"/>
    </div>



    このように宣言、言い換えると描画したいデータを配置することにより、あとはブラウザが解釈してよしなに描画してくれる、このような理解だと思います。

    ですが、Canvas2D、WebGL等のレイヤーの低いAPIは違っていて、「ここにこれを描画してね」と命令を与えることで描画されます。
    例として、上と同等のCanvas2Dのコードを挙げますと、

    var context = game.getContext('2d');
    context.drawImage(a_jpg, 0, 0);
    context.drawImage(b_jpg, 0, 0);

    になります。
    座標(0,0)にa.jpgを描画して、その後(0,0)にb.jpgを描画するというコードになります。これを画面更新毎にちょっとずつずらすことによって、画像が移動してるように見せかけることが可能です。

    勘のよい方は疑問に思うかもしれません。a.jpgを描いたあと、b.jpgを描く、この手順が見えてしまう。つまりちらつくのでは?と。



    これにはちょっとしたテクニックが使われています。画面に表示されない場所にあらかじめ全部書いておいて、その後、現在表示してる場所とそっくり入れ替えます。見えない場所に描画することをオフスクリーンレンダリングと呼び、そして、その見えない場所と現在表示している場所、二枚の場所(バッファ)を更新毎に入れ替えることを、二枚のバッファを使うことにちなんで、ダブルバッファリングと呼びます。



    これによりちらつきは最小限に食い止められます。このあたりは、わざわざこちらが実装しなくても、ブラウザがよしなにしてくれます。
    正確にはこれをもってしてもちらつきが起きえるのですが(ティアリング)、そこもブラウザがよしなにしてくれるので、安心してください。


    #低レベルAPIとイベントドリブン
    さて、この命令を積み重ねるスタイル、メインループがエンジニアの手のうちにあったときは、とても使いやすいものでした。
    ですが、幸か不幸か、JavaScriptにはメインループが存在しません。あるのは、特定のタイミング(イベント)で起動する関数、イベントハンドラのみです。

    これをイベントドリブンと呼びますが、これが命令型と相性があまりよくなく、低レベルAPIをオブジェクト指向的に包み込んだライブラリが求められてきました。

    #Pixi、そしてそのAPI設計
    Pixiは、そんな低レベルAPIをオブジェクト指向的に使えるようになったライブラリの一つになります。
    これを使うことで、最初のDOM操作のような気軽さで、低レベルAPIを扱えるようになります。ツクールMVでは、さらにそれを独自にラップしたもの、BitmapとSpriteを使っています。

    var spriteA = new Sprite(ImageManager.loadPicture('a.png'));
    var spriteB = new Sprite(ImageManager.loadPicture('b.png'));
    scene.addChild(spriteA);
    scene.addChild(spriteB);

    Bitmap、Sprite等を扱うためには、PixiのAPI設計を知ることが近道です。
    PixiのAPIはシーングラフと呼ばれる、木構造を成すことで、そこに設定されている情報をもとに描画を行う、という設計になります。

    この設計において、Bitmapは実際の画像データを指し、Spriteはそれの入れ物を指しています。
    Bitmapは直接接続できず、Spriteにつながないと描画されません。なぜできないのか理由は様々ありますが、一番大きいのは、きれいに責務を分離し、単一継承を保つためです。これはAPI設計者等、特殊な人ではないなら気にする必要はないので、そういうものなんだな、と思っていてください。

    ともあれ、BitmapはSpriteにつながないと描画されず、SpriteはSceneにつながないと描画されない、これだけは覚えておいてください。




    本記事は「ニコニコ自作ゲームフェスMV」の依頼を受けて執筆しました。

    MVは素材がたくさんあり、エンジニアでもゲームを作れます!
    技術賞もあるという話も聞きましたので、ぜひぜひご応募を!!
    • 前の記事
      これより過去の記事はありません。
    広告
    コメントを書く
    コメントをするには、
    ログインして下さい。