プレイヤーごとに異なる描画を行う

前節ではマルチプレイに対応する方法として、「イベントを生成したプレイヤーごとに処理を行えばよい」ということを述べました。また「全プレイヤーの手元で、同じタイミングで同じイベントを消化することで同じ状態を再現する」という、Akashic のマルチプレイのデザインに触れました。

しかしこれだけでは、作成できるゲームは、全プレイヤーがまったく同じ画面を見るものに限られます。たとえば将棋は作成できるでしょう。将棋盤や持ち駒の情報は全プレイヤー共通で、一画面にすべて表示してしまうことが可能だからです(後手が盤面上側になってしまうという問題はありますが)。しかし別のゲームでは、たとえば各プレイヤーで違うアイテムを使うために、プレイヤーごとのアイテム切り替え UI を作りたいかもしれません。この場合プレイヤーごとにアイテム切り替え UI の状態は異なる必要があります。

そのような状況に対応するため、Akashic Engine はローカルエンティティ・ローカルイベントという概念を持っています。

ローカルエンティティ とは、「そのエンティティを参照するイベントがすべてローカルになる」エンティティ (g.E) です。 ローカルイベント は、通常のイベントと同様ですが「それを生成したプレイヤーにだけ通知される」という特徴を持つイベントです。

ローカルイベント

Akashic のイベント g.Event は、原則的に全てプレイヤー間で共有されます。前述のとおり Akashic のマルチプレイは、各プレイヤーが発生させたイベントを全プレイヤー間で共有することで実現されているためです。

この原則の例外がローカルイベントです。あるイベントがローカルであるとき、そのイベントは、それを生成したプレイヤー以外には共有されません。ローカルイベントは「生成したプレイヤー自身にしか通知されない」ため、プレイヤーごとに異なる処理を実現するのに利用することができます。

イベントがローカルであるか否かは、真理値 g.Event#local によって表されます。この値はイベントの生成時に決定され、変化しません。ゲーム開発者はこの値を変更してはいけません。

その性質から、ローカルイベントに起因する処理においては、ゲームのグローバルな実行状態を破壊してはいけません。特に非ローカルなエンティティの生成や操作を行ってはいけない点に注意が必要です。これについては後述します。

ローカルエンティティ

一部のケースを除き、ゲーム開発者が自分でイベントを生成することはありません。ローカルイベントは多くの場合、ローカルエンティティによって生成されます。

ローカルエンティティとは、「そのエンティティを参照するイベントがすべてローカルになる」エンティティ g.E です。

例えばポイント押下イベント g.PointDownEvent には、押下した位置のエンティティを参照する target プロパティが存在します。この target のエンティティがローカルである時、 g.PointDownEvent はローカルになります。そのイベントはそれを生成した(画面を押下した)プレイヤー自身にしか通知されません。

ローカルエンティティは、エンティティのコンストラクタ引数の local プロパティに true を与えることで生成できます。以下は、シーン scene に属するローカルな g.Sprite を生成するコードの例です。

var sprite = new g.Sprite({
  scene: scene,
  src: scene.assets["chara1"], // (アセットID "chara1" のイメージアセットがある想定)
  local: true // ローカルであることを指定
});

その性質から、ローカルエンティティは、必ずしも全てのエンジンインスタンス上に同じように存在し表示されている必要がありません。すなわちプレイヤー別の UI を実現することができます。

クリックした位置に四角形を表示するだけのコンテンツに、プレイヤー別の色選択 UI を表示するコードは次のようなものになります。

var scene = new g.Scene({ game: g.game });

scene.loaded.add(function() {
  var userColors = {}; // ユーザ別の色テーブル

  // 画面がタッチされた時、矩形を置く
  scene.pointDownCapture.add(function(ev) {
    // 対象がある(他のtouchableに対するpointDown)なら無視
    if (ev.target != null) {
      return;
    }

    // クリックしたプレイヤーの色で矩形を生成
    var rect = new g.FilledRect({
      scene: scene,
      x: ev.point.x,
      y: ev.point.y,
      width: 15,
      height: 15,
      cssColor: userColors[ev.player.id] || "red" // そのプレイヤーの選択している色(なければ赤)
    });
    scene.append(rect);
  });

  // ローカルエンティティで色切り替えボタン (緑)
  var greenButton = new g.FilledRect({
    scene: scene,
    local: true, // ローカルにする
    x: 5,
    y: 5,
    width: 10,
    height: 10,
    cssColor: "green",
    touchable: true,
    opacity: 0.3
  });
  greenButton.pointUp.add(function(ev) {
    // ローカルエンティティのUIの表示を変更
    greenButton.opacity = 1;
    greenButton.modified();
    blueButton.opacity = 0.3; // 非選択状態のものは半透明に
    blueButton.modified();

    // 全員に自分の色変更イベントを送信 (後述)
    g.game.raiseEvent(new g.MessageEvent({ color: "green" }));
  });
  scene.append(greenButton);

  // ローカルエンティティ色切り替えボタン (青)
  var blueButton = new g.FilledRect({
    scene: scene,
    local: true, // ローカルエンティティ
    x: 20,
    y: 5,
    width: 10,
    height: 10,
    cssColor: "blue",
    touchable: true,
    opacity: 0.3
  });
  blueButton.pointUp.add(function() {
    // ローカルエンティティのUIの表示を変更
    greenButton.opacity = 0.3; // 非選択状態のものは半透明に
    greenButton.modified();
    blueButton.opacity = 1;
    blueButton.modified();

    // 全員に自分の色変更イベントを送信 (後述)
    g.game.raiseEvent(new g.MessageEvent({ color: "blue" }));
  });
  scene.append(blueButton);

  // (raiseEvent()で全プレイヤーに送信された)MessageEventを受け取る処理: 送信プレイヤーの色情報を更新する
  scene.message.add(function(msg) {
    // 関係ないイベントは無視して抜ける
    if (!msg.data || !msg.data.color) return;

    // イベントを送信したプレイヤーの色情報を更新する
    userColors[msg.player.id] = msg.data.color;
  });
});

このコードでは、画面左上に緑と青のボタン(ただの矩形ですが)が表示され、画面のその他の位置をタッチすると矩形が表示されるものです。

緑と青のボタンはローカルエンティティで作られていて、プレイヤー別に異なる状態になり、またボタンに対する操作は操作したプレイヤー自身にしか通知されません。そのプレイヤーが非選択状態のボタンは半透明で表示されます。

raiseEvent()

上のコード例の中で説明していないのが g.game.raiseEvent() です。

上述のとおり、ローカルエンティティの操作はすべて操作した本人にしか通知されません (イベントが全体に共有されません)。ローカルイベントに応じてゲームの実行状態を変更しても、それは本人にしか反映されません。上のコード例で言えば、仮に blueButton.pointUp で直接 userColors を変更しても、それはあくまでボタンを押した本人の手元でしか反映されないことになります。この場合、その後画面をクリックした時、本人の手元では青の矩形が表示されるようになりますが、他のプレイヤーの手元では (userColors が変更されていないので) 同じ位置に赤の矩形が表示されてしまいます。

これを解決するには、ローカル処理の中から「ゲーム開発者が明示的に全体にイベントを共有する」処理が必要です。それが g.game.raiseEvent() です。

raiseEvent()g.MessageEvent を生成してそれを全体に共有し、 userColors の変更は g.MessageEvent のハンドラ g.Scene#message の中で行うことで、すべてのプレイヤーの手元で userColors の変更を行うことができます。

g.MessageEvent はゲーム開発者が生成できるイベントで、第一引数に任意のデータを与えることができます。このデータは、受信側 (scene.message の中) では msg.data として参照できます。イベントにはそれを生成したプレイヤー情報がついているので、 上のコード例では scene.message 内で msg.player.id で送信したプレイヤーを特定して、色情報を更新しています。

流れを整理すると次のようになります。

  1. ローカルエンティティで表示した UI をプレイヤー A が操作する
  2. プレイヤー A にローカルイベントが通知される
  3. プレイヤー A の手元で起きたローカルイベントの処理が raiseEvent() を呼び出す (プレイヤー A の色情報を送信する)
  4. raiseEvent() されたイベントが全プレイヤーに通知される
  5. raiseEvent() されたイベントを処理してゲームのグローバルな実行状態を変更する (全員がプレイヤー A の矩形の色情報を更新する)

raiseEvent() はローカルイベントの処理中にしか呼び出すべきではないことに注意してください。非ローカルイベントで行う処理は全プレイヤーが実行するため、そこで raiseEvent() を呼び出すと、プレイヤーの人数分のイベントが輻輳して送信されることになってしまいます。

ローカル処理とその制限

ローカルイベントに起因して実行される処理を、ローカル処理 と呼ぶことにします。

これは、主に g.E#pointDown などにローカルイベントが通知された時の処理を指します。ローカル処理中に呼び出された g.Scene#setTimeout() が引き起こす処理もローカル処理に含みます。

ローカル処理には、そこでのみ許される処理や、逆に禁止される処理が存在します。

  • ローカル処理で禁止される処理
    • ローカルでないエンティティの生成・操作
    • シーン遷移
    • (ローカル処理以外で利用している)乱数生成器(g.Game#random など)の利用
  • ローカル処理で許される処理
    • ローカルエンティティの生成・操作
    • g.Game#focusingCamera の変更 (次節で説明します)
    • Math.random() の利用 (ローカル処理でのみ可能)
    • 全体への通知 (g.Game#raiseEvent() の呼び出し) (ローカル処理でのみ可能)

ローカルイベントはそれを発生させたプレイヤーにとってしか存在しないので、ローカル処理では「プレイヤー間で間接的に共有されている実行状態」を破壊してはいけません。すなわち、同じ操作によって同じ実行状態が再現できなくなるような変更をしてはいけません。

例として、幅・高さが 100 で、 touchable が真である、ローカルでない矩形のエンティティを考えます。ローカル処理の中でこのエンティティの位置を原点から (200, 0) の位置に移動させたとします。するとそれ以降、たとえば (50, 50) の位置に対するポイント押下イベントの結果は、プレイヤー間によって異なるものになってしまいます。このようなエンティティ操作に限らず、ローカル処理では「非ローカルエンティティの生成」や「シーン遷移」「(ゲーム開発者が作った)ゲーム状態の変更」など、破壊的操作のほとんどを行ってはいけません。

通常の場合と異なり、Akashic Engine の乱数生成器 (g.Game#random) の利用も避けてください。ローカル処理によって特定のプレイヤーでのみ乱数生成が行われると、乱数生成器の内部状態が変わります。その結果、それ以降で各プレイヤーが乱数を生成した時に得られる数の系列が、一人だけ異なるものになってしまいます。ローカル処理で乱数を利用したい時(またその時のみ)は、 Math.random() を用いるべきです。 (正確には、ローカル処理以外でまったく使われていない乱数生成器であれば、ローカル処理中で利用しても問題はありません。 new g.XorshiftRandomGenerator() によって独自に生成した乱数生成器を、ローカル処理専用に用いることはできます。ただしこの場合、乱数生成器の生成時に同一シードを与えてしまうと、ローカル処理であるにも関わらず乱数の系列が他プレイヤーと同じになることには注意してください)