バーチャルリアリティでエンドレスランナーゲームを構築する方法(パート2)
公開: 2022-03-10このシリーズのパート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(); }
プレビューを再度開くと、ツリーが表示されなくなります。 プレビューは、このチュートリアルの開始時にゲームと一致する必要があります。
これで、テンプレートツリーの設計は完了です。
このステップでは、A-Frameミックスインについて説明し、使用しました。これにより、共通のプロパティを定義することでコードを簡素化できます。 さらに、A-FrameとDOMの統合を活用して、A-FrameVRシーンからオブジェクトを削除しました。
次のステップでは、複数の障害物を生成し、異なるレーンにツリーを分散するための単純なアルゴリズムを設計します。
ステップ2:スポーン障害物
無限のランナーゲームでは、私たちの目標は私たちに向かって飛んでいる障害物を避けることです。 ゲームのこの特定の実装では、最も一般的な3つのレーンを使用します。
ほとんどのエンドレスランナーゲームとは異なり、このゲームは左右の動きのみをサポートします。 これにより、障害物を生成するためのアルゴリズムに制約が課せられます。3つのレーンすべてに3つの障害物を同時に配置して、障害物を飛ばすことはできません。 それが発生した場合、プレイヤーは生存の可能性がゼロになります。 結果として、スポーンアルゴリズムはこの制約に対応する必要があります。
このステップでは、すべてのコード編集がassets /ergo.jsで行われます。 HTMLファイルは同じままです。 Assets /ergo.jsのTREES
セクションに移動します。
まず、スポーンツリーにユーティリティを追加します。 すべてのツリーには一意の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
次に、木をランダムにスポーンするアルゴリズムを作成します。
- ランダムにレーンを選択します(このタイムステップでは、まだ選択されていません)。
- ある程度の確率で木をスポーンします。
- このタイムステップで最大数の木がスポーンされた場合は、停止します。 それ以外の場合は、手順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コントローラー、およびその他の対話の選択肢。
次のパートでは、いくつかの仕上げを追加し、ゲームの状態を同期します。これにより、マルチプレイヤーゲームに一歩近づきます。