如何构建实时多人虚拟现实游戏(第 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 的实时多人游戏体验。

基于我们已经谈到的概念,您将如何确保两位玩家获得更流畅的体验? 这可能包括检查游戏状态是否同步,否则会提醒用户。 您还可以为终端状态和播放器连接状态制作简单的视觉指示器。

鉴于我们已经建立的框架和我们引入的概念,您现在拥有回答这些问题并构建更多内容的工具。

你可以在这里找到完成的源代码。

跳跃后更多! 继续往下看↓