在现代互联网应用中,实时通信技术变得越来越重要,这在网络游戏中尤其突出。在传统的网络游戏中,玩家需要手动在游戏客户端中进行信息的发送和接收。这种方式虽然可以保证高效的实时性,但是很难处理并发的数据连接,特别是在高网络负载的情况下。
Server-sent Events (SSE) 是一种非常适合实现实时信息交流的协议,它可以让服务器向客户端推送数据,SSE 和 WebSocket 很像,但是 SSE 更加稳定和易用,并且不需要复杂的协议和额外的库来支持。与 WebSocket 不同,SSE 是基于 HTTP 的单向数据流,更适合常规的 HTTP 应用程序和服务。
在本文中,我们将使用 SSE 和 Node.js 来构建一个简单的实时网络游戏,展示其优雅的设计和强大的性能。
开始构建
我们需要用到 Node.js 的一些核心模块(http、fs、url、path),首先创建一个新的目录并在其中创建一个新的 package.json 文件:
mkdir sse-game cd sse-game npm init -y
接下来,我们安装 express 和 socket.io 这两个依赖:
npm install express socket.io
然后创建一个 index.js 文件,将以下内容放入其中:
// www.javascriptcn.com code example
const http = require('http');
const express = require('express');
const socketio = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketio(server);
app.use(express.static(__dirname + '/client'));
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
io.on('connection', (socket) => {
console.log('User connected.');
socket.on('disconnect', () => {
console.log('User disconnected.');
});
});
server.listen(3000, () => {
console.log('Listening on port 3000...');
});这里使用了 express 来处理静态资源并提供一个简单的页面。
我们还需要创建一个 client 文件夹,并在其中创建一个 index.html 文件,以下是示例代码:
// www.javascriptcn.com code example <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>SSE Game</title> </head> <body> <h1>SSE Game</h1> <canvas id="game-canvas"></canvas> <script src="/socket.io/socket.io.js"></script> <script src="client.js"></script> </body> </html>
在这里,我们通过 socket.io.js 资源引入了 socket.io,这个库将在稍后用于建立实时连接。我们还在这个页面中添加了一个 canvas 元素,稍后将用来渲染游戏画面。
构建客户端代码
为了完成我们的客户端代码,我们首先需要在 client 目录中创建一个名为 client.js 的文件,以下是示例代码:
// www.javascriptcn.com code example
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const SSE = new EventSource('/sse');
SSE.onerror = () => {
console.log('Failed to connect.');
};
SSE.onmessage = (e) => {
console.log('Message:', e.data);
};在这里,我们首先实例化了一个 SSE 对象(使用了 EventSource 的构造函数),并指定服务器的 /sse 连接作为 URL。SSE 对象可以收到服务器端推送的信息。
接下来,我们分别处理连接和信息推送。当连接失败时,我们向控制台输出一个信息。当收到信息时,我们向控制台输出该信息。
向 SSE 发送信息
现在,我们更新服务器端代码,以便向客户端发送 SSE 消息。要完成此操作,我们必须在 http 响应中包含与 SSE 相关的首部,例如 'Content-Type': 'text/event-stream'。
为了实现 SSE 的推送,我们需要三个步骤:1)打开一个 solo(即每个连接独立的)的通道;2)对每个通道发送一个头部信息;3)对每个通道发送一个内容消息。
以下是我们更新后的 index.js 的代码,其中包括上述三个步骤:
// www.javascriptcn.com code example
const http = require('http');
const express = require('express');
const socketio = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketio(server);
app.use(express.static(__dirname + '/client'));
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
app.get('/sse', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
res.write('retry: 10000\n');
// new client connected
io.on('connection', (socket) => {
const msg = 'data: connected\n\n';
res.write(msg);
socket.on('disconnect', () => {
const msg = 'data: disconnected\n\n';
res.write(msg);
});
});
});
server.listen(3000, () => {
console.log('Listening on port 3000...');
});在此代码中,我们为 SSE 连接创建了一个新的路由,并使用 writeHead 方法设置了此连接的响应头。我们使用了 SSE 的格式来建立持久连接,这个连接将不会被立即关闭,同时每个事件通过回车和换行符来标记(\n)。在推送的消息中必须包含 data: 实体。
在发送的第一条消息中,我们将 retry 设置为 10000(即 10 秒),这意味着如果服务器端无响应,客户端将在 10 秒后尝试重新连接。如果您的服务器非常稳定,您也可以将其设置为 0。
当用户接入时,我们将发送一个“连接成功”的消息(设置 res.write),并为 socket 建立一个断开连接的事件处理程序。在每个连接断开时,我们将向连接的 SSE 通道发送一个消息。
游戏业务逻辑
现在,我们已经建立了 SSE 通道并且可以向客户端发送实时消息,但是我们需要一个更有意思的游戏(而不仅仅是连接和断开连接)才能展示这种技术的真正威力。因此,在这一节中,我们将为我们的游戏添加一些业务逻辑。
为了达到我们的目的,我们将需要两个主要的客户端脚本:一个是渲染我们的游戏,另一个是控制我们的游戏。
以下是 client/game.js(即第一个脚本):
// www.javascriptcn.com code example
class Game {
constructor() {
this.players = {};
this.obstacles = [
{ x: 20, y: 10 },
{ x: 20, y: 20 },
{ x: 30, y: 10 },
{ x: 30, y: 20 },
{ x: 40, y: 10 },
{ x: 40, y: 20 }
];
}
update(playerId, command) {
if (command === 'up') {
this.players[playerId].y--;
} else if (command === 'down') {
this.players[playerId].y++;
} else if (command === 'left') {
this.players[playerId].x--;
} else if (command === 'right') {
this.players[playerId].x++;
}
this.collisions();
return this.players;
}
collisions() {
for (let i = 0; i < this.obstacles.length; i++) {
const obstacle = this.obstacles[i];
for (const id in this.players) {
const player = this.players[id];
if (player.x === obstacle.x && player.y === obstacle.y) {
player.x = Math.floor(Math.random() * 60) + 1;
player.y = Math.floor(Math.random() * 40) + 1;
}
}
}
}
}
const game = new Game();在这里,我们定义了一个 Game 类,该类存储了游戏中的所有状态信息,因此我们可以更轻松地进行游戏逻辑处理。在构造函数中,我们设置了一个 obstacles 数组,用于在随机位置添加障碍物,以及一个 players 对象,将存储每个玩家的位置和状态。
该类中的 update 方法用于更新玩家的位置,并检测是否与其他物体产生碰撞。最后,我们的 collision 方法会检查当前正面临的所有障碍物,并检查所有玩家的位置是否与障碍物重叠。如果重叠,我们将重新生成该位置。这将确保游戏中的玩家和物体不会产生重叠。
以下是 client/client.js(第二个脚本):
// www.javascriptcn.com code example
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const SSE = new EventSource('/sse');
let playerId;
const renderPlayers = (players) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const id in players) {
const player = players[id];
ctx.beginPath();
ctx.arc(player.x, player.y, 5, 0, 2 * Math.PI);
ctx.fillStyle = '#000';
ctx.fill();
if (id === playerId) {
ctx.beginPath();
ctx.arc(player.x, player.y, 15, 0, 2 * Math.PI);
ctx.strokeStyle = 'rgba(0, 0, 0, .2)';
ctx.stroke();
}
}
};
SSE.onerror = () => {
console.log('Failed to connect.');
};
SSE.onmessage = (e) => {
const [type, data] = e.data.split(':');
switch (type) {
case 'connect':
playerId = data;
break;
case 'update':
const players = JSON.parse(data);
renderPlayers(players);
break;
case 'disconnect':
console.log('Disconnected');
break;
default:
break;
}
};
document.addEventListener('keydown', (e) => {
console.log('Keypress:', e.keyCode);
switch (e.keyCode) {
case 37:
SSE.send('left');
break;
case 38:
SSE.send('up');
break;
case 39:
SSE.send('right');
break;
case 40:
SSE.send('down');
break;
default:
break;
}
});在此 client.js 脚本中,我们首先获取了 canvas,设置了 SSE 连接,并创建了一个 renderPlayers 函数,用于在 canvas 中渲染每个玩家。
在处理 SSE 的信息推送时,我们分别检查了三种不同类型的消息:'connect'(即连接成功),'update'(即状态更新)和'disconnect'(即离线通知)。在每种情况下,我们都采取不同的行动:对于连接,我们从响应数据中提取玩家 ID;对于更新,我们解析返回的 JSON 数据,并使用 renderPlayers 函数更新画布。对于断开连接,我们只是记录信息。
最后,我们还添加了一个键盘监听器,用于处理用户按下上、下、左、右键后发送 SSE 消息。
运行游戏
我们已经准备好了所有服务器和客户端的代码,现在需要编译并运行它。运行以下命令开始:
npm start
接下来,打开浏览器,并访问 http://localhost:3000,您应该看到一个具有游戏画布的页面。打开 DevTools(按 F12),并切换到 Console 标签页。然后,打开一个新标签页,并使用相同的 URL 访问两次。
现在,我们已经成功地在本地运行了我们的基于 SSE 的网络游戏!您应该可以看到两个黑色的小球,在随机位置移动和重生,如果它们碰到障碍物,它们就会消失并在新地方重新生成。您还可以按上、下、左、右键来操纵小球。如果您在一个标签页上移动小球,那么第二个标签页上的小球也会发生相同的移动。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/67d7e732a941bf7134e21c53