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

Phaser 3+Socket.IO多场景切换时多人同步异常问题求助

场景切换后Socket.IO多人同步失效的解决方案

问题背景

使用Phaser 3和Socket.IO开发多人游戏,包含CommonScene和BridgeScene两个场景:

  • 在CommonScene中玩家可正常移动并看到其他玩家
  • 进入触发区域切换到BridgeScene后,无法再看到其他玩家
  • 两个场景使用了完全相同的多人逻辑,已尝试:复用Socket连接、重新初始化连接、发送场景切换事件,均未解决问题

核心问题分析

  1. 重复创建Socket连接:每个场景单独初始化Socket会导致旧连接断开,服务器删除原玩家数据,新连接生成新socket.id,客户端与服务器的玩家ID不匹配
  2. 无场景隔离机制:服务器未记录玩家所在场景,会把所有玩家数据推送给客户端,而其他玩家可能不在当前场景内
  3. 场景切换未同步状态:客户端切换场景时未通知服务器更新自身场景信息,也未主动请求当前场景的玩家列表

解决方案

1. 全局复用Socket连接

创建独立的Socket管理模块,避免在每个场景重复初始化连接:

// socket.js
import io from 'socket.io-client';

// 全局单例Socket连接
export const socket = io('http://localhost:3000', {
    withCredentials: false
});

2. 修改客户端场景代码

CommonScene.js

import { socket } from './socket.js';

export default class CommonScene extends Phaser.Scene {
    constructor() {
        super("CommonScene");
        this.otherPlayers = {};
    }

    create() {
        this.player = this.physics.add.sprite(400, 300, 'player');
        
        // 绑定事件前移除旧监听,避免重复绑定
        socket.off('currentPlayers');
        socket.off('newPlayer');
        socket.off('playerMoved');
        socket.off('playerDisconnected');
        socket.off('scenePlayers');

        // 监听当前场景玩家列表
        socket.on('scenePlayers', (players) => {
            // 清空旧玩家实例
            Object.values(this.otherPlayers).forEach(player => player.destroy());
            this.otherPlayers = {};
            
            // 添加当前场景的其他玩家
            Object.keys(players).forEach((id) => {
                if (id !== socket.id) {
                    this.addOtherPlayer(players[id]);
                }
            });
        });

        socket.on('newPlayer', (playerInfo) => {
            // 只添加同场景的玩家
            if (playerInfo.scene === 'CommonScene') {
                this.addOtherPlayer(playerInfo);
            }
        });

        socket.on('playerMoved', (playerInfo) => {
            if (this.otherPlayers[playerInfo.playerId]) {
                this.otherPlayers[playerInfo.playerId].setPosition(playerInfo.x, playerInfo.y);
            }
        });

        socket.on('playerDisconnected', (playerId) => {
            if (this.otherPlayers[playerId]) {
                this.otherPlayers[playerId].destroy();
                delete this.otherPlayers[playerId];
            }
        });

        // 通知服务器进入当前场景,并请求场景玩家列表
        socket.emit('enterScene', 'CommonScene', { x: this.player.x, y: this.player.y });

        // 场景切换触发区
        this.bridgeZone = this.add.zone(100, 100, 50, 50);
        this.physics.world.enable(this.bridgeZone);
        this.physics.add.overlap(this.player, this.bridgeZone, () => {
            this.scene.start('BridgeScene');
        });
    }

    addOtherPlayer(playerInfo) {
        const otherPlayer = this.physics.add.sprite(playerInfo.x, playerInfo.y, 'player');
        otherPlayer.playerId = playerInfo.playerId;
        this.otherPlayers[playerInfo.playerId] = otherPlayer;
    }

    update() {
        if (this.player && socket) {
            const x = this.player.x;
            const y = this.player.y;

            if (this.player.oldPosition && (x !== this.player.oldPosition.x || y !== this.player.oldPosition.y)) {
                socket.emit('playerMovement', { x, y });
            }

            this.player.oldPosition = { x, y };
        }
    }
}

BridgeScene.js

import { socket } from './socket.js';

export default class BridgeScene extends Phaser.Scene {
    constructor() {
        super("BridgeScene");
        this.otherPlayers = {};
    }

    create() {
        this.player = this.physics.add.sprite(400, 300, 'player');
        
        // 移除旧监听,避免重复绑定
        socket.off('currentPlayers');
        socket.off('newPlayer');
        socket.off('playerMoved');
        socket.off('playerDisconnected');
        socket.off('scenePlayers');

        // 监听当前场景玩家列表
        socket.on('scenePlayers', (players) => {
            Object.values(this.otherPlayers).forEach(player => player.destroy());
            this.otherPlayers = {};
            
            Object.keys(players).forEach((id) => {
                if (id !== socket.id) {
                    this.addOtherPlayer(players[id]);
                }
            });
        });

        socket.on('newPlayer', (playerInfo) => {
            if (playerInfo.scene === 'BridgeScene') {
                this.addOtherPlayer(playerInfo);
            }
        });

        socket.on('playerMoved', (playerInfo) => {
            if (this.otherPlayers[playerInfo.playerId]) {
                this.otherPlayers[playerInfo.playerId].setPosition(playerInfo.x, playerInfo.y);
            }
        });

        socket.on('playerDisconnected', (playerId) => {
            if (this.otherPlayers[playerId]) {
                this.otherPlayers[playerId].destroy();
                delete this.otherPlayers[playerId];
            }
        });

        // 通知服务器进入当前场景
        socket.emit('enterScene', 'BridgeScene', { x: this.player.x, y: this.player.y });
    }

    addOtherPlayer(playerInfo) {
        const otherPlayer = this.physics.add.sprite(playerInfo.x, playerInfo.y, 'player');
        otherPlayer.playerId = playerInfo.playerId;
        this.otherPlayers[playerInfo.playerId] = otherPlayer;
    }

    update() {
        if (this.player && socket) {
            const x = this.player.x;
            const y = this.player.y;

            if (this.player.oldPosition && (x !== this.player.oldPosition.x || y !== this.player.oldPosition.y)) {
                socket.emit('playerMovement', { x, y });
            }

            this.player.oldPosition = { x, y };
        }
    }
}

3. 修改服务器端代码

服务器需要记录玩家场景信息,并实现场景隔离的同步逻辑:

// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
    cors: {
        origin: ['http://localhost:8080'],
        credentials: false
    }
});

const players = {};

io.on('connection', (socket) => {
    console.log('Player connected:', socket.id);

    // 处理玩家进入场景
    socket.on('enterScene', (scene, position) => {
        // 更新玩家场景和位置
        players[socket.id] = {
            playerId: socket.id,
            scene: scene,
            x: position.x,
            y: position.y
        };

        // 筛选当前场景的所有玩家
        const scenePlayers = Object.fromEntries(
            Object.entries(players).filter(([id, player]) => player.scene === scene)
        );

        // 发送当前场景玩家列表给当前玩家
        socket.emit('scenePlayers', scenePlayers);

        // 通知同场景其他玩家有新玩家加入
        socket.broadcast.emit('newPlayer', players[socket.id]);
    });

    // 处理玩家移动
    socket.on('playerMovement', (movementData) => {
        if (players[socket.id]) {
            players[socket.id].x = movementData.x;
            players[socket.id].y = movementData.y;
            // 广播移动信息给所有客户端
            io.emit('playerMoved', {
                playerId: socket.id,
                x: movementData.x,
                y: movementData.y
            });
        }
    });

    // 处理玩家断开连接
    socket.on('disconnect', () => {
        console.log('Player disconnected:', socket.id);
        delete players[socket.id];
        io.emit('playerDisconnected', socket.id);
    });
});

server.listen(3000, () => {
    console.log('Server running on port 3000');
});

关键改进点

  • 全局Socket单例:避免重复创建连接导致的玩家ID变更
  • 场景隔离:服务器记录玩家所在场景,只同步同场景的玩家数据
  • 场景切换同步:客户端切换场景时主动通知服务器,获取当前场景的玩家列表
  • 事件去重:场景创建时移除旧的Socket监听,避免重复绑定导致的逻辑混乱

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 05:34:57