よくある落とし穴

これは

Akashic Engine でゲームを開発する際の、よくある落とし穴をまとめます。

Akashic ゲームでマルチプレイやリプレイを実現するには、「同じコードが同じ入力に対して同じ動作をする」必要があります。環境が異なっても、実行日時が違っても、同じ動作でなければなりません。

なぜなら、Akashic Engine は「ユーザの操作」を共有することで間接的にゲームの実行状態を同期・共有するためです。同じ操作に対する動作にばらつきがあると、リプレイを見るたびにゲームのスコアが変わってしまったり、マルチプレイのプレイヤー間でゲームの状態が食い違ってしまうといった問題が生じます。

JavaScript の機能には、同じ呼び出しでも結果が異なりうるものが存在します。 Akashic Engine 自体は、それらの機能を意図的に使えなくするなどの制限は行なっていません。その種の機能を使う場合、注意深く利用する必要があります。

現在時刻を使ってしまう

Date.now()new Date() など、 Date 関連のいくつかの機能は「現在時刻」の情報を扱います。これらの実行結果は、当然ながら実行環境の時刻設定と呼び出されたタイミングに依存します。

Akashic ゲームでは、 現在時刻に依存してゲームの実行状態が変わらないようにしてください

例えば、自動生成のダンジョンを生成する時のシード情報として「現在時刻」を使ってしまうと、リプレイを後から見た時(違う時刻で実行した時)に異なるダンジョンが生成されてしまいます。ダンジョンが変わっても操作自体は最初のプレイのままなので、でたらめな操作が行われたようなリプレイになります。 (ランダムな値が必要な場合は、後述のとおり g.Game#random を利用してください。)

現在時刻を利用する場合は、あくまでも画面演出のためだけに使うなど、実行状態に影響しないよう注意してください。

なお Akashic Engine v2.3.6 以降では、試験的に g.Game#getCurrentTime() を導入しています。これは Date.now() と同じく 1970 年 1 月 1 日 0 時 0 分 0 秒(UTC)からの経過時刻(ミリ秒)を返す関数(ただし小数点以下を含む)です。 Date.now() と異なり、リプレイ時にも元のプレイ当時の時刻が再現されます。しかし、この関数もローディングシーンの時間に影響は受けるため、マルチプレイのプレイヤー間で一致する時刻情報としては利用できません。あくまで演出上のものとして扱う必要があります。

Math.random()を使ってしまう

明らかに、 Math.random() の戻り値(擬似乱数)は一定ではありません。 Akashic ゲームでは基本的に Math.random() を利用しないでください(次節の例外を除き)。

代わりに、Akashic Engine が提供する乱数生成器 g.Game#random を利用してください。これは乱数シードとアルゴリズムが一つのプレイの中で統一された乱数生成器です。 Akashic Engine のプレイはこの乱数生成器のシードを記録に含めるため、 g.Game#random の生成結果はリプレイ時にも保たれます。

g.Game#random をローカル処理に使ってしまう

ただし、例外的に g.Game#random を利用すべきでない箇所も存在します。

ローカルエンティティがもたらすローカルイベントの処理中 (ローカル処理中) がそれです。ローカルイベントは「プレイヤー間で共有されない」「プレイとして記録に残らない」例外的なイベントであるため、むしろ g.Game#random を使ってしまうと、マルチプレイの他プレイヤーと乱数生成の系列がずれてしまいます。ローカル処理においてだけはむしろ Math.random() (やゲーム開発者が独自に生成した g.XorshiftRandomGenerator など) を使う必要があります。

イベントフィルタ機能 (g.Game#addEventFilter()) を使っている場合、イベントフィルタやそこから実行される処理もローカル処理です。それらの中でも g.Game#random を利用しないでください。

Math.random()g.Game#random の使い分けは非常に紛らわしいのですが、少なくとも ローカルエンティティやイベントフィルタを利用しない一般的なコンテンツでは、 Math.random() を使うべき局面はない ということは言えます。

@akashic/akashic-engine を require()/import してしまう

Akashic Engine のパッケージ "@akashic/akashic-engine" を、スクリプトアセット内で require() しないでください

つまり、次のコードは(Akashic ゲームとしては)誤りです。

var g = require("@akashic/akashic-engine");

TypeScript の場合の、次の記述も同様に誤りです。

import * as g from "@akashic/akashic-engine";

なぜなら、Akashic Engine の機能を提供する変数 g は、スクリプトアセット内では暗黙に存在するグローバル変数であるためです。変数 g は実行環境(akashic-sandbox など)によって初期化され、スクリプトアセットに与えられます。ゲーム開発者が自力で require() してしまった場合、実行環境が与えるものとコンフリクトして正しく動作しない可能性があります。

基本的に、ゲーム開発者が明示的に @akashic/akashic-engine をインストールする必要はありません。

ただしゼロから TypeScript で開発する場合は、 g の型定義(d.ts ファイル)が必要になります。その場合は npm install -DE @akashic/akashic-engine を実行して(devDependencies として)Akashic Engine をインストールし、 node_modules/@akashic/akashic-engine/lib/main.d.ts をコンパイル対象に含めてください。

akashic init --type typescript で生成される TypeScript 用テンプレートでは、最初から Akashic Engine の型定義ファイルがコンパイル対象に含まれているので、特別な対応は必要ありません。

ES2015 以降の機能・構文を使ってしまう

JavaScript の言語仕様である ECMAScript (ECMA-262)にはいくつかのバージョンがあり、俗に ES5, ES2015(ES6), ES2016, ES2017 などと呼称されます。 2015 年策定の ES2015 以降については、2018 年現在でも、特に古めのスマートフォンでのサポート状況にばらつきがあります。

動作の互換性のため、 現在のところ、ES2015 以降の構文や機能は利用しないことを強く推奨します 。これには少なくとも次の構文と機能が含まれます。

  • 構文
    • const, let
    • for-of
    • => (アロー関数)
    • class, extends
    • async, await
    • スプレッド演算子 ...
    • 冪乗演算子 **
    • 分割代入
    • デフォルト引数
    • テンプレート文字列リテラル など
  • 機能
    • Map
    • Set
    • Symbol
    • Promise
    • Reflect
    • Proxy
    • Array.prototype.find
    • Array.prototype.includes
    • Object.values
    • Object.entries など

現実的に、古めのスマートフォン環境固有の問題が報告されても、ゲーム開発者にとっては現象確認さえ容易でないことが多いと考えられるためです。

もちろん、できるだけ広範な環境をサポートするために新しめの機能を避けるかどうかは、Akashic Engine 固有の議論ではありません。しかし「元のプレイと異なる環境でリプレイを閲覧する」などのユースケースが生じやすい Akashic ゲームでは、特に問題になりやすいので注意してください。

なお akashic init --type typescript で生成される TypeScript 用テンプレートは、最初からコンパイル結果が ES5 互換になるように設定されています。そのため const=> など、TypeScript がサポートする各種構文を利用することができます。 (あくまでも構文のみです。 Map など根本的に実装がないと動かない機能には対応できません。)

また akashic init --type javascript-minimal で生成される JavaScript 用テンプレート(デフォルトテンプレート)では、 npm run lint を実行するとコードを lint できます。 ES2015 以降の構文を検出するとエラーを表示するように設定されているので、チェックに使うことができます。

akashic export も、ES2015 以降の構文を検出すると警告を表示します。

Array.prototype.sort の安定性を仮定してしまう

ECMAScript の言語仕様上、 Array.prototype.sort が安定なソートであることは保証されていません。つまり、 sort() に与えられた比較関数が、ある二つの要素について 0 を返した時(等価であるとした時)、ソート後のそれらの順序は実装依存です。 (実際、あるバージョンの Safari と Chrome では sort() の結果が異なる場合があることが分かっています。)

例えば次のコードの実行結果は環境によって異なる可能性があります。

var a = { key: 1, value: "foo" };
var b = { key: 1, value: "bar" };
var c = { key: 1, value: "zoo" };
var d = { key: 2, value: "the last" };
var e = { key: 0, value: "the first" };

var array = [a, b, c, d, e];
array.sort(function (x, y) { return x.key - y.key; });

console.log(array[0].value);  // ==> "the first"
console.log(array[4].value);  // ==> "the last"
console.log(array[1].value);  // ==> "foo", "bar" or "zoo" ???

このコードは array の各要素を key プロパティの値の大小でソートするものです。 sort() の実行後、 array の第 0 要素は e と、第 4 要素は d と必ず同値ですが、第 1 要素が何になるかは実装依存です。

ゲーム開発者は、sort() の比較関数が 0 を返す(等価な)要素の順序に依存しないように注意してください 。可能ならば、 0 を返さないような比較関数を使うことを推奨します。

Microsoft Edge でデコードエラーが出てしまう場合

Microsoft Edge で、オーディオファイル(.aac)の読み込み時に下記のエラーが出力されてゲームの進行が止まることがあります。

WEBAUDIO17014:デコードエラー:指定されたストリームが破損しているか、サポートされていません

詳細な発生条件はわかっていませんが、他の環境では再生できるファイルでも再生できないことがあるようです。このエラーが出た場合、元ファイルのサンプルレートの変更や、出力するオーディオファイルのビットレートを変更を行うとエラーが解消される可能性があります。

complete-audio で変換している場合は、

complete-audio -b 32k sound.wav

のように -b オプションで出力するオーディオファイルのビットレートを指定することができます。

一部の環境でゲーム開始時に自動的に音声が再生されない

Akashic ゲームで「一部の音声が、ユーザが操作 (クリックなど) した時にしか再生できない」という現象が発生することがあり、以下のブラウザでその現象が確認されています。

  • Google Chrome
  • Safari
  • 一部環境の Opera

この現象の原因は、ブラウザの Web 広告対策のために音声再生が制限されることがあるためです。 しかし現状では、そのブラウザの制限を完全になくしてゲーム開始時に自動的に音声を再生させる方法は見いだせていません。ですので、ゲームデザインとして最初に一度以上クリックさせるような作りにするという形で対応してください。

ゲームアツマール環境で背景が真っ黒になる

akashic export html コマンドのオプション --inject を使うことで回避できます。

詳細は逆引きリファレンス ゲームアツマールでの背景色を指定する を参照してください。