From d785a8a1bff7382be6bb0ea0fd1974b5d329b4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 22 Jun 2020 11:58:07 +0200 Subject: [PATCH 1/7] Refactoring connection to be part of a GameScene Most of the refactoring issues we are seeing are probably due to the fact that we are trying to manipulate a ScenePlugin out of a Scene (the GameManager is not a Scene and holds a reference to a ScenePlugin coming from a Scene that might get invalidated by Phaser 3). Furthermore, if we want in the future to be able to scale, scenes could be hosted on different servers. Therefore, it makes no sense to have one connexion for the whole application. Instead, we should have one connexion for each scene. --- front/src/Connection.ts | 123 ++++++------------ front/src/Phaser/Game/GameManager.ts | 97 +++----------- front/src/Phaser/Game/GameScene.ts | 80 ++++++++++-- .../src/Phaser/Login/SelectCharacterScene.ts | 53 ++++---- front/src/WebRtc/SimplePeer.ts | 6 +- 5 files changed, 157 insertions(+), 202 deletions(-) diff --git a/front/src/Connection.ts b/front/src/Connection.ts index b6b251f0..931f7737 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -1,4 +1,4 @@ -import {GameManager} from "./Phaser/Game/GameManager"; +import {gameManager, GameManager} from "./Phaser/Game/GameManager"; import Axios from "axios"; import {API_URL} from "./Enum/EnvironmentVariable"; import {MessageUI} from "./Logger/MessageUI"; @@ -118,36 +118,12 @@ export interface WebRtcSignalMessageInterface { signal: SignalData } -export interface ConnectionInterface { - socket: Socket|null; - token: string|null; - name: string|null; - userId: string|null; - - createConnection(name: string, characterSelected: string): Promise; - - loadStartMap(): Promise; - - joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): void; - - sharePosition(x: number, y: number, direction: string, moving: boolean): void; - - /*webrtc*/ - sendWebrtcSignal(signal: unknown, roomId: string, userId?: string|null, receiverId?: string): void; - - receiveWebrtcSignal(callBack: Function): void; - - receiveWebrtcStart(callBack: (message: WebRtcStartMessageInterface) => void): void; - - disconnectMessage(callBack: (message: WebRtcDisconnectMessageInterface) => void): void; -} - export interface StartMapInterface { mapUrlStart: string, startInstance: string } -export class Connection implements ConnectionInterface { +export class Connection implements Connection { socket: Socket|null = null; token: string|null = null; name: string|null = null; // TODO: drop "name" storage here @@ -159,32 +135,28 @@ export class Connection implements ConnectionInterface { lastPositionShared: PointInterface|null = null; lastRoom: string|null = null; - constructor(GameManager: GameManager) { + private constructor(GameManager: GameManager) { this.GameManager = GameManager; } - createConnection(name: string, characterSelected: string): Promise { - this.name = name; - this.character = characterSelected; + public static createConnection(name: string, characterSelected: string): Promise { + let connection = new Connection(gameManager); + connection.name = name; + connection.character = characterSelected; return Axios.post(`${API_URL}/login`, {name: name}) .then((res) => { - this.token = res.data.token; - this.socket = SocketIo(`${API_URL}`, { + connection.token = res.data.token; + connection.socket = SocketIo(`${API_URL}`, { query: { - token: this.token + token: connection.token } }); //listen event - this.disconnectServer(); - this.errorMessage(); - this.groupUpdatedOrCreated(); - this.groupDeleted(); - this.onUserJoins(); - this.onUserMoved(); - this.onUserLeft(); + connection.disconnectServer(); + connection.errorMessage(); - return this.connectSocketServer(); + return connection.connectSocketServer(); }) .catch((err) => { console.error(err); @@ -192,6 +164,14 @@ export class Connection implements ConnectionInterface { }); } + public closeConnection(): void { + this.socket?.close(); + this.socket = null; + this.lastPositionShared = null; + this.lastRoom = null; + + } + private getSocket(): Socket { if (this.socket === null) { throw new Error('Socket not initialized while using Connection') @@ -199,12 +179,8 @@ export class Connection implements ConnectionInterface { return this.socket; } - /** - * - * @param character - */ - connectSocketServer(): Promise{ - return new Promise((resolve, reject) => { + connectSocketServer(): Promise{ + return new Promise((resolve, reject) => { this.getSocket().emit(EventMessage.SET_PLAYER_DETAILS, { name: this.name, character: this.character @@ -237,24 +213,18 @@ export class Connection implements ConnectionInterface { }); } - //TODO add middleware with access token to secure api - loadStartMap() : Promise { - return Axios.get(`${API_URL}/start-map`) - .then((res) => { - return res.data; - }).catch((err) => { - console.error(err); - throw err; - }); - } - joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): void { + joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise { const point = new Point(startX, startY, direction, moving); this.lastPositionShared = point; - this.getSocket().emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { - this.GameManager.initUsersPosition(userPositions); - }); + let promise = new Promise((resolve, reject) => { + this.getSocket().emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { + //this.GameManager.initUsersPosition(userPositions); + resolve(userPositions); + }); + }) this.lastRoom = roomId; + return promise; } sharePosition(x : number, y : number, direction : string, moving: boolean) : void{ @@ -266,35 +236,24 @@ export class Connection implements ConnectionInterface { this.getSocket().emit(EventMessage.USER_POSITION, point); } - private onUserJoins(): void { - this.getSocket().on(EventMessage.JOIN_ROOM, (message: MessageUserJoined) => { - this.GameManager.onUserJoins(message); - }); + public onUserJoins(callback: (message: MessageUserJoined) => void): void { + this.getSocket().on(EventMessage.JOIN_ROOM, callback); } - private onUserMoved(): void { - this.getSocket().on(EventMessage.USER_MOVED, (message: MessageUserMovedInterface) => { - this.GameManager.onUserMoved(message); - }); + public onUserMoved(callback: (message: MessageUserMovedInterface) => void): void { + this.getSocket().on(EventMessage.USER_MOVED, callback); } - private onUserLeft(): void { - this.getSocket().on(EventMessage.USER_LEFT, (userId: string) => { - this.GameManager.onUserLeft(userId); - }); + public onUserLeft(callback: (userId: string) => void): void { + this.getSocket().on(EventMessage.USER_LEFT, callback); } - private groupUpdatedOrCreated(): void { - this.getSocket().on(EventMessage.GROUP_CREATE_UPDATE, (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => { - //console.log('Group ', groupCreateUpdateMessage.groupId, " position :", groupCreateUpdateMessage.position.x, groupCreateUpdateMessage.position.y) - this.GameManager.shareGroupPosition(groupCreateUpdateMessage); - }) + public onGroupUpdatedOrCreated(callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void): void { + this.getSocket().on(EventMessage.GROUP_CREATE_UPDATE, callback); } - private groupDeleted(): void { - this.getSocket().on(EventMessage.GROUP_DELETE, (groupId: string) => { - this.GameManager.deleteGroup(groupId); - }) + public onGroupDeleted(callback: (groupId: string) => void): void { + this.getSocket().on(EventMessage.GROUP_DELETE, callback) } sendWebrtcSignal(signal: unknown, roomId: string, userId? : string|null, receiverId? : string) { diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index ec20411f..6c91972f 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,19 +1,21 @@ import {GameScene} from "./GameScene"; import { - Connection, ConnectionInterface, + Connection, GroupCreatedUpdatedMessageInterface, ListMessageUserPositionInterface, MessageUserJoined, MessageUserMovedInterface, MessageUserPositionInterface, Point, - PointInterface + PointInterface, StartMapInterface } from "../../Connection"; import {SimplePeer} from "../../WebRtc/SimplePeer"; import {AddPlayerInterface} from "./AddPlayerInterface"; import {ReconnectingScene, ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; import ScenePlugin = Phaser.Scenes.ScenePlugin; import {Scene} from "phaser"; +import Axios from "axios"; +import {API_URL} from "../../Enum/EnvironmentVariable"; /*export enum StatusGameManagerEnum { IN_PROGRESS = 1, @@ -34,7 +36,7 @@ export interface MapObject { export class GameManager { //status: number; - private ConnectionInstance: Connection; + //private ConnectionInstance: Connection; private currentGameScene: GameScene|null = null; private playerName: string; SimplePeer : SimplePeer; @@ -44,24 +46,26 @@ export class GameManager { //this.status = StatusGameManagerEnum.IN_PROGRESS; } - public connect(name: string, characterUserSelected : string): Promise { + public storePlayerDetails(name: string, characterUserSelected : string) /*: Promise*/ { this.playerName = name; this.characterUserSelected = characterUserSelected; - this.ConnectionInstance = new Connection(this); - return this.ConnectionInstance.createConnection(name, characterUserSelected).then((data : ConnectionInterface) => { + /*this.ConnectionInstance = new Connection(this); + return this.ConnectionInstance.createConnection(name, characterUserSelected).then((data : Connection) => { this.SimplePeer = new SimplePeer(this.ConnectionInstance); return data; }).catch((err) => { throw err; - }); + });*/ } - loadStartMap(){ - return this.ConnectionInstance.loadStartMap().then((data) => { - return data; - }).catch((err) => { - throw err; - }); + loadStartMap() : Promise { + return Axios.get(`${API_URL}/start-map`) + .then((res) => { + return res.data; + }).catch((err) => { + console.error(err); + throw err; + }); } setCurrentGameScene(gameScene: GameScene) { @@ -78,81 +82,14 @@ export class GameManager { //this.status = StatusGameManagerEnum.CURRENT_USER_CREATED; }*/ - joinRoom(sceneKey: string, startX: number, startY: number, direction: string, moving: boolean){ - this.ConnectionInstance.joinARoom(sceneKey, startX, startY, direction, moving); - } - - onUserJoins(message: MessageUserJoined): void { - const userMessage: AddPlayerInterface = { - userId: message.userId, - character: message.character, - name: message.name, - position: message.position - } - this.getCurrentGameScene().addPlayer(userMessage); - } - - onUserMoved(message: MessageUserMovedInterface): void { - this.getCurrentGameScene().updatePlayerPosition(message); - } - - onUserLeft(userId: string): void { - this.getCurrentGameScene().removePlayer(userId); - } - - initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { - // Shall we wait for room to be loaded? - /*if (this.status === StatusGameManagerEnum.IN_PROGRESS) { - return; - }*/ - try { - this.getCurrentGameScene().initUsersPosition(usersPosition) - } catch (e) { - console.error(e); - } - } - - /** - * Share group position in game - */ - shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface): void { - /*if (this.status === StatusGameManagerEnum.IN_PROGRESS) { - return; - }*/ - try { - this.getCurrentGameScene().shareGroupPosition(groupPositionMessage) - } catch (e) { - console.error(e); - } - } - - deleteGroup(groupId: string): void { - /*if (this.status === StatusGameManagerEnum.IN_PROGRESS) { - return; - }*/ - try { - this.getCurrentGameScene().deleteGroup(groupId) - } catch (e) { - console.error(e); - } - } - getPlayerName(): string { return this.playerName; } - getPlayerId(): string|null { - return this.ConnectionInstance.userId; - } - getCharacterSelected(): string { return this.characterUserSelected; } - pushPlayerPosition(event: HasMovedEvent) { - this.ConnectionInstance.sharePosition(event.x, event.y, event.direction, event.moving); - } - loadMap(mapUrl: string, scene: Phaser.Scenes.ScenePlugin, instance: string): string { const sceneKey = GameScene.getMapKeyByUrl(mapUrl); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index c6647cd9..a1181c0e 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,6 +1,7 @@ import {GameManager, gameManager, HasMovedEvent} from "./GameManager"; import { - GroupCreatedUpdatedMessageInterface, + Connection, + GroupCreatedUpdatedMessageInterface, MessageUserJoined, MessageUserMovedInterface, MessageUserPositionInterface, PointInterface, PositionInterface } from "../../Connection"; @@ -23,6 +24,7 @@ import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; import {RemotePlayer} from "../Entity/RemotePlayer"; import GameObject = Phaser.GameObjects.GameObject; import { Queue } from 'queue-typescript'; +import {SimplePeer} from "../../WebRtc/SimplePeer"; export enum Textures { @@ -81,6 +83,9 @@ export class GameScene extends Phaser.Scene { pendingEvents: Queue = new Queue(); private initPosition: PositionInterface|null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); + private connection: Connection; + private simplePeer : SimplePeer; + private connectionPromise: Promise MapKey: string; MapUrlFile: string; @@ -99,8 +104,10 @@ export class GameScene extends Phaser.Scene { private PositionNextScene: Array> = new Array>(); private startLayerName: string|undefined; - static createFromUrl(mapUrlFile: string, instance: string): GameScene { - const key = GameScene.getMapKeyByUrl(mapUrlFile); + static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene { + if (key === null) { + key = GameScene.getMapKeyByUrl(mapUrlFile); + } return new GameScene(key, mapUrlFile, instance); } @@ -117,6 +124,7 @@ export class GameScene extends Phaser.Scene { this.MapKey = MapKey; this.MapUrlFile = MapUrlFile; this.RoomId = this.instance + '__' + this.MapKey; + } //hook preload scene @@ -144,6 +152,49 @@ export class GameScene extends Phaser.Scene { }); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); + + this.connectionPromise = Connection.createConnection(gameManager.getPlayerName(), gameManager.getCharacterSelected()).then((connection : Connection) => { + this.connection = connection; + + connection.onUserJoins((message: MessageUserJoined) => { + const userMessage: AddPlayerInterface = { + userId: message.userId, + character: message.character, + name: message.name, + position: message.position + } + this.addPlayer(userMessage); + }); + + connection.onUserMoved((message: MessageUserMovedInterface) => { + this.updatePlayerPosition(message); + }); + + connection.onUserLeft((userId: string) => { + this.removePlayer(userId); + }); + + connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { + this.shareGroupPosition(groupPositionMessage); + }) + + connection.onGroupDeleted((groupId: string) => { + try { + this.deleteGroup(groupId); + } catch (e) { + console.error(e); + } + }) + + // When connection is performed, let's connect SimplePeer + this.simplePeer = new SimplePeer(this.connection); + + if (this.scene.isPaused()) { + this.scene.resume(); + } + + return connection; + }); } // FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving. @@ -272,6 +323,11 @@ export class GameScene extends Phaser.Scene { path += '#'+this.startLayerName; } window.history.pushState({}, 'WorkAdventure', path); + + // Let's pause the scene if the connection is not established yet + if (this.connection === undefined) { + this.scene.pause(); + } } private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { @@ -430,10 +486,14 @@ export class GameScene extends Phaser.Scene { this.createCollisionObject(); //join room - this.GameManager.joinRoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false); + this.connectionPromise.then((connection: Connection) => { + connection.joinARoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false).then((userPositions: MessageUserPositionInterface[]) => { + this.initUsersPosition(userPositions); + }); - //listen event to share position of user - this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) + //listen event to share position of user + this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) + }); } pushPlayerPosition(event: HasMovedEvent) { @@ -465,7 +525,7 @@ export class GameScene extends Phaser.Scene { private doPushPlayerPosition(event: HasMovedEvent): void { this.lastMoveEventSent = event; this.lastSentTick = this.currentTick; - this.GameManager.pushPlayerPosition(event); + this.connection.sharePosition(event.x, event.y, event.direction, event.moving); } EventToClickOnTile(){ @@ -525,6 +585,8 @@ export class GameScene extends Phaser.Scene { const nextSceneKey = this.checkToExit(); if(nextSceneKey){ // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. + this.connection.closeConnection(); + this.scene.stop(); this.scene.remove(this.scene.key); this.scene.start(nextSceneKey.key, { startLayerName: nextSceneKey.hash @@ -549,7 +611,7 @@ export class GameScene extends Phaser.Scene { /** * Called by the connexion when the full list of user position is received. */ - public initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { + private initUsersPosition(usersPosition: MessageUserPositionInterface[]): void { this.pendingEvents.enqueue({ type: "InitUserPositionEvent", event: usersPosition @@ -561,7 +623,7 @@ export class GameScene extends Phaser.Scene { * Put all the players on the map on map load. */ private doInitUsersPosition(usersPosition: MessageUserPositionInterface[]): void { - const currentPlayerId = this.GameManager.getPlayerId(); + const currentPlayerId = this.connection.userId; // clean map this.MapPlayersByKey.forEach((player: RemotePlayer) => { diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index 316ee897..5175a7b8 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -117,34 +117,31 @@ export class SelectCharacterScene extends Phaser.Scene { } private async login(name: string): Promise { - return gameManager.connect(name, this.selectedPlayer.texture.key).then(() => { - // Do we have a start URL in the address bar? If so, let's redirect to this address - const instanceAndMapUrl = this.findMapUrl(); - if (instanceAndMapUrl !== null) { - const [mapUrl, instance] = instanceAndMapUrl; - const key = gameManager.loadMap(mapUrl, this.scene, instance); - this.scene.start(key, { - startLayerName: window.location.hash ? window.location.hash.substr(1) : undefined - } as GameSceneInitInterface); - return { - mapUrlStart: mapUrl, - startInstance: instance - }; - } else { - // If we do not have a map address in the URL, let's ask the server for a start map. - return gameManager.loadStartMap().then((startMap: StartMapInterface) => { - const key = gameManager.loadMap(window.location.protocol + "//" + startMap.mapUrlStart, this.scene, startMap.startInstance); - this.scene.start(key); - return startMap; - }).catch((err) => { - console.error(err); - throw err; - }); - } - }).catch((err) => { - console.error(err); - throw err; - }); + gameManager.storePlayerDetails(name, this.selectedPlayer.texture.key); + + // Do we have a start URL in the address bar? If so, let's redirect to this address + const instanceAndMapUrl = this.findMapUrl(); + if (instanceAndMapUrl !== null) { + const [mapUrl, instance] = instanceAndMapUrl; + const key = gameManager.loadMap(mapUrl, this.scene, instance); + this.scene.start(key, { + startLayerName: window.location.hash ? window.location.hash.substr(1) : undefined + } as GameSceneInitInterface); + return { + mapUrlStart: mapUrl, + startInstance: instance + }; + } else { + // If we do not have a map address in the URL, let's ask the server for a start map. + return gameManager.loadStartMap().then((startMap: StartMapInterface) => { + const key = gameManager.loadMap(window.location.protocol + "//" + startMap.mapUrlStart, this.scene, startMap.startInstance); + this.scene.start(key); + return startMap; + }).catch((err) => { + console.error(err); + throw err; + }); + } } /** diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index f5c8e7ef..4adb1b18 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -1,5 +1,5 @@ import { - ConnectionInterface, + Connection, WebRtcDisconnectMessageInterface, WebRtcSignalMessageInterface, WebRtcStartMessageInterface @@ -18,7 +18,7 @@ export interface UserSimplePeer{ * This class manages connections to all the peers in the same group as me. */ export class SimplePeer { - private Connection: ConnectionInterface; + private Connection: Connection; private WebRtcRoomId: string; private Users: Array = new Array(); @@ -26,7 +26,7 @@ export class SimplePeer { private PeerConnectionArray: Map = new Map(); - constructor(Connection: ConnectionInterface, WebRtcRoomId: string = "test-webrtc") { + constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") { this.Connection = Connection; this.WebRtcRoomId = WebRtcRoomId; this.MediaManager = new MediaManager((stream : MediaStream) => { From f88f28db3f6ba637b4c0b174a531390c999bb282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 22 Jun 2020 15:00:23 +0200 Subject: [PATCH 2/7] Refactoring reconnection: putting it into the GameScene directly. --- front/src/Connection.ts | 114 +++++++++++++++------------ front/src/Phaser/Game/GameManager.ts | 4 +- front/src/Phaser/Game/GameScene.ts | 34 ++++++-- front/src/WebRtc/SimplePeer.ts | 6 ++ front/tsconfig.json | 1 + 5 files changed, 101 insertions(+), 58 deletions(-) diff --git a/front/src/Connection.ts b/front/src/Connection.ts index 931f7737..ea382365 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -124,7 +124,7 @@ export interface StartMapInterface { } export class Connection implements Connection { - socket: Socket|null = null; + socket: Socket; token: string|null = null; name: string|null = null; // TODO: drop "name" storage here character: string|null = null; @@ -135,53 +135,59 @@ export class Connection implements Connection { lastPositionShared: PointInterface|null = null; lastRoom: string|null = null; - private constructor(GameManager: GameManager) { + private constructor(GameManager: GameManager, name: string, character: string, token: string) { this.GameManager = GameManager; + this.name = name; + this.character = character; + this.token = token; + + this.socket = SocketIo(`${API_URL}`, { + query: { + token: this.token + }, + reconnection: false // Reconnection is handled by the application itself + }); + + this.socket.on(EventMessage.CONNECT_ERROR, () => { + console.error("Connection failed") + }); + + this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => { + console.error(EventMessage.MESSAGE_ERROR, message); + }) } public static createConnection(name: string, characterSelected: string): Promise { - let connection = new Connection(gameManager); - connection.name = name; - connection.character = characterSelected; return Axios.post(`${API_URL}/login`, {name: name}) .then((res) => { - connection.token = res.data.token; - connection.socket = SocketIo(`${API_URL}`, { - query: { - token: connection.token - } - }); - - //listen event - connection.disconnectServer(); - connection.errorMessage(); + let connection = new Connection(gameManager, name, characterSelected, res.data.token); + // FIXME: we should wait for the complete connexion here (i.e. the "connected" message from socket.io)! + // Otherwise, the connection MAY fail and we will never know! return connection.connectSocketServer(); }) .catch((err) => { - console.error(err); - throw err; + // Let's retry in 4-6 seconds + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve(Connection.createConnection(name, characterSelected)); + }, 4000 + Math.floor(Math.random() * 2000) ); + }); + + //console.error(err); + //throw err; }); } public closeConnection(): void { this.socket?.close(); - this.socket = null; this.lastPositionShared = null; this.lastRoom = null; - - } - - private getSocket(): Socket { - if (this.socket === null) { - throw new Error('Socket not initialized while using Connection') - } - return this.socket; } connectSocketServer(): Promise{ return new Promise((resolve, reject) => { - this.getSocket().emit(EventMessage.SET_PLAYER_DETAILS, { + this.socket.emit(EventMessage.SET_PLAYER_DETAILS, { name: this.name, character: this.character } as SetPlayerDetailsMessage, (id: string) => { @@ -218,7 +224,7 @@ export class Connection implements Connection { const point = new Point(startX, startY, direction, moving); this.lastPositionShared = point; let promise = new Promise((resolve, reject) => { - this.getSocket().emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { + this.socket.emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { //this.GameManager.initUsersPosition(userPositions); resolve(userPositions); }); @@ -233,31 +239,31 @@ export class Connection implements Connection { } const point = new Point(x, y, direction, moving); this.lastPositionShared = point; - this.getSocket().emit(EventMessage.USER_POSITION, point); + this.socket.emit(EventMessage.USER_POSITION, point); } public onUserJoins(callback: (message: MessageUserJoined) => void): void { - this.getSocket().on(EventMessage.JOIN_ROOM, callback); + this.socket.on(EventMessage.JOIN_ROOM, callback); } public onUserMoved(callback: (message: MessageUserMovedInterface) => void): void { - this.getSocket().on(EventMessage.USER_MOVED, callback); + this.socket.on(EventMessage.USER_MOVED, callback); } public onUserLeft(callback: (userId: string) => void): void { - this.getSocket().on(EventMessage.USER_LEFT, callback); + this.socket.on(EventMessage.USER_LEFT, callback); } public onGroupUpdatedOrCreated(callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void): void { - this.getSocket().on(EventMessage.GROUP_CREATE_UPDATE, callback); + this.socket.on(EventMessage.GROUP_CREATE_UPDATE, callback); } public onGroupDeleted(callback: (groupId: string) => void): void { - this.getSocket().on(EventMessage.GROUP_DELETE, callback) + this.socket.on(EventMessage.GROUP_DELETE, callback) } sendWebrtcSignal(signal: unknown, roomId: string, userId? : string|null, receiverId? : string) { - return this.getSocket().emit(EventMessage.WEBRTC_SIGNAL, { + return this.socket.emit(EventMessage.WEBRTC_SIGNAL, { userId: userId ? userId : this.userId, receiverId: receiverId ? receiverId : this.userId, roomId: roomId, @@ -266,47 +272,53 @@ export class Connection implements Connection { } receiveWebrtcStart(callback: (message: WebRtcStartMessageInterface) => void) { - this.getSocket().on(EventMessage.WEBRTC_START, callback); + this.socket.on(EventMessage.WEBRTC_START, callback); } receiveWebrtcSignal(callback: (message: WebRtcSignalMessageInterface) => void) { - return this.getSocket().on(EventMessage.WEBRTC_SIGNAL, callback); + return this.socket.on(EventMessage.WEBRTC_SIGNAL, callback); } - private errorMessage(): void { - this.getSocket().on(EventMessage.MESSAGE_ERROR, (message: string) => { - console.error(EventMessage.MESSAGE_ERROR, message); - }) - } + public onServerDisconnected(callback: (reason: string) => void): void { + /*this.socket.on(EventMessage.CONNECT_ERROR, (error: object) => { + callback(error); + });*/ - private disconnectServer(): void { - this.getSocket().on(EventMessage.CONNECT_ERROR, () => { - this.GameManager.switchToDisconnectedScene(); + this.socket.on('disconnect', (reason: string) => { + if (reason === 'io client disconnect') { + // The client asks for disconnect, let's not trigger any event. + return; + } + callback(reason); }); - this.getSocket().on(EventMessage.RECONNECTING, () => { + /*this.socket.on(EventMessage.CONNECT_ERROR, (error: object) => { + this.GameManager.switchToDisconnectedScene(); + });*/ + + /*this.socket.on(EventMessage.RECONNECTING, () => { console.log('Trying to reconnect'); }); - this.getSocket().on(EventMessage.RECONNECT_ERROR, () => { + this.socket.on(EventMessage.RECONNECT_ERROR, () => { console.log('Error while trying to reconnect.'); }); - this.getSocket().on(EventMessage.RECONNECT_FAILED, () => { + this.socket.on(EventMessage.RECONNECT_FAILED, () => { console.error('Reconnection failed. Giving up.'); }); - this.getSocket().on(EventMessage.RECONNECT, () => { + this.socket.on(EventMessage.RECONNECT, () => { console.log('Reconnect event triggered'); this.connectSocketServer(); if (this.lastPositionShared === null) { throw new Error('No last position shared found while reconnecting'); } this.GameManager.reconnectToGameScene(this.lastPositionShared); - }); + });*/ } disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void { - this.getSocket().on(EventMessage.WEBRTC_DISCONNECT, callback); + this.socket.on(EventMessage.WEBRTC_DISCONNECT, callback); } } diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 6c91972f..5d77e6a7 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -122,7 +122,7 @@ export class GameManager { this.currentGameScene = null; } - private timeoutCallback: NodeJS.Timeout|null = null; + /*private timeoutCallback: NodeJS.Timeout|null = null; reconnectToGameScene(lastPositionShared: PointInterface): void { if (this.timeoutCallback !== null) { console.log('Reconnect called but setTimeout in progress for the reconnection'); @@ -150,7 +150,7 @@ export class GameManager { const game : Phaser.Scene = GameScene.createFromUrl(this.oldMapUrlFile, this.oldInstance); this.reconnectScene.scene.add(this.oldSceneKey, game, true, { initPosition: lastPositionShared }); this.reconnectScene = null; - } + }*/ private getCurrentGameScene(): GameScene { if (this.currentGameScene === null) { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index a1181c0e..3374fc08 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -25,6 +25,7 @@ import {RemotePlayer} from "../Entity/RemotePlayer"; import GameObject = Phaser.GameObjects.GameObject; import { Queue } from 'queue-typescript'; import {SimplePeer} from "../../WebRtc/SimplePeer"; +import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; export enum Textures { @@ -123,8 +124,7 @@ export class GameScene extends Phaser.Scene { this.MapKey = MapKey; this.MapUrlFile = MapUrlFile; - this.RoomId = this.instance + '__' + this.MapKey; - + this.RoomId = this.instance + '__' + GameScene.getMapKeyByUrl(MapUrlFile); } //hook preload scene @@ -186,11 +186,29 @@ export class GameScene extends Phaser.Scene { } }) + connection.onServerDisconnected(() => { + console.log('Player disconnected from server. Reloading scene.'); + + this.simplePeer.closeAllConnections(); + + let key = 'somekey'+Math.round(Math.random()*10000); + const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, key); + this.scene.add(key, game, false, + { + initPosition: { + x: this.CurrentPlayer.x, + y: this.CurrentPlayer.y + } + }); + this.scene.start(key); + }) + // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection); - if (this.scene.isPaused()) { - this.scene.resume(); + if (this.scene.isSleeping()) { + this.scene.wake(); + this.scene.sleep(ReconnectingSceneName); } return connection; @@ -326,7 +344,13 @@ export class GameScene extends Phaser.Scene { // Let's pause the scene if the connection is not established yet if (this.connection === undefined) { - this.scene.pause(); + // Let's wait 0.5 seconds before printing the "connecting" screen to avoid blinking + setTimeout(() => { + if (this.connection === undefined) { + this.scene.sleep(); + this.scene.launch(ReconnectingSceneName); + } + }, 500); } } diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 4adb1b18..381b3ac2 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -209,6 +209,12 @@ export class SimplePeer { } } + public closeAllConnections() { + for (const userId of this.PeerConnectionArray.keys()) { + this.closeConnection(userId); + } + } + /** * * @param userId diff --git a/front/tsconfig.json b/front/tsconfig.json index 84882e74..1661efa2 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -6,6 +6,7 @@ "noImplicitAny": true, "module": "CommonJS", "target": "es5", + "downlevelIteration": true, "jsx": "react", "allowJs": true, From 403ea223a844b5d51901e7d67018dccd3080b6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 22 Jun 2020 16:10:18 +0200 Subject: [PATCH 3/7] Reconnecting also on socket error --- front/src/Connection.ts | 66 ++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/front/src/Connection.ts b/front/src/Connection.ts index ea382365..1bd11948 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -148,10 +148,6 @@ export class Connection implements Connection { reconnection: false // Reconnection is handled by the application itself }); - this.socket.on(EventMessage.CONNECT_ERROR, () => { - console.error("Connection failed") - }); - this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => { console.error(EventMessage.MESSAGE_ERROR, message); }) @@ -160,17 +156,31 @@ export class Connection implements Connection { public static createConnection(name: string, characterSelected: string): Promise { return Axios.post(`${API_URL}/login`, {name: name}) .then((res) => { - let connection = new Connection(gameManager, name, characterSelected, res.data.token); - // FIXME: we should wait for the complete connexion here (i.e. the "connected" message from socket.io)! - // Otherwise, the connection MAY fail and we will never know! - return connection.connectSocketServer(); + return new Promise((resolve, reject) => { + let connection = new Connection(gameManager, name, characterSelected, res.data.token); + + connection.onConnectError((error: object) => { + console.log('An error occurred while connecting to socket server. Retrying'); + reject(error); + }); + + connection.socket.emit(EventMessage.SET_PLAYER_DETAILS, { + name: connection.name, + character: connection.character + } as SetPlayerDetailsMessage, (id: string) => { + connection.userId = id; + }); + + resolve(connection); + }); }) .catch((err) => { // Let's retry in 4-6 seconds return new Promise((resolve, reject) => { setTimeout(() => { - resolve(Connection.createConnection(name, characterSelected)); + Connection.createConnection(name, characterSelected).then((connection) => resolve(connection)) + .catch((error) => reject(error)); }, 4000 + Math.floor(Math.random() * 2000) ); }); @@ -185,40 +195,6 @@ export class Connection implements Connection { this.lastRoom = null; } - connectSocketServer(): Promise{ - return new Promise((resolve, reject) => { - this.socket.emit(EventMessage.SET_PLAYER_DETAILS, { - name: this.name, - character: this.character - } as SetPlayerDetailsMessage, (id: string) => { - this.userId = id; - }); - - //if try to reconnect with last position - /*if(this.lastRoom) { - //join the room - this.joinARoom(this.lastRoom, - this.lastPositionShared ? this.lastPositionShared.x : 0, - this.lastPositionShared ? this.lastPositionShared.y : 0, - this.lastPositionShared ? this.lastPositionShared.direction : PlayerAnimationNames.WalkDown, - this.lastPositionShared ? this.lastPositionShared.moving : false); - }*/ - - /*if(this.lastPositionShared) { - - //share your first position - this.sharePosition( - this.lastPositionShared ? this.lastPositionShared.x : 0, - this.lastPositionShared ? this.lastPositionShared.y : 0, - this.lastPositionShared.direction, - this.lastPositionShared.moving - ); - }*/ - - resolve(this); - }); - } - joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise { const point = new Point(startX, startY, direction, moving); @@ -262,6 +238,10 @@ export class Connection implements Connection { this.socket.on(EventMessage.GROUP_DELETE, callback) } + public onConnectError(callback: (error: object) => void): void { + this.socket.on(EventMessage.CONNECT_ERROR, callback) + } + sendWebrtcSignal(signal: unknown, roomId: string, userId? : string|null, receiverId? : string) { return this.socket.emit(EventMessage.WEBRTC_SIGNAL, { userId: userId ? userId : this.userId, From 1e4ffa20abba2ac0df883f5c1ef2eac816aefd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 22 Jun 2020 16:10:34 +0200 Subject: [PATCH 4/7] Cleaning GameManager --- front/src/Phaser/Game/GameManager.ts | 94 +--------------------------- 1 file changed, 1 insertion(+), 93 deletions(-) diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 5d77e6a7..071f1780 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -17,11 +17,6 @@ import {Scene} from "phaser"; import Axios from "axios"; import {API_URL} from "../../Enum/EnvironmentVariable"; -/*export enum StatusGameManagerEnum { - IN_PROGRESS = 1, - CURRENT_USER_CREATED = 2 -}*/ - export interface HasMovedEvent { direction: string; moving: boolean; @@ -35,27 +30,12 @@ export interface MapObject { } export class GameManager { - //status: number; - //private ConnectionInstance: Connection; - private currentGameScene: GameScene|null = null; private playerName: string; - SimplePeer : SimplePeer; private characterUserSelected: string; - constructor() { - //this.status = StatusGameManagerEnum.IN_PROGRESS; - } - - public storePlayerDetails(name: string, characterUserSelected : string) /*: Promise*/ { + public storePlayerDetails(name: string, characterUserSelected : string): void { this.playerName = name; this.characterUserSelected = characterUserSelected; - /*this.ConnectionInstance = new Connection(this); - return this.ConnectionInstance.createConnection(name, characterUserSelected).then((data : Connection) => { - this.SimplePeer = new SimplePeer(this.ConnectionInstance); - return data; - }).catch((err) => { - throw err; - });*/ } loadStartMap() : Promise { @@ -68,20 +48,6 @@ export class GameManager { }); } - setCurrentGameScene(gameScene: GameScene) { - this.currentGameScene = gameScene; - } - - - /** - * Permit to create player in started room - */ - /*createCurrentPlayer(): void { - //Get started room send by the backend - this.currentGameScene.createCurrentPlayer(); - //this.status = StatusGameManagerEnum.CURRENT_USER_CREATED; - }*/ - getPlayerName(): string { return this.playerName; } @@ -100,64 +66,6 @@ export class GameManager { } return sceneKey; } - - private oldSceneKey : string; - private oldMapUrlFile : string; - private oldInstance : string; - private scenePlugin: ScenePlugin; - private reconnectScene: Scene|null = null; - switchToDisconnectedScene(): void { - if (this.currentGameScene === null) { - return; - } - console.log('Switching to disconnected scene'); - this.oldSceneKey = this.currentGameScene.scene.key; - this.oldMapUrlFile = this.currentGameScene.MapUrlFile; - this.oldInstance = this.currentGameScene.instance; - this.currentGameScene.scene.start(ReconnectingSceneName); - this.reconnectScene = this.currentGameScene.scene.get(ReconnectingSceneName); - // Let's completely delete an purge the disconnected scene. We will start again from 0. - this.currentGameScene.scene.remove(this.oldSceneKey); - this.scenePlugin = this.currentGameScene.scene; - this.currentGameScene = null; - } - - /*private timeoutCallback: NodeJS.Timeout|null = null; - reconnectToGameScene(lastPositionShared: PointInterface): void { - if (this.timeoutCallback !== null) { - console.log('Reconnect called but setTimeout in progress for the reconnection'); - return; - } - if (this.reconnectScene === null) { - console.log('Reconnect called without switchToDisconnectedScene called first'); - - if (!this.currentGameScene) { - console.error('Reconnect called but we are not on a GameScene'); - return; - } - - // In case we are asked to reconnect even if switchToDisconnectedScene was not triggered (can happen when a laptop goes to sleep) - this.switchToDisconnectedScene(); - // Wait a bit for scene to load. Otherwise, starting ReconnectingSceneName and then starting GameScene one after the other fails for some reason. - this.timeoutCallback = setTimeout(() => { - console.log('Reconnecting to game scene from setTimeout'); - this.timeoutCallback = null; - this.reconnectToGameScene(lastPositionShared); - }, 500); - return; - } - console.log('Reconnecting to game scene'); - const game : Phaser.Scene = GameScene.createFromUrl(this.oldMapUrlFile, this.oldInstance); - this.reconnectScene.scene.add(this.oldSceneKey, game, true, { initPosition: lastPositionShared }); - this.reconnectScene = null; - }*/ - - private getCurrentGameScene(): GameScene { - if (this.currentGameScene === null) { - throw new Error('No current game scene enabled'); - } - return this.currentGameScene; - } } export const gameManager = new GameManager(); From 9b174836cdbc398cce27c7a311e50095cece3779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 22 Jun 2020 16:11:02 +0200 Subject: [PATCH 5/7] Making sure connection is established again EVEN if the tab is not displayed --- front/src/Phaser/Game/GameScene.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 3374fc08..503de384 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -129,7 +129,6 @@ export class GameScene extends Phaser.Scene { //hook preload scene preload(): void { - this.GameManager.setCurrentGameScene(this); this.load.on('filecomplete-tilemapJSON-'+this.MapKey, (key: string, type: string, data: unknown) => { this.onMapLoad(data); }); @@ -193,23 +192,23 @@ export class GameScene extends Phaser.Scene { let key = 'somekey'+Math.round(Math.random()*10000); const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, key); - this.scene.add(key, game, false, + this.scene.add(key, game, true, { initPosition: { x: this.CurrentPlayer.x, y: this.CurrentPlayer.y } }); - this.scene.start(key); + + this.scene.stop(this.scene.key); + this.scene.remove(this.scene.key); }) // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection); - if (this.scene.isSleeping()) { - this.scene.wake(); - this.scene.sleep(ReconnectingSceneName); - } + this.scene.wake(); + this.scene.sleep(ReconnectingSceneName); return connection; }); @@ -719,7 +718,6 @@ export class GameScene extends Phaser.Scene { } private doRemovePlayer(userId: string) { - //console.log('Removing player ', userId) const player = this.MapPlayersByKey.get(userId); if (player === undefined) { console.error('Cannot find user with id ', userId); From 3f927280a6a82c0421b15d8b2102d35c4a72cbc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 22 Jun 2020 16:11:48 +0200 Subject: [PATCH 6/7] Lint --- front/src/Connection.ts | 4 ++-- front/src/Phaser/Game/GameScene.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/front/src/Connection.ts b/front/src/Connection.ts index 1bd11948..cad639f9 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -158,7 +158,7 @@ export class Connection implements Connection { .then((res) => { return new Promise((resolve, reject) => { - let connection = new Connection(gameManager, name, characterSelected, res.data.token); + const connection = new Connection(gameManager, name, characterSelected, res.data.token); connection.onConnectError((error: object) => { console.log('An error occurred while connecting to socket server. Retrying'); @@ -199,7 +199,7 @@ export class Connection implements Connection { joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise { const point = new Point(startX, startY, direction, moving); this.lastPositionShared = point; - let promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { this.socket.emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { //this.GameManager.initUsersPosition(userPositions); resolve(userPositions); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 503de384..4a23a7c4 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -190,7 +190,7 @@ export class GameScene extends Phaser.Scene { this.simplePeer.closeAllConnections(); - let key = 'somekey'+Math.round(Math.random()*10000); + const key = 'somekey'+Math.round(Math.random()*10000); const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, key); this.scene.add(key, game, true, { From 407c6db070e48abf1f78754a22dd115e663b51db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 22 Jun 2020 17:14:56 +0200 Subject: [PATCH 7/7] Fixing difference between MapKey and scene key (prevented proper caching of resources) --- front/src/Phaser/Game/GameScene.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 4a23a7c4..67e953f2 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -106,15 +106,16 @@ export class GameScene extends Phaser.Scene { private startLayerName: string|undefined; static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene { + const mapKey = GameScene.getMapKeyByUrl(mapUrlFile); if (key === null) { - key = GameScene.getMapKeyByUrl(mapUrlFile); + key = mapKey; } - return new GameScene(key, mapUrlFile, instance); + return new GameScene(mapKey, mapUrlFile, instance, key); } - constructor(MapKey : string, MapUrlFile: string, instance: string) { + constructor(MapKey : string, MapUrlFile: string, instance: string, key: string) { super({ - key: MapKey + key: key }); this.GameManager = gameManager; @@ -124,7 +125,7 @@ export class GameScene extends Phaser.Scene { this.MapKey = MapKey; this.MapUrlFile = MapUrlFile; - this.RoomId = this.instance + '__' + GameScene.getMapKeyByUrl(MapUrlFile); + this.RoomId = this.instance + '__' + MapKey; } //hook preload scene