You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 });
            }
        }
    }
}

问题分析与解决方案

核心问题

  1. 服务器未维护在线玩家列表,新玩家加入时无法获取已存在的所有玩家数据
  2. 客户端newPlayer事件未实现处理逻辑,无法接收其他玩家的创建通知
  3. 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 });
            }
        }
    }
}

修改说明

  1. 服务器端:

    • 添加onlinePlayers Map维护在线玩家状态
    • 新玩家加入时,发送所有在线玩家数据给新玩家(allPlayers事件)
    • 移除io.emit('playerCreated'),改用socket.broadcast.emit('newPlayer')通知其他玩家
    • 添加断开连接事件,移除离线玩家并通知其他客户端
  2. 客户端:

    • 实现allPlayers事件处理,初始化所有玩家(包括自己和其他玩家)
    • 完善newPlayer事件处理,添加新加入的其他玩家
    • 移除冗余的currentPlayers初始化监听,改为从allPlayers和newPlayer构建
    • 添加playerDisconnected事件处理,清理离线玩家的Sprite

内容的提问来源于stack exchange,提问作者Kory

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 11:54:55