バーチャルリアリティでエンドレスランナーゲームを構築する方法(パート3)

公開: 2022-03-10
簡単なまとめ↬パート1で、Alvinは仮想現実モデルを設計する方法の基本を説明しました。 パート2では、ゲームのコアロジックを実装する方法を示しました。 彼のチュートリアルのこの最後の部分では、「スタート」と「ゲームオーバー」メニュー、およびモバイルクライアントとデスクトップクライアント間のゲーム状態の同期などの最後の仕上げが追加されます。 これにより、マルチプレイヤーゲームを構築するための概念への道が開かれます。

そして、私たちの旅は続きます。 エンドレスランナーVRゲームの作成方法に関するシリーズの最後のパートでは、2つのデバイス間でゲームの状態を同期して、マルチプレイヤーゲームの作成に一歩近づける方法を紹介します。 クライアント間通信で仲介サーバーを処理するMirrorVRを具体的に紹介します。

このゲームは、VRヘッドセットの有無にかかわらずプレイできます。 最終製品のデモはergo-3.glitch.meで見ることができます。

開始するには、次のものが必要です。

  • インターネットアクセス(特にglitch.comへ)。
  • このチュートリアルのパート2から完了したGlitchプロジェクト。 https://glitch.com/edit/#!/ergo-2に移動し、[リミックスして編集]をクリックすると、パート2の完成品から始めることができます。
  • バーチャルリアリティヘッドセット(オプション、推奨)。 (私は1枚15ドルで提供されているGoogle Cardboardを使用しています。)

ステップ1:スコアを表示する

ゲームは現状のままで機能し、プレーヤーには課題が与えられます。障害物を避けてください。 ただし、オブジェクトの衝突以外では、ゲームはゲームの進行状況に関するフィードバックをプレーヤーに提供しません。 これを修正するには、このステップでスコア表示を実装します。 スコアは、ユーザーの視野に接着されたインターフェイスではなく、仮想現実の世界に配置された大きなテキストオブジェクトになります。

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

バーチャルリアリティでは、一般的に、ユーザーインターフェイスは、ユーザーの頭にとらわれるのではなく、世界に最もよく統合されています。

スコア表示
スコア表示(大プレビュー)

オブジェクトをindex.htmlに追加することから始めます。 他のテキスト要素に再利用されるtextミックスインを追加します。

 <a-assets> ... <a-mixin text=" font:exo2bold; anchor:center; align:center;"></a-mixin> ... </a-assets>

次に、プレーヤーの直前のプラットフォームにtext要素を追加します。

 <!-- Score --> <a-text value="" mixin="text" height="40" width="40" position="0 1.2 -3" opacity="0.75"></a-text> <!-- Player --> ...

これにより、仮想現実シーンにテキストエンティティが追加されます。 値が空に設定されているため、テキストは現在表示されていません。 ただし、JavaScriptを使用して、テキストエンティティに動的にデータを入力します。 Assets /ergo.jsに移動します。 collisionsセクションの後に、 scoreセクションを追加し、いくつかのグローバル変数を定義します。

  • score :現在のゲームスコア。
  • countedTrees :スコアに含まれるすべてのツリーのID。 (これは、衝突テストが同じツリーに対して複数回トリガーされる可能性があるためです。)
  • scoreDisplay :仮想現実世界のテキストオブジェクトに対応するDOMオブジェクトへの参照。
 /********* * SCORE * *********/ var score; var countedTrees; var scoreDisplay;

次に、グローバル変数を初期化するためのセットアップ関数を定義します。 同じように、 teardown関数を定義します。

 ... var scoreDisplay; function setupScore() { score = 0; countedTrees = new Set(); scoreDisplay = document.getElementById('score'); } function teardownScore() { scoreDisplay.setAttribute('value', ''); }

[ Game ]セクションで、 gameOverstartGamewindow.onloadを更新して、スコアの設定と分解を含めます。

 /******** * GAME * ********/ function gameOver() { ... teardownScore(); } function startGame() { ... setupScore(); addTreesRandomlyLoop(); } window.onload = function() { setupScore(); ... }

特定のツリーのスコアをインクリメントする関数を定義します。 この関数は、 countedTreesをチェックして、ツリーが二重にカウントされていないことを確認します。

 function addScoreForTree(tree_id) { if (countedTrees.has(tree_id)) return; score += 1; countedTrees.add(tree_id); }

さらに、グローバル変数を使用してスコア表示を更新するユーティリティを追加します。

 function updateScoreDisplay() { scoreDisplay.setAttribute('value', score); }

障害物がプレーヤーを通過するたびにこのスコア増分関数を呼び出すために、それに応じて衝突テストを更新します。 まだassets/ergo.jsで、 collisionsセクションに移動します。 次のチェックを追加して更新します。

 AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_LINE_END) { addScoreForTree(tree_id); updateScoreDisplay(); } }) } })

最後に、ゲームが開始したらすぐにスコア表示を更新します。 [ Game ]セクションに移動し、 updateScoreDisplay();を追加します。 startGameへ:

 function startGame() { ... setupScore(); updateScoreDisplay(); ... }

Assets /ergo.jsindex.htmlが対応するソースコードファイルと一致していることを確認してください。 次に、プレビューに移動します。 次のように表示されます。

スコア表示
スコア表示(大プレビュー)

これでスコア表示は終了です。 次に、適切なスタートメニューとゲームオーバーメニューを追加して、プレーヤーが必要に応じてゲームをリプレイできるようにします。

ステップ2:スタートメニューを追加する

ユーザーが進行状況を追跡できるようになったので、最後の仕上げを追加してゲーム体験を完成させます。 このステップでは、スタートメニューとゲームオーバーメニューを追加して、ユーザーがゲームを開始および再開できるようにします。

プレイヤーが「スタート」ボタンをクリックしてゲームを開始する「スタート」メニューから始めましょう。 このステップの後半では、「再起動」ボタンを備えたゲームオーバーメニューを追加します。

メニューの開始とゲームオーバー
スタートとゲームオーバーメニュー(大きなプレビュー)

エディターでindex.htmlに移動します。 次に、 Mixinsセクションを見つけます。 ここに、特に大きなテキストのスタイルを定義するtitleミックスインを追加します。 以前と同じフォントを使用し、テキストを中央に揃え、テキストの種類に適したサイズを定義します。 (以下のanchorは、テキストオブジェクトがその位置にアンカーされる場所であることに注意してください。)

 <a-assets> ... <a-mixin text=" font:exo2bold; height:40; width:40; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>

次に、2番目の見出しに2番目のミックスインを追加します。 このテキストは少し小さいですが、それ以外はタイトルと同じです。

 <a-assets> ... <a-mixin text=" font:exo2bold; height:10; width:10; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>

3番目の最後のミックスインでは、説明テキストのプロパティを定義します。これは、2番目の見出しよりもさらに小さくなります。

 <a-assets> ... <a-mixin text=" font:exo2bold; height:5; width:5; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>

すべてのテキストスタイルを定義したら、インワールドテキストオブジェクトを定義します。 スタートメニュー用の空のコンテナを使用して、 Scoreセクションの下に新しいMenusセクションを追加します。

 <!-- Score --> ... <!-- Menus --> <a-entity> <a-entity position="0 1.1 -3"> </a-entity> </a-entity>

スタートメニューコンテナ内で、タイトルとすべての非タイトルテキストのコンテナを定義します。

 ... <a-entity ...> <a-entity position="0 1 0"> </a-entity> <a-text value="ERGO" mixin="title"></a-text> </a-entity> </a-entity>

タイトル以外のテキストのコンテナ内に、ゲームをプレイするための手順を追加します。

 <a-entity...> <a-text value="Turn left and right to move your player, and avoid the trees!" mixin="copy"></a-text> </a-entity>

[スタート]メニューを完了するには、[スタート]というボタンを追加します。

 <a-entity...> ... <a-text value="Start" position="0 0.75 0" mixin="heading"></a-text> <a-box position="0 0.65 -0.05" width="1.5" height="0.6" depth="0.1"></a-box> </a-entity>

スタートメニューのHTMLコードが次のコードと一致することを再確認してください。

 <!-- Menus --> <a-entity> <a-entity position="0 1.1 -3"> <a-entity position="0 1 0"> <a-text value="Turn left and right to move your player, and avoid the trees!" mixin="copy"></a-text> <a-text value="Start" position="0 0.75 0" mixin="heading"></a-text> <a-box position="0 0.65 -0.05" width="1.5" height="0.6" depth="0.1"></a-box> </a-entity> <a-text value="ERGO" mixin="title"></a-text> </a-entity> </a-entity>

プレビューに移動すると、次のスタートメニューが表示されます。

スタートメニューの画像
スタートメニュー(大プレビュー)

引き続き[ Menus ]セクション( startメニューのすぐ下)で、同じミックスインを使用してgame-overメニューを追加します。

 <!-- Menus --> <a-entity> ... <a-entity position="0 1.1 -3"> <a-text value="?" mixin="heading" position="0 1.7 0"></a-text> <a-text value="Score" mixin="copy" position="0 1.2 0"></a-text> <a-entity> <a-text value="Restart" mixin="heading" position="0 0.7 0"></a-text> <a-box position="0 0.6 -0.05" width="2" height="0.6" depth="0.1"></a-box> </a-entity> <a-text value="Game Over" mixin="title"></a-text> </a-entity> </a-entity>

JavaScriptファイルassets / ergo.jsに移動します。 Gameセクションの前に新しいMenusセクションを作成します。 さらに、 setupAllMenushideAllMenus 、およびshowGameOverMenuの3つの空の関数を定義します。

 /******** * MENU * ********/ function setupAllMenus() { } function hideAllMenus() { } function showGameOverMenu() { } /******** * GAME * ********/

次に、 Gameセクションを3か所で更新します。 gameOverで、 GameOverメニューを表示します。

 function gameOver() { ... showGameOverMenu(); } ``` In `startGame`, hide all menus: ``` function startGame() { ... hideAllMenus(); }

次に、 window.onloadで、startGameへの直接呼び出しを削除し、代わりにstartGameを呼び出しsetupAllMenus 。 以下に一致するようにリスナーを更新します。

 window.onload = function() { setupAllMenus(); setupScore(); setupTrees(); }

Menuセクションに戻ります。 さまざまなDOMオブジェクトへの参照を保存します。

 /******** * MENU * ********/ var menuStart; var menuGameOver; var menuContainer; var isGameRunning = false; var startButton; var restartButton; function setupAllMenus() { menuStart = document.getElementById('start-menu'); menuGameOver = document.getElementById('game-over'); menuContainer = document.getElementById('menu-container'); startButton = document.getElementById('start-button'); restartButton = document.getElementById('restart-button'); }

次に、「開始」ボタンと「再起動」ボタンの両方をstartGameにバインドします。

 function setupAllMenus() { ... startButton.addEventListener('click', startGame); restartButton.addEventListener('click', startGame); }

showStartMenuを定義し、 showStartMenuから呼び出しsetupAllMenus

 function setupAllMenus() { ... showStartMenu(); } function hideAllMenus() { } function showGameOverMenu() { } function showStartMenu() { }

3つの空の関数を設定するには、いくつかのヘルパー関数が必要です。 次の2つの関数を定義します。これらの関数は、AフレームVRエンティティを表すDOM要素を受け入れ、それを表示または非表示にします。 showAllMenusの上に両方の関数を定義します。

 ... var restartButton; function hideEntity(el) { el.setAttribute('visible', false); } function showEntity(el) { el.setAttribute('visible', true); } function showAllMenus() { ...

最初にhideAllMenusにデータを入力します。 オブジェクトを見えなくしてから、両方のメニューのクリックリスナーを削除します。

 function hideAllMenus() { hideEntity(menuContainer); startButton.classList.remove('clickable'); restartButton.classList.remove('clickable'); }

次に、 showGameOverMenuにデータを入力します。 ここで、両方のメニュー、およびゲームオーバーメニューと[再起動]ボタンのクリックリスナーのコンテナを復元します。 ただし、[スタート]ボタンのクリックリスナーを削除し、[スタート]メニューを非表示にします。

 function showGameOverMenu() { showEntity(menuContainer); hideEntity(menuStart); showEntity(menuGameOver); startButton.classList.remove('clickable'); restartButton.classList.add('clickable'); }

3番目に、 showStartMenuにデータを入力します。 ここで、 showGameOverMenuが行ったすべての変更を元に戻します。

 function showStartMenu() { showEntity(menuContainer); hideEntity(menuGameOver); showEntity(menuStart); startButton.classList.add('clickable'); restartButton.classList.remove('clickable'); }

コードが対応するソースファイルと一致することを再確認してください。 次に、プレビューに移動すると、次の動作が観察されます。

メニューの開始とゲームオーバー
スタートメニューとゲームオーバーメニュー(大きなプレビュー)

これで、 [スタート]メニューと[ゲームオーバー]メニューは終了です。

おめでとう! これで、適切な開始と終了を備えた完全に機能するゲームができました。 ただし、このチュートリアルにはもう1つのステップがあります。異なるプレーヤーデバイス間でゲームの状態を同期する必要があります。 これにより、マルチプレイヤーゲームに一歩近づくことができます。

ステップ3:ゲームの状態をMirrorVRと同期する

前のチュートリアルでは、サーバーとクライアント間の一方向の通信を容易にするために、ソケット間でリアルタイム情報を送信する方法を学びました。 このステップでは、そのチュートリアルの本格的な製品であるMirrorVRの上に構築します。これは、クライアント間通信で仲介サーバーを処理します。

MirrorVRについて詳しくは、こちらをご覧ください。

index.htmlに移動します。 ここでは、MirrorVRをロードしてカメラにコンポーネントを追加し、該当する場合はモバイルデバイスのビューをミラーリングする必要があることを示します。 socket.io依存関係とMirrorVR0.2.3をインポートします。

 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script> <script src="https://cdn.jsdelivr.net/gh/alvinwan/[email protected]/dist/mirrorvr.min.js"></script>

次に、コンポーネント、 camera-listenerをカメラに追加します。

 <a-camera camera-listener ...>

Assets /ergo.jsに移動します。 このステップでは、モバイルデバイスはコマンドを送信し、デスクトップデバイスはモバイルデバイスのみをミラーリングします。

これを容易にするには、デスクトップデバイスとモバイルデバイスを区別するためのユーティリティが必要です。 ファイルの最後で、 shuffle後にmobileCheck関数を追加します。

 /** * Checks for mobile and tablet platforms. */ function mobileCheck() { var check = false; (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[aw])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); return check; };

まず、ゲームの開始を同期します。 startGameの[ゲーム]セクションで、最後にmirrorVR通知を追加します。

 function startGame() { ... if (mobileCheck()) { mirrorVR.notify('startGame', {}) } }

モバイルクライアントは、ゲームの開始に関する通知を送信するようになりました。 ここで、デスクトップの応答を実装します。

ウィンドウロードリスナーで、 setupMirrorVR関数を呼び出します。

 window.onload = function() { ... setupMirrorVR(); }

MirrorVRセットアップのGameセクションの上に新しいセクションを定義します。

 /************ * MirrorVR * ************/ function setupMirrorVR() { mirrorVR.init(); }

次に、mirrorVRの初期化関数にキーワード引数を追加します。 具体的には、ゲーム開始通知のハンドラーを定義します。 さらに部屋IDを指定します。 これにより、アプリケーションをロードするすべての人がすぐに同期されます。

 function setupMirrorVR() { mirrorVR.init({ roomId: 'ergo', state: { startGame: { onNotify: function(data) { hideAllMenus(); setupScore(); updateScoreDisplay(); } }, } }); }

ゲームオーバーに対して同じ同期プロセスを繰り返します。 GameセクションのgameOverで、モバイルデバイスのチェックを追加し、それに応じて通知を送信します。

 function gameOver() { ... if (mobileCheck()) { mirrorVR.notify('gameOver', {}); } }

MirrorVRセクションに移動し、 gameOverリスナーでキーワード引数を更新します。

 function setupMirrorVR() { mirrorVR.init({ state: { startGame: {... }, gameOver: { onNotify: function(data) { gameOver(); } }, } }) }

次に、ツリーを追加するために同じ同期プロセスを繰り返します。 [ Trees ]セクションでaddTreesRandomlyに移動します。 どのレーンが新しい木を受け取るかを追跡します。 次に、 returnディレクティブの直前で、それに応じて通知を送信します。

 function addTreesRandomly(...) { ... var numberOfTreesAdded ... var position_indices = []; trees.forEach(function (tree) { if (...) { ... position_indices.push(tree.position_index); } }); if (mobileCheck()) { mirrorVR.notify('addTrees', position_indices); } return ... }

MirrorVRセクションに移動し、ツリーの新しいリスナーを使用して、 mirrorVR.initのキーワード引数を更新します。

 function setupMirrorVR() { mirrorVR.init({ state: { ... gameOver: {... }, addTrees: { onNotify: function(position_indices) { position_indices.forEach(addTreeTo) } }, } }) }

最後に、ゲームのスコアを同期します。 [ Score ]セクションのupdateScoreDisplayで、該当する場合は通知を送信します。

 function updateScoreDisplay() { ... if (mobileCheck()) { mirrorVR.notify('score', score); } }

スコア変更のリスナーを使用して、 mirrorVRの初期化を最後に更新します。

 function setupMirrorVR() { mirrorVR.init({ state: { addTrees: { }, score: { onNotify: function(data) { score = data; updateScoreDisplay(); } } } }); }

コードがこのステップの適切なソースコードファイルと一致することを再確認してください。 次に、デスクトッププレビューに移動します。 さらに、モバイルデバイスで同じURLを開きます。 モバイルデバイスがWebページをロードするとすぐに、デスクトップはモバイルデバイスのゲームのミラーリングをすぐに開始する必要があります。

これがデモです。 デスクトップカーソルが移動していないことに注意してください。これは、モバイルデバイスがデスクトッププレビューを制御していることを示しています。

MirrorVRゲーム状態同期を使用した最終的なエンドレスランナーゲーム
MirrorVRゲーム状態同期を使用したエンドレスランナーゲームの最終結果(大プレビュー)

これで、mirrorVRを使用した拡張プロジェクトは終了です。

この3番目のステップでは、いくつかの基本的なゲーム状態の同期ステップを紹介しました。 これをより堅牢にするために、より多くの健全性チェックとより多くの同期ポイントを追加できます。

結論

このチュートリアルでは、エンドレスランナーゲームに仕上げを追加し、デスクトップクライアントとモバイルクライアントのリアルタイム同期を実装して、デスクトップ上のモバイルデバイスの画面を効果的にミラーリングしました。 これで、バーチャルリアリティで無限のランナーゲームを構築するシリーズは終わりです。 A-Frame VR技術に加えて、3Dモデリング、クライアント間通信、およびその他の広く適用可能な概念を習得しました。

次のステップには、次のものが含まれます。

  • より高度なモデリング
    これは、より現実的な3Dモデルを意味し、サードパーティのソフトウェアで作成されてインポ​​ートされる可能性があります。 たとえば、(MagicaVoxel)はボクセルアートの作成を簡単にし、(Blender)は完全な3Dモデリングソリューションです。
  • より複雑
    リアルタイムストラテジーゲームなどのより複雑なゲームでは、サードパーティのエンジンを活用して効率を高めることができます。 これは、コンパイルされた(Unity3d)ゲームを公開する代わりに、A-FrameとwebVRを完全に回避することを意味する場合があります。

他の手段には、マルチプレイヤーサポートとより豊富なグラフィックスが含まれます。 このチュートリアルシリーズの終わりに、さらに探索するためのフレームワークができました。