Merge pull request #273 from thecodingmachine/viewport_sharing
[WIP] Performance: Sending position only for players in the viewport
This commit is contained in:
commit
30e15fbcab
@ -6,8 +6,8 @@ import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO f
|
|||||||
import Jwt, {JsonWebTokenError} from "jsonwebtoken";
|
import Jwt, {JsonWebTokenError} from "jsonwebtoken";
|
||||||
import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS, ALLOW_ARTILLERY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS, ALLOW_ARTILLERY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||||
import {World} from "../Model/World";
|
import {World} from "../Model/World";
|
||||||
import {Group} from "_Model/Group";
|
import {Group} from "../Model/Group";
|
||||||
import {UserInterface} from "_Model/UserInterface";
|
import {UserInterface} from "../Model/UserInterface";
|
||||||
import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage";
|
import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage";
|
||||||
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
|
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
|
||||||
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
|
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
|
||||||
@ -19,12 +19,14 @@ import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterfac
|
|||||||
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
||||||
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
||||||
import {uuid} from 'uuidv4';
|
import {uuid} from 'uuidv4';
|
||||||
|
import {isUserMovesInterface} from "../Model/Websocket/UserMovesMessage";
|
||||||
|
import {isViewport} from "../Model/Websocket/ViewportMessage";
|
||||||
|
|
||||||
enum SockerIoEvent {
|
enum SockerIoEvent {
|
||||||
CONNECTION = "connection",
|
CONNECTION = "connection",
|
||||||
DISCONNECT = "disconnect",
|
DISCONNECT = "disconnect",
|
||||||
JOIN_ROOM = "join-room", // bi-directional
|
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_MOVED = "user-moved", // From server to client
|
||||||
USER_LEFT = "user-left", // From server to client
|
USER_LEFT = "user-left", // From server to client
|
||||||
WEBRTC_SIGNAL = "webrtc-signal",
|
WEBRTC_SIGNAL = "webrtc-signal",
|
||||||
@ -36,6 +38,20 @@ enum SockerIoEvent {
|
|||||||
GROUP_DELETE = "group-delete",
|
GROUP_DELETE = "group-delete",
|
||||||
SET_PLAYER_DETAILS = "set-player-details",
|
SET_PLAYER_DETAILS = "set-player-details",
|
||||||
SET_SILENT = "set_silent", // Set or unset the silent mode for this user.
|
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: unknown): 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 {
|
export class IoSocketController {
|
||||||
@ -152,6 +168,11 @@ export class IoSocketController {
|
|||||||
ioConnection() {
|
ioConnection() {
|
||||||
this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
|
this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
|
||||||
const client : ExSocketInterface = socket as ExSocketInterface;
|
const client : ExSocketInterface = socket as ExSocketInterface;
|
||||||
|
client.batchedMessages = [];
|
||||||
|
client.batchTimeout = null;
|
||||||
|
client.emitInBatch = (event: string | symbol, payload: unknown): void => {
|
||||||
|
emitInBatch(client, event, payload);
|
||||||
|
}
|
||||||
this.sockets.set(client.userId, client);
|
this.sockets.set(client.userId, client);
|
||||||
|
|
||||||
// Let's log server load when a user joins
|
// Let's log server load when a user joins
|
||||||
@ -192,22 +213,24 @@ export class IoSocketController {
|
|||||||
//join new previous room
|
//join new previous room
|
||||||
const world = this.joinRoom(Client, roomId, message.position);
|
const world = this.joinRoom(Client, roomId, message.position);
|
||||||
|
|
||||||
//add function to refresh position user in real time.
|
const users = world.setViewport(Client, message.viewport);
|
||||||
//this.refreshUserPosition(Client);
|
const listOfUsers = users.map((user: UserInterface) => {
|
||||||
|
|
||||||
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 player: ExSocketInterface|undefined = this.sockets.get(user.id);
|
const player: ExSocketInterface|undefined = this.sockets.get(user.id);
|
||||||
if (player === undefined) {
|
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!");
|
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 null;
|
||||||
}
|
}
|
||||||
return new MessageUserPosition(user.id, player.name, player.characterLayers, player.position);
|
return new MessageUserPosition(user.id, player.name, player.characterLayers, player.position);
|
||||||
}).filter((item: MessageUserPosition|null) => item !== null);
|
}, 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);
|
answerFn(listOfUsers);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('An error occurred on "join_room" event');
|
console.error('An error occurred on "join_room" event');
|
||||||
@ -215,29 +238,53 @@ export class IoSocketController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on(SockerIoEvent.USER_POSITION, (position: unknown): void => {
|
socket.on(SockerIoEvent.SET_VIEWPORT, (message: unknown): void => {
|
||||||
console.log(SockerIoEvent.USER_POSITION, position);
|
|
||||||
try {
|
try {
|
||||||
if (!isPointInterface(position)) {
|
//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("In SET_VIEWPORT, 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 {
|
||||||
|
if (!isUserMovesInterface(userMovesMessage)) {
|
||||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'});
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Client = (socket as ExSocketInterface);
|
const Client = (socket as ExSocketInterface);
|
||||||
|
|
||||||
// sending to all clients in room except sender
|
// sending to all clients in room except sender
|
||||||
Client.position = position;
|
Client.position = userMovesMessage.position;
|
||||||
|
Client.viewport = userMovesMessage.viewport;
|
||||||
|
|
||||||
// update position in the world
|
// update position in the world
|
||||||
const world = this.Worlds.get(Client.roomId);
|
const world = this.Worlds.get(Client.roomId);
|
||||||
if (!world) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
world.updatePosition(Client, position);
|
world.updatePosition(Client, Client.position);
|
||||||
|
world.setViewport(Client, Client.viewport);
|
||||||
socket.to(Client.roomId).emit(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, Client.position));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('An error occurred on "user_position" event');
|
console.error('An error occurred on "user_position" event');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -312,7 +359,7 @@ export class IoSocketController {
|
|||||||
// update position in the world
|
// update position in the world
|
||||||
const world = this.Worlds.get(Client.roomId);
|
const world = this.Worlds.get(Client.roomId);
|
||||||
if (!world) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
world.setSilent(Client, silent);
|
world.setSilent(Client, silent);
|
||||||
@ -372,8 +419,6 @@ export class IoSocketController {
|
|||||||
// leave previous room and world
|
// leave previous room and world
|
||||||
if(Client.roomId){
|
if(Client.roomId){
|
||||||
try {
|
try {
|
||||||
Client.to(Client.roomId).emit(SockerIoEvent.USER_LEFT, Client.userId);
|
|
||||||
|
|
||||||
//user leave previous world
|
//user leave previous world
|
||||||
const world: World | undefined = this.Worlds.get(Client.roomId);
|
const world: World | undefined = this.Worlds.get(Client.roomId);
|
||||||
if (world) {
|
if (world) {
|
||||||
@ -409,6 +454,25 @@ export class IoSocketController {
|
|||||||
this.sendUpdateGroupEvent(group);
|
this.sendUpdateGroupEvent(group);
|
||||||
}, (groupUuid: string, lastUser: UserInterface) => {
|
}, (groupUuid: string, lastUser: UserInterface) => {
|
||||||
this.sendDeleteGroupEvent(groupUuid, lastUser);
|
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);
|
this.Worlds.set(roomId, world);
|
||||||
}
|
}
|
||||||
|
120
back/src/Model/PositionNotifier.ts
Normal file
120
back/src/Model/PositionNotifier.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldZones = user.listenedZones;
|
||||||
|
const newZones = new Set<Zone>();
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Also, let's stop listening on viewports
|
||||||
|
for (const zone of user.listenedZones) {
|
||||||
|
zone.stopListening(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getZone(i: number, j: number): Zone {
|
||||||
|
let zoneRow = this.zones[j];
|
||||||
|
if (zoneRow === undefined) {
|
||||||
|
zoneRow = new Array<Zone>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
import { Group } from "./Group";
|
import { Group } from "./Group";
|
||||||
import { PointInterface } from "./Websocket/PointInterface";
|
import { PointInterface } from "./Websocket/PointInterface";
|
||||||
|
import {Zone} from "_Model/Zone";
|
||||||
|
|
||||||
export interface UserInterface {
|
export interface UserInterface {
|
||||||
id: string,
|
id: string,
|
||||||
group?: Group,
|
group?: Group,
|
||||||
position: PointInterface,
|
position: PointInterface,
|
||||||
silent: boolean
|
silent: boolean,
|
||||||
|
listenedZones: Set<Zone>
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import {Socket} from "socket.io";
|
|||||||
import {PointInterface} from "./PointInterface";
|
import {PointInterface} from "./PointInterface";
|
||||||
import {Identificable} from "./Identificable";
|
import {Identificable} from "./Identificable";
|
||||||
import {TokenInterface} from "../../Controller/AuthenticateController";
|
import {TokenInterface} from "../../Controller/AuthenticateController";
|
||||||
|
import {ViewportInterface} from "_Model/Websocket/ViewportMessage";
|
||||||
|
|
||||||
export interface ExSocketInterface extends Socket, Identificable {
|
export interface ExSocketInterface extends Socket, Identificable {
|
||||||
token: string;
|
token: string;
|
||||||
@ -11,5 +12,12 @@ export interface ExSocketInterface extends Socket, Identificable {
|
|||||||
name: string;
|
name: string;
|
||||||
characterLayers: string[];
|
characterLayers: string[];
|
||||||
position: PointInterface;
|
position: PointInterface;
|
||||||
|
viewport: ViewportInterface;
|
||||||
isArtillery: boolean; // Whether this socket is opened by Artillery for load testing (hack)
|
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: unknown) => void;
|
||||||
|
batchedMessages: Array<{ event: string | symbol, payload: unknown }>;
|
||||||
|
batchTimeout: NodeJS.Timeout|null;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
import {isPointInterface} from "./PointInterface";
|
import {isPointInterface} from "./PointInterface";
|
||||||
|
import {isViewport} from "./ViewportMessage";
|
||||||
|
|
||||||
export const isJoinRoomMessageInterface =
|
export const isJoinRoomMessageInterface =
|
||||||
new tg.IsInterface().withProperties({
|
new tg.IsInterface().withProperties({
|
||||||
roomId: tg.isString,
|
roomId: tg.isString,
|
||||||
position: isPointInterface,
|
position: isPointInterface,
|
||||||
|
viewport: isViewport
|
||||||
}).get();
|
}).get();
|
||||||
export type JoinRoomMessageInterface = tg.GuardedType<typeof isJoinRoomMessageInterface>;
|
export type JoinRoomMessageInterface = tg.GuardedType<typeof isJoinRoomMessageInterface>;
|
||||||
|
11
back/src/Model/Websocket/UserMovesMessage.ts
Normal file
11
back/src/Model/Websocket/UserMovesMessage.ts
Normal file
@ -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<typeof isUserMovesInterface>;
|
10
back/src/Model/Websocket/ViewportMessage.ts
Normal file
10
back/src/Model/Websocket/ViewportMessage.ts
Normal file
@ -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<typeof isViewport>;
|
@ -6,6 +6,9 @@ import {UserInterface} from "./UserInterface";
|
|||||||
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
import {Identificable} from "_Model/Websocket/Identificable";
|
import {Identificable} from "_Model/Websocket/Identificable";
|
||||||
|
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 ConnectCallback = (user: string, group: Group) => void;
|
||||||
export type DisconnectCallback = (user: string, group: Group) => void;
|
export type DisconnectCallback = (user: string, group: Group) => void;
|
||||||
@ -27,12 +30,17 @@ export class World {
|
|||||||
private readonly groupUpdatedCallback: GroupUpdatedCallback;
|
private readonly groupUpdatedCallback: GroupUpdatedCallback;
|
||||||
private readonly groupDeletedCallback: GroupDeletedCallback;
|
private readonly groupDeletedCallback: GroupDeletedCallback;
|
||||||
|
|
||||||
|
private readonly positionNotifier: PositionNotifier;
|
||||||
|
|
||||||
constructor(connectCallback: ConnectCallback,
|
constructor(connectCallback: ConnectCallback,
|
||||||
disconnectCallback: DisconnectCallback,
|
disconnectCallback: DisconnectCallback,
|
||||||
minDistance: number,
|
minDistance: number,
|
||||||
groupRadius: number,
|
groupRadius: number,
|
||||||
groupUpdatedCallback: GroupUpdatedCallback,
|
groupUpdatedCallback: GroupUpdatedCallback,
|
||||||
groupDeletedCallback: GroupDeletedCallback)
|
groupDeletedCallback: GroupDeletedCallback,
|
||||||
|
onUserEnters: UserEntersCallback,
|
||||||
|
onUserMoves: UserMovesCallback,
|
||||||
|
onUserLeaves: UserLeavesCallback)
|
||||||
{
|
{
|
||||||
this.users = new Map<string, UserInterface>();
|
this.users = new Map<string, UserInterface>();
|
||||||
this.groups = new Set<Group>();
|
this.groups = new Set<Group>();
|
||||||
@ -42,6 +50,8 @@ export class World {
|
|||||||
this.groupRadius = groupRadius;
|
this.groupRadius = groupRadius;
|
||||||
this.groupUpdatedCallback = groupUpdatedCallback;
|
this.groupUpdatedCallback = groupUpdatedCallback;
|
||||||
this.groupDeletedCallback = groupDeletedCallback;
|
this.groupDeletedCallback = groupDeletedCallback;
|
||||||
|
// A zone is 10 sprites wide.
|
||||||
|
this.positionNotifier = new PositionNotifier(320, 320, onUserEnters, onUserMoves, onUserLeaves);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getGroups(): Group[] {
|
public getGroups(): Group[] {
|
||||||
@ -56,7 +66,8 @@ export class World {
|
|||||||
this.users.set(socket.userId, {
|
this.users.set(socket.userId, {
|
||||||
id: socket.userId,
|
id: socket.userId,
|
||||||
position: userPosition,
|
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<Zone>()
|
||||||
});
|
});
|
||||||
// Let's call update position to trigger the join / leave room
|
// Let's call update position to trigger the join / leave room
|
||||||
this.updatePosition(socket, userPosition);
|
this.updatePosition(socket, userPosition);
|
||||||
@ -71,6 +82,10 @@ export class World {
|
|||||||
this.leaveGroup(userObj);
|
this.leaveGroup(userObj);
|
||||||
}
|
}
|
||||||
this.users.delete(user.userId);
|
this.users.delete(user.userId);
|
||||||
|
|
||||||
|
if (userObj !== undefined) {
|
||||||
|
this.positionNotifier.leave(userObj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEmpty(): boolean {
|
public isEmpty(): boolean {
|
||||||
@ -83,6 +98,8 @@ export class World {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.positionNotifier.updatePosition(user, userPosition);
|
||||||
|
|
||||||
user.position = userPosition;
|
user.position = userPosition;
|
||||||
|
|
||||||
if (user.silent) {
|
if (user.silent) {
|
||||||
@ -316,4 +333,12 @@ export class World {
|
|||||||
}
|
}
|
||||||
return 0;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
96
back/src/Model/Zone.ts
Normal file
96
back/src/Model/Zone.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import {UserInterface} from "./UserInterface";
|
||||||
|
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||||
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
|
|
||||||
|
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>();
|
||||||
|
private listeners: Set<UserInterface> = new Set<UserInterface>();
|
||||||
|
|
||||||
|
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, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, listener);
|
||||||
|
} else {
|
||||||
|
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, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public startListening(listener: UserInterface): void {
|
||||||
|
for (const player of this.players) {
|
||||||
|
if (player !== listener) {
|
||||||
|
this.onUserEnters(player, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.add(listener);
|
||||||
|
listener.listenedZones.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopListening(listener: UserInterface): void {
|
||||||
|
for (const player of this.players) {
|
||||||
|
if (player !== listener) {
|
||||||
|
this.onUserLeaves(player, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
listener.listenedZones.delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPlayers(): Set<UserInterface> {
|
||||||
|
return this.players;
|
||||||
|
}
|
||||||
|
}
|
196
back/tests/PositionNotifierTest.ts
Normal file
196
back/tests/PositionNotifierTest.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = {
|
||||||
|
id: "1",
|
||||||
|
position: {
|
||||||
|
x: 500,
|
||||||
|
y: 500,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
},
|
||||||
|
listenedZones: new Set<Zone>(),
|
||||||
|
} as UserInterface;
|
||||||
|
|
||||||
|
const user2 = {
|
||||||
|
id: "2",
|
||||||
|
position: {
|
||||||
|
x: -9999,
|
||||||
|
y: -9999,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
},
|
||||||
|
listenedZones: new Set<Zone>(),
|
||||||
|
} 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = {
|
||||||
|
id: "1",
|
||||||
|
position: {
|
||||||
|
x: 500,
|
||||||
|
y: 500,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
},
|
||||||
|
listenedZones: new Set<Zone>(),
|
||||||
|
} as UserInterface;
|
||||||
|
|
||||||
|
const user2 = {
|
||||||
|
id: "2",
|
||||||
|
position: {
|
||||||
|
x: -9999,
|
||||||
|
y: -9999,
|
||||||
|
moving: false,
|
||||||
|
direction: 'down'
|
||||||
|
},
|
||||||
|
listenedZones: new Set<Zone>(),
|
||||||
|
} as UserInterface;
|
||||||
|
|
||||||
|
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);
|
||||||
|
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.
|
||||||
|
newUsers = 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;
|
||||||
|
expect(newUsers.length).toBe(1);
|
||||||
|
});
|
||||||
|
})
|
@ -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));
|
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));
|
world.join({ userId: "foo" }, new Point(100, 100));
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ describe("World", () => {
|
|||||||
disconnectCallNumber++;
|
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));
|
world.join({ userId: "foo" }, new Point(100, 100));
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each 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. */
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
"outDir": "./dist", /* Redirect output structure to the directory. */
|
"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. */
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
@ -28,16 +28,27 @@ scenarios:
|
|||||||
y: 170
|
y: 170
|
||||||
direction: 'down'
|
direction: 'down'
|
||||||
moving: false
|
moving: false
|
||||||
|
viewport:
|
||||||
|
left: 500
|
||||||
|
top: 0
|
||||||
|
right: 800
|
||||||
|
bottom: 200
|
||||||
- think: 1
|
- think: 1
|
||||||
- loop:
|
- loop:
|
||||||
- function: "setYRandom"
|
- function: "setYRandom"
|
||||||
- emit:
|
- emit:
|
||||||
channel: "user-position"
|
channel: "user-position"
|
||||||
data:
|
data:
|
||||||
|
position:
|
||||||
x: "{{ x }}"
|
x: "{{ x }}"
|
||||||
y: "{{ y }}"
|
y: "{{ y }}"
|
||||||
direction: 'down'
|
direction: 'down'
|
||||||
moving: false
|
moving: false
|
||||||
|
viewport:
|
||||||
|
left: "{{ left }}"
|
||||||
|
top: "{{ top }}"
|
||||||
|
right: "{{ right }}"
|
||||||
|
bottom: "{{ bottom }}"
|
||||||
- think: 0.2
|
- think: 0.2
|
||||||
count: 100
|
count: 100
|
||||||
- think: 10
|
- think: 10
|
||||||
|
@ -5,7 +5,16 @@ module.exports = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function setYRandom(context, events, done) {
|
function setYRandom(context, events, done) {
|
||||||
context.vars.x = (883 + Math.round(Math.random() * 300));
|
if (context.angle === undefined) {
|
||||||
context.vars.y = (270 + Math.round(Math.random() * 300));
|
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;
|
||||||
|
context.vars.bottom = context.vars.y + 200;
|
||||||
return done();
|
return done();
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ enum EventMessage{
|
|||||||
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
|
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
|
||||||
WEBRTC_START = "webrtc-start",
|
WEBRTC_START = "webrtc-start",
|
||||||
JOIN_ROOM = "join-room", // bi-directional
|
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_MOVED = "user-moved", // From server to client
|
||||||
USER_LEFT = "user-left", // From server to client
|
USER_LEFT = "user-left", // From server to client
|
||||||
MESSAGE_ERROR = "message-error",
|
MESSAGE_ERROR = "message-error",
|
||||||
@ -25,6 +25,8 @@ enum EventMessage{
|
|||||||
|
|
||||||
CONNECT_ERROR = "connect_error",
|
CONNECT_ERROR = "connect_error",
|
||||||
SET_SILENT = "set_silent", // Set or unset the silent mode for this user.
|
SET_SILENT = "set_silent", // Set or unset the silent mode for this user.
|
||||||
|
SET_VIEWPORT = "set-viewport",
|
||||||
|
BATCH = "batch",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PointInterface {
|
export interface PointInterface {
|
||||||
@ -95,6 +97,23 @@ export interface StartMapInterface {
|
|||||||
startInstance: string
|
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: unknown
|
||||||
|
}
|
||||||
|
|
||||||
export class Connection implements Connection {
|
export class Connection implements Connection {
|
||||||
private readonly socket: Socket;
|
private readonly socket: Socket;
|
||||||
private userId: string|null = null;
|
private userId: string|null = null;
|
||||||
@ -111,6 +130,18 @@ export class Connection implements Connection {
|
|||||||
this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => {
|
this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => {
|
||||||
console.error(EventMessage.MESSAGE_ERROR, message);
|
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<Connection> {
|
public static createConnection(name: string, characterLayersSelected: string[]): Promise<Connection> {
|
||||||
@ -151,27 +182,35 @@ 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) => {
|
const promise = new Promise<MessageUserPositionInterface[]>((resolve, reject) => {
|
||||||
this.socket.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 },
|
||||||
|
viewport,
|
||||||
|
}, (userPositions: MessageUserPositionInterface[]) => {
|
||||||
resolve(userPositions);
|
resolve(userPositions);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
return promise;
|
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){
|
if(!this.socket){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const point = new Point(x, y, direction, moving);
|
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 {
|
public setSilent(silent: boolean): void {
|
||||||
this.socket.emit(EventMessage.SET_SILENT, silent);
|
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 {
|
public onUserJoins(callback: (message: MessageUserJoined) => void): void {
|
||||||
this.socket.on(EventMessage.JOIN_ROOM, callback);
|
this.socket.on(EventMessage.JOIN_ROOM, callback);
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
private startLayerName: string|undefined;
|
private startLayerName: string|undefined;
|
||||||
private presentationModeSprite!: Sprite;
|
private presentationModeSprite!: Sprite;
|
||||||
private chatModeSprite!: Sprite;
|
private chatModeSprite!: Sprite;
|
||||||
private repositionCallback!: (this: Window, ev: UIEvent) => void;
|
private onResizeCallback!: (this: Window, ev: UIEvent) => void;
|
||||||
private gameMap!: GameMap;
|
private gameMap!: GameMap;
|
||||||
|
|
||||||
static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene {
|
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.stop(this.scene.key);
|
||||||
this.scene.remove(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
|
// When connection is performed, let's connect SimplePeer
|
||||||
@ -412,8 +412,8 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
this.switchLayoutMode();
|
this.switchLayoutMode();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.repositionCallback = this.reposition.bind(this);
|
this.onResizeCallback = this.onResize.bind(this);
|
||||||
window.addEventListener('resize', this.repositionCallback);
|
window.addEventListener('resize', this.onResizeCallback);
|
||||||
this.reposition();
|
this.reposition();
|
||||||
|
|
||||||
// From now, this game scene will be notified of reposition events
|
// 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
|
//join room
|
||||||
this.connectionPromise.then((connection: Connection) => {
|
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);
|
this.initUsersPosition(userPositions);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -677,7 +687,13 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
private doPushPlayerPosition(event: HasMovedEvent): void {
|
private doPushPlayerPosition(event: HasMovedEvent): void {
|
||||||
this.lastMoveEventSent = event;
|
this.lastMoveEventSent = event;
|
||||||
this.lastSentTick = this.currentTick;
|
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(){
|
EventToClickOnTile(){
|
||||||
@ -741,7 +757,7 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
this.simplePeer.unregister();
|
this.simplePeer.unregister();
|
||||||
this.scene.stop();
|
this.scene.stop();
|
||||||
this.scene.remove(this.scene.key);
|
this.scene.remove(this.scene.key);
|
||||||
window.removeEventListener('resize', this.repositionCallback);
|
window.removeEventListener('resize', this.onResizeCallback);
|
||||||
this.scene.start(nextSceneKey.key, {
|
this.scene.start(nextSceneKey.key, {
|
||||||
startLayerName: nextSceneKey.hash
|
startLayerName: nextSceneKey.hash
|
||||||
});
|
});
|
||||||
@ -930,6 +946,19 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||||||
return mapUrlStart.substring(startPos, endPos);
|
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 {
|
private reposition(): void {
|
||||||
this.presentationModeSprite.setY(this.game.renderer.height - 2);
|
this.presentationModeSprite.setY(this.game.renderer.height - 2);
|
||||||
this.chatModeSprite.setY(this.game.renderer.height - 2);
|
this.chatModeSprite.setY(this.game.renderer.height - 2);
|
||||||
|
Loading…
Reference in New Issue
Block a user