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:
Using the same socket connection for both scenes
Reinitializing the socket connection in BridgeScene
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');
});
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.
Update Multiplayer two Browser Simulation
I usually don't create such giant examples, but...
... in short:
// 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".