From d24ec0bd75d7930ed698137391357a94d49b3d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Sep 2020 16:21:41 +0200 Subject: [PATCH] Plugin PositionNotifier into the main application. --- back/src/Controller/IoSocketController.ts | 74 ++++++++++++++------- back/src/Model/PositionNotifier.ts | 18 ++++- back/src/Model/Websocket/JoinRoomMessage.ts | 2 + back/src/Model/World.ts | 27 +++++++- back/src/Model/Zone.ts | 45 ++++++++----- back/tests/PositionNotifierTest.ts | 7 +- back/tests/WorldTest.ts | 6 +- back/tsconfig.json | 2 +- front/src/Connection.ts | 16 +++-- front/src/Phaser/Game/GameScene.ts | 35 ++++++++-- 10 files changed, 170 insertions(+), 62 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index e18a5beb..878c6c75 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -20,6 +20,7 @@ import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMes import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface"; import {uuid} from 'uuidv4'; import {isUserMovesInterface} from "../Model/Websocket/UserMovesMessage"; +import {isViewport} from "../Model/Websocket/ViewportMessage"; enum SockerIoEvent { CONNECTION = "connection", @@ -212,22 +213,16 @@ export class IoSocketController { //join new previous room const world = this.joinRoom(Client, roomId, message.position); - //add function to refresh position user in real time. - //this.refreshUserPosition(Client); - - const messageUserJoined = new MessageUserJoined(Client.userId, Client.name, Client.characterLayers, Client.position); - - socket.to(roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserJoined); - - // The answer shall contain the list of all users of the room with their positions: - const listOfUsers = Array.from(world.getUsers(), ([key, user]) => { + const users = world.setViewport(Client, message.viewport); + const listOfUsers = users.map((user: UserInterface) => { const player: ExSocketInterface|undefined = this.sockets.get(user.id); if (player === undefined) { console.warn('Something went wrong. The World contains a user "'+user.id+"' but this user does not exist in the sockets list!"); return null; } return new MessageUserPosition(user.id, player.name, player.characterLayers, player.position); - }).filter((item: MessageUserPosition|null) => item !== null); + }, users); + answerFn(listOfUsers); } catch (e) { console.error('An error occurred on "join_room" event'); @@ -235,6 +230,30 @@ export class IoSocketController { } }); + socket.on(SockerIoEvent.SET_VIEWPORT, (message: unknown): void => { + try { + //console.log('SET_VIEWPORT') + if (!isViewport(message)) { + socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_VIEWPORT message.'}); + console.warn('Invalid SET_VIEWPORT message received: ', message); + return; + } + + const Client = (socket as ExSocketInterface); + Client.viewport = message; + + const world = this.Worlds.get(Client.roomId); + if (!world) { + console.error("Could not find world with id '", Client.roomId, "'"); + return; + } + world.setViewport(Client, Client.viewport); + } catch (e) { + console.error('An error occurred on "SET_VIEWPORT" event'); + console.error(e); + } + }); + socket.on(SockerIoEvent.USER_POSITION, (userMovesMessage: unknown): void => { console.log(SockerIoEvent.USER_POSITION, userMovesMessage); try { @@ -257,19 +276,7 @@ export class IoSocketController { return; } world.updatePosition(Client, Client.position); - - const clientsInRoom = this.Io.sockets.adapter.rooms[Client.roomId]; - console.log('clientsInRoom', clientsInRoom); - for (const clientId in clientsInRoom.sockets) { - console.log('client: %s', clientId); - const targetSocket = this.Io.sockets.connected[clientId] as ExSocketInterface; - if (socket === targetSocket) { - continue; - } - //targetSocket.emit(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, Client.position)); - targetSocket.emitInBatch(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, Client.position)); - } - //socket.to(Client.roomId).emit(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, Client.position)); + world.setViewport(Client, Client.viewport); } catch (e) { console.error('An error occurred on "user_position" event'); console.error(e); @@ -404,8 +411,6 @@ export class IoSocketController { // leave previous room and world if(Client.roomId){ try { - Client.to(Client.roomId).emit(SockerIoEvent.USER_LEFT, Client.userId); - //user leave previous world const world: World | undefined = this.Worlds.get(Client.roomId); if (world) { @@ -441,6 +446,25 @@ export class IoSocketController { this.sendUpdateGroupEvent(group); }, (groupUuid: string, lastUser: UserInterface) => { this.sendDeleteGroupEvent(groupUuid, lastUser); + }, (user, listener) => { + const clientUser = this.searchClientByIdOrFail(user.id); + const clientListener = this.searchClientByIdOrFail(listener.id); + const messageUserJoined = new MessageUserJoined(clientUser.userId, clientUser.name, clientUser.characterLayers, clientUser.position); + + clientListener.emit(SockerIoEvent.JOIN_ROOM, messageUserJoined); + //console.log("Sending JOIN_ROOM event"); + }, (user, position, listener) => { + const clientUser = this.searchClientByIdOrFail(user.id); + const clientListener = this.searchClientByIdOrFail(listener.id); + + clientListener.emitInBatch(SockerIoEvent.USER_MOVED, new MessageUserMoved(clientUser.userId, clientUser.position)); + //console.log("Sending USER_MOVED event"); + }, (user, listener) => { + const clientUser = this.searchClientByIdOrFail(user.id); + const clientListener = this.searchClientByIdOrFail(listener.id); + + clientListener.emit(SockerIoEvent.USER_LEFT, clientUser.userId); + //console.log("Sending USER_LEFT event"); }); this.Worlds.set(roomId, world); } diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts index f5edf8d3..9d6975e3 100644 --- a/back/src/Model/PositionNotifier.ts +++ b/back/src/Model/PositionNotifier.ts @@ -34,10 +34,14 @@ export class PositionNotifier { } } - public setViewport(user: UserInterface, viewport: ViewportInterface): void { + /** + * Sets the viewport coordinates. + * Returns the list of new users to add + */ + public setViewport(user: UserInterface, viewport: ViewportInterface): UserInterface[] { if (viewport.left > viewport.right || viewport.top > viewport.bottom) { console.warn('Invalid viewport received: ', viewport); - return; + return []; } const oldZones = user.listenedZones; @@ -55,12 +59,17 @@ export class PositionNotifier { const addedZones = [...newZones].filter(x => !oldZones.has(x)); const removedZones = [...oldZones].filter(x => !newZones.has(x)); + + let users: UserInterface[] = []; for (const zone of addedZones) { zone.startListening(user); + users = users.concat(Array.from(zone.getPlayers())) } for (const zone of removedZones) { zone.stopListening(user); } + + return users; } public updatePosition(user: UserInterface, userPosition: PointInterface): void { @@ -87,6 +96,11 @@ export class PositionNotifier { const oldZoneDesc = this.getZoneDescriptorFromCoordinates(user.position.x, user.position.y); const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j); oldZone.leave(user, null); + + // Also, let's stop listening on viewports + for (const zone of user.listenedZones) { + zone.stopListening(user); + } } private getZone(i: number, j: number): Zone { diff --git a/back/src/Model/Websocket/JoinRoomMessage.ts b/back/src/Model/Websocket/JoinRoomMessage.ts index 16613488..2036a441 100644 --- a/back/src/Model/Websocket/JoinRoomMessage.ts +++ b/back/src/Model/Websocket/JoinRoomMessage.ts @@ -1,9 +1,11 @@ import * as tg from "generic-type-guard"; import {isPointInterface} from "./PointInterface"; +import {isViewport} from "./ViewportMessage"; export const isJoinRoomMessageInterface = new tg.IsInterface().withProperties({ roomId: tg.isString, position: isPointInterface, + viewport: isViewport }).get(); export type JoinRoomMessageInterface = tg.GuardedType; diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 0f2cb050..4422e95f 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -6,7 +6,9 @@ import {UserInterface} from "./UserInterface"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; import {PositionInterface} from "_Model/PositionInterface"; import {Identificable} from "_Model/Websocket/Identificable"; -import {Zone} from "_Model/Zone"; +import {UserEntersCallback, UserLeavesCallback, UserMovesCallback, Zone} from "_Model/Zone"; +import {PositionNotifier} from "./PositionNotifier"; +import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; export type ConnectCallback = (user: string, group: Group) => void; export type DisconnectCallback = (user: string, group: Group) => void; @@ -28,12 +30,17 @@ export class World { private readonly groupUpdatedCallback: GroupUpdatedCallback; private readonly groupDeletedCallback: GroupDeletedCallback; + private readonly positionNotifier: PositionNotifier; + constructor(connectCallback: ConnectCallback, disconnectCallback: DisconnectCallback, minDistance: number, groupRadius: number, groupUpdatedCallback: GroupUpdatedCallback, - groupDeletedCallback: GroupDeletedCallback) + groupDeletedCallback: GroupDeletedCallback, + onUserEnters: UserEntersCallback, + onUserMoves: UserMovesCallback, + onUserLeaves: UserLeavesCallback) { this.users = new Map(); this.groups = new Set(); @@ -43,6 +50,8 @@ export class World { this.groupRadius = groupRadius; this.groupUpdatedCallback = groupUpdatedCallback; this.groupDeletedCallback = groupDeletedCallback; + // A zone is 10 sprites wide. + this.positionNotifier = new PositionNotifier(320, 320, onUserEnters, onUserMoves, onUserLeaves); } public getGroups(): Group[] { @@ -73,6 +82,10 @@ export class World { this.leaveGroup(userObj); } this.users.delete(user.userId); + + if (userObj !== undefined) { + this.positionNotifier.leave(userObj); + } } public isEmpty(): boolean { @@ -85,6 +98,8 @@ export class World { return; } + this.positionNotifier.updatePosition(user, userPosition); + user.position = userPosition; if (user.silent) { @@ -318,4 +333,12 @@ export class World { } return 0; }*/ + setViewport(socket : Identificable, viewport: ViewportInterface): UserInterface[] { + const user = this.users.get(socket.userId); + if(typeof user === 'undefined') { + console.warn('In setViewport, could not find user with ID "'+socket.userId+'" in world.'); + return []; + } + return this.positionNotifier.setViewport(user, viewport); + } } diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts index 45d6cdd2..bd748b0f 100644 --- a/back/src/Model/Zone.ts +++ b/back/src/Model/Zone.ts @@ -2,9 +2,9 @@ import {UserInterface} from "./UserInterface"; import {PointInterface} from "_Model/Websocket/PointInterface"; import {PositionInterface} from "_Model/PositionInterface"; -export type UserEntersCallback = (user: UserInterface) => void; -export type UserMovesCallback = (user: UserInterface, position: PointInterface) => void; -export type UserLeavesCallback = (user: UserInterface) => void; +export type UserEntersCallback = (user: UserInterface, listener: UserInterface) => void; +export type UserMovesCallback = (user: UserInterface, position: PointInterface, listener: UserInterface) => void; +export type UserLeavesCallback = (user: UserInterface, listener: UserInterface) => void; export class Zone { private players: Set = new Set(); @@ -27,7 +27,7 @@ export class Zone { private notifyUserLeft(user: UserInterface, newZone: Zone|null) { for (const listener of this.listeners) { if (listener !== user && (newZone === null || !listener.listenedZones.has(newZone))) { - this.onUserLeaves(user); + this.onUserLeaves(user, listener); } } } @@ -46,40 +46,51 @@ export class Zone { continue; } if (oldZone === null || !listener.listenedZones.has(oldZone)) { - this.onUserEnters(user); + this.onUserEnters(user, listener); } else { - this.onUserMoves(user, position); + this.onUserMoves(user, position, listener); } } } public move(user: UserInterface, position: PointInterface) { + if (!this.players.has(user)) { + this.players.add(user); + const foo = this.players; + this.notifyUserEnter(user, null, position); + return; + } + for (const listener of this.listeners) { if (listener !== user) { - this.onUserMoves(user,position); + this.onUserMoves(user,position, listener); } } } - public startListening(user: UserInterface): void { + public startListening(listener: UserInterface): void { for (const player of this.players) { - if (player !== user) { - this.onUserEnters(user); + if (player !== listener) { + this.onUserEnters(player, listener); } } - this.listeners.add(user); - user.listenedZones.add(this); + this.listeners.add(listener); + listener.listenedZones.add(this); } - public stopListening(user: UserInterface): void { + public stopListening(listener: UserInterface): void { for (const player of this.players) { - if (player !== user) { - this.onUserLeaves(user); + if (player !== listener) { + this.onUserLeaves(player, listener); } } - this.listeners.delete(user); - user.listenedZones.delete(this); + this.listeners.delete(listener); + listener.listenedZones.delete(this); + } + + public getPlayers(): Set { + return this.players; } } diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts index ac82878b..0b8b466f 100644 --- a/back/tests/PositionNotifierTest.ts +++ b/back/tests/PositionNotifierTest.ts @@ -139,13 +139,15 @@ describe("PositionNotifier", () => { listenedZones: new Set(), } as UserInterface; - positionNotifier.setViewport(user1, { + let newUsers = positionNotifier.setViewport(user1, { left: 200, right: 600, top: 100, bottom: 500 }); + expect(newUsers.length).toBe(0); + move(user2, 500, 500, positionNotifier); expect(enterTriggered).toBe(true); @@ -178,7 +180,7 @@ describe("PositionNotifier", () => { leaveTriggered = false; // Move the viewport back on the user. - positionNotifier.setViewport(user1, { + newUsers = positionNotifier.setViewport(user1, { left: 200, right: 600, top: 100, @@ -189,5 +191,6 @@ describe("PositionNotifier", () => { expect(moveTriggered).toBe(false); expect(leaveTriggered).toBe(false); enterTriggered = false; + expect(newUsers.length).toBe(1); }); }) diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index c436eed7..580677c7 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -13,7 +13,7 @@ describe("World", () => { } - const world = new World(connect, disconnect, 160, 160, () => {}, () => {}); + const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}, () => {}, () => {}); world.join({ userId: "foo" }, new Point(100, 100)); @@ -40,7 +40,7 @@ describe("World", () => { } - const world = new World(connect, disconnect, 160, 160, () => {}, () => {}); + const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}, () => {}, () => {}); world.join({ userId: "foo" }, new Point(100, 100)); @@ -69,7 +69,7 @@ describe("World", () => { disconnectCallNumber++; } - const world = new World(connect, disconnect, 160, 160, () => {}, () => {}); + const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}, () => {}, () => {}); world.join({ userId: "foo" }, new Point(100, 100)); diff --git a/back/tsconfig.json b/back/tsconfig.json index 397bb8a2..de6314a3 100644 --- a/back/tsconfig.json +++ b/back/tsconfig.json @@ -12,7 +12,7 @@ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./dist", /* Redirect output structure to the directory. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ diff --git a/front/src/Connection.ts b/front/src/Connection.ts index c04e5b4f..dee27ae5 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -182,11 +182,15 @@ export class Connection implements Connection { } - public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise { + public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean, viewport: ViewportInterface): Promise { const promise = new Promise((resolve, reject) => { - this.socket.emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (userPositions: MessageUserPositionInterface[]) => { - resolve(userPositions); - }); + this.socket.emit(EventMessage.JOIN_ROOM, { + roomId, + position: {x: startX, y: startY, direction, moving }, + viewport, + }, (userPositions: MessageUserPositionInterface[]) => { + resolve(userPositions); + }); }) return promise; } @@ -203,6 +207,10 @@ export class Connection implements Connection { this.socket.emit(EventMessage.SET_SILENT, silent); } + public setViewport(viewport: ViewportInterface): void { + this.socket.emit(EventMessage.SET_VIEWPORT, viewport); + } + public onUserJoins(callback: (message: MessageUserJoined) => void): void { this.socket.on(EventMessage.JOIN_ROOM, callback); } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 595fc9d3..6a6656c2 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -111,7 +111,7 @@ export class GameScene extends Phaser.Scene implements CenterListener { private startLayerName: string|undefined; private presentationModeSprite!: Sprite; private chatModeSprite!: Sprite; - private repositionCallback!: (this: Window, ev: UIEvent) => void; + private onResizeCallback!: (this: Window, ev: UIEvent) => void; private gameMap!: GameMap; static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene { @@ -226,7 +226,7 @@ export class GameScene extends Phaser.Scene implements CenterListener { this.scene.stop(this.scene.key); this.scene.remove(this.scene.key); - window.removeEventListener('resize', this.repositionCallback); + window.removeEventListener('resize', this.onResizeCallback); }) // When connection is performed, let's connect SimplePeer @@ -412,8 +412,8 @@ export class GameScene extends Phaser.Scene implements CenterListener { this.switchLayoutMode(); }); - this.repositionCallback = this.reposition.bind(this); - window.addEventListener('resize', this.repositionCallback); + this.onResizeCallback = this.onResize.bind(this); + window.addEventListener('resize', this.onResizeCallback); this.reposition(); // From now, this game scene will be notified of reposition events @@ -636,7 +636,17 @@ export class GameScene extends Phaser.Scene implements CenterListener { //join room this.connectionPromise.then((connection: Connection) => { - connection.joinARoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false).then((userPositions: MessageUserPositionInterface[]) => { + const camera = this.cameras.main; + connection.joinARoom(this.RoomId, + this.startX, + this.startY, + PlayerAnimationNames.WalkDown, + false, { + left: camera.scrollX, + top: camera.scrollY, + right: camera.scrollX + camera.width, + bottom: camera.scrollY + camera.height, + }).then((userPositions: MessageUserPositionInterface[]) => { this.initUsersPosition(userPositions); }); @@ -747,7 +757,7 @@ export class GameScene extends Phaser.Scene implements CenterListener { this.simplePeer.unregister(); this.scene.stop(); this.scene.remove(this.scene.key); - window.removeEventListener('resize', this.repositionCallback); + window.removeEventListener('resize', this.onResizeCallback); this.scene.start(nextSceneKey.key, { startLayerName: nextSceneKey.hash }); @@ -936,6 +946,19 @@ export class GameScene extends Phaser.Scene implements CenterListener { return mapUrlStart.substring(startPos, endPos); } + private onResize(): void { + this.reposition(); + + // Send new viewport to server + const camera = this.cameras.main; + this.connection.setViewport({ + left: camera.scrollX, + top: camera.scrollY, + right: camera.scrollX + camera.width, + bottom: camera.scrollY + camera.height, + }); + } + private reposition(): void { this.presentationModeSprite.setY(this.game.renderer.height - 2); this.chatModeSprite.setY(this.game.renderer.height - 2);