Skip to content

Join と Leave とニコ生の話

Join と Leave の話

Join と Leave

Akashic の世界では、プレイヤーはゲームに Join(参加)している状態と Leave(離脱)している状態の二つが存在します。 これは定義上存在しているだけで、基本的に どのように扱うかはゲーム開発者に委ねられています。 例えば前回で作った早押しゲームは Join、Leave に関係なく全てのプレイヤーがボタンに触ることができました。

Leave(離脱)しているのにボタンが押せたり画面が見えたりするのはおかしいと思うかもしれません。 世界観としてはボードゲームやババ抜きなどのカードゲームを想像してください。実際の参加者とは別に、 ゲームを観戦したりアドバイスしたりする人間は存在しうる はずです。 つまり実際に着席してゲームに参加している人と、場合によっては干渉してくる、ゲームを傍から見ている人を表す概念と理解できます。

たとえば前回の早押しゲームに、ボタンが押せなかった人はライフが減り 0 になったら参加できなくなる、というようなルールを追加しようとしたとき

  • 全てのプレイヤーは開始時に Join する
  • ライフが 0 になったプレイヤーは Leave する
  • Join していないユーザーがボタンを押しても何も起きない

というような作り方ができれば実現できます。しかし別にフラグを設ける、ボタンを押したプレイヤーのライフを見て 0 なら何もしない、などの解決方法もあります。 Join と Leave の利用は マルチプレイゲームにおいて必須ではなく、 組み込みのフラグの一種という解釈をするといいかもしれません。

ここで前回のおさらいを少しします。 あるプレイヤーがゲームに Join するとイベントが発生します。このイベントはグローバル処理でしょうか、ローカル処理でしょうか。

正解はグローバル処理です。特定のプレイヤーがゲームに Join した、というイベントは全員に通知されます。

Join の補足

重要なことですが、どのような条件で Join するかは サービス提供者に委ねられます。 ゲームの開発者ではないことに注意です。 このことはあまり直感的ではないかもしれませんが仕組みとしてそのようになっています。 みなさんがローカルでマルチプレイのテストをするとき、サービス提供者とは akashic serve コマンドです。

akashic serve でアクセスできるブラウザではアクセス直後はジョインしておらず、手で join me を押す必要があります (デフォルトの場合)。 従って前述した「全てのプレイヤーは開始時に Join する」というようなことは実現できず、それ前提で組んだプログラムはテストできません。

これから解説しますが ニコ生に持っていっても動きません。

ニコ生ゲームの話

ニコ生における Join

さて、 マルチプレイゲームを公開する現状唯一の手段 は「ニコ生ゲーム」として公開することです。すなわちニコニコ生放送を使う事です。 自分でサーバーを用意し、akashic serve の仕組みを十分に理解し場合によっては手を加えるなどすれば可能かもしれませんが、大多数の方にとってはニコ生ゲームが現状唯一の手段でしょう。

Akashic Engine 入門 » Join と Leave にもありますが、ニコ生ゲームにおいては Join が発生するのは一度だけ。生主のみがゲームに Join し Leave することはありません。 (厳密に言えばゲームを起動した人、なのですが詳細は割愛します)

つまり世界観としては、Join した生主とそこに干渉する視聴者、という構図が出来上がります。絵を描きました。

絵

この図はあくまでニコ生上での Join 状態を表した図です。このような構図を意識した視聴者全員 vs 生主ゲームを作ってもいいですし、まったく無視して全員参加のバトロワを作っても構いません。結局のところ使い方次第です。

生主の区別と役割

ニコニコ生放送において 生主には以下の特権が与えられています。

  • カウンターからゲームを選んで起動できる
  • カウンターからゲームを強制終了できる

兎にも角にも生主にゲームを起動してもらわないことには始まりません。自分のゲームが起動してもらえるようにアピールするか、自分で生主になるしかありません。 全く脈絡はありませんが nicocas アプリはスマホがあれば誰でも配信できるので一度試してみると良いでしょう。 http://site.nicovideo.jp/nicocas/app/

さて、前述した生主の特権と、生主はゲーム上に一人しか存在しない、という特性を考慮すると、 生主にゲームマスター的な役割を与えるのは自然な発想 です。 以下に例を出してみましょう

  • タイトル画面でスタートボタンをおす
  • 参加者募集を終了し実際のゲーム画面に遷移する
  • 圧倒的なパワーで視聴者を薙ぎ払う
  • 参加者同士の対戦における審判の役割を負う

現在公式に提供されているマルチプレイゲームでは、生主は実際に以下のような役割を担っていることがあります。

  • だるま役となって片っ端から参加者をアウトにしていく
  • 画面を動き回る参加者に向かって爆弾を落とす
  • 投稿されたイラストを審査していくつか選んで発表する

などなど。 生主に起動してもらうことを考えると生主には何かしらの特権があった方がいいかもしれません。しかし生主が強すぎるとそもそも人が集まらないためバランスは重要です。

生主を区別する

生主にゲームマスター的な役割を持たせるサンプルコードを作ってみましょう。 ニコ生上でよくあるマルチプレイゲームを踏襲したものを作りました。 masterviewer

見た目はやや残念ですが、放送者が募集役、それ以外の人が参加する、という 最近のニコ生でありがちなシーン を再現しました。 前回の早押しゲームとは異なり、今回は放送者と視聴者で画面の状態がかなり異なるので、ローカル処理、ローカルなオブジェクト、を意識していくことが重要になります。

また今回より、ソースコードが長くなってきてしまったため全文掲載を見送ることにしました。サンプルコード全体は以下で公開されています。

https://github.com/akashic-contents/with-game-master

ゲームの流れ

  1. 放送者の Join(一番最初の join)を待つ
  2. ゲームが参加者募集状態になる。放送者は参加者を締め切ることができ、視聴者は参加することができる
  3. 放送者が募集を締め切るとゲームが開始される
  4. 数秒経つとゲームが終了し 2.に戻る

ゲームの状態というのはマルチプレイに関係なく大事な考え方です。自分のゲームが今何をする状態なのか、例えばタイトル画面なのか、メニュー画面なのか、といったことを意識することが必要です。 今回のゲームは前回の早押しゲームとは異なり、先述した通り放送者待ち、募集中、プレイ中、などいくつかの状態を持ちます。いずれもニコ生においてはよくある流れです。 状態管理に複数のシーンを行き来する手法や main 関数を切り替える手法もありますが、今回は状態管理用の変数を使ってみたいと思います。

以下で個別に解説していきますが、コード全文を読む際にはゲームの状態変化の流れを追うとわかりやすいかもしれません。

放送者の Join と初期化処理

ゲームに誰かが join するとイベントが発生します。その時の処理を追加するコードが以下です。最初に一人だけ join する想定のため、addOnce を使うことで一回だけ処理が行われるようにしました。 ニコ生のみを想定するなら関係ありませんが、以後誰が join してもこの処理は行われません。最初の一度だけです。

ローカル環境で単純に akashic serve した場合、join は自動では行われません。 放送者相当の画面で、画面上部にある Join Me ボタンを押せばその画面のプレイヤーが join します。つまり今回のサンプルにおいて放送者と扱われます。

追記

現在の akashic serve には、ニコニコ生放送と同じ感覚で使えるよう「自動的に最初の一人だけ join する」モードが追加されています。 --target-service nicolive をつけて起動してください。詳細は ニコ生ゲームを作ろう » マルチプレイゲーム を参照してください。

そしてこの処理の中で、Join した人の ID をゲームマスターの ID として覚え、以後使うようにします。

ts
// 一番最初にJoinした人を覚える変数
let gameMasterId = null;

g.game.join.addOnce(e => {
  gameMasterId = e.player.id;
});
// 一番最初にJoinした人を覚える変数
let gameMasterId = null;

g.game.join.addOnce(e => {
  gameMasterId = e.player.id;
});

あとはゲームのメインループ内でこの値の変化をチェックし、null じゃなくなったときに募集を開始すればいいわけです。gameStatus、つまりゲームの状態を初期化(initializing)に進めます。

ts
// 毎フレーム呼び出される処理。ゲームステータスで分岐する
function mainLoop() {
  if (gameStatus === "gameMasterWaiting") {
    if (gameMasterId !== null) {
      onGameMasterArrive();
      gameStatus = "initializing";
    }
  }
  // -- 略 --
}
// 毎フレーム呼び出される処理。ゲームステータスで分岐する
function mainLoop() {
  if (gameStatus === "gameMasterWaiting") {
    if (gameMasterId !== null) {
      onGameMasterArrive();
      gameStatus = "initializing";
    }
  }
  // -- 略 --
}

onGameMasterArrive()の中は大まかに以下のようになっていて、自分がゲームマスターなのかどうかによって参加締め切りボタンか参加ボタンのいずれかを出しています。 またこの時に画面のテキストも変えています。

ts
function onGameMasterArrive() {
  // 自分のIDがゲームマスターIDかどうかで分岐。ここはローカル処理
  if (g.game.selfId === gameMasterId) {
    scene.append(closeButton);
    infoLabel.text =
      "あなたが一番最初にjoinしました。あなたが放送者です。\n参加者の受付を終了することができます";
    infoLabel.invalidate();
  } else {
    scene.append(entryButton);
    infoLabel.text = "あなたは視聴者です。ゲームに参加することができます。";
    infoLabel.invalidate();
  }
}
function onGameMasterArrive() {
  // 自分のIDがゲームマスターIDかどうかで分岐。ここはローカル処理
  if (g.game.selfId === gameMasterId) {
    scene.append(closeButton);
    infoLabel.text =
      "あなたが一番最初にjoinしました。あなたが放送者です。\n参加者の受付を終了することができます";
    infoLabel.invalidate();
  } else {
    scene.append(entryButton);
    infoLabel.text = "あなたは視聴者です。ゲームに参加することができます。";
    infoLabel.invalidate();
  }
}

ゲームが募集状態の時、 ゲームマスター(放送者)と視聴者の役割は大きく異なります。 ゲームマスターは参加募集を打ち切る権限を持ち、視聴者はゲームに参加するか否かの選択が行えます。次の節で、各々の操作、つまりローカルイベントによってゲーム全体へ影響を与える raiseEvent についてみてみましょう。

参加待ちと raiseEvent の話

上の方に貼った画像のように、ゲームマスターと視聴者それぞれが操作できるボタンを考えます。 ゲームマスターと視聴者のボタンはそれぞれ役割が違うので、ローカルエンティティにしておく必要があります。それぞれのボタンの役割は

  • 参加締め切りボタン:gameStatus を playStarting に変更する
  • 参加ボタン:参加者テーブル(players)に自分の id を追加する

となります。例えば、マスター側のボタン、参加締め切りボタンが押された時の処理を抜き出してみます。

ts
closeButton.pointDown.add(() => {
  scene.remove(closeButton);

  // このボタンの処理は放送者でしか発生しないので、ゲーム全体の進行のため全体に通知する
  g.game.raiseEvent(new g.MessageEvent({ message: "EntryClosed" }));
});
closeButton.pointDown.add(() => {
  scene.remove(closeButton);

  // このボタンの処理は放送者でしか発生しないので、ゲーム全体の進行のため全体に通知する
  g.game.raiseEvent(new g.MessageEvent({ message: "EntryClosed" }));
});

処理の中で gameStatus を代入する代わりに、 raiseEvent を使っています。重要です。 なぜなら このイベントはマスターの PC 上でしか発生しない ため、視聴者全員の PC にある gameStatus が変わらないのです。 逆にマスター以外の PC ではシーンに closeButton がないため、ここでローカル処理として closeButton を消しています。

さて、raiseEvent についてはAkashic Engine 入門 » プレイヤーごとに異なる描画を行うにも説明がありますが少しみてみましょう。 ローカル処理のなかで全体へ影響を及ぼしたくなった場合は、raiseEvent で全員にイベントを送信するのが Akashic の流儀です。以下に受信側のコードを貼っていきます。

ts
// raiseEventを処理するところ。raiseEvent時につけたmessage名で処理を分岐する
scene.message.add(ev => {
  if (ev.data.message === "EntryClosed") {
    // 募集締め切り
    gameStatus = "playStarting";

    // 参加者が参加ボタンを押さなかった場合参加ボタンが残っているので消しとく
    scene.remove(entryButton);
  }

  if (ev.data.message === "Entry") {
    const playerId = ev.player.id;
    if (players.indexOf(playerId) < 0) {
      players.push(playerId);
    }

    // エントリーしたのが自分だった時。これはローカル処理
    if (playerId === g.game.selfId) {
      infoLabel.text = "あなたは参加しました。\n放送者の受付終了を待っています";
      infoLabel.invalidate();
    }
  }
});
// raiseEventを処理するところ。raiseEvent時につけたmessage名で処理を分岐する
scene.message.add(ev => {
  if (ev.data.message === "EntryClosed") {
    // 募集締め切り
    gameStatus = "playStarting";

    // 参加者が参加ボタンを押さなかった場合参加ボタンが残っているので消しとく
    scene.remove(entryButton);
  }

  if (ev.data.message === "Entry") {
    const playerId = ev.player.id;
    if (players.indexOf(playerId) < 0) {
      players.push(playerId);
    }

    // エントリーしたのが自分だった時。これはローカル処理
    if (playerId === g.game.selfId) {
      infoLabel.text = "あなたは参加しました。\n放送者の受付終了を待っています";
      infoLabel.invalidate();
    }
  }
});

raiseEvent によって 全員にイベントが送信される ため、この処理は全員の PC で実行されます。なので募集締め切りのメッセージを受けた時に gameStatus を playStarting に進めます。 操作を共有し状態を変更するのは常に自分 というのが Akashic の原則です。 全体で共有する情報を変更する際には 「こういう操作を行ったのでみなさんあとはわかっていますね?」 というメッセージだけを送り、受信した全員が自分で状態を変更するのがマルチプレイ作成時のルールです。全体で共有する情報、つまりグローバルを書き換える時は必ずこの段取りを踏みます。

さて、メッセージの受信処理の中で Entry という処理があることからもわかるように、視聴者側に表示される参加ボタンも同じように raiseEvent しています。その中で参加者テーブルへ書き込みを行います。 これを全員が自力でやるので、全員の PC 上の参加者テーブルが一致するのです。

RaiseEvent によるメッセージの送信はマルチプレイゲームを作る上でのもっとも重要な概念になります。 送るべきでない情報があったり、送るべきでないタイミングがあったり、ちょっと難しい部分もあります。今後解説するタイミングがあるかもしれません。

ゲーム開始

ゲームマスターが募集を締め切ったらゲーム開始です。ゲーム画面では参加者一覧が表示されます。それだけです。 実際のゲームの中身はありません。 ありませんが、基本的な流れはこれで全て整いました。ここから実際に動くゲームを考えていけばいいだけです。

しかしかなり長くなってきてしまったため、今回はこのぐらいにしておきましょう。次回以降、少しずつゲームを作っていければと思います。

次は

Join と Leave の話に絡めて、ニコ生上でゲームを作る際のサンプルについて解説しました。 次回は、少しずつゲームの肉付けをしていきながら、気になった点を解説していければと思います。