マルチプレイゲームを作ろう3 JoinとLeaveとニコ生の話 - akashic-contents/shin-ichiba-doc GitHub Wiki

このページの情報は古くなっています。最新の情報は https://akashic-games.github.io/shin-ichiba/ を参照してください。

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公式のガイドにもありますが、ニコ生においてはJoinが発生するのは一度だけ。生主のみがゲームにJoinしLeaveすることはありません。 (厳密に言えばゲームを起動した人、なのですが詳細は割愛します)

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

絵

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

生主の区別と役割

生主の役割

ニコ生 x ニコニコ新市場、という観点から考えると 生主には以下の特権が与えられています。

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

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

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

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

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

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

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

生主を区別する

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

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

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

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します。つまり今回のサンプルにおいて放送者と扱われます。

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

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

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

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

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


// -- 略 --

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

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を追加する

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

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

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

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

さて、raiseEventについては公式ドキュメントにも説明がありますが少しみてみましょう。 ローカル処理のなかで全体へ影響を及ぼしたくなった場合は、raiseEventで全員にイベントを送信するのがAkashicの流儀です。以下に受信側のコードを貼っていきます。

// 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の話に絡めて、ニコ生上でゲームを作る際のサンプルについて解説しました。第三回はこれでおしまいになります。 第四回以降は、少しずつゲームの肉付けをしていきながら、気になった点を解説していければと思います。

これからも新市場投稿者向けの情報や新しいコンテンツ情報などを配信していくので、引き続きよろしくお願いいたします。