🥳

Vue 3 でマルチプレイブラウザゲームを作る (6) プレイ開始編

2025/01/14に公開

つづき

ラウンジ(ゲスト)編では、ホストとゲストのラウンジ画面共通部分を整理し、共通部分から継承してゲストのラウンジ画面を作りました。

今回は、参加者が集まった後でプレイ画面に切り替える処理を作っていきます。

プレイ開始イメージ

ホストは、参加者が集まったらプレイ開始ボタンを押します。

ホストのラウンジ画面がプレイ画面に切り替わります。

同時に、ゲストのラウンジ画面もプレイ画面に切り替わります。

プレイ画面では、ホストとゲストに機能的な違いはありません。タイトルの色分けがされているだけです。

プレイ開始処理

ホストからサーバーへプレイ開始

Vue で開発しているホストのラウンジ画面において、プレイ開始ボタンを押したときの処理は以下のようになっています(Web じゃんけんのソースコードは GitHub に上げてあります)。

lounge_host.vue(抜粋)
<template>
    <div>
        <p id="loungeTitle">ラウンジ(ホスト)</p>
        <p>グループを作ります。一緒にプレイしたい人に、以下の URL にアクセスしてもらってください。</p>
        <div id="showQr"></div>
        <p>{{ invitationUrl }} <button @click="onCopyClicked">コピー</button></p>
        <button @click="onPlayClicked" :disabled="isPlayButtonDisabled">{{ numParticipants }} 人でプレイ開始</button>
        <p>{{ errorMessage }}</p>
    </div>
</template>

<script>
export default {
    methods: {
        onPlayClicked() {
            this.socket.emit(csConstants.socketEvents.startPlay);
        }
    },
}
</script>

onPlayClicked() において、ソケットでサーバーに startPlay イベントを送信しています。

サーバーから全員へプレイ開始

サーバー側では、startPlay イベントを受信した時の処理は以下のようになっています。繰り返しになりますが、サーバー側では Vue は使いません。

socket.js(抜粋)
// プレイ開始イベント
socket.on(csc.socketEvents.startPlay, async () => {
    try {
        await onStartPlayRequestedAsync(io, socket);
    } catch (e) {
        notifyException(socket, e);
    }
});

// プレイ開始イベント(ホストから受領し、ホストを含む全員にプレイ開始イベントを送信)
async function onStartPlayRequestedAsync(io, socket) {
    const db = new sqlite3.Database(dbc.path);

    // グループ検索
    const groupRecord = await selectGroupBySocketIdAsync(db, socket.id);

    // グループステータス更新
    if (groupRecord[dbc.group.cStatus] !== dbc.group.status.hiring) {
        // 多重にプレイ開始を受領
        throw new Error("既にプレイ開始されています。");
    }
    groupRecord[dbc.group.cStatus] = dbc.group.status.playing;
    await updateGroupAsync(db, groupRecord);

    // プレイ開始を全員に通知
    io.to(groupRecord[dbc.group.cUuid]).emit(csc.socketEvents.startPlay);
}

selectGroupBySocketIdAsync() でどのグループのホストがプレイ開始したのかを特定し、データベースのそのグループのステータスを playing に更新します。

グループテーブルの構造を再掲しておきます。

続いて、io.to().emit() で startPlay イベントを全員に送信しています。ラウンジ(ゲスト)編同様、一度の送信でホストを含む全員に送信できます。

ホストとゲストの画面切替

ホストおよびゲストのラウンジ画面において、サーバーから startPlay イベントを受信した時にプレイ画面に切り替えますが、その処理はホストゲスト共通部分の lounge_base.vue で定義しています。

lounge_base.vue(抜粋)
<script>
export default {
    methods: {
        // ソケットイベントハンドラー(ホストとゲストの共通部分)を設定
        setSocketOn() {
            // プレイ開始通知が来た
            this.socket.on(csConstants.socketEvents.startPlay, () => {
                let vueApp = this.vueApp;
                vueApp.unmount();

                // 新しいコンポーネント
                const options = loadModuleOptions();
                const { loadModule } = window["vue3-sfc-loader"];
                let props = {};
                props["socket"] = this.socket;
                vueApp = Vue.createApp(Vue.defineAsyncComponent(() => loadModule("./play.vue", options)), props);
                props["vueApp"] = vueApp;
                vueApp.mount(document.body);
            });
        },
    },
}
</script>

まず、unmount() で HTML <body> 配下に挿入したラウンジ画面を取り除いています。

その後、mount() で play.vue コンポーネント(プレイ画面)を新たに <body> 配下に挿入しています(プレイ画面はホストもゲストも同じです)。

現状問題なく動作していますが、個人的な心情としては、app.mount() ですべてをマウントし直すのは大丈夫なのだろうかという気がかりがあります。その辺りをご存じの方はご教示いただけると幸いです。

プレイ画面の初期設定

クライアント側(ホストおよびゲスト)は play.vue コンポーネントに切替が終わった後、beforeMount() で初期設定を行っています。

play.vue(抜粋)
// socket.on イベントハンドラー設定完了後
this.socket.emit(csConstants.socketEvents.playReady);

プレイ画面の初期状態では以下の情報が分かっていません。

  • サーバー側で参加者の名前を勝手に付けているが、自分の名前は何か?
  • 他の参加者の情報は?

これらの情報を教えて欲しい、という意味を込めてサーバーに playReady イベントを送信します。

サーバー側は playReady イベントを受信すると以下の処理を行います。

socket.js(抜粋)
// プレイ開始準備完了イベント
socket.on(csc.socketEvents.playReady, async () => {
    try {
        await onPlayReadyRequestedAsync(socket);
    } catch (e) {
        notifyException(socket, e);
    }
});

// プレイ開始準備完了イベント
async function onPlayReadyRequestedAsync(socket) {
    const db = new sqlite3.Database(dbc.path);

    // グループ検索
    const groupRecord = await selectGroupBySocketIdAsync(db, socket.id);

    // 参加者名を通知
    await notifyPlayerNameAsync(socket, db);

    // 参加者情報群を通知
    await notifyParticipantInfosToOneAsync(socket, db, groupRecord);
}

参加者の名前と、全員の情報(名前、勝利点)を送信しています。

playReady イベントは参加者一人一人が個別に送信してくるので、返事も個別(一人宛)に返信しています。

流れ整理

プレイ開始時のサーバーとクライアントのやり取りをまとめると以下のようになります。

サーバー側は、プレイ開始通知直後に立て続けに参加者名や参加者情報群を通知すれば良さそうに見えますが、そうすると、クライアント側がまだ画面切替が終わっておらず、参加者名イベントなどを受け取れないという事態が発生することがあります。

このため、クライアント側からの playReady を契機として送信するようにしています。

プレイ開始編まとめ

今回は、プレイ開始時の画面切替において、サーバーとクライアントそれぞれの処理を作りました。

次回

主な改訂履歴

  • 2025/01/14 初版。
  • 2025/01/17 次回について記載。

Discussion