Node.js+Socket.io+Phaser3在线游戏:实现玩家互见及移动同步
在线游戏玩家可见性与移动同步问题
我用Node.js、Socket.io、Phaser3、Express开发在线游戏,遇到以下问题:
- 第一个玩家能看到第二个玩家及其移动状态,但第二个玩家看不到第一个玩家及移动
- 刷新页面后两者状态互换,循环往复
- 无报错信息,推测是算法逻辑问题
- 目标:实现所有玩家互相可见并同步移动
服务器端代码
io.on('connection', (socket) => { console.log('A user connected'); socket.on('createPlayer', async () => { try { const userId = socket.handshake.session.user.id; socket.emit('currentUserId', userId); const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [userId]); if (rows.length > 0) { let playerData = { playerId: userId, x: rows[0].x_position, y: rows[0].y_position, }; socket.broadcast.emit('newPlayer', playerData); io.emit('playerCreated', playerData); } else { io.to(socket.id).emit('playerCreationError', 'Игрок не найден.'); } } catch (error) { console.error(error); io.to(socket.id).emit('playerCreationError', 'Error'); } }); socket.on('playerMovement', async (data) => { try { const userId = socket.handshake.session.user.id; const { x, y } = data; await pool.query('UPDATE users SET x_position = ?, y_position = ? WHERE id = ?', [x, y, userId]); socket.broadcast.emit('playerMoved', { playerId: userId, x, y }); } catch (error) { console.error(error); } }); });
客户端代码
const socket = io(); class BaseScene extends Phaser.Scene { constructor(key) { super({ key }); this.currentUserId = null; this.currentPlayers = {}; } preload() { this.load.image('player', 'assets/player.png'); } create() { const self = this; this.cursors = this.input.keyboard.createCursorKeys(); socket.emit('createPlayer'); socket.on('currentPlayers', (players) => { this.currentPlayers = players; }); socket.on('currentUserId', (userId) => { console.log('Current user ID:', userId); this.currentUserId = userId; }); socket.on('playerCreationError', (message) => { console.log(message); }); socket.on('playerCreated', (playerData) => { if (playerData.playerId === this.currentUserId) { this.player = this.physics.add.image(playerData.x, playerData.y, 'player').setDisplaySize(200, 200); this.add.existing(this.player); } else { const otherPlayer = this.physics.add.image(playerData.x, playerData.y, 'player').setDisplaySize(200, 200); this.add.existing(otherPlayer); this.currentPlayers[playerData.playerId] = { sprite: otherPlayer }; } }); socket.on('playerMoved', (playerData) => { if (this.currentPlayers[playerData.playerId]) { const otherPlayer = this.currentPlayers[playerData.playerId].sprite; if (otherPlayer) { otherPlayer.x = playerData.x; otherPlayer.y = playerData.y; otherPlayer.setPosition(playerData.x, playerData.y); } } }); socket.on('newPlayer', (playerData) => { }); } update() { if (this.player) { let moved = false; if (this.cursors.left.isDown) { this.player.setVelocityX(-160); moved = true; } else if (this.cursors.right.isDown) { this.player.setVelocityX(160); moved = true; } else { this.player.setVelocityX(0); } if (this.cursors.up.isDown) { this.player.setVelocityY(-160); moved = true; } else if (this.cursors.down.isDown) { this.player.setVelocityY(160); moved = true; } else { this.player.setVelocityY(0); } if (moved) { socket.emit('playerMovement', { x: this.player.x, y: this.player.y }); } } } }
问题分析与解决方案
核心问题
- 服务器未维护在线玩家列表,新玩家加入时无法获取已存在的所有玩家数据
- 客户端
newPlayer事件未实现处理逻辑,无法接收其他玩家的创建通知 playerCreated广播给所有客户端,会导致当前玩家重复创建自身Sprite
修改后的服务器端代码
// 维护在线玩家列表 const onlinePlayers = new Map(); io.on('connection', (socket) => { console.log('A user connected'); socket.on('createPlayer', async () => { try { const userId = socket.handshake.session.user.id; socket.emit('currentUserId', userId); const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [userId]); if (rows.length > 0) { const playerData = { playerId: userId, x: rows[0].x_position, y: rows[0].y_position, }; // 将新玩家加入在线列表 onlinePlayers.set(userId, playerData); // 把所有在线玩家发给当前新玩家 socket.emit('allPlayers', Array.from(onlinePlayers.values())); // 把新玩家通知给其他所有在线玩家 socket.broadcast.emit('newPlayer', playerData); } else { io.to(socket.id).emit('playerCreationError', 'Игрок не найден.'); } } catch (error) { console.error(error); io.to(socket.id).emit('playerCreationError', 'Error'); } }); socket.on('playerMovement', async (data) => { try { const userId = socket.handshake.session.user.id; const { x, y } = data; await pool.query('UPDATE users SET x_position = ?, y_position = ? WHERE id = ?', [x, y, userId]); // 更新在线玩家列表中的位置 if (onlinePlayers.has(userId)) { onlinePlayers.set(userId, {...onlinePlayers.get(userId), x, y}); } socket.broadcast.emit('playerMoved', { playerId: userId, x, y }); } catch (error) { console.error(error); } }); // 玩家断开连接时移除在线列表 socket.on('disconnect', () => { const userId = socket.handshake.session.user.id; if (userId && onlinePlayers.has(userId)) { onlinePlayers.delete(userId); socket.broadcast.emit('playerDisconnected', userId); } console.log('A user disconnected'); }); });
修改后的客户端代码
const socket = io(); class BaseScene extends Phaser.Scene { constructor(key) { super({ key }); this.currentUserId = null; this.currentPlayers = {}; } preload() { this.load.image('player', 'assets/player.png'); } create() { this.cursors = this.input.keyboard.createCursorKeys(); socket.emit('createPlayer'); socket.on('currentUserId', (userId) => { console.log('Current user ID:', userId); this.currentUserId = userId; }); socket.on('playerCreationError', (message) => { console.log(message); }); // 接收所有已存在的玩家 socket.on('allPlayers', (players) => { players.forEach(playerData => { if (playerData.playerId === this.currentUserId) { // 创建自己的玩家Sprite this.player = this.physics.add.image(playerData.x, playerData.y, 'player').setDisplaySize(200, 200); this.add.existing(this.player); } else { // 创建其他玩家的Sprite const otherPlayer = this.physics.add.image(playerData.x, playerData.y, 'player').setDisplaySize(200, 200); this.add.existing(otherPlayer); this.currentPlayers[playerData.playerId] = { sprite: otherPlayer }; } }); }); // 处理新加入的玩家 socket.on('newPlayer', (playerData) => { if (playerData.playerId !== this.currentUserId && !this.currentPlayers[playerData.playerId]) { const otherPlayer = this.physics.add.image(playerData.x, playerData.y, 'player').setDisplaySize(200, 200); this.add.existing(otherPlayer); this.currentPlayers[playerData.playerId] = { sprite: otherPlayer }; } }); socket.on('playerMoved', (playerData) => { if (this.currentPlayers[playerData.playerId]) { const otherPlayer = this.currentPlayers[playerData.playerId].sprite; if (otherPlayer) { otherPlayer.setPosition(playerData.x, playerData.y); } } }); // 处理玩家断开连接 socket.on('playerDisconnected', (userId) => { if (this.currentPlayers[userId]) { this.currentPlayers[userId].sprite.destroy(); delete this.currentPlayers[userId]; } }); } update() { if (this.player) { let moved = false; if (this.cursors.left.isDown) { this.player.setVelocityX(-160); moved = true; } else if (this.cursors.right.isDown) { this.player.setVelocityX(160); moved = true; } else { this.player.setVelocityX(0); } if (this.cursors.up.isDown) { this.player.setVelocityY(-160); moved = true; } else if (this.cursors.down.isDown) { this.player.setVelocityY(160); moved = true; } else { this.player.setVelocityY(0); } if (moved) { socket.emit('playerMovement', { x: this.player.x, y: this.player.y }); } } } }
修改说明
服务器端:
- 添加
onlinePlayersMap维护在线玩家状态 - 新玩家加入时,发送所有在线玩家数据给新玩家(
allPlayers事件) - 移除
io.emit('playerCreated'),改用socket.broadcast.emit('newPlayer')通知其他玩家 - 添加断开连接事件,移除离线玩家并通知其他客户端
- 添加
客户端:
- 实现
allPlayers事件处理,初始化所有玩家(包括自己和其他玩家) - 完善
newPlayer事件处理,添加新加入的其他玩家 - 移除冗余的
currentPlayers初始化监听,改为从allPlayers和newPlayer构建 - 添加
playerDisconnected事件处理,清理离线玩家的Sprite
- 实现
内容的提问来源于stack exchange,提问作者Kory
相关产品推荐
相关产品推荐

