From 9b702c75e3f17b7a4f200eecc173150b7501cbed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Sep 2020 10:06:11 +0200 Subject: [PATCH 1/4] Adding batched messages + the notion of notifier / zones (not plugged in the system yet) --- back/src/Controller/IoSocketController.ts | 52 ++++- back/src/Model/PositionNotifier.ts | 106 ++++++++++ back/src/Model/UserInterface.ts | 4 +- back/src/Model/Websocket/ExSocketInterface.ts | 8 + back/src/Model/Websocket/UserMovesMessage.ts | 11 + back/src/Model/Websocket/ViewportMessage.ts | 10 + back/src/Model/World.ts | 4 +- back/src/Model/Zone.ts | 85 ++++++++ back/tests/PositionNotifierTest.ts | 193 ++++++++++++++++++ benchmark/socketio-load-test.yaml | 14 +- benchmark/socketioLoadTest.js | 8 +- front/src/Connection.ts | 37 +++- front/src/Phaser/Game/GameScene.ts | 8 +- 13 files changed, 518 insertions(+), 22 deletions(-) create mode 100644 back/src/Model/PositionNotifier.ts create mode 100644 back/src/Model/Websocket/UserMovesMessage.ts create mode 100644 back/src/Model/Websocket/ViewportMessage.ts create mode 100644 back/src/Model/Zone.ts create mode 100644 back/tests/PositionNotifierTest.ts diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 34e0dbc8..228a0da8 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -6,8 +6,8 @@ import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO f import Jwt, {JsonWebTokenError} from "jsonwebtoken"; import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS, ALLOW_ARTILLERY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." import {World} from "../Model/World"; -import {Group} from "_Model/Group"; -import {UserInterface} from "_Model/UserInterface"; +import {Group} from "../Model/Group"; +import {UserInterface} from "../Model/UserInterface"; import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage"; import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined"; import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved"; @@ -19,12 +19,13 @@ import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterfac import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage"; import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface"; import {uuid} from 'uuidv4'; +import {isUserMovesInterface} from "../Model/Websocket/UserMovesMessage"; enum SockerIoEvent { CONNECTION = "connection", DISCONNECT = "disconnect", JOIN_ROOM = "join-room", // bi-directional - USER_POSITION = "user-position", // bi-directional + USER_POSITION = "user-position", // From client to server USER_MOVED = "user-moved", // From server to client USER_LEFT = "user-left", // From server to client WEBRTC_SIGNAL = "webrtc-signal", @@ -36,6 +37,20 @@ enum SockerIoEvent { GROUP_DELETE = "group-delete", SET_PLAYER_DETAILS = "set-player-details", SET_SILENT = "set_silent", // Set or unset the silent mode for this user. + SET_VIEWPORT = "set-viewport", + BATCH = "batch", +} + +function emitInBatch(socket: ExSocketInterface, event: string | symbol, payload: any): void { + socket.batchedMessages.push({ event, payload}); + + if (socket.batchTimeout === null) { + socket.batchTimeout = setTimeout(() => { + socket.emit(SockerIoEvent.BATCH, socket.batchedMessages); + socket.batchedMessages = []; + socket.batchTimeout = null; + }, 100); + } } export class IoSocketController { @@ -152,6 +167,11 @@ export class IoSocketController { ioConnection() { this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => { const client : ExSocketInterface = socket as ExSocketInterface; + client.batchedMessages = []; + client.batchTimeout = null; + client.emitInBatch = (event: string | symbol, payload: any): void => { + emitInBatch(client, event, payload); + } this.sockets.set(client.userId, client); // Let's log server load when a user joins @@ -215,19 +235,20 @@ export class IoSocketController { } }); - socket.on(SockerIoEvent.USER_POSITION, (position: unknown): void => { - console.log(SockerIoEvent.USER_POSITION, position); + socket.on(SockerIoEvent.USER_POSITION, (userMovesMessage: unknown): void => { + console.log(SockerIoEvent.USER_POSITION, userMovesMessage); try { - if (!isPointInterface(position)) { + if (!isUserMovesInterface(userMovesMessage)) { socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'}); - console.warn('Invalid USER_POSITION message received: ', position); + console.warn('Invalid USER_POSITION message received: ', userMovesMessage); return; } const Client = (socket as ExSocketInterface); // sending to all clients in room except sender - Client.position = position; + Client.position = userMovesMessage.position; + Client.viewport = userMovesMessage.viewport; // update position in the world const world = this.Worlds.get(Client.roomId); @@ -235,9 +256,20 @@ export class IoSocketController { console.error("Could not find world with id '", Client.roomId, "'"); return; } - world.updatePosition(Client, position); + world.updatePosition(Client, Client.position); - socket.to(Client.roomId).emit(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, 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)); } catch (e) { console.error('An error occurred on "user_position" event'); console.error(e); diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts new file mode 100644 index 00000000..f5edf8d3 --- /dev/null +++ b/back/src/Model/PositionNotifier.ts @@ -0,0 +1,106 @@ +/** + * Tracks the position of every player on the map, and sends notifications to the players interested in knowing about the move + * (i.e. players that are looking at the zone the player is currently in) + * + * Internally, the PositionNotifier works with Zones. A zone is a square area of a map. + * Each player is in a given zone, and each player tracks one or many zones (depending on the player viewport) + * + * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted + * number of players around the current player. + */ +import {UserEntersCallback, UserLeavesCallback, UserMovesCallback, Zone} from "./Zone"; +import {PointInterface} from "_Model/Websocket/PointInterface"; +import {UserInterface} from "_Model/UserInterface"; +import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; + +interface ZoneDescriptor { + i: number; + j: number; +} + +export class PositionNotifier { + + // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) + + private zones: Zone[][] = []; + + constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: UserEntersCallback, private onUserMoves: UserMovesCallback, private onUserLeaves: UserLeavesCallback) { + } + + private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { + return { + i: Math.floor(x / this.zoneWidth), + j: Math.floor(y / this.zoneHeight), + } + } + + public setViewport(user: UserInterface, viewport: ViewportInterface): void { + if (viewport.left > viewport.right || viewport.top > viewport.bottom) { + console.warn('Invalid viewport received: ', viewport); + return; + } + + const oldZones = user.listenedZones; + const newZones = new Set(); + + const topLeftDesc = this.getZoneDescriptorFromCoordinates(viewport.left, viewport.top); + const bottomRightDesc = this.getZoneDescriptorFromCoordinates(viewport.right, viewport.bottom); + + for (let j = topLeftDesc.j; j <= bottomRightDesc.j; j++) { + for (let i = topLeftDesc.i; i <= bottomRightDesc.i; i++) { + newZones.add(this.getZone(i, j)); + } + } + + const addedZones = [...newZones].filter(x => !oldZones.has(x)); + const removedZones = [...oldZones].filter(x => !newZones.has(x)); + + for (const zone of addedZones) { + zone.startListening(user); + } + for (const zone of removedZones) { + zone.stopListening(user); + } + } + + public updatePosition(user: UserInterface, userPosition: PointInterface): void { + // Did we change zone? + const oldZoneDesc = this.getZoneDescriptorFromCoordinates(user.position.x, user.position.y); + const newZoneDesc = this.getZoneDescriptorFromCoordinates(userPosition.x, userPosition.y); + + if (oldZoneDesc.i != newZoneDesc.i || oldZoneDesc.j != newZoneDesc.j) { + const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j); + const newZone = this.getZone(newZoneDesc.i, newZoneDesc.j); + + // Leave old zone + oldZone.leave(user, newZone); + + // Enter new zone + newZone.enter(user, oldZone, userPosition); + } else { + const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j); + zone.move(user, userPosition); + } + } + + public leave(user: UserInterface): void { + const oldZoneDesc = this.getZoneDescriptorFromCoordinates(user.position.x, user.position.y); + const oldZone = this.getZone(oldZoneDesc.i, oldZoneDesc.j); + oldZone.leave(user, null); + } + + private getZone(i: number, j: number): Zone { + let zoneRow = this.zones[j]; + if (zoneRow === undefined) { + zoneRow = new Array(); + this.zones[j] = zoneRow; + } + + let zone = this.zones[j][i]; + if (zone === undefined) { + zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves); + this.zones[j][i] = zone; + } + return zone; + } +} diff --git a/back/src/Model/UserInterface.ts b/back/src/Model/UserInterface.ts index 89994a31..d19ecd6f 100644 --- a/back/src/Model/UserInterface.ts +++ b/back/src/Model/UserInterface.ts @@ -1,9 +1,11 @@ import { Group } from "./Group"; import { PointInterface } from "./Websocket/PointInterface"; +import {Zone} from "_Model/Zone"; export interface UserInterface { id: string, group?: Group, position: PointInterface, - silent: boolean + silent: boolean, + listenedZones: Set } diff --git a/back/src/Model/Websocket/ExSocketInterface.ts b/back/src/Model/Websocket/ExSocketInterface.ts index 974fe63d..c5132eb7 100644 --- a/back/src/Model/Websocket/ExSocketInterface.ts +++ b/back/src/Model/Websocket/ExSocketInterface.ts @@ -2,6 +2,7 @@ import {Socket} from "socket.io"; import {PointInterface} from "./PointInterface"; import {Identificable} from "./Identificable"; import {TokenInterface} from "../../Controller/AuthenticateController"; +import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; export interface ExSocketInterface extends Socket, Identificable { token: string; @@ -11,5 +12,12 @@ export interface ExSocketInterface extends Socket, Identificable { name: string; characterLayers: string[]; position: PointInterface; + viewport: ViewportInterface; isArtillery: boolean; // Whether this socket is opened by Artillery for load testing (hack) + /** + * Pushes an event that will be sent in the next batch of events + */ + emitInBatch: (event: string | symbol, payload: any) => void; + batchedMessages: Array<{ event: string | symbol, payload: any }>; + batchTimeout: NodeJS.Timeout|null; } diff --git a/back/src/Model/Websocket/UserMovesMessage.ts b/back/src/Model/Websocket/UserMovesMessage.ts new file mode 100644 index 00000000..2277d4c4 --- /dev/null +++ b/back/src/Model/Websocket/UserMovesMessage.ts @@ -0,0 +1,11 @@ +import * as tg from "generic-type-guard"; +import {isPointInterface} from "./PointInterface"; +import {isViewport} from "./ViewportMessage"; + + +export const isUserMovesInterface = + new tg.IsInterface().withProperties({ + position: isPointInterface, + viewport: isViewport, + }).get(); +export type UserMovesInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/ViewportMessage.ts b/back/src/Model/Websocket/ViewportMessage.ts new file mode 100644 index 00000000..62e2fc81 --- /dev/null +++ b/back/src/Model/Websocket/ViewportMessage.ts @@ -0,0 +1,10 @@ +import * as tg from "generic-type-guard"; + +export const isViewport = + new tg.IsInterface().withProperties({ + left: tg.isNumber, + top: tg.isNumber, + right: tg.isNumber, + bottom: tg.isNumber, + }).get(); +export type ViewportInterface = tg.GuardedType; diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 8855702e..0f2cb050 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -6,6 +6,7 @@ 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"; export type ConnectCallback = (user: string, group: Group) => void; export type DisconnectCallback = (user: string, group: Group) => void; @@ -56,7 +57,8 @@ export class World { this.users.set(socket.userId, { id: socket.userId, position: userPosition, - silent: false // FIXME: silent should be set at the correct value when joining a room. + silent: false, // FIXME: silent should be set at the correct value when joining a room. + listenedZones: new Set() }); // Let's call update position to trigger the join / leave room this.updatePosition(socket, userPosition); diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts new file mode 100644 index 00000000..45d6cdd2 --- /dev/null +++ b/back/src/Model/Zone.ts @@ -0,0 +1,85 @@ +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 class Zone { + private players: Set = new Set(); + private listeners: Set = new Set(); + + constructor(private onUserEnters: UserEntersCallback, private onUserMoves: UserMovesCallback, private onUserLeaves: UserLeavesCallback) { + } + + /** + * A user leaves the zone + */ + public leave(user: UserInterface, newZone: Zone|null) { + this.players.delete(user); + this.notifyUserLeft(user, newZone); + } + + /** + * Notify listeners of this zone that this user left + */ + 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); + } + } + } + + public enter(user: UserInterface, oldZone: Zone|null, position: PointInterface) { + this.players.add(user); + this.notifyUserEnter(user, oldZone, position); + } + + /** + * Notify listeners of this zone that this user entered + */ + private notifyUserEnter(user: UserInterface, oldZone: Zone|null, position: PointInterface) { + for (const listener of this.listeners) { + if (listener === user) { + continue; + } + if (oldZone === null || !listener.listenedZones.has(oldZone)) { + this.onUserEnters(user); + } else { + this.onUserMoves(user, position); + } + } + } + + public move(user: UserInterface, position: PointInterface) { + for (const listener of this.listeners) { + if (listener !== user) { + this.onUserMoves(user,position); + } + } + } + + public startListening(user: UserInterface): void { + for (const player of this.players) { + if (player !== user) { + this.onUserEnters(user); + } + } + + this.listeners.add(user); + user.listenedZones.add(this); + } + + public stopListening(user: UserInterface): void { + for (const player of this.players) { + if (player !== user) { + this.onUserLeaves(user); + } + } + + this.listeners.delete(user); + user.listenedZones.delete(this); + } +} diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts new file mode 100644 index 00000000..833852b0 --- /dev/null +++ b/back/tests/PositionNotifierTest.ts @@ -0,0 +1,193 @@ +import "jasmine"; +import {World, ConnectCallback, DisconnectCallback } from "../src/Model/World"; +import {Point} from "../src/Model/Websocket/MessageUserPosition"; +import { Group } from "../src/Model/Group"; +import {PositionNotifier} from "../src/Model/PositionNotifier"; +import {UserInterface} from "../src/Model/UserInterface"; +import {PointInterface} from "../src/Model/Websocket/PointInterface"; +import {Zone} from "_Model/Zone"; + +function move(user: UserInterface, x: number, y: number, positionNotifier: PositionNotifier): void { + positionNotifier.updatePosition(user, { + x, + y, + moving: false, + direction: 'down' + }); + user.position.x = x; + user.position.y = y; +} + +describe("PositionNotifier", () => { + it("should receive notifications when player moves", () => { + let enterTriggered = false; + let moveTriggered = false; + let leaveTriggered = false; + + const positionNotifier = new PositionNotifier(300, 300, (user: UserInterface) => { + enterTriggered = true; + }, (user: UserInterface, position: PointInterface) => { + moveTriggered = true; + }, (user: UserInterface) => { + leaveTriggered = true; + }); + + let user1 = { + id: "1", + position: { + x: 500, + y: 500, + moving: false, + direction: 'down' + }, + listenedZones: new Set(), + } as UserInterface; + + let user2 = { + id: "2", + position: { + x: -9999, + y: -9999, + moving: false, + direction: 'down' + }, + listenedZones: new Set(), + } as UserInterface; + + positionNotifier.setViewport(user1, { + left: 200, + right: 600, + top: 100, + bottom: 500 + }); + + move(user2, 500, 500, positionNotifier); + + expect(enterTriggered).toBe(true); + expect(moveTriggered).toBe(false); + enterTriggered = false; + + // Move inside the zone + move(user2, 501, 500, positionNotifier); + + expect(enterTriggered).toBe(false); + expect(moveTriggered).toBe(true); + moveTriggered = false; + + // Move out of the zone in a zone that we don't track + move(user2, 901, 500, positionNotifier); + + expect(enterTriggered).toBe(false); + expect(moveTriggered).toBe(false); + expect(leaveTriggered).toBe(true); + leaveTriggered = false; + + // Move back in + move(user2, 500, 500, positionNotifier); + expect(enterTriggered).toBe(true); + expect(moveTriggered).toBe(false); + expect(leaveTriggered).toBe(false); + enterTriggered = false; + + // Move out of the zone in a zone that we do track + move(user2, 200, 500, positionNotifier); + expect(enterTriggered).toBe(false); + expect(moveTriggered).toBe(true); + expect(leaveTriggered).toBe(false); + moveTriggered = false; + + // Leave the room + positionNotifier.leave(user2); + expect(enterTriggered).toBe(false); + expect(moveTriggered).toBe(false); + expect(leaveTriggered).toBe(true); + leaveTriggered = false; + }); + + it("should receive notifications when camera moves", () => { + let enterTriggered = false; + let moveTriggered = false; + let leaveTriggered = false; + + const positionNotifier = new PositionNotifier(300, 300, (user: UserInterface) => { + enterTriggered = true; + }, (user: UserInterface, position: PointInterface) => { + moveTriggered = true; + }, (user: UserInterface) => { + leaveTriggered = true; + }); + + let user1 = { + id: "1", + position: { + x: 500, + y: 500, + moving: false, + direction: 'down' + }, + listenedZones: new Set(), + } as UserInterface; + + let user2 = { + id: "2", + position: { + x: -9999, + y: -9999, + moving: false, + direction: 'down' + }, + listenedZones: new Set(), + } as UserInterface; + + positionNotifier.setViewport(user1, { + left: 200, + right: 600, + top: 100, + bottom: 500 + }); + + move(user2, 500, 500, positionNotifier); + + expect(enterTriggered).toBe(true); + expect(moveTriggered).toBe(false); + enterTriggered = false; + + // Move the viewport but the user stays inside. + positionNotifier.setViewport(user1, { + left: 201, + right: 601, + top: 100, + bottom: 500 + }); + + expect(enterTriggered).toBe(false); + expect(moveTriggered).toBe(false); + expect(leaveTriggered).toBe(false); + + // Move the viewport out of the user. + positionNotifier.setViewport(user1, { + left: 901, + right: 1001, + top: 100, + bottom: 500 + }); + + expect(enterTriggered).toBe(false); + expect(moveTriggered).toBe(false); + expect(leaveTriggered).toBe(true); + leaveTriggered = false; + + // Move the viewport back on the user. + positionNotifier.setViewport(user1, { + left: 200, + right: 600, + top: 100, + bottom: 500 + }); + + expect(enterTriggered).toBe(true); + expect(moveTriggered).toBe(false); + expect(leaveTriggered).toBe(false); + enterTriggered = false; + }); +}) diff --git a/benchmark/socketio-load-test.yaml b/benchmark/socketio-load-test.yaml index 2f9f689d..e84b0402 100644 --- a/benchmark/socketio-load-test.yaml +++ b/benchmark/socketio-load-test.yaml @@ -34,10 +34,16 @@ scenarios: - emit: channel: "user-position" data: - x: "{{ x }}" - y: "{{ y }}" - direction: 'down' - moving: false + position: + x: "{{ x }}" + y: "{{ y }}" + direction: 'down' + moving: false + viewport: + left: "{{ left }}" + top: "{{ top }}" + right: "{{ right }}" + bottom: "{{ bottom }}" - think: 0.2 count: 100 - think: 10 diff --git a/benchmark/socketioLoadTest.js b/benchmark/socketioLoadTest.js index 907982b2..540cd8bd 100644 --- a/benchmark/socketioLoadTest.js +++ b/benchmark/socketioLoadTest.js @@ -5,7 +5,11 @@ module.exports = { }; function setYRandom(context, events, done) { - context.vars.x = (883 + Math.round(Math.random() * 300)); - context.vars.y = (270 + Math.round(Math.random() * 300)); + context.vars.x = (0 + Math.round(Math.random() * 1472)); + context.vars.y = (0 + Math.round(Math.random() * 1090)); + context.vars.left = context.vars.x - 320; + context.vars.top = context.vars.y - 200; + context.vars.right = context.vars.x + 320; + context.vars.bottom = context.vars.y + 200; return done(); } diff --git a/front/src/Connection.ts b/front/src/Connection.ts index 4a184c52..fd06329d 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -14,7 +14,7 @@ enum EventMessage{ WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal", WEBRTC_START = "webrtc-start", JOIN_ROOM = "join-room", // bi-directional - USER_POSITION = "user-position", // bi-directional + USER_POSITION = "user-position", // From client to server USER_MOVED = "user-moved", // From server to client USER_LEFT = "user-left", // From server to client MESSAGE_ERROR = "message-error", @@ -25,6 +25,8 @@ enum EventMessage{ CONNECT_ERROR = "connect_error", SET_SILENT = "set_silent", // Set or unset the silent mode for this user. + SET_VIEWPORT = "set-viewport", + BATCH = "batch", } export interface PointInterface { @@ -95,6 +97,23 @@ export interface StartMapInterface { startInstance: string } +export interface ViewportInterface { + left: number, + top: number, + right: number, + bottom: number, +} + +export interface UserMovesInterface { + position: PositionInterface, + viewport: ViewportInterface, +} + +export interface BatchedMessageInterface { + event: string, + payload: any +} + export class Connection implements Connection { private readonly socket: Socket; private userId: string|null = null; @@ -111,6 +130,18 @@ export class Connection implements Connection { this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => { console.error(EventMessage.MESSAGE_ERROR, message); }) + + /** + * Messages inside batched messages are extracted and sent to listeners directly. + */ + this.socket.on(EventMessage.BATCH, (batchedMessages: BatchedMessageInterface[]) => { + for (const message of batchedMessages) { + const listeners = this.socket.listeners(message.event); + for (const listener of listeners) { + listener(message.payload); + } + } + }) } public static createConnection(name: string, characterLayersSelected: string[]): Promise { @@ -160,12 +191,12 @@ export class Connection implements Connection { return promise; } - public sharePosition(x : number, y : number, direction : string, moving: boolean) : void{ + public sharePosition(x : number, y : number, direction : string, moving: boolean, viewport: ViewportInterface) : void{ if(!this.socket){ return; } const point = new Point(x, y, direction, moving); - this.socket.emit(EventMessage.USER_POSITION, point); + this.socket.emit(EventMessage.USER_POSITION, { position: point, viewport } as UserMovesInterface); } public setSilent(silent: boolean): void { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 8a5630dc..595fc9d3 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -677,7 +677,13 @@ export class GameScene extends Phaser.Scene implements CenterListener { private doPushPlayerPosition(event: HasMovedEvent): void { this.lastMoveEventSent = event; this.lastSentTick = this.currentTick; - this.connection.sharePosition(event.x, event.y, event.direction, event.moving); + const camera = this.cameras.main; + this.connection.sharePosition(event.x, event.y, event.direction, event.moving, { + left: camera.scrollX, + top: camera.scrollY, + right: camera.scrollX + camera.width, + bottom: camera.scrollY + camera.height, + }); } EventToClickOnTile(){ From f8d462b0d7231bc244b95b87983103e9d512b31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Sep 2020 10:10:35 +0200 Subject: [PATCH 2/4] Fixing "any" type --- back/src/Controller/IoSocketController.ts | 4 ++-- back/src/Model/Websocket/ExSocketInterface.ts | 4 ++-- back/tests/PositionNotifierTest.ts | 8 ++++---- front/src/Connection.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 228a0da8..e18a5beb 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -41,7 +41,7 @@ enum SockerIoEvent { BATCH = "batch", } -function emitInBatch(socket: ExSocketInterface, event: string | symbol, payload: any): void { +function emitInBatch(socket: ExSocketInterface, event: string | symbol, payload: unknown): void { socket.batchedMessages.push({ event, payload}); if (socket.batchTimeout === null) { @@ -169,7 +169,7 @@ export class IoSocketController { const client : ExSocketInterface = socket as ExSocketInterface; client.batchedMessages = []; client.batchTimeout = null; - client.emitInBatch = (event: string | symbol, payload: any): void => { + client.emitInBatch = (event: string | symbol, payload: unknown): void => { emitInBatch(client, event, payload); } this.sockets.set(client.userId, client); diff --git a/back/src/Model/Websocket/ExSocketInterface.ts b/back/src/Model/Websocket/ExSocketInterface.ts index c5132eb7..bbe18cbb 100644 --- a/back/src/Model/Websocket/ExSocketInterface.ts +++ b/back/src/Model/Websocket/ExSocketInterface.ts @@ -17,7 +17,7 @@ export interface ExSocketInterface extends Socket, Identificable { /** * Pushes an event that will be sent in the next batch of events */ - emitInBatch: (event: string | symbol, payload: any) => void; - batchedMessages: Array<{ event: string | symbol, payload: any }>; + emitInBatch: (event: string | symbol, payload: unknown) => void; + batchedMessages: Array<{ event: string | symbol, payload: unknown }>; batchTimeout: NodeJS.Timeout|null; } diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts index 833852b0..ac82878b 100644 --- a/back/tests/PositionNotifierTest.ts +++ b/back/tests/PositionNotifierTest.ts @@ -32,7 +32,7 @@ describe("PositionNotifier", () => { leaveTriggered = true; }); - let user1 = { + const user1 = { id: "1", position: { x: 500, @@ -43,7 +43,7 @@ describe("PositionNotifier", () => { listenedZones: new Set(), } as UserInterface; - let user2 = { + const user2 = { id: "2", position: { x: -9999, @@ -117,7 +117,7 @@ describe("PositionNotifier", () => { leaveTriggered = true; }); - let user1 = { + const user1 = { id: "1", position: { x: 500, @@ -128,7 +128,7 @@ describe("PositionNotifier", () => { listenedZones: new Set(), } as UserInterface; - let user2 = { + const user2 = { id: "2", position: { x: -9999, diff --git a/front/src/Connection.ts b/front/src/Connection.ts index fd06329d..c04e5b4f 100644 --- a/front/src/Connection.ts +++ b/front/src/Connection.ts @@ -111,7 +111,7 @@ export interface UserMovesInterface { export interface BatchedMessageInterface { event: string, - payload: any + payload: unknown } export class Connection implements Connection { 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 3/4] 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); From 7e7b42ce19eb64502b1af2a1c9443bef000b0016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 16 Sep 2020 09:31:44 +0200 Subject: [PATCH 4/4] Changing load test to run in circles --- back/src/Controller/IoSocketController.ts | 16 ++++++++++++---- benchmark/socketio-load-test.yaml | 5 +++++ benchmark/socketioLoadTest.js | 9 +++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 878c6c75..c1c00761 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -223,6 +223,14 @@ export class IoSocketController { return new MessageUserPosition(user.id, player.name, player.characterLayers, player.position); }, users); + //console.warn('ANSWER PLAYER POSITIONS', listOfUsers); + if (answerFn === undefined && ALLOW_ARTILLERY === true) { + /*console.error("TYPEOF answerFn", typeof(answerFn)); + console.error("answerFn", answerFn); + process.exit(1)*/ + // For some reason, answerFn can be undefined if we use Artillery (?) + return; + } answerFn(listOfUsers); } catch (e) { console.error('An error occurred on "join_room" event'); @@ -244,7 +252,7 @@ export class IoSocketController { const world = this.Worlds.get(Client.roomId); if (!world) { - console.error("Could not find world with id '", Client.roomId, "'"); + console.error("In SET_VIEWPORT, could not find world with id '", Client.roomId, "'"); return; } world.setViewport(Client, Client.viewport); @@ -255,7 +263,7 @@ export class IoSocketController { }); socket.on(SockerIoEvent.USER_POSITION, (userMovesMessage: unknown): void => { - console.log(SockerIoEvent.USER_POSITION, userMovesMessage); + //console.log(SockerIoEvent.USER_POSITION, userMovesMessage); try { if (!isUserMovesInterface(userMovesMessage)) { socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'}); @@ -272,7 +280,7 @@ export class IoSocketController { // update position in the world const world = this.Worlds.get(Client.roomId); if (!world) { - console.error("Could not find world with id '", Client.roomId, "'"); + console.error("In USER_POSITION, could not find world with id '", Client.roomId, "'"); return; } world.updatePosition(Client, Client.position); @@ -351,7 +359,7 @@ export class IoSocketController { // update position in the world const world = this.Worlds.get(Client.roomId); if (!world) { - console.error("Could not find world with id '", Client.roomId, "'"); + console.error("In SET_SILENT, could not find world with id '", Client.roomId, "'"); return; } world.setSilent(Client, silent); diff --git a/benchmark/socketio-load-test.yaml b/benchmark/socketio-load-test.yaml index e84b0402..df2f580b 100644 --- a/benchmark/socketio-load-test.yaml +++ b/benchmark/socketio-load-test.yaml @@ -28,6 +28,11 @@ scenarios: y: 170 direction: 'down' moving: false + viewport: + left: 500 + top: 0 + right: 800 + bottom: 200 - think: 1 - loop: - function: "setYRandom" diff --git a/benchmark/socketioLoadTest.js b/benchmark/socketioLoadTest.js index 540cd8bd..f898d7b9 100644 --- a/benchmark/socketioLoadTest.js +++ b/benchmark/socketioLoadTest.js @@ -5,8 +5,13 @@ module.exports = { }; function setYRandom(context, events, done) { - context.vars.x = (0 + Math.round(Math.random() * 1472)); - context.vars.y = (0 + Math.round(Math.random() * 1090)); + if (context.angle === undefined) { + context.angle = Math.random() * Math.PI * 2; + } + context.angle += 0.05; + + context.vars.x = 320 + 1472/2 * (1 + Math.sin(context.angle)); + context.vars.y = 200 + 1090/2 * (1 + Math.cos(context.angle)); context.vars.left = context.vars.x - 320; context.vars.top = context.vars.y - 200; context.vars.right = context.vars.x + 320;