我对WebSockets和socket.io有点陌生,我正在制作一个两人蛇游戏,其中一个人创建游戏,另一个人加入。
游戏状态将由服务器管理,客户端将每100毫秒接收更新的状态以进行渲染。
现在,在传统的基于restapi的应用程序中,我们拥有MVC架构,其中的所有内容都分离为模型,视图和路线。可以单独测试。
我想知道如何在基于socket.io的应用程序上工作。构造应用程序的好方法是什么?
这是我想出的一个天真的实现,
文件夹结构:
- package.json
- server.js
- Game.js
package.json
{
"name": "rattle-battle-backend",
"version": "1.0.0",
"main": "server.js",
"license": "MIT",
"dependencies": {
"express": "^4.16.4",
"socket.io": "^2.2.0"
}
}
server.js
const express = require('express');
const socket = require('socket.io');
const PORT = process.env.PORT || 3000;
const app = express();
const server = app.listen(PORT);
const Game = require('./Game');
const currentlyOnlineGames = {};
const io = socket(server);
io.sockets.on('connection', socket => {
socket.on('create_game', () => {
if (socket.current_room) {
socket.emit('fail', 'you have already created a room');
} else {
const game = new Game(socket.id, io);
currentlyOnlineGames[game.roomId] = game;
socket.join(game.roomId);
socket.current_room = game.roomId;
socket.emit('game_created', game);
}
});
socket.on('join_game', roomId => {
const game = games_running[roomId];
if (!game) {
socket.emit('fail', 'game was not found');
} else if (socket.id === game.snake1.id) {
socket.emit('fail', 'you cannot join your own game');
} else {
socket.join(roomId);
socket.current_room = roomId;
game.joinGame(socket.id);
game.start(currentlyOnlineGames);
}
});
socket.on('change_snake_direction', ({ roomId, direction, id }) => {
const game = currentlyOnlineGames[roomId];
game.updateDirection(direction, id);
});
});
Game.js
class Game {
constructor(snake1Id, io) {
this.roomId = (Math.random() * 100000000000).toFixed(0);
this.snake1 = {
id: snake1Id,
positions: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0 }],
color: 'green',
dir: {
x: 1,
y: 0
}
};
this.freeToJoin = true;
this.food = this.makeFood();
this.interval = null;
this.winner = null;
this.ended = false;
this.io = io;
}
joinGame(snake2Id) {
this.snake2 = {
id: snake2Id,
positions: [{ x: 37, y: 39 }, { x: 38, y: 39 }, { x: 39, y: 39 }],
color: 'red',
dir: {
x: -1,
y: 0
}
};
this.freeToJoin = false;
}
start(currentlyOnlineGames) {
this.io.to(roomId).emit('game started', game);
this.interval = setInterval(() => {
if (!this.ended) {
this.moveSnakes();
this.io.to(this.roomId).emit('snakes_moved', game);
} else {
clearInterval(this.interval);
delete currentlyOnlineGames[this.roomId];
this.io.to(game.roomId).emit('game_ended', game.winner);
}
}, 100);
}
makeFood() {
const x = Math.floor(Math.random() * 40);
const y = Math.floor(Math.random() * 40);
return {
x,
y
};
}
moveSnakes() {
[this.snake1, this.snake2].forEach(snake => {
const [head] = snake.positions;
const { x, y } = head;
const newHead = {
x: x + snake.dir.x,
y: y + snake.dir.y
};
const otherSnake = snake === this.snake1 ? this.snake2 : this.snake1;
const collides = otherSnake.positions.find(({ x, y }) => {
return x === newHead.x && y === newHead.y;
});
if (newHead.x > 39 || newHead.y > 39 || newHead.x < 0 || newHead.y < 0) {
const winner = this.snake1 === snake ? this.snake2.id : this.snake1.id;
this.winner = winner;
this.ended = true;
} else if (collides) {
const winner = otherSnake.id;
this.winner = winner;
this.ended = true;
} else {
snake.positions.pop();
snake.positions.unshift(newHead);
this.checkFoodEaten();
}
});
}
checkFoodEaten() {
[this.snake1, this.snake2].forEach(snake => {
const [head] = snake.positions;
if (this.food.x === head.x && this.food.y === head.y) {
snake.positions.push({ ...this.food });
this.food = this.makeFood();
}
});
}
updateDirection(direction, id) {
const snakeToMove = this.snake1.id === id ? this.snake1 : this.snake2;
switch (direction) {
case 'Right':
snakeToMove.dir.x = 1;
snakeToMove.dir.y = 0;
break;
case 'Left':
snakeToMove.dir.x = -1;
snakeToMove.dir.y = 0;
break;
case 'Up':
snakeToMove.dir.x = 0;
snakeToMove.dir.y = -1;
break;
case 'Down':
snakeToMove.dir.x = 0;
snakeToMove.dir.y = 1;
break;
default:
break;
}
}
}
module.exports = Game;