Plugin PositionNotifier into the main application.

This commit is contained in:
David Négrier 2020-09-15 16:21:41 +02:00
parent f8d462b0d7
commit d24ec0bd75
10 changed files with 170 additions and 62 deletions

View file

@ -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);
}

View file

@ -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 {

View file

@ -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<typeof isJoinRoomMessageInterface>;

View file

@ -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<string, UserInterface>();
this.groups = new Set<Group>();
@ -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);
}
}

View file

@ -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<UserInterface> = new Set<UserInterface>();
@ -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<UserInterface> {
return this.players;
}
}

View file

@ -139,13 +139,15 @@ describe("PositionNotifier", () => {
listenedZones: new Set<Zone>(),
} 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);
});
})

View file

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

View file

@ -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. */

View file

@ -182,11 +182,15 @@ export class Connection implements Connection {
}
public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise<MessageUserPositionInterface[]> {
public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean, viewport: ViewportInterface): Promise<MessageUserPositionInterface[]> {
const promise = new Promise<MessageUserPositionInterface[]>((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);
}

View file

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