如何在虛擬現實中構建無盡的跑步遊戲(第 3 部分)
已發表: 2022-03-10所以我們的旅程還在繼續。 在我關於如何構建無盡奔跑 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
部分,更新gameOver
、 startGame
和window.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.js和index.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
部分。 此外,定義三個空函數: setupAllMenus
、 hideAllMenus
和showGameOverMenu
。
/******** * 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。 一旦您的移動設備加載網頁,您的桌面應立即開始鏡像移動設備的遊戲。
這是一個演示。 請注意,桌面光標沒有移動,表明移動設備正在控制桌面預覽。
使用mirrorVR 結束您的增強項目。
第三步介紹了一些基本的遊戲狀態同步步驟; 為了使它更健壯,您可以添加更多的健全性檢查和更多的同步點。
結論
在本教程中,您為無盡的跑步遊戲添加了收尾工作,並實現了桌面客戶端與移動客戶端的實時同步,有效地將移動設備的屏幕鏡像到桌面上。 在虛擬現實中構建無盡的跑步遊戲系列到此結束。 除了 A-Frame VR 技術,您還學習了 3D 建模、客戶端到客戶端通信以及其他廣泛適用的概念。
接下來的步驟可以包括:
- 更高級的建模
這意味著更逼真的 3D 模型,可能在第三方軟件中創建並導入。 例如,(MagicaVoxel) 使創建體素藝術變得簡單,(Blender) 是一個完整的 3D 建模解決方案。 - 更複雜
更複雜的遊戲,例如即時戰略遊戲,可以利用第三方引擎來提高效率。 這可能意味著完全迴避 A-Frame 和 webVR,而是發布已編譯的 (Unity3d) 遊戲。
其他途徑包括多人遊戲支持和更豐富的圖形。 隨著本教程系列的結束,您現在有了一個可以進一步探索的框架。