よくある落とし穴

# これは

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() は利用しないでください。 Date.now() 同様、リプレイを後から見た時に (生成される乱数が変わってしまうため) おかしくなってしまうためです。

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

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

ローカル処理では、 g.Game#random ではなく g.Game#localRandom を利用してください。

ローカル処理とは、「一人のプレイヤーの手元でしか発生しない処理」です。主に (ローカルエンティティをクリックした時に生じる) ローカルイベント起因の処理が該当します。ローカルイベントは「プレイヤー間で共有されない」「プレイとして記録に残らない」例外的なイベントであるため、一人のプレイヤーの手元でしか生じません。ここで g.Game#random を使ってしまうと、マルチプレイの他プレイヤーと乱数生成の系列がずれてしまいます。

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

なお g.Game#localRandom は Akashic Engine v3.0.0 で追加されました。 それ以前のバージョンでは Math.random() を利用してください。これは Akashic ゲームで Math.random() を利用すべき唯一のケースでした。v3 系以降、 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 の型定義ファイルがコンパイル対象に含まれているので、特別な対応は必要ありません。

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

# ランキング対応ニコ生ゲームで、意図せずプレイヤーごとに乱数が異なる

ランキング対応ニコ生ゲームでは、g.Game#random を利用してもプレイヤーごとに異なる乱数が生成されます。(g.Game#localRandom と同じになってしまう)

プレイヤー間で共通の乱数を生成する場合は、ランキングゲームテンプレートが独自に提供する乱数生成器を利用する必要があります。詳細は、テンプレートでわかるランキング対応ゲーム の 注意点: 乱数について を参照してください。