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

公開: 2022-03-10
簡単なまとめ↬VRヘッドセットをキーボードなしでサポートするゲームがどのように構築されているのか疑問に思ったことがある場合は、このチュートリアルで探しているものを説明します。 これが、あなたも基本的で機能的なVRゲームを実現する方法です。

このシリーズのパート1では、照明とアニメーション効果を備えたバーチャルリアリティモデルを作成する方法を説明しました。 このパートでは、ゲームのコアロジックを実装し、より高度なA-Frame環境操作を利用して、このアプリケーションの「ゲーム」パートを構築します。 最後に、あなたは本当の挑戦で機能するバーチャルリアリティゲームを手に入れるでしょう。

このチュートリアルには、衝突検出やミックスインなどのA-Frameの概念を含む(ただしこれらに限定されない)いくつかの手順が含まれます。

  • 最終製品のデモ

前提条件

前のチュートリアルと同様に、次のものが必要になります。

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

ステップ1:障害物の設計

このステップでは、障害物として使用する木を設計します。 次に、次のように、木をプレーヤーに向かって移動する簡単なアニメーションを追加します。

プレイヤーに向かって移動するテンプレートツリー
プレイヤーに向かって移動するテンプレートツリー(大きなプレビュー)

これらの木は、ゲーム中に生成する障害物のテンプレートとして機能します。 このステップの最後の部分では、これらの「テンプレートツリー」を削除します。

まず、さまざまなA-Frameミックスインを追加します。 ミックスインは、一般的に使用されるコンポーネントプロパティのセットです。 この場合、すべての木は同じ色、高さ、幅、深さなどになります。つまり、すべての木は同じように見えるため、いくつかの共有ミックスインを使用します。

このチュートリアルでは、アセットはミックスインのみになります。 詳細については、A-FrameMixinsページにアクセスしてください。

エディターで、 index.htmlに移動します。 空の直後でライトの前に、アセットを保持するための新しいA-Frameエンティティを追加します。

 <a-sky...></a-sky> <!-- Mixins --> <a-assets> </a-assets> <!-- Lights --> ...

新しいa-assetsエンティティで、葉にミックスインを追加することから始めます。 このミックスインは、テンプレートツリーの葉の一般的なプロパティを定義します。 要するに、それは低ポリ効果のための白い、平らな陰影のピラミッドです。

 <a-assets> <a-mixin geometry=" primitive: cone; segments-height: 1; segments-radial:4; radius-bottom:0.3;" material="color:white;flat-shading: true;"></a-mixin> </a-assets>

葉のミックスインのすぐ下に、トランクのミックスインを追加します。 このトランクは、小さな白い四角柱になります。

 <a-assets> ... <a-mixin geometry=" primitive: box; height:0.5; width:0.1; depth:0.1;" material="color:white;"></a-mixin> </a-assets>

次に、これらのミックスインを使用するテンプレートツリーオブジェクトを追加します。 まだindex.htmlで、プラットフォームセクションまでスクロールダウンします。 プレーヤーセクションの直前に、3つの空のツリーエンティティを含む新しいツリーセクションを追加します。

 <a-entity ...> <!-- Trees --> <a-entity></a-entity> <a-entity></a-entity> <a-entity></a-entity> <!-- Player --> ...

次に、ツリーエンティティにシャドウを再配置、再スケーリング、および追加します。

 <!-- Trees --> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity>

次に、前に定義したミックスインを使用して、ツリーエンティティにトランクと葉を配置します。

 <!-- Trees --> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity>

プレビューに移動すると、次のテンプレートツリーが表示されます。

障害物のテンプレートツリー
障害物のテンプレートツリー(大プレビュー)

次に、プラットフォーム上の離れた場所からユーザーに向かって木をアニメートします。 前と同じように、 a-animationタグを使用します。

 <!-- Trees --> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation> </a-entity> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation> </a-entity>

コードが以下と一致することを確認してください。

 <a-entity...> <!-- Trees --> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation> </a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="-0.5 0.55 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0.5 0.55 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <!-- Player --> ...

プレビューに移動すると、木が手前に移動しているのがわかります。

プレイヤーに向かって移動するテンプレートツリー
プレーヤーに向かって移動するテンプレートツリープレーヤーに向かって移動するテンプレートツリー(大プレビュー)

エディターに戻ります。 今回は、 assets /ergo.jsを選択します。 ゲームセクションで、ウィンドウがロードされた後にツリーを設定します。

 /******** * GAME * ********/ ... window.onload = function() { setupTrees(); }

コントロールの下で、ゲームセクションの前に、新しいTREESセクションを追加します。 このセクションでは、新しいsetupTrees関数を定義します。

 /************ * CONTROLS * ************/ ... /********* * TREES * *********/ function setupTrees() { } /******** * GAME * ********/ ...

新しいsetupTrees関数で、テンプレートツリーのDOMオブジェクトへの参照を取得し、その参照をグローバルに使用できるようにします。

 /********* * TREES * *********/ var templateTreeLeft; var templateTreeCenter; var templateTreeRight; function setupTrees() { templateTreeLeft = document.getElementById('template-tree-left'); templateTreeCenter = document.getElementById('template-tree-center'); templateTreeRight = document.getElementById('template-tree-right'); }

次に、新しいremoveTreeユーティリティを定義します。 このユーティリティを使用すると、シーンからテンプレートツリーを削除できます。 setupTrees関数の下で、新しいユーティリティを定義します。

 function setupTrees() { ... } function removeTree(tree) { tree.parentNode.removeChild(tree); }

setupTreesに戻り、新しいユーティリティを使用してテンプレートツリーを削除します。

 function setupTrees() { ... removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); }

ツリーとゲームのセクションが次のセクションと一致していることを確認してください。

 /********* * TREES * *********/ var templateTreeLeft; var templateTreeCenter; var templateTreeRight; function setupTrees() { templateTreeLeft = document.getElementById('template-tree-left'); templateTreeCenter = document.getElementById('template-tree-center'); templateTreeRight = document.getElementById('template-tree-right'); removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); } function removeTree(tree) { tree.parentNode.removeChild(tree); } /******** * GAME * ********/ setupControls(); // TODO: AFRAME.registerComponent has to occur before window.onload? window.onload = function() { setupTrees(); }

プレビューを再度開くと、ツリーが表示されなくなります。 プレビューは、このチュートリアルの開始時にゲームと一致する必要があります。

パート1完成品
パート1完成品(大プレビュー)

これで、テンプレートツリーの設計は完了です。

このステップでは、A-Frameミックスインについて説明し、使用しました。これにより、共通のプロパティを定義することでコードを簡素化できます。 さらに、A-FrameとDOMの統合を活用して、A-FrameVRシーンからオブジェクトを削除しました。

次のステップでは、複数の障害物を生成し、異なるレーンにツリーを分散するための単純なアルゴリズムを設計します。

ステップ2:スポーン障害物

無限のランナーゲームでは、私たちの目標は私たちに向かって飛んでいる障害物を避けることです。 ゲームのこの特定の実装では、最も一般的な3つのレーンを使用します。

ほとんどのエンドレスランナーゲームとは異なり、このゲームは左右の動きのみをサポートします。 これにより、障害物を生成するためのアルゴリズムに制約が課せられます。3つのレーンすべてに3つの障害物を同時に配置して、障害物を飛ばすことはできません。 それが発生した場合、プレイヤーは生存の可能性がゼロになります。 結果として、スポーンアルゴリズムはこの制約に対応する必要があります。

このステップでは、すべてのコード編集がassets /ergo.jsで行われます。 HTMLファイルは同じままです。 Assets /ergo.jsTREESセクションに移動します。

まず、スポーンツリーにユーティリティを追加します。 すべてのツリーには一意のIDが必要です。これは、ツリーが生成されたときに存在するツリーの数として単純に定義されます。 グローバル変数内のツリーの数を追跡することから始めます。

 /********* * TREES * *********/ ... var numberOfTrees = 0; function setupTrees() { ...

次に、spawn関数がツリーを追加するツリーコンテナDOM要素への参照を初期化します。 TREESセクションで、グローバル変数を追加してから参照を作成します。

 ... var treeContainer; var numberOfTrees ... function setupTrees() { ... templateTreeRight = ... treeContainer = document.getElementById('tree-container'); removeTree(...); ... }

ツリーの数とツリーコンテナの両方を使用して、ツリーを生成する新しい関数を記述します。

 function removeTree(tree) { ... } function addTree(el) { numberOfTrees += 1; el.id = 'tree-' + numberOfTrees; treeContainer.appendChild(el); } ...

後で使いやすくするために、正しいツリーを正しいレーンに追加する2番目の関数を作成します。 まず、 TREESセクションで新しいtemplates配列を定義します。

 var templates; var treeContainer; ... function setupTrees() { ... templates = [templateTreeLeft, templateTreeCenter, templateTreeRight]; removeTree(...); ... }

このテンプレート配列を使用して、左、中央、または右を表すIDを指定して、特定のレーンにツリーを生成するユーティリティを追加します。

 function function addTree(el) { ... } function addTreeTo(position_index) { var template = templates[position_index]; addTree(template.cloneNode(true)); }

プレビューに移動し、開発者コンソールを開きます。 開発者コンソールで、グローバルaddTreeTo関数を呼び出します。

 > addTreeTo(0); # spawns tree in left lane 
addTreeToを手動で呼び出す
addTreeToを手動で呼び出します(大プレビュー)

次に、木をランダムにスポーンするアルゴリズムを作成します。

  1. ランダムにレーンを選択します(このタイムステップでは、まだ選択されていません)。
  2. ある程度の確率で木をスポーンします。
  3. このタイムステップで最大数の木がスポーンされた場合は、停止します。 それ以外の場合は、手順1を繰り返します。

このアルゴリズムを実行するには、代わりにテンプレートのリストをシャッフルし、一度に1つずつ処理します。 いくつかの異なるキーワード引数を受け入れる新しい関数addTreesRandomlyを定義することから始めます。

 function addTreeTo(position_index) { ... } /** * Add any number of trees across different lanes, randomly. **/ function addTreesRandomly( { probTreeLeft = 0.5, probTreeCenter = 0.5, probTreeRight = 0.5, maxNumberTrees = 2 } = {}) { }

新しいaddTreesRandomly関数で、テンプレートツリーのリストを定義し、リストをシャッフルします。

 function addTreesRandomly( ... ) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); }

ファイルの一番下までスクロールし、新しいshuffleユーティリティとともに新しいユーティリティセクションを作成します。 このユーティリティは、アレイを所定の位置でシャッフルします。

 /******** * GAME * ********/ ... /************* * UTILITIES * *************/ /** * Shuffles array in place. * @param {Array} a items An array containing the items. */ function shuffle(a) { var j, x, i; for (i = a.length - 1; i > 0; i--) { j = Math.floor(Math.random() * (i + 1)); x = a[i]; a[i] = a[j]; a[j] = x; } return a; }

TreesセクションのaddTreesRandomly関数に戻ります。 新しい変数numberOfTreesAddedを追加し、上記で定義されたツリーのリストを反復処理します。

 function addTreesRandomly( ... ) { ... var numberOfTreesAdded = 0; trees.forEach(function (tree) { }); }

ツリーの反復では、追加されたツリーの数が2を超えない場合にのみ、ある程度の確率でツリーをスポーンします。 次のようにforループを更新します。

 function addTreesRandomly( ... ) { ... trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); }

関数を終了するには、追加されたツリーの数を返します。

 function addTreesRandomly( ... ) { ... return numberOfTreesAdded; }

addTreesRandomly関数が以下と一致することを再確認してください。

 /** * Add any number of trees across different lanes, randomly. **/ function addTreesRandomly( { probTreeLeft = 0.5, probTreeCenter = 0.5, probTreeRight = 0.5, maxNumberTrees = 2 } = {}) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); var numberOfTreesAdded = 0; trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); return numberOfTreesAdded; }

最後に、ツリーを自動的にスポーンするには、定期的にツリースポーンをトリガーするタイマーを設定します。 タイマーをグローバルに定義し、このタイマーに新しいティアダウン関数を追加します。

 /********* * TREES * *********/ ... var treeTimer; function setupTrees() { ... } function teardownTrees() { clearInterval(treeTimer); }

次に、タイマーを初期化し、以前に定義したグローバル変数にタイマーを保存する新しい関数を定義します。 以下のタイマーは0.5秒ごとに実行されます。

 function addTreesRandomlyLoop({intervalLength = 500} = {}) { treeTimer = setInterval(addTreesRandomly, intervalLength); }

最後に、ウィンドウがロードされた後、ゲームセクションからタイマーを開始します。

 /******** * GAME * ********/ ... window.onload = function() { ... addTreesRandomlyLoop(); }

プレビューに移動すると、木がランダムにスポーンするのがわかります。 一度に3本の木が存在することは決してないことに注意してください。

ランダムに木が産卵する
ランダムにスポーンするツリー(大プレビュー)

これで障害のステップは終了です。 多数のテンプレートツリーを取得し、テンプレートから無数の障害物を生成することに成功しました。 また、スポーンアルゴリズムは、ゲームの自然な制約を尊重して、ゲームをプレイ可能にします。

次のステップでは、衝突テストを追加しましょう。

ステップ3:衝突テスト

このセクションでは、障害物とプレイヤーの間の衝突テストを実装します。 これらの衝突テストは、他のほとんどのゲームの衝突テストよりも簡単です。 ただし、プレーヤーはx軸に沿って移動するだけなので、木がx軸と交差するときは常に、木のレーンがプレーヤーのレーンと同じであるかどうかを確認してください。 このゲームのこの簡単なチェックを実装します。

index.htmlに移動し、 TREESセクションに移動します。 ここでは、各樹木に車線情報を追加します。 ツリーごとに、次のようにdata-tree-position-index=追加します。 さらに、 class="tree"を追加して、次の行にあるすべての木を簡単に選択できるようにします。

 <a-entity data-tree-position-index="1" class="tree" ...> </a-entity> <a-entity data-tree-position-index="0" class="tree" ...> </a-entity> <a-entity data-tree-position-index="2" class="tree" ...> </a-entity>

Assets / ergo.jsに移動し、 GAMEセクションで新しいsetupCollisions関数を呼び出します。 さらに、既存のゲームがすでに実行されているかどうかを示す新しいisGameRunningグローバル変数を定義します。

 /******** * GAME * ********/ var isGameRunning = false; setupControls(); setupCollision(); window.onload = function() { ...

TREESセクションの直後で、Gameセクションの前に新しいCOLLISIONSセクションを定義します。 このセクションでは、setupCollisions関数を定義します。

 /********* * TREES * *********/ ... /************** * COLLISIONS * **************/ const POSITION_Z_OUT_OF_SIGHT = 1; const POSITION_Z_LINE_START = 0.6; const POSITION_Z_LINE_END = 0.7; function setupCollision() { } /******** * GAME * ********/

前と同じように、AFRAMEコンポーネントを登録し、 tickイベントリスナーを使用してすべてのタイムステップでコードを実行します。 この場合、コンポーネントをplayerに登録し、そのリスナーのすべてのツリーに対してチェックを実行します。

 function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { } } } }

forループでは、ツリーの関連情報を取得することから始めます。

 document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); }

次に、まだforループ内で、ツリーのプロパティを抽出した直後に、ツリーが見えない場合は削除します。

 document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } }

次に、実行中のゲームがない場合は、衝突があるかどうかを確認しないでください。

 document.querySelectorAll('.tree').forEach(function(tree) { if (!isGameRunning) return; }

最後に(まだforループにあります)、ツリーがプレーヤーと同時に同じ位置を共有しているかどうかを確認します。 その場合は、まだ定義されていないgameOver関数を呼び出します。

 document.querySelectorAll('.tree').forEach(function(tree) { ... if (POSITION_Z_LINE_START < position.z && position.z < POSITION_Z_LINE_END && tree_position_index == player_position_index) { gameOver(); } }

setupCollisions関数が以下と一致することを確認してください。

 function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } if (!isGameRunning) return; if (POSITION_Z_LINE_START < position.z && position.z < POSITION_Z_LINE_END && tree_position_index == player_position_index) { gameOver(); } }) } }) }

これで衝突の設定は完了です。 ここで、 startGameシーケンスとgameOverシーケンスを抽象化するためのいくつかの機能を追加します。 GAMEセクションに移動します。 window.onloadブロックを次のように更新し、 addTreesRandomlyLoopをまだ定義されていないstartGame関数に置き換えます。

 window.onload = function() { setupTrees(); startGame(); }

セットアップ関数の呼び出しの下に、新しいstartGame関数を作成します。 この関数は、それに応じてisGameRunning変数を初期化し、冗長な呼び出しを防ぎます。

 window.onload = function() { ... } function startGame() { if (isGameRunning) return; isGameRunning = true; addTreesRandomlyLoop(); }

最後に、 gameOverを定義します。これにより、「GameOver!」が警告されます。 今のところメッセージ。

 function startGame() { ... } function gameOver() { isGameRunning = false; alert('Game Over!'); teardownTrees(); }

これで、エンドレスランナーゲームの衝突テストセクションは終了です。

このステップでは、A-Frameコンポーネントと、以前に追加した他の多くのユーティリティを再び使用しました。 さらに、ゲーム機能を再編成して適切に抽象化しました。 その後、これらのゲーム機能を強化して、より完全なゲーム体験を実現します。

結論

パート1では、VRヘッドセットに適したコントロールを追加しました。左を見ると左に移動し、右を見ると右に移動します。 シリーズのこの第2部では、基本的な機能するバーチャルリアリティゲームを簡単に作成できることを紹介しました。 ゲームロジックを追加して、無限のランナーがあなたの期待に一致するようにしました。永遠に走り、無限の一連の危険な障害物がプレーヤーに向かって飛んでいきます。 これまでのところ、バーチャルリアリティヘッドセットをキーボードなしでサポートする機能的なゲームを構築しました。

さまざまなVRコントロールとヘッドセットの追加リソースは次のとおりです。

  • VRヘッドセット用のAフレーム
    A-FrameVRがサポートするブラウザとヘッドセットの調査。
  • VRコントローラー用のAフレーム
    A-Frameがコントローラーをサポートしない方法、3DoFコントローラー、6DoFコントローラー、およびその他の対話の選択肢。

次のパートでは、いくつかの仕上げを追加し、ゲームの状態を同期します。これにより、マルチプレイヤーゲームに一歩近づきます。