如何在虛擬現實中構建無盡的跑步遊戲(第 2 部分)

已發表: 2022-03-10
快速總結↬如果您想知道如何構建支持 VR 耳機的無鍵盤遊戲,那麼本教程將解釋您正在尋找的內容。 以下是您也可以將基本的、功能正常的 VR 遊戲變為現實的方法。

在本系列的第 1 部分中,我們了解瞭如何創建具有照明和動畫效果的虛擬現實模型。 在這部分,我們將實現遊戲的核心邏輯,並利用更高級的 A-Frame 環境操作來構建這個應用程序的“遊戲”部分。 到最後,您將擁有一款功能強大且具有真正挑戰的虛擬現實遊戲。

本教程涉及多個步驟,包括(但不限於)碰撞檢測和更多 A-Frame 概念,例如 mixin。

  • 最終產品的演示

先決條件

就像在上一個教程中一樣,您將需要以下內容:

  • 互聯網訪問(特別是 glitch.com);
  • 從第 1 部分完成的 Glitch 項目。(您可以通過導航到 https://glitch.com/edit/#!/ergo-1 並單擊“Remix to edit”從完成的產品繼續;
  • 虛擬現實耳機(可選,推薦)。 (我使用 Google Cardboard,每張 15 美元。)
跳躍後更多! 繼續往下看↓

第 1 步:設計障礙

在此步驟中,您將設計我們將用作障礙物的樹。 然後,您將添加一個簡單的動畫,將樹木移向玩家,如下所示:

模板樹向玩家移動
向玩家移動的模板樹(大預覽)

這些樹將作為您在遊戲中生成的障礙的模板。 對於這一步的最後一部分,我們將刪除這些“模板樹”。

首先,添加一些不同的 A-Frame mixins 。 Mixin 是常用的組件屬性集。 在我們的例子中,我們所有的樹都將具​​有相同的顏色、高度、寬度、深度等。換句話說,你所有的樹看起來都一樣,因此將使用一些共享的 mixin。

注意在我們的教程中,您唯一的資產將是 mixins。 訪問 A-Frame Mixins 頁面了解更多信息。

在您的編輯器中,導航到index.html 。 在天空之後和燈光之前,添加一個新的 A-Frame 實體來保存您的資產:

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

在您的新a-assets實體中,首先為您的植物添加一個 mixin。 這個 mixins 定義了模板樹的葉子的通用屬性。 簡而言之,它是一個白色的平底金字塔,用於低多邊形效果。

 <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>

接下來,添加將使用這些 mixin 的模板樹對象。 仍在index.html中,向下滾動到平台部分。 在播放器部分之前,添加一個新的樹部分,其中包含三個空樹實體:

 <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>

現在,使用我們之前定義的 mixin,用樹乾和樹葉填充樹實體。

 <!-- 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(); }

在控件下方但在 Game 部分之前,添加一個新的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 mixins,它允許我們通過定義通用屬性來簡化代碼。 此外,我們利用 A-Frame 與 DOM 的集成從 A-Frame VR 場景中移除對象。

在下一步中,我們將生成多個障礙物並設計一個簡單的算法來將樹木分佈在不同的車道上。

第 2 步:產卵障礙

在無盡的跑步遊戲中,我們的目標是避開飛向我們的障礙物。 在這個特定的遊戲實現中,我們使用最常見的三個車道。

與大多數無盡的跑步遊戲不同,該遊戲僅支持左右移動。 這對我們生成障礙物的算法施加了限制:我們不能在所有三個車道上同時有三個障礙物飛向我們。 如果發生這種情況,玩家的生存機會將為零。 因此,我們的生成算法需要適應這個約束。

在這一步中,我們所有的代碼編輯都將在assets/ergo.js中進行。 HTML 文件將保持不變。 導航到assets/ergo.jsTREES部分。

首先,我們將添加實用程序來生成樹。 每棵樹都需要一個唯一的 ID,我們將天真地定義為生成樹時存在的樹的數量。 首先跟踪全局變量中的樹數。

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

接下來,我們將初始化對樹容器 DOM 元素的引用,我們的 spawn 函數將向其中添加樹。 仍然在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); } ...

為了以後易於使用,您將創建第二個函數,將正確的樹添加到正確的車道。 首先,在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。

為了實現該算法,我們將改組模板列表並一次處理一個。 首先定義一個新函數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); }

接下來,定義一個新函數來初始化計時器並將計時器保存在先前定義的全局變量中。 下面的計時器每半秒運行一次。

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

最後,在窗口加載後,從 Game 部分啟動計時器。

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

導航到您的預覽,您會看到隨機生成的樹木。 請注意,永遠不會同時出現三棵樹。

樹隨機生成
樹隨機生成(大預覽)

障礙步驟到此結束。 我們已經成功地獲取了許多模板樹,並從模板中生成了無數個障礙物。 我們的生成算法還尊重遊戲中的自然約束,使其具有可玩性。

在下一步中,讓我們添加碰撞測試。

第 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(); } }) } }) }

碰撞設置到此結束。 現在,我們將添加一些細節來抽像出startGamegameOver序列。 導航到GAME部分。 更新window.onload塊以匹配以下內容,將addTreesRandomlyLoop替換為尚未定義的startGame函數。

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

在 setup 函數調用下,創建一個新的startGame函數。 此函數將相應地初始化isGameRunning變量,並防止冗餘調用。

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

最後,定義gameOver ,它會提示“遊戲結束!” 現在留言。

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

無盡奔跑遊戲的碰撞測試部分到此結束。

在這一步中,我們再次使用了 A-Frame 組件和我們之前添加的許多其他實用程序。 我們還對遊戲功能進行了重新組織和適當抽象; 我們後續會擴充這些遊戲功能,以達到更完整的遊戲體驗。

結論

在第 1 部分中,我們添加了對 VR 耳機友好的控件:向左看向左移動,向右移動向右移動。 在本系列的第二部分中,我向您展示了構建一個基本的、功能強大的虛擬現實遊戲是多麼容易。 我們添加了遊戲邏輯,讓無盡的跑步者符合您的期望:永遠奔跑,並讓一系列無盡的危險障礙物飛向玩家。 到目前為止,您已經構建了一個功能強大的遊戲,它對虛擬現實耳機具有無鍵盤支持。

以下是不同 VR 控件和耳機的其他資源:

  • 用於 VR 耳機的 A 型框架
    對 A-Frame VR 支持的瀏覽器和耳機的調查。
  • 用於 VR 控制器的 A-Frame
    A-Frame 如何支持無控制器、3DoF 控制器和 6DoF 控制器,以及其他交互方式。

在下一部分中,我們將添加一些收尾工作並同步遊戲狀態,這讓我們離多人遊戲更近了一步。