使用 WebSocket 构建多人游戏 - 第 1 部分

2025-05-26

使用 WebSocket 构建多人游戏 - 第 1 部分

一份关于如何在 NodeJS 和 React 上使用 socket.io 在浏览器上正确构建多人游戏的权威指南。涵盖从文件夹结构到项目部署的所有内容。

拥有一个独特的想法固然重要,但更重要的是,在项目开始时,找到正确的方向。

“未来属于那些学习更多技能并以创造性的方式结合这些技能的人。”

  • 罗伯特·格林,《精通》

为什么要有另一个教程?

澄清这一点非常重要。网上有很多关于“Socket.io 入门”的指南,如果全是聊天应用,那就更让人抓狂了。但这里我们重点介绍“使用 Socket.io 构建可扩展项目入门”,它可不是聊天应用 :)。

本指南将更多地讲解代码基础架构,而非 UI/UX。如果您觉得 UI 看起来不太吸引人,请耐心等待。

什么是 socket.io?

Socket.io 是基于 WebSockets 协议构建的抽象。WebSockets 是一种允许客户端和服务器之间进行双向同步数据交换的协议。或者简单地说,它是一个双向通信管道。
注意:除非另有说明,否则 WebSockets 和 socket.io 可以互换使用(尽管它们在某些方面有所不同)。

为什么是 WebSockets 而不是 HTTP?

对于实时多人游戏,我们要求客户端向服务器发送信息包,服务器也同时发送/广播数据。这无法通过 HTTP 实现,因为客户端必须向服务器发送请求才能真正接收数据。这对于多人游戏来说并不可行。

您说的“正确方法”是什么意思?

正确的方法——这里指的是从易于进一步扩展的代码库入手,同时不会给小型项目带来太多麻烦。它涵盖了模块化程度更高的项目可以遵循的常见实践。这并不意味着它是构建 WebSocket 项目的官方方法。这只是我个人的看法,你可以轻松地替换掉项目中你不喜欢的部分 :D

这个项目是关于什么的?⚡⚡

现在进入指南的核心部分。本指南通过一个真实项目讲解了如何构建一个“多人 socket.io 游戏”。这样,我们就能更轻松地了解项目的运行情况,并且确保代码/基础架构能够正常运行!该项目是……

多人足球选秀模拟器

这个游戏有什么用?⚡

这是一款回合制多人游戏。玩家进入并创建一个房间。其他人进入房间。游戏开始后,所有玩家洗牌,第一人称玩家有机会选择自己想要的足球运动员。他可以从球员列表中搜索(查看他们的数据、位置、评分和其他详细信息),并在指定时间内确认选择。现在,轮到另一位玩家了。重复此操作,直到每个人都选完了自己想要的球员。

很简单?是或否,没关系。我们将详细分析其背后的代码基础架构。

服务器架构

游戏架构

游戏架构

上图从鸟瞰的角度解释了一切是如何联系在一起的。

本指南中的 HTTP 和 Websockets 服务器均使用 NodeJS。我们使用 Redis DB,因为 socket.io 支持开箱即用的集成,而且由于数据存储在内存中,读写操作速度更快。MongoDB 用作更持久的存储解决方案。每轮选秀结束后,每个房间的比赛结果和用户队伍信息都会存储在 MongoDB 中。如果用户希望注册,它还会存储用户凭证(本项目包含可选的注册/登录步骤)。

WebCrawler 使用 Python3 编写,使用了 Scrapy 库。足球运动员数据集是从https://sofifa.com爬取的。该数据集包含超过 20,000 名球员的数据,包括他们的评分、统计数据、身价、俱乐部等。它还有一个可选的数据分析工具 jupyter-notebook,可用于处理爬取的数据,但本指南不讨论该工具。

文件夹结构(ExpressJS + MongoDB + socket.io)

NodeJS 不会强制你遵循代码结构。这给了我们很大的灵活性来设计它们,但你可能会犯下严重的错误,从而导致项目维护和扩展困难。这种特殊的项目结构可以在使用 Socket + NodeJS 时使用。

让我们深入了解一下项目代码库的结构

.{src}  
├── controller  
│   ├── authController.js      # Handles authentication requests  
│   ├── searchController.js    # Handles search queries  
│   ├── userController.js      # Handles user profile operations  
│   └── ...  
├── database  
│   ├── db.js                  # Initialize DB connection  
│   └── ...  
├── middlewares  
│   ├── authenticated.js       # Decode and verify JWT token  
│   ├── error.js               # Common Error Handler  
│   ├── logger.js              # Control logging levels  
│   └── ...  
├── models  
│   ├── roomsModels.js         # DB model for rooms  
│   ├── usersModel.js          # DB model for users  
│   └── ...  
├── schema  
│   ├── rooms.js               # DB Schema for rooms  
│   ├── users.js               # DB Schema for users  
│   └── ...  
├── socker  
│   ├── roomManager.js         # Socket listeners/emitters handle  
│   ├── sockerController.js    # Control socket connections  
│   └── ...  
├── app.js                     # Entry file for the project  
├── env.js                     # Store environment variables  
├── routes.js                  # All routes initializer  
└── ...
Enter fullscreen mode Exit fullscreen mode

后端根据项目需求划分到不同的目录。如果您想跳过或替换某些模块,只需添加新的目录即可。

大多数子目录在 Node 项目中都是通用的,所以我就不在这里详细解释了。每个目录旁边的注释应该能让你大概了解它的含义。

我们将重点关注子目录 socker/。这是核心 socket.io 代码所在的位置。

socket.io 的入口点(App.js)

import { socker } from './socker';
import express from 'express';
import http from 'http';
import { API_PORT, host } from './env';
const app = express();
const server = new http.Server(app);
socker(server);
app.listen(API_PORT, () => {
logger.info(`Api listening on port ${Number(API_PORT)}!`);
});
server.listen(Number(API_PORT) + 1, () => {
logger.info(`Socker listening on port ${Number(API_PORT) + 1}!`);
logger.info(`Api and socker whitelisted for ${host}`);
});
view raw app.js hosted with ❤ by GitHub

这里创建了两个服务器,app一个监听 HTTP 请求,server另一个监听 WebSocket 连接。建议将它们连接到不同的端口,以免混淆。

您可能想知道第 1 行和第 8 行的“socker”是什么。

socker 是什么?

Socker 只是一个函数别名(因为我在这里构建一个足球选秀游戏,呵呵!)。这个函数将Server(在 app.js 第 8 行传入的)附加到 new 上的 engine.io 实例http.Server。简而言之,它将 socket.io 引擎附加到传递给它的服务器上。

但上面的代码并没有解释太多。现在,出现了以下问题:

  • 我如何与连接的客户端进行交互?
  • 命名空间在哪里?
  • 房间/频道在哪里?
  • 最重要的是,游戏在哪里?

创建命名空间以及为什么?

命名空间是 socket.io 的一个重要特性。它代表一个套接字池,这些套接字在给定的范围内连接,并通过路径名(例如 、 、 等)进行标识/classic-mode/football-draft/pokemon-draft本质上是在创建不同的端点或路径。它允许我们最小化资源(TCP 连接)的数量,同时通过在通信通道之间引入隔离来分离应用程序中的关注点。默认情况下,socket.io 连接到/命名空间。

创建房间/频道以及为什么?

在每个命名空间内,您可以创建任意频道或房间。这进一步允许您创建套接字可以连接join或连接的连接leave。在这里,我们使用channels来创建不同的房间,用户可以加入或创建房间一起玩。

加入房间的示例

join()操作会检查所需的房间是否roomId已创建。如果没有,则创建房间并将玩家添加到指定的 roomId。如果已创建,则直接加入该房间。

总结命名空间和通道使用的完整示例:

第一部分就到这里。这里展示的代码结构非常适合中型项目。如果您正在构建一个快速原型,可以省略或合并 schema 和 models 文件夹。如果需要,请随时简化项目 :)

如果项目规模扩大怎么办?当前的结构可能无法正常工作。您可以根据所需的服务和组件创建子文件夹(user-authentication__tests__analytics等)。您甚至可以创建微服务,即单独部署每个进程或服务,以便在高负载下进行负载均衡或仅扩展进程。

请记住不要过度设计您的产品,分阶段构建和部署!

开玩笑

无论是一个笑话还是一个工程项目,没有人喜欢做得过火:)

如果您有兴趣,以下是我的项目链接:

下一篇文章将重点关注:

  • 为每个命名空间创建和处理房间。
  • 项目客户端的简要说明(使用 ReactJS)
  • 每个项目包的CI和部署。

如果您对以后的文章有任何问题或建议,请告诉我

✌️️祝你有美好的一天!

在 Twitter 上关注我@sauravmh了解最新动态!

文章来源:https://dev.to/sauravmh/building-a-multiplayer-game-using-websockets-1n63
PREV
111 个您一定会喜欢的精彩资源 💖 Hello Devs 👋
NEXT
16 种基本问题解决模式