前回のアドベントカレンダー14日目では、レンタルサーバにゲームデータをアップロードして
ブラウザでプレイができるまでを紹介しました。
その中で
・URLを開いてタイトル画面が表示されるまで13秒、BGMが鳴り始めるまでさらに9秒、
・「ニューゲーム」を選択してからマップ画面が表示されるまでに5秒
・エンカウントして戦闘画面が始まるまでに6秒と、BGMが鳴り始めるまでにさらに7秒
というウェブ実行時には避けて通れない Now Loading 問題が残っているところで、前編が終わりました。
今回は後編ということで、Now Loading 問題を解決するために使えるテクニックを順番に説明していきます。
ゲームのURLにアクセスした時には、ブラウザには何もデータがないためローディングが必ず発生しますが、
・スクリプトをロードする第1フェーズ
・ゲームデータとフォントをロードする第2フェーズ
・タイトル画面の素材(1枚絵とBGM)をロードする第3フェーズ
という段階に分けられます。
第1フェーズ:スクリプトのロード
第1フェーズでは、依存ライブラリ(Pixi.js と LZstring.js と fpsmeter.js) と 標準のエンジンコア(rpg_ で始まる JavaScript ファイル群) を連続で読み出しますが、ライブラリとエンジンコアの合計で 1.5MB 程度のファイルサイズがあるため、100KB の帯域制限がある場合には、単純計算で 15秒 かかることになります。
実際にはリクエストとレスポンス毎にラグがあるため、今回は最初の HTML ファイルをロードした後に読み込みが始まるため、18秒程度かかりました。
JavaScript で記述されたソースコードは、高い圧縮率が期待出来るテキストファイルなので、ウェブサーバにデータを圧縮してもらう mod_deflate モジュールで gzip をかけるだけでかなりの転送量の削減が行われます。
RPGツクールMVで使われる JavaScript ファイルは 大体 1/6 から 1/7 のサイズになるので、圧縮が効けば 250KB くらいになるのでロードにかかる時間は15秒から3秒程度に削減されます。
この後の第2フェーズの話ではフォントファイルのロードについて説明していますが、ゲームデータとなる data/*.json ファイルのロードも行われます。
このJSONファイルも、JavaScript と同じ程度に gzip 圧縮が効果的なデータであるため、 mod_deflate モジュールに圧縮してもらいましょう。
mod_deflate モジュールにデータを圧縮してもらうには、対象のフォルダに .htaccess (さくらインターネットは Apache 2.2 を利用しており、.htaccess でレスポンスのコントロールを行うことができる設定となっています) を置くことで可能になります。
今回は data フォルダと js フォルダの直下に以下の内容が書かれたテキストファイルを .htaccess という名前でアップロードするだけです。
SetOutputFilter DEFLATE
第2フェーズ:ゲームデータとフォントのロード
第2フェーズはマップデータを除くゲームデータとフォントファイルのロードになります。
ゲームデータについては、第1フェーズのオマケで説明したので、ここではフォントファイルのロードについて説明します。
本来フォントファイルは使われるべきなら先読みして欲しいファイルなのですが、去年あたりに多くのブラウザで「無駄なフォントロードをやめて必要になった時にオンデマンドでロードを行う」という処理に変わってしまったため、意図的に指定フォントを使う処理を行わない限りロードが始まらないという実装になっています。そのため、エンジンコアのロードが終わった後で初めてフォントファイルのロードが始まるというロジックになります。
データの転送速度が遅い環境の場合は、まず間違いなく最初にブラウザでゲームのURLを開いた時に "Fail to load GameFont" というエラーメッセージで停止してしまいますが、何が起こっているかを解明します。
このエラーが発生する理由は、エンジンコアの初期化の中でゲームで使うフォントファイルは 20秒 以内にロードが完了しなければ強制でエラーとするというロジックになっているため発生しています。
Scene_Boot.prototype.isGameFontLoaded = function() {
if (Graphics.isFontLoaded('GameFont')) { // GameFont が読み込めているか?
return true;
} else { // まだの場合
var elapsed = Date.now() - this._startDate; // 読み込み始めた時間からの経過時間
if (elapsed >= 20000) { // 20秒以上経過したら
throw new Error('Failed to load GameFont'); // 致命的エラー
}
}
};
このエラーが出た時は裏では当然フォントファイルのロードが続いているので、できることであればロードが完了してブラウザにキャッシュされるまではプレイヤーにブラウザのリロードを待って欲しいのですが、プレイヤーはそんな事情なんて預り知らないため、すぐにリロードボタンを押してしまいがちです。何度リロードしてもフォントエラーが出ると不満がツイートされることがありますが、実はこんな事情があったりします。
フォントファイルのロードエラーが表示されている時にロードが完了するまでユーザが待ってくれた場合は、フォントファイルがブラウザのキャッシュに格納されているため、次にリロードした場合はウェブサーバに「ブラウザが持ってるキャッシュ使ってね」というレスポンスをもらえるため、ようやくフォントロード地獄から解放されることになります。
フォントの圧縮
第1フェーズで説明したデータの圧縮はフォントファイルにも当然有効です。(標準で使われている TrueType フォント形式が圧縮が効く状態というのがファイルサイズが大きいことの問題にもなっているのですが……)
RPGツクールMVで標準で使われる GameFont フォントは mplus-1m-regular.ttf という 1,541,768バイト のファイルなので、これを gzip 圧縮転送すれば大体 900キロバイト 程度のサイズになるため、それだけ時間短縮が望めます。
また最近のブラウザは色々な形式のフォントファイルに対応しており、特に Google Chrome (nwjsを含む Chromium) や Firefox は WOFF2 という形式のウェブフォントに対応しています。WOFF2形式は大きくなりがちな日本語フォントなどをガッツリ圧縮することを視野に入れて策定されたものなので、WOFF2形式に変換することでファイルサイズはかなり小さくなります。
WOFF2形式へのコンバートツールは、公式の配布元が実行バイナリ形式を配布していないので、有志がビルド済みのものを配布しているものを使います。
https://hail2u.net/blog/software/woff2_compress.exe-and-woff2_decompress.exe.html
Thanks hail2u.net.
mplus-1m-regular.ttf を変換すると mplus-1m-regular.woff2 というファイルが生成され、サイズは 642,308バイトまで削減できます。ここから gzip 圧縮をかけようとしてもサイズは全く縮まらない(むしろ肥大化する)ので、WOFF2形式のファイルに敢えて gzip 圧縮を噛ませる必要はありません。
WOFF2形式のファイルも用意したので対応ブラウザにはそちらのファイルを使ってもらい、対応してないブラウザには標準の TrueType ファイルを使ってもらう設定をする必要があります。
フォントの設定をしているのは、fonts/gamefont.css というファイルで、その中で記述されている以下の内容を
@font-face {
font-family: GameFont;
src: url("mplus-1m-regular.ttf");
}
次のように変更します。
@font-face {
font-family: GameFont;
src: url('mplus-1m-regular.woff2') format('woff'),
url('mplus-1m-regular.ttf') format('truetype');
}
ちなみに WOFF2 の前に策定されている woff というウェブフォント形式もありますが、gzip された ttf と変わらないサイズなので、多くのブラウザで共通で使える TrueType ファイルを gzip 圧縮転送させる場合には敢えて採用する必要はありません。
ここまでのデータ圧縮によって、ウェブサーバから転送されるファイルサイズが、3メガバイト以上あったものが 1.2メガバイトまで減るため、100KB 程度の帯域制限された回線でも、フォントファイルのロードエラーは回避することができるようになりました。
第3フェーズ:タイトル画面のロード
第3フェーズまでたどり着くと、タイトル画面を表示するためのロードが始まります。
ここまでは「ただHTTP転送で gzip 圧縮するだけでいい」という単純なものでしたが、ここから先は既に高圧縮された画像やサウンドファイルになるため、mod_deflate を設定しても解決しません。(むしろファイルサイズが少しずつ肥大化して逆効果になります)
ここから先は、既に圧縮されている画像やサウンドといったメディアファイルをどう扱うか?という領域となります。
サウンドデータ
RPGツクールMVが使用している素材ファイルの形式は、
・サウンドファイルは全て Ogg と AAC(mp4/m4a)ファイル
・画像ファイルは全て PNG ファイル
となっています。
Ogg形式のサウンドファイルは 512kbps、AAC形式のサウンドファイルは 256kbps という非常識なほどに高いビットレートなので、一般的にCD音質と言われるビットレートである 128kbps(Ogg) と 64kbps(AAC) に再エンコードしてしまいましょう。
(再エンコード方法は今回は説明しません)
それぞれ約1/4のファイルサイズになるので、ゲームの各シチュエーションでサウンド再生が始まるまでの時間は1/4まで短縮されます。サウンドの再生開始までに 20秒 かかっていたものが、5秒まで短縮されたのでこれで遅延問題は解決……とプレイヤーさんは納得してくれないのがBGMという存在です。ゲームのシチュエーションが変わった後に無音の状態が続いて数秒後に突然BGMの再生が始まる。違和感バリバリです。
なぜこのような遅延が起こっているのかを説明すると、ブラウザからループ制御なども可能な状態でサウンド再生を行える機能として提供されている WebAudio という技術の仕様に起因します。
RPGツクールMVで使用している WebAudio は、createBufferSource() というメソッドでオーディオバッファを用意し、その buffer プロパティに楽曲全体のPCMデータを渡さなければならない方式のため、サウンドデータのダウンロードが完了するまでは、どうあがいても再生を始められないという制約に縛られます。
それでは解決策はないのか? というと実は WebAudio には別の手法が用意されていて、波形データを順次オーディオバッファに書き出していくことでサウンド再生を行えるリアルタイムにストリーミング再生させる機能が用意されています。
その機能を使ったサウンド再生ができるプラグインとして、私が以前作成した NANO_BGM_Loader.js というものがあります。
http://forums.rpgmakerweb.com/index.php?/topic/50359-nano-bgm-loader-01-under-experimental/
このプラグインはサウンド再生について先に説明したストリーミング処理を行う他に、サウンドファイルの先頭部分だけを先読みしてその部分だけでBGMの再生を開始し、先読みした部分の再生が終わるまでに残りのサウンドデータをダウンロード完了させてシームレスに繋げるというテクニックを使っています。
Oggやaac(他にmp3も)などのサウンドファイルはストリーミング再生に対応しているので、ファイルの実体やストリームデータがぶつ切りな状態でもヘッダに格納されている必要な情報さえ事前に取得できていれば、サウンドデータの一部分だけをデコードしてサウンド再生することができる形式となっています。
例えばサウンドデータの先頭の 100キロバイト分 (100KBの帯域なら1秒で転送できるデータ量、ビットレートが 64kbps なら12秒分) だけを Rangeリクエストで取得してデコードを行えば、その部分だけを直ちに再生開始することができます。
これによってどれだけ長いサウンドファイルであっても1秒の遅延だけでBGMが再生できることになります。
この先が綱渡りになりますが、64kbpsで100キロバイト分=約12秒分の猶予が生まれ、12秒の間に残りのデータをダウンロードできればよいことになります。(12秒は帯域が100キロバイトの場合でも1.2メガバイト(64kbps なら 2分強)のサウンドデータの転送ができる)
RPGツクールMVのエンジンコアそのままの状態では、Oggが再生できない Mac/iOS やモバイル環境の場合に、AAC(mp4/m4a)形式のサウンドデータが使用されますが、実は Google Chrome (nwjs や Chromium は含まない) も aac 形式を再生することができるのに、AAC 形式が使われずにサイズの大きい Ogg を利用してしまうロジックとなっています。
せっかく Google Chrome は aac(mp4) のロイヤリティが支払われているのだから高圧縮で高音質なaac形式を使いたいものです。
そのためにaac形式が使えるかどうかを判定するロジックを修正したいと思います。
エンジンコアの中での、対応メディア判定は以下のようなコードになっています。
WebAudio._detectCodecs = function() {
var audio = document.createElement('audio');
if (audio.canPlayType) {
this._canPlayOgg = audio.canPlayType('audio/ogg');
this._canPlayM4a = audio.canPlayType('audio/mp4');
}
};
mp4 というファイル形式は実は多彩なコンテナやコーデックの集合体であるため、確実に再生できるかどうかの判定ができません。
.canPlayType() というメソッドは指定したメディアタイプの再生に対応しているかどうかの判定結果を返すのですが、'audio/mp4' を与えた場合は Google Chrome でも Chromium でも "maybe" (もしかしたら) という結果が返ります。
ここで明確に 'audio/aac' という形式を与えた場合、Google Chrome であれば "probably" (たぶん,おそらく,十中八九) という結果が返り、AAC形式のロイヤリティが支払われていない状態の Chromium では対応していないために空文字が返ります。
MimeType | Chrome | nwjs |
---|---|---|
'audio/mp4' | "maybe" | "maybe" |
'audio/aac' | "probably" | "" (空文字) |
この差により確実性をもって aac(mp4/m4a)形式のファイルを使っても問題ないと判断できます。
64kbpsでも音質のよいAACであれば100KBで12秒分を稼げるのは、今回使うストリーミング形式では強力なメリットになります。
画像データ
タイトル画面の画像ファイルについて考えます。
デフォルトで表示されるのはお城の画像である Castle.png というファイルになるのですが、このファイルのサイズは1メガバイトほどあり、100KB 回線では第3フェーズで10秒待たせる要因となっています。
ここでは画像をいかにしてファイルサイズを小さくするために再圧縮することを考えます。
タイトル画面で使われる img/title1 フォルダにあるファイルは、透過用のアルファ情報が不要な全画面の画像であり、かつ風景画的な画像であるため、本来であれば jpeg 形式が適した画像になるが、仕様としてPNG形式しか使えないため、PNGのままにサイズを小さくすることを考えると非可逆圧縮のコンバータを使うことになります。
PNGファイルを非可逆圧縮でサイズを小さくできるツールとしては、画像で使われている色数を減らして(減色)してファイルサイズを小さくするアルゴリズムを使っている pngquant というソフトを使います。
pngquant.exe Castle.png
特に何も考えずに pngquant を実行すると Castle-fs8.png というファイルが出力され、オリジナルでは 1,049,553バイト あった画像が見た目はあまり変わらずに 281,013バイトにファイルサイズが削減され、結果として73%の圧縮ができました。
10秒の待ち時間が3秒に短縮されるのは少々の画像の劣化を妥協して余りあるメリットになります。
Google は画像データ転送量を削減することを目的に、独自に webp という画像形式を策定しており、Chromium ベースのブラウザや Firefox で使用できます。webp は可逆を必要とする PNG である必要があった画像にも風景画などの JPEG が使われている画像にも利用できるというメリットがあり、Chromium と判定できるなら何も考えずに画像は PNG を使わず全て webp に置き換えることができるので、プラグインなどで画像のロードを差し替えることができれば絶大な効果が得られます。
Castle.png を 非可逆PNG、JPG、可逆webp、非可逆webp のそれぞれに変換すると
convert -quality 96 Castle_org.png Castle.jpg (ImageMagic で jpeg 形式に変換)
cwebp.exe -q 100 Castle.png -o Castle.webp (Webp コンバータで非可逆変換)
cwebp.exe -lossless Castle.png -o Castle2.webp (Webp コンバータでロスレス(可逆)変換)
ファイル | サイズ | データ形式 |
---|---|---|
Castle.png | 1,049,553 | オリジナルファイル |
Castle-fs8.png | 218,013 | pngquant デフォルト (非可逆) |
Castle-q97.jpg | 206,360 | 品質 97 の JPEG (非可逆) |
Castle-q100.webp | 201,110 | 品質100 の webp (非可逆) |
Castle-lossless.webp | 519,626 | ロスレス圧縮 (可逆!) |
のようなサイズになります。
他のデータ削減を行う方法として、プラグインで画像を読み込むロジックを変更する必要がありますが、スプライトシート (画像1ファイルに複数のチップ画像を並べたもの。ツクールでは多用されている) を別のファイルに分割するようにするのも、ブラウザに保持する画像メモリ量の削減や、オンデマンド(必要になったら取得する)タイプのローディングに効果的になります。
特にキャラクタの顔グラフィック画像 (img/faces 以下)は、1ファイルに8つの画像が並んでおり300KB~400KBのサイズのためメニュー画面を呼び出した際に初めて3つの画像ファイルをリクエストすると約 1MB のファイルサイズとなります。
例:デフォルトパーティのハロルドとテレーゼが Actor1、ルキウスが Actor2、マーシャが Actor3 に入っている。
各ファイルを8分割すると、それぞれ 40~45KB まで小さくなるので、4人分で180KB 程度で済むようになる。
元々、faces 画像はファイル名の先頭に $ 文字を付加することで、1ファイル=1キャラ画像というルールがあるので、分割された画像を事前に用意しておけば、faces 画像を呼び出すメソッドで、分割済みのファイル名を指定するように改造すれば画像の転送コストが減ることになります。
スプライトシートを分割する副次的なメリットとして、JPEG形式のような可逆圧縮によって発生するぼかしが生じるアルゴリズムの画像圧縮のトラブルを気にしなくてもよくなるというものがあります。
スプライトシートは複数の画像を並べた画像であるため、個々の画像部分がエッジに触れている場合、ぼかしが発生する非可逆な圧縮を行うとエッジ部分の色がエッジを超えて隣のスプライトまで色が染みだしてしまいます。
スプライトを別々のファイルに分割することでエッジの染み出しは考慮しなくてもよくなるため、スプライトシートをwebp形式に圧縮する場合はロスレス圧縮にする必要があったのが、個別の画像にすることで可逆圧縮できるようになり、画像サイズが一回り小さくすることができるようになります。
その他のゲームデータ転送の最適化
RPGツクールシリーズは伝統的に RTP という共通素材を共有する仕組みとなっていたのが、RPGツクールMVを期にRTPという共通素材を共有する概念はなくなってしまいました。しかし、ウェブでゲームを複数公開する場合にこそ、今までのRTPという共有された素材という存在が重要なファクターとなります。
複数のゲームを同じサーバにアップロードする場合ですが、標準の画像やサウンドファイルなどは全く同じファイルを使うことになりますが、当然プロジェクト別のフォルダにアップロードされ、ブラウザはURLが違うためにゲーム毎にデータをダウンロードし、キャッシュも別々に管理されます。
キャッシュ容量は限られているので、キャッシュが増えてくれば使われない方のキャッシュは削除され、次にゲームを再開した時に再度データをダウンロードする処理になります。
これは非常に無駄なデータになるので、同じファイルは同じURLにアクセスしてもらうようにすることで、ブラウザのキャッシュ機能を最大限活用できるようになります。
プロジェクト固有のデータをどう管理するかは、また別問題ですが
・ファイル名がバッティングしないように管理し、画像やサウンドは共通のフォルダに全部まとめてアップロードする
・プロジェクトの素材のうち、どれが共通のファイルでどれが固有のファイルかをリストし、リクエスト時にURLを動的に変更する
のいずれかのアプローチをとることになります。
最適化の結果
さて、これまでにあげた素材の最適化や先読みやロード短縮方法などを駆使した結果、どれだけ待ち時間が減った状態でプレイできるようになったかは以下の通りです。
(短縮されたタイムチャート)
ウェブ実行のプレイ環境を快適にするために、ロード時間を短縮するには今までに説明した他に以下のようなテクニックが考えられます。
・ブラウザが利用できるデータ形式を利用(特に Google Chrome の場合は webp, m4a, woff2 を利用)
・スプライト分割、メディアファイルの先読みとストリーミング再生
・共通ファイル(RTP)のURL書き換えと効率的なキャッシュ利用
・https/SPDYプロトコルによるリクエスト処理の高速化
・エンジンコアスクリプトの1ファイル化&変数名最適化&事前圧縮
・プラグインファイルやゲームデータ(JSON)ファイルの1ファイル化と圧縮
これらのテクニックを組み合わせ、さらに素材ファイルを配信でCDN(コンテンツデリバリーネットワーク、超高速なデータ配信サーバ)を使用した場合はこのようになります。
(さらに短縮されたタイムチャート)
キャッシュが存在しない場合でも、そこそこ快適なネットワーク回線がある環境では、タイトル画面が表示されるまで2秒、タイトル画面が表示されてBGMが再生はじまるまで0.5秒のラグとなっています。
これくらいであれば、ロード時間もロゴ表示もなかったファミコンやスーファミ時代の懐かしい快適性がよみがえります。
ブラウザ実行であればゲームのリセットは F5 を押せばいいだけで、既にデータはキャッシュされているので1秒かからずにタイトル画面が始まります。
犠牲になったアイデア
ウェブ実行でどうしてもロード画面が出るのであれば、そのタイミングで広告を表示すればマネタイズの仕組みとしてシステム化できるかなと目論んでいたこともあるのですが、あまりにも最適化でロード時間が短くなりすぎたために広告を一定時間表示させるために敢えてウェイトと入れるようにしないといけないという本末転倒な状況になってしまいました。
結論
最初にも書きましたが、ウェブでプレイしてもらう場合にローディングで待たせてしまう、タイムアウトでエラー表示になってしまう、というのはコンテンツにマイナスイメージを与えてしまうデメリットをはらんでいます。
今回紹介したような手法をサーバに設定したり、プラグインを利用することで快適にプレイしてもらえばそれだけでもゲームのプラスイメージに繋がりますので、ウェブ公開する場合には是非快適な環境になるように設置してもらいたいと思います。
最後に
ちょっとした宣伝になってしまいますが、最後の最適化の事例として紹介したCDN配信を使ったRPGツクールMV専用のウェブ配信サーバのサービスを構築しています。私のツイッターでいずれ告知されると思いますので興味ある方は是非フォローして頂き、サービス開始の告知をお待ちください。