如何构建实时多人虚拟现实游戏(第 2 部分)
已发表: 2022-03-10在本教程系列中,我们将构建一个基于 Web 的多人虚拟现实游戏,玩家需要合作解决难题。 在本系列的第一部分,我们设计了游戏中的球体。 在本系列的这一部分中,我们将添加游戏机制并设置成对玩家之间的通信协议。
这里的游戏描述摘自该系列的第一部分:每对玩家都获得一个圆环。 目标是“打开”所有球体,如果球体升高且明亮,则该球体“打开”。 如果球体较低且昏暗,则该球体“关闭”。 然而,某些“主导”球体会影响它们的邻居:如果它切换状态,它的邻居也会切换状态。 玩家 2 可以控制偶数球,玩家 1 可以控制奇数球。 这迫使双方玩家合作解决难题。
本教程中的 8 个步骤分为 3 个部分:
- 填充用户界面(步骤 1 和 2)
- 添加游戏机制(步骤 3 到 5)
- 设置通信(步骤 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 步源代码匹配。您的球体现在应与以下内容匹配:
我们将需要的其他视觉指标到此结束。 接下来,我们将使用此模板 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 个动态填充的球体。 此外,其中一个球体还应具有依赖关系的可视指示符,如下所示:
动态添加球体的第一部分到此结束。 在下一节中,我们将通过三个步骤来添加游戏机制。 具体来说,玩家将只能根据玩家 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 的实时多人游戏体验。
基于我们已经谈到的概念,您将如何确保两位玩家获得更流畅的体验? 这可能包括检查游戏状态是否同步,否则会提醒用户。 您还可以为终端状态和播放器连接状态制作简单的视觉指示器。
鉴于我们已经建立的框架和我们引入的概念,您现在拥有回答这些问题并构建更多内容的工具。
你可以在这里找到完成的源代码。