マルチプレイの基礎

Akashic Engine では、マルチプレイのゲームを作成することができます。

ただしマルチプレイゲームの実行には、独自のサーバソフトウェアが必要になります。そのためシングルプレイのゲームと比べ、Web 上で公開するハードルは高くなります。 (シングルプレイであれば、出力した HTML ファイルをレンタルサーバなどにアップロードするだけでよく、独自のサーバソフトウェアの実行は必要ありません。)

Akashic Engine で作成されたゲームは、ニコニコ生放送の放送内で遊ぶことができます。この場合、ゲーム開発者がサーバを用意する必要はありません。放送内では放送者と視聴者複数でマルチプレイすることもでき、現時点では、事実上これが Akashic Engine のマルチプレイゲームを一般に公開する方法です。ニコニコ生放送上で実行するためのコンテンツについては ニコニコ生放送で遊べるゲームの作成 をご覧ください。

詳細はリンク先に譲りますが、ゲーム作成上は特別な対応は必要なく、 game.json の environment.nicolive.supportedModes["multi_admission"] を指定する必要がある程度です。

マルチプレイの Hello World

マルチプレイゲームのコーディングに特別な準備は必要ありません。

Akashic Engine の提供する各種イベントには、全て「誰がそのイベントを生成したか」の情報が付与されています。たとえば g.E#pointDown のハンドラに与えられる g.PointDownEvent には player プロパティがあり、「誰が画面をタッチしたのか」識別することができます。

操作したプレイヤーに応じて処理を分ければ、自動的にマルチプレイゲームに対応できます。

例えば、一番最初にボタンを押したプレイヤーに得点が入る「早押しボタン」のようなコンテンツは、次のようなコードで作ることができます。

var scene = new g.Scene({
    game: g.game,
    assetIds: ["button"]  // button は適当な画像のアセットID
});

scene.loaded.add(function () {
    var button = new g.Sprite({
        scene: scene,
        src: scene.assets["button"],
        touchable: true
    });

    var scores = {};  // 得点を格納するテーブル

    // エンティティ button が押された時:
    button.pointDown.add(funciton (ev) {
        var playerId = ev.player.id;   // 押したプレイヤーのID

        if (scores[playerId] == null) {  // 初回の場合は0点で初期化
            scores[playerId] = 0;
        }

        scores[playerId]++;   // 押したプレイヤーの得点を加算
    });

    scene.append(button);
});

pointDown のハンドラに与えられる引数 ev (g.PointDownEvent) の player プロパティでプレイヤーを識別し、プレイヤーごとに得点を求めています。 player.id はプレイヤーごとにユニークな文字列または undefined です。

このコンテンツは、もちろん現実的には「ボタンの表示タイミングをランダムにする」「ボタンを押した時間によって得点を変える」「ボタンを押した時の画像や効果音をつける」「一定時間後に全員のスコアを表示して勝敗を決める」といった処理を足していかないとゲームらしくはなりませんが、骨格としてはマルチプレイに対応したものになっています。

もう少し画面上で分かりやすい例として、「各プレイヤーに対応するキャラクタ画像がクリックした位置に移動するコンテンツ」を考えます。

キャラ画像としては 素材ページ の次の画像を chara.png として利用します。

chara.png

コードは次のようなものになります。

var scene = new g.Scene({
  game: g.game,
  assetIds: ["chara"]
});

scene.loaded.add(function() {
  var characters = {}; // キャラクタ管理テーブル

  // 画面内のどこかが押されたとき
  scene.pointDownCapture.add(function(ev) {
    var x = ev.point.x; // 押された位置のX座標
    var y = ev.point.y; // 押された位置のY座標
    var playerId = ev.player.id; // 押したプレイヤーのID

    if (characters[playerId] == null) {
      // プレイヤー playerId にとって初めての操作の場合: キャラクタデータを生成
      var chara = {
        targetX: x,
        targetY: y,
        entity: new g.Sprite({
          scene: scene,
          src: scene.assets["chara"],
          x: x,
          y: y
        })
      };
      characters[playerId] = chara;

      // キャラクタのフレームごとの処理(目標座標 (targetX, targetY) に近づくように位置を更新)を登録
      chara.entity.update.add(function() {
        var diffX = chara.targetX - chara.entity.x;
        var diffY = chara.targetY - chara.entity.y;

        // 既に目標座標にいるなら何もせず抜ける
        if (diffX === 0 && diffY === 0) return;

        // 目標座標から各軸10px以上離れていたら、1割ずつ接近。10px以下なら目標座標に移動
        chara.entity.x += Math.abs(diffX) > 10 ? Math.floor(diffX / 10) : diffX;
        chara.entity.y += Math.abs(diffY) > 10 ? Math.floor(diffY / 10) : diffY;

        chara.entity.modified(); // 変更を反映
      });

      scene.append(chara.entity); // シーンに生成したキャラクタ画像を追加
    } else {
      // プレイヤー playerId のキャラが既にいる場合: キャラの目標座標を、クリックされた位置に更新
      characters[playerId].targetX = x;
      characters[playerId].targetY = y;
    }
  });
});

コードは増えていますが、

  • プレイヤーごとの情報を管理するテーブル (この例では characters) を作り、
  • イベントの player プロパティを参照して、プレイヤーごとの処理を行う

という点では「早押しボタン」と同じ構造になっています。

動作確認

ここまで、ゲームの動作確認には Akashic Sandbox (akashic-sandbox) を利用してきました。しかしこのツールはシングルプレイの動作確認を行うためのものです。マルチプレイの確認を行うことはできません。ブラウザで複数のウィンドウを開いても、それぞれ独立した別のゲームプレイになってしまいます。

akashic-cli v1.5.1 以降には、マルチプレイゲームの動作確認機能 (serve コマンド) が追加されています。これは(独立したツールではなく) akashic-cli に組み込まれていますが、 akashic-sandbox 同様サーバとして動作するコマンドです。

インストールされている akashic-cli のバージョンは次のコマンドで確認できます。

akashic -V

1.5.1 より小さい値が表示された場合は、次のコマンドで akashic-cli の最新版をインストールしてください。

npm install -g @akashic/akashic-cli

インストール後、game.json のあるディレクトリで、次のコマンドを実行してください。

akashic serve

実行後、 http://localhost:3300/ にアクセスしてください。次のような画面が表示されます。

serve

先の例の「各プレイヤーに対応するキャラクタ画像がクリックした位置に移動するコンテンツ」の場合、画面をクリックするとキャラクタ画像が表示されます。もう一度クリックするとキャラクタ画像が移動します。

この状態でもう一つブラウザウィンドウを開き、同じ URL を読み込むと、同じゲームプレイに接続することができます。それぞれの画面をクリックすると、それぞれのキャラクタ画像が操作されます。これでマルチプレイの動作を確認することができます。

複数ウィンドウ

なお akashic serveakashic-sandbox と異なり、ブラウザをリロードしてもゲームプレイがリセットされません。リセットは画面内のボタンで行います。

プレイコントロール

画面内のツールバー左側に、ゲームプレイ全般の操作を行うボタンが配置されています。次の機能があります。

  • 一番左の電源ボタン: ゲームプレイをリセットします。
  • 二番目のポーズボタン: ゲームプレイをポーズします。
  • 三番目のインスタンス追加ボタン: 同じ URL でブラウザウィンドウを開きます(マルチプレイのプレイヤーを増やします)。

akashic serve コマンドは、 akashic-sandbox と同様に Ctrl-C で終了することができます。

Akashic におけるマルチプレイ

akashic serve でしばらくプレイした後に、ブラウザウィンドウを追加すると、最初にビデオ映像を早送りするような画面が表示されることに気がつきます。これは Akashic Engine のマルチプレイの実現方法に起因する動作です。

Akashic におけるマルチプレイは、プレイヤー間でゲームの実行状態を「間接的に」共有することで実現されています。 Akashic はプレイヤー間で実行状態を直接的に共有することはしませんが、その代わりに 全てのプレイヤーが行う全ての操作を共有します 。ゲーム内で行われた全操作を全プレイヤーに通知することで、全プレイヤーの手元でまったく同じ処理を行い、結果として同じ実行状態を再現します。 同じゲームスクリプトに、同じ経過フレーム数で同じイベントを与えれば、同じ実行状態に至るはずだ 、というのが、Akashic Engine のマルチプレイの基本的なデザインです。後から参加したプレイヤーに、早送りで最新状態に追いつく動作が発生するのはこのためです。

Math.random() ではなく g.game.random を使う必要があるのもこのためです。 Math.random() を使うと、その結果は実行環境(ブラウザウィンドウ)ごとに異なるので、同じイベントを与えても同じ状態が再現されなくなってしまいます。 g.game.random は一つのゲームプレイ内で全員が同じ乱数シードと乱数生成アルゴリズムを使うように作られているので、この問題がありません。

Math.random() のような制約はつきますが、このデザインによってゲーム開発者は、ゲーム状態の通知や共有などの問題をほぼ気にすることなく、手軽にマルチプレイのゲームを作成することができます。