リアルタイムマルチプレイヤーバーチャルリアリティゲームを構築する方法(パート2)
公開: 2022-03-10このチュートリアルシリーズでは、プレイヤーがパズルを解くために協力する必要があるWebベースのマルチプレイヤーバーチャルリアリティゲームを構築します。 このシリーズの最初のパートでは、ゲームに登場するオーブをデザインしました。 シリーズのこのパートでは、ゲームの仕組みを追加し、プレーヤーのペア間の通信プロトコルを設定します。
ここでのゲームの説明は、シリーズの最初の部分から抜粋したものです。プレーヤーの各ペアには、オーブのリングが与えられます。 目標は、すべてのオーブを「オン」にすることです。オーブが高くて明るい場合は、オーブが「オン」になります。 オーブが低くて薄暗い場合、オーブは「オフ」になっています。 ただし、特定の「優勢な」オーブは隣接するオーブに影響を与えます。状態を切り替えると、隣接するオーブも状態を切り替えます。 プレーヤー2は偶数のオーブを制御でき、プレーヤー1は奇数のオーブを制御できます。 これにより、両方のプレイヤーが協力してパズルを解く必要があります。
このチュートリアルの8つのステップは、次の3つのセクションにグループ化されています。
- ユーザーインターフェイスへの入力(ステップ1および2)
- ゲームの仕組みを追加する(ステップ3から5)
- 通信の設定(ステップ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のソースコードと一致することを確認します。これで、オーブが次のコードと一致するはずです。
これで、必要な追加の視覚的インジケーターは終わりです。 次に、このテンプレートオーブを使用して、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つには、以下のように、依存関係の視覚的なインジケーターがさらに必要です。
これで、オーブを動的に追加する最初のセクションは終了です。 次のセクションでは、ゲームの仕組みを追加する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」を選択し、 dependencies
にsocket-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のhead
にsocket.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人のプレーヤーがよりスムーズに体験できるようにするにはどうすればよいでしょうか。 これには、ゲームの状態が同期されていることを確認し、そうでない場合はユーザーに警告することが含まれます。 また、端末の状態とプレーヤーの接続状態を視覚的に示す簡単なインジケーターを作成することもできます。
私たちが確立したフレームワークと導入した概念を考えると、これらの質問に答え、さらに多くを構築するためのツールが手に入ります。
完成したソースコードはここにあります。