socket.iogame-developmentphaser-frameworkmultiplayerphaserjs

Phaser: Beginner and having issues syncing multiplayers into different scene


I'm building a multiplayer game using Phaser 3 and Socket.IO. I have two scenes: CommonScene and BridgeScene. Players can move around and see each other in CommonScene, and there's a zone that triggers a scene switch to BridgeScene.

The issue is that when players switch to BridgeScene, they can't see each other anymore, even though I'm using the same multiplayer logic in both scenes.

What I've tried:

  1. Using the same socket connection for both scenes

  2. Reinitializing the socket connection in BridgeScene

  3. Emitting scene change events

How can I maintain multiplayer functionality when switching between scenes?

Here's my simplified code (Commonscene and bridgescene to separate files) -

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

    create() {
        // Create player sprite
        this.player = this.physics.add.sprite(400, 300, 'player');
        
        // Setup socket connection
        this.socket = io('http://localhost:3000', {
            withCredentials: false
        });

        // Add socket listeners
        this.socket.on('currentPlayers', (players) => {
            Object.keys(players).forEach((id) => {
                if (id !== this.socket.id) {
                    this.addOtherPlayer(players[id]);
                }
            });
        });

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

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

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

        // Add scene switch collision
        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 && this.socket) {
            // Emit player movement
            const x = this.player.x;
            const y = this.player.y;

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

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

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

    create() {
        // Create player sprite
        this.player = this.physics.add.sprite(400, 300, 'player');
        
        // Setup socket connection
        this.socket = io('http://localhost:3000', {
            withCredentials: false
        });

        // Add socket listeners
        this.socket.on('currentPlayers', (players) => {
            Object.keys(players).forEach((id) => {
                if (id !== this.socket.id) {
                    this.addOtherPlayer(players[id]);
                }
            });
        });

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

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

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

    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 && this.socket) {
            // Emit player movement
            const x = this.player.x;
            const y = this.player.y;

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

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

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

    // Send existing players to new player
    socket.emit('currentPlayers', players);

    // Add new player
    players[socket.id] = {
        playerId: socket.id,
        x: 400,
        y: 300
    };
    socket.broadcast.emit('newPlayer', players[socket.id]);

    // Handle player movement
    socket.on('playerMovement', (movementData) => {
        players[socket.id].x = movementData.x;
        players[socket.id].y = movementData.y;
        socket.broadcast.emit('playerMoved', {
            playerId: socket.id,
            x: movementData.x,
            y: movementData.y
        });
    });

    // Handle disconnection
    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');
});

Solution

  • Well the main issue is, that you are binding the socket to a specific scene, and when you switch to another scene this information is lost.

    The solution would be to use a global variable/object where you store the socket connection, you could maybe pass the socket to the new scene, OR how I like to do it, create a "Manager/Background" Scene that manages the real gamesScenes, and passes the events to the current scenes.

    Here a short demo, how I would do it. the BGScene is the ManagerScene, that switches between the Demo1 and Demo2 Scene, and passes the emitted data to he correct/active scene.

    Short Demo (Updated Demo):
    (ShowCasing the Background Manager Scene)
    The player will switch scenes, as soon as it collides with the yellow "zone", but the fakeSocket will continue emitting data and current active scene will receive it.

    // for Demo Socket
    let fakeSocket = { 
        events: [],
        on(eventName, callback){
            if(!this.events[eventName]){
                this.events[eventName] = []
             }
            this.events[eventName].push(callback);
        },
        emit(eventName, data){
            if(this.events[eventName] && this.events[eventName].length > 0){
                this.events[eventName].forEach( callback => callback(data) );
            }
        }
    };
    
    // emits every second a fake Event
    // other player moves from x: 250px < - > 750px (world coordinates)
    let otherPlayerWorldPosition = { x: 250, y: 50}
    
    // 1 = Right / -1 = Left
    let otherPlayerMoveDirection = 1;
    let otherPlayerMoveSpeed = 30;
    setInterval( () => { 
        // update move direction
        if(otherPlayerMoveDirection == 1 && otherPlayerWorldPosition.x > 750){
            otherPlayerMoveDirection = -1;
        } else if(otherPlayerMoveDirection == -1 && otherPlayerWorldPosition.x < 250){
            otherPlayerMoveDirection = 1;
        }
        otherPlayerWorldPosition.x += otherPlayerMoveSpeed * otherPlayerMoveDirection
        fakeSocket.emit('move', otherPlayerWorldPosition)
    }, 200);
    
    // Background Scene
    class BGScene extends Phaser.Scene {
        constructor(){
            super('BG Scene')
        }
        
        create () {
            this.label = this.add.text(10, 140, 'Info Label')
                .setScale(1.5)
                .setOrigin(0)
                .setStyle({fontStyle: 'bold', fontFamily: 'Arial'});
    
            // here the socket connection is managed for the whole game
            this.socket =  fakeSocket.on('move', data => this.updateOtherPlayer(data))
            
            // adding the two main gamescenes
            this.demo1 = this.scene.add('DemoScene1', DemoScene1);
            this.demo2 = this.scene.add('DemoScene2', DemoScene2);
            
            // add a event watcher for a collision
            this.demo1.events.on('switch', this.switch,  this);
    
            // starting the demoScene
            this.scene.run('DemoScene1');
        }
        
        switch(data){
            console.info(data);
            this.scene.stop('DemoScene1');
            this.scene.run('DemoScene2', data);
        }
        
        updateOtherPlayer(position){
           let x = position.x.toFixed(0);
           let y = position.y.toFixed(0);
           this.label.setText(`other player: (${x} , ${y})`);
           
           // passes the event data to the current active scene
           if(this.scene.isActive(this.demo1)){
               this.demo1.events.emit('enemy-movement', position);
           }
           
           if(this.scene.isActive(this.demo2)){
               this.demo2.events.emit('enemy-movement', position);
           }
        } 
    }
    
    
    class DemoScene1 extends Phaser.Scene {
        constructor(){
            super('DemoScene1')
        }
        
        create () {
           this.add.text(10, 10, 'Demo Scene 1')
              .setScale(1.5)
              .setOrigin(0)
              .setStyle({fontStyle: 'bold', fontFamily: 'Arial'});
           
           let switchZone = this.add.rectangle(500, 0, 40, 180, 0xffff00)
               .setOrigin(0);
               
           this.physics.add.existing(switchZone, true);
           
           this.player = this.add.rectangle(10, 90, 20, 20, 0xff0000)
               .setOrigin(0);
               
           this.physics.add.existing(this.player);
           this.player.body.setVelocityX(200)
           
           this.physics.add.collider(this.player, switchZone, (p, z) =>{
               this.events.emit('switch', this.player);
           });
           
           this.otherPlayer = this.add.rectangle(50, 50, 20, 20, 0x00ff00)
               .setVisible(false)
               .setOrigin(0);
           
           // listens to emited movement data
           this.events.on('enemy-movement', pos => {          
               // hide player if world position not in "viewport", is not really needed
               let isVisible = this.cameras.main.worldView.contains(pos.x, pos.y);
               this.otherPlayer.setVisible(isVisible);
               this.otherPlayer.x = pos.x
               this.otherPlayer.y = pos.y
           });
        }
        
    }
    
    class DemoScene2 extends Phaser.Scene {
        constructor(){
            super('DemoScene2')
            
            //Offset to first Scene = Width of canvas
            this._baseOffSetX = 540
            
        }
        
        create (data) {
           this.add.text(10, 10, 'Demo Scene 2')
              .setScale(1.5)
              .setOrigin(0)
              .setStyle({fontStyle: 'bold', fontFamily: 'Arial'});
              
           this.player = this.add.rectangle(data.x, data.y, 20, 20, 0xff0000)
               .setOrigin(0);
               
           this.physics.add.existing(this.player);
           
           this.otherPlayer = this.add.rectangle(50, 50, 20, 20, 0x00ff00)
               .setVisible(false)
               .setOrigin(0);
               
           this.physics.add.existing(this.player);
           
           // listens to emited movement data
           this.events.on('enemy-movement', pos => {
               let x = pos.x - this._baseOffSetX;
               let y = pos.y;
               
               // hide player if world position not in "viewport"
               let isVisible = this.cameras.main.worldView.contains(x, y);
               this.otherPlayer.x = x ;
               this.otherPlayer.y = y;
               this.otherPlayer.setVisible(isVisible);
           });
        }
    }
    
    var config = {
        width: 540,
        height: 180,
        physics: {
            default: 'arcade',
            arcade: { debug: true }
        },
        scene: [BGScene],
    }; 
    
    new Phaser.Game(config);
    
    console.clear();
    document.body.style = 'margin:0;';
    <script src="//cdn.jsdelivr.net/npm/phaser/dist/phaser.min.js"></script>

    Update

    Some more context,...
    Basically you players are moving in "a world" and each scene has a different position in the world. But each scene has its own 0,0 coordinate at the top left (except if you the scene position yourself, here an example), so you would have to calculate the offset of the incoming coordinates, so that it fits to the scene you switched to.
    the Demo has be updated, to use some sort of world offset and the other player is moving from one scene to the other.

    mini sketch

    Update Multiplayer two Browser Simulation
    I usually don't create such giant examples, but...
    ... in short:

    1. You have to send the players position over the server to the other browser.
    2. That data is the "otherPlayers" position, which you have to update.
    3. depending how your scenes are arranged you might have to pass offsets off sort*(here scene2 is 540px further right)*
    4. ... there are some comments in the code the explain the sections..

    // for Demo Fake Server
    let server = {
      players: [],
      emit(eventName, { data, playerId }) {
        let players = this.players.filter((x) => x.playerId != playerId);
        players.forEach((player) => {
          if (player.events[eventName]) {
            player.events[eventName].forEach((e) => e(data));
          }
        });
      },
      connect() {
        let playerId = this.players.push({ events: [] }) - 1;
        this.players[playerId].playerId = playerId;
        let server = this;
        return {
          events: [],
          playerId,
          on(eventName, callback) {
            if (!server.players[this.playerId].events[eventName]) {
              server.players[this.playerId].events[eventName] = [];
            }
            server.players[this.playerId].events[eventName].push(callback);
          },
          emit(eventName, data) {
            server.emit('enemy-move', { data, playerId: this.playerId });
          },
        };
      },
    };
    
    // helper Function to create to Game instances
    function createBrowserGame(elementId, data) {
      var config = {
        width: 540,
        height: 180,
        parent: elementId,
        zoom:.5,
        physics: {
          default: 'arcade',
          arcade: { debug: true },
        },
        scene: [BGScene],
      };
    
      let game = new Phaser.Game(config);
    
      game.scene.start('BGScene', data);
    }
    
    // Background Scene
    class BGScene extends Phaser.Scene {
      constructor() {
        super({ key: 'BGScene', active: false });
      }
    
      create(playerStartPosition) {
        this.label = this.add
          .text(10, 140, 'Info Label')
          .setScale(1.5)
          .setOrigin(0)
          .setStyle({ fontStyle: 'bold', fontFamily: 'Arial' });
    
        // here the socket connection is managed for the whole game
        this.socket = server.connect.bind(server)();
        this.socket.on('enemy-move', (data) => this.updateOtherPlayer(data));
    
        // adding the two main gamescenes
        this.demo1 = this.scene.add('DemoScene1', DemoScene1);
        this.demo2 = this.scene.add('DemoScene2', DemoScene2);
    
        // add a event watcher for a collision
        this.demo1.events.on('switch', this.switch, this);
        this.demo1.events.on('player-move', (data) => this.socket.emit('player-move', data), this);
        this.demo2.events.on('switch', this.switch, this);
        this.demo2.events.on('player-move', (data) => this.socket.emit('player-move', data), this);
    
        // starting the demoScene
        this.scene.run('DemoScene1', playerStartPosition);
      }
    
      switch(data) {
        if (data.sceneName == 'DemoScene1') {
          this.scene.stop('DemoScene1');
          this.scene.run('DemoScene2', data);
        } else {
          this.scene.stop('DemoScene2');
          this.scene.run('DemoScene1', data);
        }
      }
    
      updateOtherPlayer(position) {
        let x = position.x.toFixed(0);
        let y = position.y.toFixed(0);
        this.label.setText(`other player: (${x} , ${y})`);
    
        // passes the event data to the current active scene
        if (this.scene.isActive(this.demo1)) {
          this.demo1.events.emit('enemy-movement', position);
        }
    
        if (this.scene.isActive(this.demo2)) {
          this.demo2.events.emit('enemy-movement', position);
        }
      }
    }
    
    class DemoScene1 extends Phaser.Scene {
      constructor() {
        super('DemoScene1');
      }
    
      create(data) {
        this._lastUpdate = 0;
        this.add
          .text(100, 10, 'Demo Scene 1')
          .setScale(1.5)
          .setOrigin(0)
          .setStyle({ fontStyle: 'bold', fontFamily: 'Arial' });
    
        let switchZone = this.add.rectangle(500, 0, 40, 180, 0xffff00).setOrigin(0);
        this.physics.add.existing(switchZone, true);
    
        this.player = this.add.rectangle(data.pos.x, data.pos.y, 20, 20, 0xff0000).setOrigin(0);
    
        this.physics.add.existing(this.player);
        this.player.body.setVelocityX(data.speed);
    
        this.physics.add.overlap(this.player, switchZone, (p, z) => {
          if (p.body.velocity.x > 0) {
            this.events.emit('switch', { sceneName: this.scene.key, pos: { x: 0, y: p.y }, speed: p.body.velocity.x });
          }
        });
    
        this.otherPlayer = this.add.rectangle(50, 50, 20, 20, 0x00ff00).setVisible(false).setOrigin(0);
    
        // listens to emited movement data
        this.events.on('enemy-movement', (pos) => {
          // hide player if world position not in "viewport", is not really needed
          let isVisible = this.cameras.main.worldView.contains(pos.x, pos.y);
          this.otherPlayer.setVisible(isVisible);
          this.otherPlayer.x = pos.x;
          this.otherPlayer.y = pos.y;
        });
      }
    
      update(time) {
        if (this.player.x < 100 && this.player.body.velocity.x < 0) {
          this.player.body.setVelocityX(this.player.body.velocity.x * -1);
        }
    
        // send the playersposition about very 500 ms
        if (!this._lastUpdate || this._lastUpdate + 500 > time) {
          this._lastUpdate = time;
          this.events.emit('player-move', this.player);
        }
      }
    }
    
    class DemoScene2 extends Phaser.Scene {
      constructor() {
        super('DemoScene2');
    
        //Offset to first Scene = Width of canvas
        this._baseOffSetX = 540;
      }
    
      create(data) {
        this._lastUpdate = 0;
        this.add
          .text(100, 10, 'Demo Scene 2')
          .setScale(1.5)
          .setOrigin(0)
          .setStyle({ fontStyle: 'bold', fontFamily: 'Arial' });
    
        let switchZone = this.add.rectangle(0, 0, 40, 180, 0xffff00).setOrigin(0);
        this.physics.add.existing(switchZone, true);
    
        this.player = this.add.rectangle(data.pos.x, data.pos.y, 20, 20, 0xff0000).setOrigin(0);
        this.physics.add.existing(this.player);
        this.player.body.setVelocityX(data.speed);
    
        this.otherPlayer = this.add.rectangle(50, 50, 20, 20, 0x00ff00).setVisible(false).setOrigin(0);
    
        this.physics.add.overlap(this.player, switchZone, (p, z) => {
          if (p.body.velocity.x < 0) {
            this.events.emit('switch', {
              sceneName: this.scene.key,
              pos: { x: this._baseOffSetX, y: p.y },
              speed: p.body.velocity.x,
            });
          }
        });
    
        // listens to emited movement data
        this.events.on('enemy-movement', (pos) => {
          let x = pos.x - this._baseOffSetX;
          let y = pos.y;
    
          // hide player if world position not in "viewport"
          let isVisible = this.cameras.main.worldView.contains(x, y);
          this.otherPlayer.x = x;
          this.otherPlayer.y = y;
          this.otherPlayer.setVisible(isVisible);
        });
      }
    
      // send the playersposition about very 500 ms
      update(time) {
        if (this.player.x > 500 && this.player.body.velocity.x > 0) {
          this.player.body.setVelocityX(this.player.body.velocity.x * -1);
        }
    
        if (!this._lastUpdate || this._lastUpdate + 500 > time) {
          this._lastUpdate = time;
          this.events.emit('player-move', { x: this.player.x + this._baseOffSetX, y: this.player.y });
        }
      }
    }
    
    createBrowserGame('browser1', { speed: 200, pos: { x: 100, y: 60 } });
    createBrowserGame('browser2', { speed: 50, pos: { x: 300, y: 120 } });
    console.clear();
    document.body.style = 'margin:0;';
    <script src="//cdn.jsdelivr.net/npm/phaser/dist/phaser.min.js"></script>
    Browser 1 <br/>
    <div id="browser1"> </div>
    Browser 2<br/>
    <div id="browser2"> </div>

    If this doesn't clear up your the issue, I think you might need to start with a single player game, to understand phaser before trying to tackle a multiplayer game.

    Disclaimer: this is just a quick example hacked together and in parts especially verbose, and would need clean up for "production".