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

已發表: 2022-03-10
快速總結 ↬在第 1 部分中,Alvin 解釋瞭如何設計虛擬現實模型的基礎知識。 在第 2 部分中,他展示瞭如何實現遊戲的核心邏輯。 在他教程的最後一部分中,將添加最後的潤色,例如“開始”和“遊戲結束”菜單以及移動和桌面客戶端之間的遊戲狀態同步。 這為構建多人遊戲的概念鋪平了道路。

所以我們的旅程還在繼續。 在我關於如何構建無盡奔跑 VR 遊戲系列的最後一部分中,我將向您展示如何在兩台設備之間同步遊戲狀態,這將使您離構建多人遊戲更近一步。 我將專門介紹MirrorVR,它負責處理客戶端到客戶端通信中的中介服務器。

注意此遊戲可以在有或沒有 VR 耳機的情況下進行。 您可以在 ergo-3.glitch.me 查看最終產品的演示。

要開始,您將需要以下內容。

  • 互聯網訪問(特別是 glitch.com);
  • 從本教程的第 2 部分完成的 Glitch 項目。 您可以通過導航到 https://glitch.com/edit/#!/ergo-2 並單擊“Remix to edit”從第 2 部分完成的產品開始;
  • 虛擬現實耳機(可選,推薦)。 (我使用 Google Cardboard,每張 15 美元。)

第 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 步:添加開始菜單

現在用戶可以跟踪進度,您將添加收尾工作以完成遊戲體驗。 在此步驟中,您將添加一個開始菜單和一個遊戲結束菜單,讓用戶開始和重新啟動遊戲。

讓我們從玩家單擊“開始”按鈕開始遊戲的開始菜單開始。 在此步驟的後半部分,您將添加一個Game Over菜單,並帶有“Restart”按鈕:

開始和遊戲結束菜單
開始和遊戲結束菜單(大預覽)

在編輯器中導航到index.html 。 然後,找到Mixins部分。 在這裡,附加title mixin,它定義了特別大的文本的樣式。 我們使用與以前相同的字體,將文本與中心對齊,並定義適合文本類型的大小。 (請注意, anchor是文本對象錨定到其位置的位置。)

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

接下來,為二級標題添加第二個 mixin。 此文本略小,但在其他方面與標題相同。

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

對於第三個也是最後一個 mixin,定義描述性文本的屬性——甚至小於二級標題。

 <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菜單下方),使用相同的 mixin 添加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部分。 此外,定義三個空函數: setupAllMenushideAllMenusshowGameOverMenu

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

接下來,在三個地方更新Game部分。 在gameOver中,顯示Game Over菜單:

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

接下來,在window.onload中,刪除對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並從setupAllMenus調用它:

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

要填充三個空函數,您將需要一些輔助函數。 定義以下兩個函數,它們接受代表 A-Frame 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 。 在這裡,恢復兩個菜單的容器,以及Game Over菜單和“重啟”按鈕的點擊監聽器。 但是,刪除“開始”按鈕的點擊偵聽器,並隱藏“開始”菜單。

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

第三,填充showStartMenu 。 在這裡,反轉showGameOverMenu影響的所有更改。

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

仔細檢查您的代碼是否與相應的源文件匹配。 然後,導航到您的預覽,您將觀察到以下行為:

開始和遊戲結束菜單
開始和遊戲結束菜單(大預覽)

開始”和“遊戲結束”菜單到此結束。

恭喜! 您現在擁有一個功能齊全的遊戲,具有正確的開始和正確的結束。 但是,本教程還剩下一步:我們需要在不同的播放器設備之間同步遊戲狀態。 這將使我們更接近多人遊戲。

第 3 步:使用 MirrorVR 同步遊戲狀態

在之前的教程中,您學習瞭如何通過套接字發送實時信息,以促進服務器和客戶端之間的單向通信。 在這一步中,您將在該教程的成熟產品 MirrorVR 之上構建,該產品在客戶端到客戶端的通信中處理中介服務器。

注意您可以在此處了解有關 MirrorVR 的更多信息。

導航到index.html 。 在這裡,我們將加載 MirrorVR 並向相機添加一個組件,指示它應該在適用的情況下鏡像移動設備的視圖。 導入 socket.io 依賴和 MirrorVR 0.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; };

首先,我們將同步遊戲開始。 在Game部分的startGame中,在末尾添加一個mirrorVR通知。

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

移動客戶端現在發送有關遊戲開始的通知。 您現在將實現桌面的響應。

在窗口加載監聽器中,調用setupMirrorVR函數:

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

Game部分上方為 MirrorVR 設置定義一個新部分:

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

接下來,將關鍵字參數添加到 mirrorVR 的初始化函數。 具體來說,我們將定義遊戲開始通知的處理程序。 我們將另外指定房間 ID; 這可確保加載您的應用程序的任何人都立即同步。

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

Game Over重複相同的同步過程。 在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。 一旦您的移動設備加載網頁,您的桌面應立即開始鏡像移動設備的遊戲。

這是一個演示。 請注意,桌面光標沒有移動,表明移動設備正在控制桌面預覽。

Final Endless Runner Game 與 MirrorVR 遊戲狀態同步
MirrorVR遊戲狀態同步的無盡奔跑遊戲最終結果(大預覽)

使用mirrorVR 結束您的增強項目。

第三步介紹了一些基本的遊戲狀態同步步驟; 為了使它更健壯,您可以添加更多的健全性檢查和更多的同步點。

結論

在本教程中,您為無盡的跑步遊戲添加了收尾工作,並實現了桌面客戶端與移動客戶端的實時同步,有效地將移動設備的屏幕鏡像到桌面上。 在虛擬現實中構建無盡的跑步遊戲系列到此結束。 除了 A-Frame VR 技術,您還學習了 3D 建模、客戶端到客戶端通信以及其他廣泛適用的概念。

接下來的步驟可以包括:

  • 更高級的建模
    這意味著更逼真的 3D 模型,可能在第三方軟件中創建並導入。 例如,(MagicaVoxel) 使創建體素藝術變得簡單,(Blender) 是一個完整的 3D 建模解決方案。
  • 更複雜
    更複雜的遊戲,例如即時戰略遊戲,可以利用第三方引擎來提高效率。 這可能意味著完全迴避 A-Frame 和 webVR,而是發布已編譯的 (Unity3d) 遊戲。

其他途徑包括多人遊戲支持和更豐富的圖形。 隨著本教程系列的結束,您現在有了一個可以進一步探索的框架。