如何構建實時多人虛擬現實遊戲(第 2 部分)

已發表: 2022-03-10
快速總結↬在本教程中,您將編寫虛擬現實遊戲的遊戲機制,它與遊戲的實時多人遊戲元素緊密結合。

在本教程系列中,我們將構建一個基於 Web 的多人虛擬現實遊戲,玩家需要合作解決難題。 在本系列的第一部分,我們設計了遊戲中的球體。 在本系列的這一部分中,我們將添加遊戲機制並設置成對玩家之間的通信協議。

這裡的遊戲描述摘自該系列的第一部分:每對玩家都獲得一個圓環。 目標是“打開”所有球體,如果球體升高且明亮,則該球體“打開”。 如果球體較低且昏暗,則該球體“關閉”。 然而,某些“主導”球體會影響它們的鄰居:如果它切換狀態,它的鄰居也會切換狀態。 玩家 2 可以控制偶數球,玩家 1 可以控制奇數球。 這迫使雙方玩家合作解決難題。

本教程中的 8 個步驟分為 3 個部分:

  1. 填充用戶界面(步驟 1 和 2)
  2. 添加遊戲機制(步驟 3 到 5)
  3. 設置通信(步驟 6 至 8)

這部分將以一個功能齊全的在線演示結束,任何人都可以玩。 您將使用 A-Frame VR 和幾個 A-Frame 擴展。

你可以在這裡找到完成的源代碼。

完成的多人遊戲,跨多個客戶端同步
完成的多人遊戲,跨多個客戶端同步。 (大預覽)

1.添加視覺指標

首先,我們將添加一個球體 ID 的視覺指示器。 在 L36 上插入一個新a-text VR 元素作為#container-orb0的第一個子元素。

 <a-entity ...> <a-text class="orb-id" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="3 3 3" position="0 -2 -0.25" material="side:double"></a-text> ... <a-entity position...> ... </a-entity> </a-entity>

一個球體的“依賴關係”是它在切換時將切換的球體:例如,假設球體 1 具有球體 2 和 3 作為依賴項。這意味著如果球體 1 被切換,則球體 2 和 3 也將被切換。 我們將在.animation-position之後直接添加依賴項的視覺指示符,如下所示。

 <a-animation class="animation-position" ... /> <a-text class="dep-right" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="10 10 10" position="0 0 1" material="side:double" ></a-text> <a-text class="dep-left" opacity="0.25"rotation="0 -90 0" value="1" color="#FFF" scale="10 10 10" position="0 0 -3" material="side:double" ></a-text>

驗證您的代碼是否與我們的第 1 步源代碼匹配。您的球體現在應與以下內容匹配:

球體,帶有球體 ID 和它將觸發的球體 ID 的視覺指示器
球體,帶有球體 ID 和將觸發的球體 ID 的視覺指示器(大預覽)

我們將需要的其他視覺指標到此結束。 接下來,我們將使用此模板 orb 將球體動態添加到 VR 場景中。

2.動態添加球體

在這一步中,我們將根據關卡的 JSON 式規範添加球體。 這使我們能夠輕鬆指定和生成新級別。 我們將使用第 1 部分最後一步中的球體作為模板。

首先,導入 jQuery,因為這將使 DOM 修改和 VR 場景的修改更容易。 直接在 A-Frame 導入之後,將以下內容添加到 L8:

 <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>

使用數組指定級別。 該數組將包含對每個球體的“依賴項”進行編碼的對象字面量。 在<head>標記內,添加以下級別配置:

 <script> var orbs = [ {left: 1, right: 4}, {}, {on: true}, {}, {on: true} ]; </script>

目前,每個球體只能在其“右側”有一個依賴項,在其“左側”有一個依賴項。 在上面聲明orbs之後,立即添加一個將在頁面加載時運行的處理程序。 此處理程序將 (1) 複製模板 orb 並 (2) 使用提供的關卡配置刪除模板 orb:

 $(document).ready(function() { function populateTemplate(orb, template, i, total) {} function remove(selector) {} for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; var template = $('#template').clone(); template = populateTemplate(orb, template, i, orbs.length); $('#carousel').append(template); } remove('#template'); } function clickOrb(i) {}

接下來,填充remove函數,它只是從 VR 場景中刪除一個項目,給定一個選擇器。 幸運的是,A-Frame 會觀察到 DOM 的變化,因此,從 DOM 中移除項目就足以將其從 VR 場景中移除。 按如下方式填充remove函數。

 function remove(selector) { var el = document.querySelector(selector); el.parentNode.removeChild(el); }

填充clickOrb函數,該函數僅觸發球體上的單擊操作。

 function clickOrb(i) { document.querySelector("#container-orb" + i).click(); }

接下來,開始編寫populateTemplate函數。 在這個函數中,首先獲取.container 。 這個球體容器還包含我們在上一步中添加的視覺指示器。 此外,我們需要根據它的依賴關係修改球體的onclick行為。 如果存在左依賴,修改視覺指示器和onclick行為以反映這一點; 右依賴也是如此:

 function populateTemplate(orb, template, i, total) { var container = template.find('.container'); var onclick = 'document.querySelector("#light-orb' + i + '").emit("switch");'; if (orb.left || orb.right) { if (orb.left) { onclick += 'clickOrb(' + orb.left + ');'; container.find('.dep-left').attr('value', orb.left); } if (orb.right) { onclick += 'clickOrb(' + orb.right + ');'; container.find('.dep-right').attr('value', orb.right); } } else { container.find('.dep-left').remove(); container.find('.dep-right').remove(); } }

仍然在populateTemplate函數中,在所有 orb 及其容器的元素中正確設置 orb ID。

 container.find('.orb-id').attr('value', i); container.attr('id', 'container-orb' + i); template.find('.orb').attr('id', 'orb' + i); template.find('.light-orb').attr('id', 'light-orb' + i); template.find('.clickable').attr('data-id', i);

仍然在populateTemplate函數中,設置onclick行為,設置隨機種子,使每個球體在視覺上有所不同,最後,根據球體的 ID 設置球體的旋轉位置。

 container.attr('onclick', onclick); container.find('lp-sphere').attr('seed', i); template.attr('rotation', '0 ' + (360 / total * i) + ' 0');

在函數結束時,返回具有上述所有配置的template

 return template;

在文檔加載處理程序中,使用remove('#template')後,打開最初配置為打開的球體。

 $(document).ready(function() { ... setTimeout(function() { for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; if (orb.on) { document.querySelector("#container-orb" + i).click(); } } }, 1000); });

Javascript 修改到此結束。 接下來,我們將模板的默認設置更改為“關閉”球體。 將#container-orb0的位置和比例更改為以下內容:

 position="8 0.5 0" scale="0.5 0.5 0.5"

然後,將#light-orb0的強度更改為 0。

 intensity="0"

驗證您的源代碼是否與我們的第 2 步源代碼匹配。

您的 VR 場景現在應該包含 5 個動態填充的球體。 此外,其中一個球體還應具有依賴關係的可視指示符,如下所示:

使用模板 orb 動態填充所有球體
所有球體都是動態填充的,使用模板球體(大預覽)

動態添加球體的第一部分到此結束。 在下一節中,我們將通過三個步驟來添加遊戲機制。 具體來說,玩家將只能根據玩家 ID 切換特定的球體。

3.添加終端狀態

在這一步中,我們將添加一個終端狀態。 如果所有球體都成功開啟,玩家會看到“勝利”頁面。 為此,您需要跟踪所有球體的狀態。 每次打開或關閉一個球體時,我們都需要更新我們的內部狀態。 假設一個輔助函數toggleOrb為我們更新狀態。 每當一個球體改變狀態時調用toggleOrb函數:(1) 添加一個點擊監聽器到​​ onload 處理程序和 (2) 添加一個toggleOrb(i); 調用clickOrb 。 最後,(3) 定義一個空的toggleOrb

 $(document).ready(function() { ... $('.orb').on('click', function() { var id = $(this).attr('data-id') toggleOrb(id); }); }); function toggleOrb(i) {} function clickOrb(i) { ... toggleOrb(i); }

為簡單起見,我們將使用關卡配置來指示遊戲狀態。 使用toggleOrb切換第 i 個球體的on狀態。 如果所有球體都打開, toggleOrb還可以觸發終端狀態。

 function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }

仔細檢查您的代碼是否與我們的第 3 步源代碼匹配。

遊戲的“單人”模式到此結束。 至此,您就擁有了一個功能齊全的虛擬現實遊戲。 但是,您現在需要編寫多人遊戲組件並通過遊戲機制鼓勵協作。

4.創建播放器對象

在這一步中,我們將為具有玩家 ID 的玩家創建一個抽象。 此玩家 ID 稍後將由服務器分配。

目前,這只是一個全局變量。 在定義orbs之後,直接定義一個玩家 ID:

 var orbs = ... var current_player_id = 1;

仔細檢查您的代碼是否與我們的第 4 步源代碼匹配。在下一步中,此玩家 ID 將用於確定玩家可以控制哪些球體。

5.有條件地切換球體

在這一步中,我們將修改球體切換行為。 具體來說,玩家1可以控制奇數球,玩家2可以控制偶數球。 首先,在球體改變狀態的兩個地方實現這個邏輯:

 $('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }

其次,在clickOrb之後定義allowedToToggle函數。 如果當前玩家是玩家 1,奇數 id 將返回一個真值,因此玩家 1 將被允許控制奇數球。 玩家 2 則相反。所有其他玩家都不能控制球體。

 function allowedToToggle(id) { if (current_player_id == 1) { return id % 2; } else if (current_player_id == 2) { return !(id % 2); } return false; }

仔細檢查您的代碼是否與我們的第 5 步源代碼匹配。默認情況下,玩家是玩家 1。這意味著您作為玩家 1 只能在預覽中控制奇數球。 關於遊戲機制的部分到此結束。

在下一節中,我們將通過服務器促進兩個玩家之間的通信。

6. 使用 WebSocket 設置服務器

在這一步中,您將設置一個簡單的服務器來 (1) 跟踪玩家 ID 和 (2) 中繼消息。 這些消息將包括遊戲狀態,以便玩家可以確定每個人都看到了其他人看到的內容。

我們將您之前的index.html稱為客戶端源代碼。 我們將此步驟中的代碼稱為服務器端源代碼。 導航到glitch.com,點擊右上角的“新項目”,然後在下拉菜單中點擊“hello-express”。

從左側面板中,選擇“package.json”,然後將socket-io添加到dependencies項。 您的dependencies項字典現在應該與以下內容匹配。

 "dependencies": { "express": "^4.16.4", "socketio": "^1.0.0" },

從左側面板中,選擇“index.js”,並將該文件的內容替換為以下最小的 socket.io Hello World:

 const express = require("express"); const app = express(); var http = require('http').Server(app); var io = require('socket.io')(http); /** * Run application on port 3000 */ var port = process.env.PORT || 3000; http.listen(port, function(){ console.log('listening on *:', port); });

上面為基本的 express 應用程序在端口 3000 上設置了 socket.io。 接下來,定義兩個全局變量,一個用於維護活躍玩家列表,另一個用於維護最小的未分配玩家 ID。

 /** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;

接下來,定義getPlayerId函數,該函數生成一個新的玩家 ID,並將新的玩家 ID 添加到playerIds數組中,將其標記為“已使用”。 特別是,該函數簡單地標記smallestPlayerId ,然後通過搜索下一個smallestPlayerId的未採用整數來更新 minimumPlayerId。

 function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }

定義removePlayer函數,該函數相應地更新smallestPlayerId並釋放提供的playerId以便其他玩家可以使用該 ID。

 function removePlayer(playerId) { if (playerId < smallestPlayerId) { smallestPlayerId = playerId; } var index = playerIds.indexOf(playerId); playerIds.splice(index, 1); }

最後,使用上述一對方法定義一對套接字事件處理程序,用於註冊新玩家和取消註冊斷開連接的玩家。

 /** * Handle socket interactions */ io.on('connection', function(socket) { socket.on('newPlayer', function() { socket.playerId = getPlayerId(); console.log("new player: ", socket.playerId); socket.emit('playerId', socket.playerId); }); socket.on('disconnect', function() { if (socket.playerId === undefined) return; console.log("disconnected player: ", socket.playerId); removePlayer(socket.playerId); }); });

仔細檢查您的代碼是否與我們的第 6 步源代碼匹配。這將結束基本的玩家註冊和註銷。 每個客戶端現在都可以使用服務器生成的玩家 ID。

在下一步中,我們將修改客戶端以接收和使用服務器發出的玩家 ID。

7.申請玩家ID

在接下來的兩個步驟中,我們將完成多人遊戲體驗的初級版本。 首先,在客戶端集成玩家 ID 分配。 特別是,每個客戶端都會向服務器詢問玩家 ID。 導航回我們在第 4 步及之前工作的客戶端index.html

在 L7 的head導入socket.io

 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>

在文檔加載處理程序之後,實例化套接字並發出一個newPlayer事件。 作為響應,服務器端將使用playerId事件生成一個新的玩家 ID。 下面,使用 Glitch 項目預覽的 URL 而不是lightful.glitch.me 。 歡迎您使用下面的演示 URL,但您所做的任何代碼更改當然不會反映。

 $(document).ready(function() { ... }); socket = io("https://lightful.glitch.me"); socket.emit('newPlayer'); socket.on('playerId', function(player_id) { current_player_id = player_id; console.log(" * You are now player", current_player_id); });

驗證您的代碼是否與我們的第 7 步源代碼匹配。現在,您可以在兩個不同的瀏覽器或選項卡上加載您的遊戲,以玩多人遊戲的兩個方面。 玩家 1 將能夠控制奇數球,玩家 2 將能夠控制偶數球。

但是,請注意切換玩家 1 的球體不會影響玩家 2 的球體狀態。接下來,我們需要同步遊戲狀態。

8.同步遊戲狀態

在這一步中,我們將同步遊戲狀態,以便玩家 1 和 2 看到相同的球體狀態。 如果玩家 1 的球體 1 開啟,那麼玩家 2 也應該開啟。 在客戶端,我們將宣布並監聽 orb 切換。 要宣布,我們將簡單地傳遞被切換的球的 ID。

在兩個toggleOrb調用之前,添加以下socket.emit調用。

 $(document).ready(function() { ... $('.orb').on('click', function() { ... socket.emit('toggleOrb', id); toggleOrb(id); }); }); ... function clickOrb(i) { ... socket.emit('toggleOrb', i); toggleOrb(i); }

接下來,監聽球體切換,並切換相應的球體。 在playerId套接字事件偵聽器的正下方,為toggleOrb事件添加另一個偵聽器。

 socket.on('toggleOrb', function(i) { document.querySelector("#container-orb" + i).click(); toggleOrb(i); });

到此結束對客戶端代碼的修改。 仔細檢查您的代碼是否與我們的第 8 步源代碼匹配。

服務器端現在需要接收和廣播切換的球體 ID。 在服務器端index.js中,添加以下監聽器。 此偵聽器應直接放置在套接字disconnect偵聽器的下方。

 socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });

仔細檢查您的代碼是否與第 8 步的源代碼匹配。現在,在一個窗口中加載的玩家 1 和在第二個窗口中加載的玩家 2 都將看到相同的遊戲狀態。 這樣,您就完成了多人虛擬現實遊戲。 此外,兩個玩家必須合作完成目標。 最終產品將匹配以下內容。

完成的多人遊戲,跨多個客戶端同步
完成的多人遊戲,跨多個客戶端同步。 (大預覽)

結論

我們關於創建多人虛擬現實遊戲的教程到此結束。 在此過程中,您觸及了許多主題,包括 A-Frame VR 中的 3-D 建模和使用 WebSockets 的實時多人遊戲體驗。

基於我們已經談到的概念,您將如何確保兩位玩家獲得更流暢的體驗? 這可能包括檢查遊戲狀態是否同步,否則會提醒用戶。 您還可以為終端狀態和播放器連接狀態製作簡單的視覺指示器。

鑑於我們已經建立的框架和我們引入的概念,您現在擁有回答這些問題並構建更多內容的工具。

你可以在這裡找到完成的源代碼。

跳躍後更多! 繼續往下看↓