如何在Repl.it上使用Socket.io将2048变成2人游戏

今天,我们将把经典的2048游戏变成一款具有竞争力的多人游戏,您可以与朋友一起比赛。 最好的部分是,使用repl.it,socket.io和原始的2048源代码使此操作非常容易!

在这里查看演示,以了解我们今天将要构建的东西! 分叉REPL,并与朋友分享您的分叉链接,与他们一起玩!

您需要对HTML / CSS / JS和Node.js有一定的了解,以开始使用本指南。

repl.it是一个功能强大的,初学者友好的基于Web的IDE,可让您通过几次单击就可以启动几乎任何您想尝试的语言的编程环境。 它具有非常简单的Node.js / Express项目托管,我们将在此多人项目中使用它!

首先,我们要开始这个入门项目,那就是上传到repl.it的2048单人游戏版本,只需进行最小的更改。

现在,有了我们的项目设置和基本文件,我们可以开始编码Node.js服务器,该服务器将协调两个播放器之间的移动。 我们将使用Socket.io来使我们的客户端和服务器进行实时通信,而不会造成HTTP请求的延迟和其他复杂性。

Socket.io是一个库,用于通过WebSockets在客户端代码(浏览器游戏)和Node.js服务器之间来回传递消息。 在我们的游戏中,它允许我们的游戏仅用几行代码就可以告诉服务器我们做出的动作,服务器可以告诉我们对手做出的动作!

安装Socket.io NPM软件包

我们首先要安装socket.io软件包,我们可以通过repl.it编辑器左侧的软件包工具执行此操作,只需搜索“ socket.io”并单击+图标即可。

初始化我们的服务器

接下来,我们可以转到Node服务器的“ index.js”文件并清除当前内容,以便重新开始! 然后,我们将要导入Express,Socket.io和HTTP服务器依赖项,以便能够处理刚刚上传到“ public”文件夹中的前端代码,并通过Socket.io接受消息。

  const express = require('express'); 
const socketio = require('socket.io');
const http = require('http'); const app = express();
const server = http.Server(app);
const io = socketio(服务器); //将socket.io附加到我们的服务器
app.use(express.static('public')); //从/ public提供我们的静态资产
server.listen(3000,()=> console.log('服务器已启动'));

处理连接

接下来,我们将要创建一个数组来容纳我们的用户,并在他们连接到我们的服务器时为他们分配一个玩家编号。 在server.listen行之后添加以下代码:

io.on('connection', function...)侦听新的客户端连接,并通过调用定义的函数来处理每个连接。

socket.broadcast.emit将向连接到服务器的所有其他客户端发送消息,而socket.emit将仅向当前连接的客户端发送消息。

现在,当用户连接时,将为他们提供玩家编号(如果游戏已满,则为0、1或-1),其他所有人都将知道用户何时连接(因此我们知道开始游戏)。

处理动作

接下来,在我们的连接回调中,我们可以处理当用户进行移动时,我们应该将其广播给其他玩家。 我们可以从上方在socket.broadcast.emit行之后粘贴此代码段。

处理断开

我们不希望玩家以真实用户的身份断开服务器上的“幽灵”连接。 因此,当用户断开连接时,我们还将清除该用户的套接字句柄。

我们可以简单地在“ actuate”消息处理程序下面添加以上代码段。

您的index.js现在应该看起来像这个文件。

现在,随着服务器资源的增加,我们可以集中精力将单板2048变成多板2048!

复制面食

我们需要做的第一件事就是从我们的public / index.html文档中复制并粘贴第二块木板。

我们首先将

...

元素包装到另一个div中,例如

...

。 我们可以复制该div并将其命名为“ player-two”。 最后,您的index.html部分应该看起来像这样。

这样,我们就有了两个木板,我们可以通过他们的班级名称来称呼每个球员,即“一个球员”或“两个球员”。

现在我们有了两个游戏板,我们希望能够通过websockets控制它们。

连接插座

首先,我们要开始通过socket.io与我们的服务器通信。 我们希望通过public/index.html的标签包含socket.io库。

像这样:

这样,我们将在我们的任何游戏代码之前加载socket.io库,以避免任何错误引用我们代码中的套接字库。

接下来,我们必须在public/js/application_manager.js实际初始化连接。

在这里,我们告诉socket.io连接到我们的服务器,该服务器位于window.location.origin 。 这样,它就可以连接到承载该页面的URL,因此,如果分叉repl.it,它将自动指向分叉的URL,而不是硬编码的URL。

修改游戏管理器

现在,我们要修改游戏管理器,以便它可以处理远程玩家和本地玩家。

public/js/game_manager.js我们可以在构造函数的前面添加两个新参数,称为remotePlayersocketremotePlayer将是一个布尔值,表示此GameManager是管理远程玩家还是本地玩家。 socket将是我们在上一节中初始化的socket.io连接。

public/js/game_manager.js的前几行应如下所示:

在构造函数结束之前,我们要“保存” socketremotePlayer值作为对象的本地属性。 我们还将传递remotePlayer作为Actuator的参数,稍后我们将使用它来决定应更新哪个HTML Grid(取决于它是针对播放器1还是播放器2)。

通过Websocket发送移动

现在我们希望每次我们在本地板上进行移动时,都应该将其告知服务器,然后服务器将告诉玩家2我们刚刚进行的移动。 首先,我们要在GameManager中创建一个函数来处理发送远程动作。 我们可以简单地将其插入上一节中编写的GameManager构造函数下面。

此功能采用一个网格:当前的棋盘状态,和元数据:当前游戏的得分,赢/输等。 如果当前的GameManager不是远程玩家(因此是本地玩家),那么我们应该发送移动。 我们将发出一条事件名称为“ actuate”的消息。 请注意,此事件名称与/index.js我们正在服务器上监听的事件名称如何匹配。 第二个参数指定gridmetadata的消息有效负载。

现在我们要从GameManager.actuate调用函数。 注意,该函数已经调用this.actuator.actuate ,它接受一个网格和一些元数据对象。 我们可以类似地在它之后立即调用sendRemoteMove函数。 actuate功能应如下所示:

通过Websocket处理远程移动

我们还希望能够从玩家2那里取得举动,并在我们的本地玩家2板上重播它们,以查看竞争对手的行为。

首先,我们要定义一个可以进行远程移动并更改电路板以进行移动的功能。 我们将在之前定义的sendRemoteMove之后定义函数。

接下来,我们将要设置一个事件处理程序,该事件处理程序将在远程玩家进行移动时侦听,但仅当当前GameManager正在处理远程板时才侦听。 如果要切换到构造函数,我们将要添加一个新的

如果您现在正在测试游戏,您可能会注意到有时一个玩家有时可以更新另一个玩家的游戏板,但是事情并不一致,而且一切都有些问题。 我们将通过以下步骤对其进行修复!

更新Player 2的HTML板

现在我们可以通过套接字连接进行移动了,我们想要更新正确的板,以便我们可以看到对手在做什么。 我们将要修改public/js/html_actuator.js ,指定HTMLActuator构造函数,以便它将目标定位在正确的游戏板上,具体取决于它是本地播放器还是远程播放器。 构造函数应如下所示:

我们将旧的querySelector换出了图块容器和游戏消息,以针对特定于玩家一或玩家二的游戏板。 另外,我们还将将remotePlayer保存为本地属性,以便以后可以访问它。

复制游戏管理员

接下来,我们可以在public/js/application.js为玩家2创建另一个GameManager,传入我们为GameManager定义的新参数,并保存GameManager实例供以后使用。

我们将创建两个名为remoteGamelocalGame变量来存储我们将创建的两个GameManager实例。 完成所有操作后, application.js文件应如下所示:

我们已经更改了GameManager以采用我们之前定义的socket ,以及一个bool值,该值表示GameManager是负责远程主板还是本地主板。

有了这个功能,我们现在实际上已经接近使两个板能够相互交互。 如果您正在测试它,您可能会注意到,通过打开两个不同的2048选项卡,它们既可以彼此交谈,又似乎被怪异地链接在一起。 我们将立即修复该问题!

不要听我的钥匙

现在的问题是GameManager(远程和本地)都在监听您的按键。 这导致了真正奇怪的行为。 让我们做到这一点,以便只有本地的GameManager可以监听按键,而远程的GameManager只能触发套接字消息。

在我们的GameManager构造函数中,如果不是远程播放器,我们只想听击键。 此外,我们会将侦听器绑定代码移到setup功能中,这将帮助我们控制用户何时可以开始向游戏中发布输入(稍后会有所帮助)。

GameManager之前:

后:

之前安装:

设置后:

这样可以确保我们仅在本地播放器时以及在调用setup程序后才收听移动。

测试!

这样,基本游戏就应该完成了! 您可以打开两个选项卡,以测试两个选项卡如何显示您的移动。 但是,仍然需要解决很多粗糙的边缘。 您可能已经注意到该板从上次保存的状态开始,并且您永远不知道朋友是否已经连接,什么都没有。 我们将在下一部分中解决所有这些问题!

删除本地保存状态

我们不想从上次停手的地方开始游戏,因为那会给我们带来不公平的优势! 相反,我们将禁用从本地存储还原游戏状态的功能。

我们将仅在public/js/game_manager.js中的GameManager.setup中删除恢复机制。 此外,如果是本地游戏,我们只会放下起始图块,因此我们不会在起始板上填充起始图块。 最后,我们的设置功能应如下所示:

游戏开始留言

我们想知道玩家2何时连接,并提供倒计时,以便玩家2何时连接与游戏开始之间存在延迟。

我们首先要添加HTML元素,以告知用户他们正在等待下一个播放器以及倒计时计时器消息。 我们将这些元素放置在玩家面板div上方,这样在游戏加载时就可以轻松看到它们。

另外,在public/style/main.css ,我们可以定义一些样式,以便我们的消息易于显示。

接下来,我们将要定义一些帮助器方法来隐藏/显示消息,以及显示倒计时的正确时间。 我们应该在public/js/application.js执行此操作。 您可以简单地在脚本顶部定义这些方法。

开始比赛

接下来,我们将要定义一个真正开始游戏的方法。 在这种情况下,我们还将让它在开始游戏之前执行倒数计时器。 我们可以类似地将其定义在public/js/application.js的顶部

该功能乍一看似乎有些复杂,但分解为相当简单。

我们首先从定义一个通过以下方式每秒调用的函数开始:

  const intervalId = setInterval(function(){ 

},1000);

接下来,在函数内部,我们要从要倒数的秒数中减去,然后使用countdownMessage函数更新UI。

 秒-; 
countdownMessage(true,秒);

当倒计时达到0秒时,我们将希望通过clearInterval停止执行该函数,隐藏倒计时消息,然后开始游戏。

接下来,我们将在用户连接时将等待和倒计时消息与之同步。 在我们当前正在创建游戏管理器的requestAnimationFrame回调中,我们可以侦听套接字事件以确定我们何时连接以及对手何时连接到服务器。

我们可以将其放在requestAnimationFrame回调的底部。

如果我们是玩家1,我们将向用户显示正在等待玩家2。 当我们检测到其他玩家已连接时,我们将隐藏等待消息并开始游戏倒计时。 如果我们是第二名玩家,我们将在连接到服务器后立即开始游戏倒计时。

现在只剩下最后一件事,就是当我们创建一个新的GameManager时不要立即启动游戏。

在我们的public/js/game_manager.js ,我们将简单地删除this.setup(); 来自构造函数。 这样,我们将不会在实例化时调用它,而只会在我们在startGame中定义的startGame显式调用它startGame调用它。 我们的构造函数应如下所示:

如果一切顺利,您的项目应该类似于此版本。

这就结束了创建多人2048游戏的基础! 最终的演示版本有一些调整,您可以查看源代码以了解我的工作方式,例如:

  • 并排游戏板
  • 对手获胜消息
  • 消除多余的按钮
  • 设定游戏目标(变得更容易或更困难,而不是赢得2048)
  • 游戏完整讯息

更!

还有更多改进的空间,例如使用socket.io与对手聊天的实时聊天,对游戏进行时间限制的计时器,或成功阻碍对手成功的能力的机制(例如冻结的磁贴)。 有很多更具创意的方式来制作游戏,令人兴奋!