リアルタイムマルチプレイヤーバーチャルリアリティゲームを構築する方法(パート2)

公開: 2022-03-10
簡単なまとめ↬このチュートリアルでは、仮想現実ゲームのゲームメカニズムを記述します。これは、ゲームのリアルタイムマルチプレーヤー要素と密接に関連しています。

このチュートリアルシリーズでは、プレイヤーがパズルを解くために協力する必要があるWebベースのマルチプレイヤーバーチャルリアリティゲームを構築します。 このシリーズの最初のパートでは、ゲームに登場するオーブをデザインしました。 シリーズのこのパートでは、ゲームの仕組みを追加し、プレーヤーのペア間の通信プロトコルを設定します。

ここでのゲームの説明は、シリーズの最初の部分から抜粋したものです。プレーヤーの各ペアには、オーブのリングが与えられます。 目標は、すべてのオーブを「オン」にすることです。オーブが高くて明るい場合は、オーブが「オン」になります。 オーブが低くて薄暗い場合、オーブは「オフ」になっています。 ただし、特定の「優勢な」オーブは隣接するオーブに影響を与えます。状態を切り替えると、隣接するオーブも状態を切り替えます。 プレーヤー2は偶数のオーブを制御でき、プレーヤー1は奇数のオーブを制御できます。 これにより、両方のプレイヤーが協力してパズルを解く必要があります。

このチュートリアルの8つのステップは、次の3つのセクションにグループ化されています。

  1. ユーザーインターフェイスへの入力(ステップ1および2)
  2. ゲームの仕組みを追加する(ステップ3から5)
  3. 通信の設定(ステップ6から8)

このパートは、誰でもプレイできるように、オンラインで完全に機能するデモで締めくくります。 A-FrameVRといくつかのA-Frame拡張機能を使用します。

完成したソースコードはここにあります。

複数のクライアント間で同期された完成したマルチプレイヤーゲーム
複数のクライアント間で同期された、完成したマルチプレイヤーゲーム。 (大プレビュー)

1.ビジュアルインジケーターを追加します

まず、オーブのIDの視覚的なインジケーターを追加します。 L36に#container-orb0の最初の子として新しいa-text -textVR要素を挿入します。

 <a-entity ...> <a-text class="orb-id" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="3 3 3" position="0 -2 -0.25" material="side:double"></a-text> ... <a-entity position...> ... </a-entity> </a-entity>

オーブの「依存関係」は、トグルされたときにトグルするオーブです。たとえば、オーブ1が依存関係としてオーブ2と3を持っているとします。これは、オーブ1がトグルされると、オーブ2と3もトグルされることを意味します。 .animation-position直後に、次のように依存関係の視覚的なインジケーターを追加します。

 <a-animation class="animation-position" ... /> <a-text class="dep-right" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="10 10 10" position="0 0 1" material="side:double" ></a-text> <a-text class="dep-left" opacity="0.25"rotation="0 -90 0" value="1" color="#FFF" scale="10 10 10" position="0 0 -3" material="side:double" ></a-text>

コードがステップ1のソースコードと一致することを確認します。これで、オーブが次のコードと一致するはずです。

オーブのIDとトリガーするオーブのIDの視覚的インジケーターを備えたオーブ
オーブのIDとトリガーするオーブのIDの視覚的インジケーターを備えたオーブ(大プレビュー)

これで、必要な追加の視覚的インジケーターは終わりです。 次に、このテンプレートオーブを使用して、VRシーンにオーブを動的に追加します。

2.オーブを動的に追加する

このステップでは、レベルのJSON風の仕様に従ってオーブを追加します。 これにより、新しいレベルを簡単に指定して生成できます。 パート1の最後のステップのオーブをテンプレートとして使用します。

まず、jQueryをインポートします。これにより、DOMの変更、つまりVRシーンへの変更が簡単になります。 A-Frameインポートの直後に、L8に以下を追加します。

 <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>

配列を使用してレベルを指定します。 配列には、各オーブの「依存関係」をエンコードするオブジェクトリテラルが含まれます。 <head>タグ内に、次のレベル構成を追加します。

 <script> var orbs = [ {left: 1, right: 4}, {}, {on: true}, {}, {on: true} ]; </script>

今のところ、各オーブは、その「右」と「左」に1つの依存関係しか持つことができません。 上記のorbsを宣言した直後に、ページの読み込み時に実行されるハンドラーを追加します。 このハンドラーは、提供されたレベル構成を使用して、(1)テンプレートオーブを複製し、(2)テンプレートオーブを削除します。

 $(document).ready(function() { function populateTemplate(orb, template, i, total) {} function remove(selector) {} for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; var template = $('#template').clone(); template = populateTemplate(orb, template, i, orbs.length); $('#carousel').append(template); } remove('#template'); } function clickOrb(i) {}

次に、セレクターを指定して、VRシーンからアイテムを削除するだけのremove機能を設定します。 幸い、A-FrameはDOMの変更を監視するため、アイテムをDOMから削除するだけで、VRシーンからアイテムを削除できます。 次のようにremove関数を設定します。

 function remove(selector) { var el = document.querySelector(selector); el.parentNode.removeChild(el); }

クリックclickOrb関数を設定します。これは、オーブのクリックアクションをトリガーするだけです。

 function clickOrb(i) { document.querySelector("#container-orb" + i).click(); }

次に、 populateTemplate関数の記述を開始します。 この関数では、 .containerを取得することから始めます。 このオーブのコンテナには、前の手順で追加した視覚的なインジケーターが追加で含まれています。 さらに、依存関係に基づいて、オーブのonclick動作を変更する必要があります。 左依存関係が存在する場合は、それを反映するように視覚的インジケーターとonclick時の動作の両方を変更します。 同じことが権利依存にも当てはまります。

 function populateTemplate(orb, template, i, total) { var container = template.find('.container'); var onclick = 'document.querySelector("#light-orb' + i + '").emit("switch");'; if (orb.left || orb.right) { if (orb.left) { onclick += 'clickOrb(' + orb.left + ');'; container.find('.dep-left').attr('value', orb.left); } if (orb.right) { onclick += 'clickOrb(' + orb.right + ');'; container.find('.dep-right').attr('value', orb.right); } } else { container.find('.dep-left').remove(); container.find('.dep-right').remove(); } }

引き続きpopulateTemplate関数で、すべてのオーブとそのコンテナの要素にオーブIDを正しく設定します。

 container.find('.orb-id').attr('value', i); container.attr('id', 'container-orb' + i); template.find('.orb').attr('id', 'orb' + i); template.find('.light-orb').attr('id', 'light-orb' + i); template.find('.clickable').attr('data-id', i);

引き続きpopulateTemplate関数で、 onclick動作を設定し、各オーブが視覚的に異なるようにランダムシードを設定し、最後にIDに基づいてオーブの回転位置を設定します。

 container.attr('onclick', onclick); container.find('lp-sphere').attr('seed', i); template.attr('rotation', '0 ' + (360 / total * i) + ' 0');

関数の最後に、上記のすべての構成を含むtemplateを返します。

 return template;

ドキュメントロードハンドラー内で、 remove('#template')を使用してテンプレートを削除した後、最初にオンになるように構成されたオーブをオンにします。

 $(document).ready(function() { ... setTimeout(function() { for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; if (orb.on) { document.querySelector("#container-orb" + i).click(); } } }, 1000); });

これでJavascriptの変更は終わりです。 次に、テンプレートのデフォルト設定を「オフ」オーブの設定に変更します。 #container-orb0の位置とスケールを次のように変更します。

 position="8 0.5 0" scale="0.5 0.5 0.5"

次に、 #light-orb0強度を0に変更します。

 intensity="0"

ソースコードがステップ2のソースコードと一致することを確認します。

これで、VRシーンに動的に配置された5つのオーブが表示されます。 オーブの1つには、以下のように、依存関係の視覚的なインジケーターがさらに必要です。

テンプレートorbを使用して、すべてのオーブが動的に入力されます
テンプレートオーブを使用して、すべてのオーブが動的に入力されます(大プレビュー)

これで、オーブを動的に追加する最初のセクションは終了です。 次のセクションでは、ゲームの仕組みを追加する3つのステップを使用します。 具体的には、プレイヤーはプレイヤーIDに応じて特定のオーブのみを切り替えることができます。

3.ターミナル状態を追加します

このステップでは、終了状態を追加します。 すべてのオーブが正常にオンになると、プレーヤーには「勝利」ページが表示されます。 これを行うには、すべてのオーブの状態を追跡する必要があります。 オーブのオンとオフを切り替えるたびに、内部状態を更新する必要があります。 ヘルパー関数toggleOrbが状態を更新するとします。 オーブが状態を変更するたびにtoggleOrb関数を呼び出します。(1)クリックリスナーをonloadハンドラーに追加し、(2) toggleOrb(i); clickOrbへの呼び出し。 最後に、(3)空のtoggleOrbを定義します。

 $(document).ready(function() { ... $('.orb').on('click', function() { var id = $(this).attr('data-id') toggleOrb(id); }); }); function toggleOrb(i) {} function clickOrb(i) { ... toggleOrb(i); }

簡単にするために、レベル構成を使用してゲームの状態を示します。 toggleOrbを使用して、i番目のオーブのon状態を切り替えます。 すべてのオーブがオンになっている場合、 toggleOrbはさらにターミナル状態をトリガーできます。

 function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }

コードがステップ3のソースコードと一致することを再確認してください。

これで、ゲームの「シングルプレイヤー」モードは終了です。 この時点で、完全に機能するバーチャルリアリティゲームができました。 ただし、マルチプレーヤーコンポーネントを作成し、ゲームの仕組みを介してコラボレーションを促進する必要があります。

4.プレーヤーオブジェクトを作成します

このステップでは、プレーヤーIDを持つプレーヤーの抽象化を作成します。 このプレーヤーIDは、後でサーバーによって割り当てられます。

今のところ、これは単にグローバル変数になります。 orbsを定義した直後に、プレーヤーIDを定義します。

 var orbs = ... var current_player_id = 1;

コードがステップ4のソースコードと一致することを再確認します。次のステップでは、このプレーヤーIDを使用して、プレーヤーが制御できるオーブを決定します。

5.条件付きでオーブを切り替えます

このステップでは、オーブの切り替え動作を変更します。 具体的には、プレーヤー1は奇数のオーブを制御でき、プレーヤー2は偶数のオーブを制御できます。 まず、オーブの状態が変化する両方の場所にこのロジックを実装します。

 $('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }

次に、 clickOrbの直後にallowedToToggle関数を定義します。 現在のプレーヤーがプレーヤー1の場合、奇数のIDはtrue-y値を返すため、プレーヤー1は奇数のオーブを制御できます。 プレーヤー2については、その逆が当てはまります。他のすべてのプレーヤーは、オーブを制御できません。

 function allowedToToggle(id) { if (current_player_id == 1) { return id % 2; } else if (current_player_id == 2) { return !(id % 2); } return false; }

コードがステップ5のソースコードと一致することを再確認します。デフォルトでは、プレーヤーはプレーヤー1です。これは、プレーヤー1として、プレビューで奇数のオーブのみを制御できることを意味します。 これでゲームの仕組みに関するセクションは終わりです。

次のセクションでは、サーバーを介した両方のプレーヤー間の通信を容易にします。

6.WebSocketを使用したサーバーのセットアップ

このステップでは、(1)プレーヤーIDを追跡し、(2)メッセージを中継するための単純なサーバーをセットアップします。 これらのメッセージにはゲームの状態が含まれるため、プレーヤーはお互いが見ているものを確実に見ることができます。

以前のindex.htmlをクライアント側のソースコードと呼びます。 このステップのコードをサーバー側のソースコードと呼びます。 glitch.comに移動し、右上の「新しいプロジェクト」をクリックし、ドロップダウンで「hello-express」をクリックします。

左側のパネルから「package.json」を選択し、 dependenciessocket-ioを追加します。 これで、 dependenciesディクショナリは次のように一致するはずです。

 "dependencies": { "express": "^4.16.4", "socketio": "^1.0.0" },

左側のパネルから「index.js」を選択し、そのファイルの内容を次の最小限のsocket.io HelloWorldに置き換えます。

 const express = require("express"); const app = express(); var http = require('http').Server(app); var io = require('socket.io')(http); /** * Run application on port 3000 */ var port = process.env.PORT || 3000; http.listen(port, function(){ console.log('listening on *:', port); });

上記は、基本的なExpressアプリケーション用にポート3000にsocket.ioを設定します。 次に、2つのグローバル変数を定義します。1つはアクティブなプレーヤーのリストを維持するためのもので、もう1つは割り当てられていない最小のプレーヤーIDを維持するためのものです。

 /** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;

次に、 getPlayerId関数を定義します。この関数は、新しいプレーヤーIDを生成し、新しいプレーヤーIDをplayerIds配列に追加することで「取得済み」としてマークします。 特に、この関数は単にsmallestPlayerIdをマークしてから、次に小さい非取得整数を検索することによってsmallestPlayerIdを更新します。

 function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }

removePlayer関数を定義します。この関数は、それに応じてsmallestPlayerIdを更新し、提供されたplayerIdを解放して、別のプレーヤーがそのIDを取得できるようにします。

 function removePlayer(playerId) { if (playerId < smallestPlayerId) { smallestPlayerId = playerId; } var index = playerIds.indexOf(playerId); playerIds.splice(index, 1); }

最後に、上記のメソッドのペアを使用して、新しいプレーヤーを登録し、切断されたプレーヤーの登録を解除するソケットイベントハンドラーのペアを定義します。

 /** * Handle socket interactions */ io.on('connection', function(socket) { socket.on('newPlayer', function() { socket.playerId = getPlayerId(); console.log("new player: ", socket.playerId); socket.emit('playerId', socket.playerId); }); socket.on('disconnect', function() { if (socket.playerId === undefined) return; console.log("disconnected player: ", socket.playerId); removePlayer(socket.playerId); }); });

コードがステップ6のソースコードと一致することを再確認します。これで、基本的なプレーヤーの登録と登録解除が完了します。 これで、各クライアントはサーバーで生成されたプレーヤーIDを使用できます。

次のステップでは、サーバーから送信されたプレーヤーIDを受信して​​使用するようにクライアントを変更します。

7.プレーヤーIDを適用します

これらの次の2つのステップでは、マルチプレイヤーエクスペリエンスの初歩的なバージョンを完成させます。 まず、クライアント側でプレーヤーIDの割り当てを統合します。 特に、各クライアントはサーバーにプレーヤーIDを要求します。 手順4以前で作業していたクライアント側のindex.htmlに戻ります。

L7のheadsocket.ioをインポートします。

 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>

ドキュメントロードハンドラーの後で、ソケットをインスタンス化し、 newPlayerイベントを発行します。 それに応じて、サーバー側はplayerIdイベントを使用して新しいプレーヤーIDを生成します。 以下では、 lightful.glitch.meの代わりにGlitchプロジェクトプレビューのURLを使用します。 以下のデモURLを使用できますが、コードの変更はもちろん反映されません。

 $(document).ready(function() { ... }); socket = io("https://lightful.glitch.me"); socket.emit('newPlayer'); socket.on('playerId', function(player_id) { current_player_id = player_id; console.log(" * You are now player", current_player_id); });

コードがステップ7のソースコードと一致することを確認します。これで、ゲームを2つの異なるブラウザまたはタブにロードして、マルチプレーヤーゲームの両面をプレイできます。 プレーヤー1は奇数のオーブを制御でき、プレーヤー2は偶数のオーブを制御できます。

ただし、プレーヤー1のオーブを切り替えても、プレーヤー2のオーブの状態には影響しないことに注意してください。次に、ゲームの状態を同期する必要があります。

8.ゲームの状態を同期します

このステップでは、ゲームの状態を同期して、プレーヤー1と2が同じオーブの状態を確認できるようにします。 オーブ1がプレーヤー1でオンになっている場合は、プレーヤー2でもオンになっている必要があります。 クライアント側では、オーブの切り替えをアナウンスしてリッスンします。 アナウンスするには、切り替えられたオーブのIDを渡すだけです。

両方のtoggleOrb呼び出しの前に、次のsocket.emit呼び出しを追加します。

 $(document).ready(function() { ... $('.orb').on('click', function() { ... socket.emit('toggleOrb', id); toggleOrb(id); }); }); ... function clickOrb(i) { ... socket.emit('toggleOrb', i); toggleOrb(i); }

次に、オーブの切り替えをリッスンし、対応するオーブを切り替えます。 playerIdソケットイベントリスナーのすぐ下に、 toggleOrbイベント用の別のリスナーを追加します。

 socket.on('toggleOrb', function(i) { document.querySelector("#container-orb" + i).click(); toggleOrb(i); });

これで、クライアント側のコードの変更は完了です。 コードがステップ8のソースコードと一致することを再確認してください。

サーバー側は、切り替えられたオーブIDを受信して​​ブロードキャストする必要があります。 サーバー側のindex.jsに、次のリスナーを追加します。 このリスナーは、ソケットdisconnectリスナーの真下に配置する必要があります。

 socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });

コードがステップ8のソースコードと一致することを再確認します。これで、1つのウィンドウにロードされたプレーヤー1と2番目のウィンドウにロードされたプレーヤー2の両方に同じゲーム状態が表示されます。 これで、マルチプレイヤーバーチャルリアリティゲームが完成しました。 さらに、2人のプレーヤーは、目的を達成するために協力する必要があります。 最終製品は以下に一致します。

複数のクライアント間で同期された完成したマルチプレイヤーゲーム
複数のクライアント間で同期された、完成したマルチプレイヤーゲーム。 (大プレビュー)

結論

これで、マルチプレイヤーバーチャルリアリティゲームの作成に関するチュートリアルは終了です。 その過程で、A-Frame VRでの3Dモデリングや、WebSocketを使用したリアルタイムのマルチプレイヤー体験など、多くのトピックに触れました。

私たちが触れた概念に基づいて、2人のプレーヤーがよりスムーズに体験できるようにするにはどうすればよいでしょうか。 これには、ゲームの状態が同期されていることを確認し、そうでない場合はユーザーに警告することが含まれます。 また、端末の状態とプレーヤーの接続状態を視覚的に示す簡単なインジケーターを作成することもできます。

私たちが確立したフレームワークと導入した概念を考えると、これらの質問に答え、さらに多くを構築するためのツールが手に入ります。

完成したソースコードはここにあります。

ジャンプした後もっと! 以下を読み続けてください↓