From 02e6b50b1619562beafcd9975acc9aa6ce5bf9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 8 May 2020 00:35:36 +0200 Subject: [PATCH 1/2] Adding the display of a circle around the group This PR adds the display of a circle around groups. This is useful to view where you need to go to speak to someone but also to debug. Note: implementation is suboptimal, relying on a "graphics" object that is known to be slow. In the future, we need to use a circle as a sprite instead. --- back/src/Controller/IoSocketController.ts | 40 +++++++++++++++++++++-- back/src/Model/World.ts | 20 +++++++++++- back/tests/WorldTest.ts | 6 ++-- front/src/Connexion.ts | 32 ++++++++++++++++-- front/src/Phaser/Game/GameManager.ts | 32 +++++++++++++++++- front/src/Phaser/Game/GameScene.ts | 34 ++++++++++++++++++- front/src/Phaser/Player/Animation.ts | 1 - 7 files changed, 153 insertions(+), 12 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 64012349..d865fc7c 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -9,6 +9,7 @@ import {ExtRooms, RefreshUserPositionFunction} from "../Model/Websocket/ExtRoom" import {ExtRoomsInterface} from "../Model/Websocket/ExtRoomsInterface"; import {World} from "../Model/World"; import {Group} from "_Model/Group"; +import {UserInterface} from "_Model/UserInterface"; enum SockerIoEvent { CONNECTION = "connection", @@ -20,6 +21,8 @@ enum SockerIoEvent { WEBRTC_START = "webrtc-start", WEBRTC_DISCONNECT = "webrtc-disconect", MESSAGE_ERROR = "message-error", + GROUP_CREATE_UPDATE = "group-create-update", + GROUP_DELETE = "group-delete", } export class IoSocketController { @@ -51,7 +54,38 @@ export class IoSocketController { this.connectedUser(user1, group); }, (user1: string, group: Group) => { this.disConnectedUser(user1, group); - }, MINIMUM_DISTANCE, GROUP_RADIUS); + }, MINIMUM_DISTANCE, GROUP_RADIUS, (group: Group) => { + this.sendUpdateGroupEvent(group); + }, (groupUuid: string, lastUser: UserInterface) => { + this.sendDeleteGroupEvent(groupUuid, lastUser); + }); + } + + private sendUpdateGroupEvent(group: Group): void { + // Let's get the room of the group. To do this, let's get anyone in the group and find its room. + // Note: this is suboptimal + let userId = group.getUsers()[0].id; + let client: ExSocketInterface|null = this.searchClientById(userId); + if (client === null) { + return; + } + let roomId = client.roomId; + this.Io.in(roomId).emit(SockerIoEvent.GROUP_CREATE_UPDATE, { + position: group.getPosition(), + groupId: group.getId() + }); + } + + private sendDeleteGroupEvent(uuid: string, lastUser: UserInterface): void { + // Let's get the room of the group. To do this, let's get anyone in the group and find its room. + // Note: this is suboptimal + let userId = lastUser.id; + let client: ExSocketInterface|null = this.searchClientById(userId); + if (client === null) { + return; + } + let roomId = client.roomId; + this.Io.in(roomId).emit(SockerIoEvent.GROUP_DELETE, uuid); } ioConnection() { @@ -149,7 +183,7 @@ export class IoSocketController { } /** - * + * TODO: each call to this method is suboptimal. It means that instead of passing an ID, we should pass a client object. * @param userId */ searchClientById(userId: string): ExSocketInterface | null { @@ -286,7 +320,7 @@ export class IoSocketController { this.joinWebRtcRoom(Client, group.getId()); } - //connected user + //disconnect user disConnectedUser(userId: string, group: Group) { let Client = this.searchClientById(userId); if (!Client) { diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 19eb8ed8..180740b2 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -9,6 +9,10 @@ import {PositionInterface} from "_Model/PositionInterface"; export type ConnectCallback = (user: string, group: Group) => void; export type DisconnectCallback = (user: string, group: Group) => void; +// callback called when a group is created or moved or changes users +export type GroupUpdatedCallback = (group: Group) => void; +export type GroupDeletedCallback = (uuid: string, lastUser: UserInterface) => void; + export class World { private minDistance: number; private groupRadius: number; @@ -19,11 +23,15 @@ export class World { private connectCallback: ConnectCallback; private disconnectCallback: DisconnectCallback; + private groupUpdatedCallback: GroupUpdatedCallback; + private groupDeletedCallback: GroupDeletedCallback; constructor(connectCallback: ConnectCallback, disconnectCallback: DisconnectCallback, minDistance: number, - groupRadius: number) + groupRadius: number, + groupUpdatedCallback: GroupUpdatedCallback, + groupDeletedCallback: GroupDeletedCallback) { this.users = new Map(); this.groups = []; @@ -31,6 +39,8 @@ export class World { this.disconnectCallback = disconnectCallback; this.minDistance = minDistance; this.groupRadius = groupRadius; + this.groupUpdatedCallback = groupUpdatedCallback; + this.groupDeletedCallback = groupDeletedCallback; } public join(userPosition: MessageUserPosition): void { @@ -86,6 +96,11 @@ export class World { this.leaveGroup(user); } } + + // At the very end, if the user is part of a group, let's call the callback to update group position + if (typeof user.group !== 'undefined') { + this.groupUpdatedCallback(user.group); + } } /** @@ -101,12 +116,15 @@ export class World { group.leave(user); if (group.isEmpty()) { + this.groupDeletedCallback(group.getId(), user); group.destroy(); const index = this.groups.indexOf(group, 0); if (index === -1) { throw new Error("Could not find group"); } this.groups.splice(index, 1); + } else { + this.groupUpdatedCallback(group); } } diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index d14aaeb0..57f0f3f4 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -15,7 +15,7 @@ describe("World", () => { } - let world = new World(connect, disconnect, 160, 160); + let world = new World(connect, disconnect, 160, 160, () => {}, () => {}); world.join(new MessageUserPosition({ userId: "foo", @@ -62,7 +62,7 @@ describe("World", () => { } - let world = new World(connect, disconnect, 160, 160); + let world = new World(connect, disconnect, 160, 160, () => {}, () => {}); world.join(new MessageUserPosition({ userId: "foo", @@ -107,7 +107,7 @@ describe("World", () => { disconnectCallNumber++; } - let world = new World(connect, disconnect, 160, 160); + let world = new World(connect, disconnect, 160, 160, () => {}, () => {}); world.join(new MessageUserPosition({ userId: "foo", diff --git a/front/src/Connexion.ts b/front/src/Connexion.ts index 4dfddee1..d4accde2 100644 --- a/front/src/Connexion.ts +++ b/front/src/Connexion.ts @@ -11,7 +11,9 @@ enum EventMessage{ JOIN_ROOM = "join-room", USER_POSITION = "user-position", MESSAGE_ERROR = "message-error", - WEBRTC_DISCONNECT = "webrtc-disconect" + WEBRTC_DISCONNECT = "webrtc-disconect", + GROUP_CREATE_UPDATE = "group-create-update", + GROUP_DELETE = "group-delete", } class Message { @@ -122,6 +124,16 @@ class ListMessageUserPosition { } } +export interface PositionInterface { + x: number, + y: number +} + +export interface GroupCreatedUpdatedMessageInterface { + position: PositionInterface, + groupId: string +} + export interface ConnexionInterface { socket: any; token: string; @@ -184,6 +196,9 @@ export class Connexion implements ConnexionInterface { this.errorMessage(); + this.groupUpdatedOrCreated(); + this.groupDeleted(); + return this; }) .catch((err) => { @@ -254,6 +269,19 @@ export class Connexion implements ConnexionInterface { }); } + private groupUpdatedOrCreated(): void { + this.socket.on(EventMessage.GROUP_CREATE_UPDATE, (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => { + //console.log('Group ', groupCreateUpdateMessage.groupId, " position :", groupCreateUpdateMessage.position.x, groupCreateUpdateMessage.position.y) + this.GameManager.shareGroupPosition(groupCreateUpdateMessage); + }) + } + + private groupDeleted(): void { + this.socket.on(EventMessage.GROUP_DELETE, (groupId: string) => { + this.GameManager.deleteGroup(groupId); + }) + } + sendWebrtcSignal(signal: any, roomId: string, userId? : string, receiverId? : string) { return this.socket.emit(EventMessage.WEBRTC_SIGNAL, JSON.stringify({ userId: userId ? userId : this.userId, @@ -280,4 +308,4 @@ export class Connexion implements ConnexionInterface { disconnectMessage(callback: Function): void { this.socket.on(EventMessage.WEBRTC_DISCONNECT, callback); } -} \ No newline at end of file +} diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 4cc95c2a..eeed7f5f 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,6 +1,11 @@ import {GameScene} from "./GameScene"; import {ROOM} from "../../Enum/EnvironmentVariable" -import {Connexion, ConnexionInterface, ListMessageUserPositionInterface} from "../../Connexion"; +import { + Connexion, + ConnexionInterface, + GroupCreatedUpdatedMessageInterface, + ListMessageUserPositionInterface +} from "../../Connexion"; import {SimplePeerInterface, SimplePeer} from "../../WebRtc/SimplePeer"; import {LogincScene} from "../Login/LogincScene"; @@ -66,6 +71,31 @@ export class GameManager { } } + /** + * Share group position in game + */ + shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface): void { + if (this.status === StatusGameManagerEnum.IN_PROGRESS) { + return; + } + try { + this.currentGameScene.shareGroupPosition(groupPositionMessage) + } catch (e) { + console.error(e); + } + } + + deleteGroup(groupId: string): void { + if (this.status === StatusGameManagerEnum.IN_PROGRESS) { + return; + } + try { + this.currentGameScene.deleteGroup(groupId) + } catch (e) { + console.error(e); + } + } + getPlayerName(): string { return this.playerName; } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index f449f076..179c6802 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,11 +1,13 @@ import {GameManager, gameManager, HasMovedEvent, StatusGameManagerEnum} from "./GameManager"; -import {MessageUserPositionInterface} from "../../Connexion"; +import {GroupCreatedUpdatedMessageInterface, MessageUserPositionInterface} from "../../Connexion"; import {CurrentGamerInterface, GamerInterface, hasMovedEventName, Player} from "../Player/Player"; import {DEBUG_MODE, RESOLUTION, ROOM, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable"; import Tile = Phaser.Tilemaps.Tile; import {ITiledMap, ITiledTileSet} from "../Map/ITiledMap"; import {cypressAsserter} from "../../Cypress/CypressAsserter"; import {PLAYER_RESOURCES} from "../Entity/PlayableCaracter"; +import Circle = Phaser.Geom.Circle; +import Graphics = Phaser.GameObjects.Graphics; export const GameSceneName = "GameScene"; export enum Textures { @@ -27,9 +29,12 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ Layers : Array; Objects : Array; map: ITiledMap; + groups: Map startX = 704;// 22 case startY = 32; // 1 case + // Note: graphics object is costly to generate. We should find another way (maybe sprite based way to draw circles) + graphics: Graphics; constructor() { super({ @@ -37,6 +42,7 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ }); this.GameManager = gameManager; this.Terrains = []; + this.groups = new Map(); } //hook preload scene @@ -115,6 +121,8 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ //initialise camera this.initCamera(); + + this.graphics = this.add.graphics(); } //todo: in a dedicated class/function? @@ -199,6 +207,13 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ */ update(time: number, delta: number) : void { this.CurrentPlayer.moveUser(delta); + + // Also, let's redraw the circle (can be costly, we need to change this!) + this.graphics.clear(); + this.graphics.lineStyle(1, 0x00ff00, 0.4); + this.groups.forEach((circle: Circle) => { + this.graphics.strokeCircleShape(circle); + }) } /** @@ -272,4 +287,21 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ CurrentPlayer.say("Hello, how are you ? "); }); } + + shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { + let groupId = groupPositionMessage.groupId; + + if (this.groups.has(groupId)) { + this.groups.get(groupId).setPosition(groupPositionMessage.position.x, groupPositionMessage.position.y); + } else { + //console.log('Adding group ', groupId, ' to the scene'); + // TODO: circle radius should not be hard stored + this.groups.set(groupId, new Circle(groupPositionMessage.position.x, groupPositionMessage.position.y, 48)); + } + } + + deleteGroup(groupId: string): void { + //console.log('Deleting group ', groupId); + this.groups.delete(groupId); + } } diff --git a/front/src/Phaser/Player/Animation.ts b/front/src/Phaser/Player/Animation.ts index 1c555ad9..332c862d 100644 --- a/front/src/Phaser/Player/Animation.ts +++ b/front/src/Phaser/Player/Animation.ts @@ -53,7 +53,6 @@ export const playAnimation = (Player : Phaser.GameObjects.Sprite, direction : st if (direction !== PlayerAnimationNames.None && (!Player.anims.currentAnim || Player.anims.currentAnim.key !== direction)) { Player.anims.play(direction); } else if (direction === PlayerAnimationNames.None && Player.anims.currentAnim) { - //Player.anims.currentAnim.destroy(); Player.anims.stop(); } } From b2a5cb1720ce6435d9905f4e1779865a92eba90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Fri, 8 May 2020 16:09:50 +0200 Subject: [PATCH 2/2] Refactoring display of circle around group The circle is now a sprite and not generated on every frame using a giant "graphics" object --- front/src/Phaser/Game/GameScene.ts | 41 +++++++++++++++++------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 179c6802..a3eaee82 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -8,6 +8,9 @@ import {cypressAsserter} from "../../Cypress/CypressAsserter"; import {PLAYER_RESOURCES} from "../Entity/PlayableCaracter"; import Circle = Phaser.Geom.Circle; import Graphics = Phaser.GameObjects.Graphics; +import Texture = Phaser.Textures.Texture; +import Sprite = Phaser.GameObjects.Sprite; +import CanvasTexture = Phaser.Textures.CanvasTexture; export const GameSceneName = "GameScene"; export enum Textures { @@ -29,12 +32,10 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ Layers : Array; Objects : Array; map: ITiledMap; - groups: Map + groups: Map startX = 704;// 22 case startY = 32; // 1 case - - // Note: graphics object is costly to generate. We should find another way (maybe sprite based way to draw circles) - graphics: Graphics; + circleTexture: CanvasTexture; constructor() { super({ @@ -42,7 +43,7 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ }); this.GameManager = gameManager; this.Terrains = []; - this.groups = new Map(); + this.groups = new Map(); } //hook preload scene @@ -122,7 +123,18 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ //initialise camera this.initCamera(); - this.graphics = this.add.graphics(); + + // Let's generate the circle for the group delimiter + + this.circleTexture = this.textures.createCanvas('circleSprite', 96, 96); + let context = this.circleTexture.context; + context.beginPath(); + context.arc(48, 48, 48, 0, 2 * Math.PI, false); + // context.lineWidth = 5; + context.strokeStyle = '#ffffff'; + context.stroke(); + + this.circleTexture.refresh(); } //todo: in a dedicated class/function? @@ -207,13 +219,6 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ */ update(time: number, delta: number) : void { this.CurrentPlayer.moveUser(delta); - - // Also, let's redraw the circle (can be costly, we need to change this!) - this.graphics.clear(); - this.graphics.lineStyle(1, 0x00ff00, 0.4); - this.groups.forEach((circle: Circle) => { - this.graphics.strokeCircleShape(circle); - }) } /** @@ -292,16 +297,18 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ let groupId = groupPositionMessage.groupId; if (this.groups.has(groupId)) { - this.groups.get(groupId).setPosition(groupPositionMessage.position.x, groupPositionMessage.position.y); + this.groups.get(groupId).setPosition(Math.round(groupPositionMessage.position.x), Math.round(groupPositionMessage.position.y)); } else { - //console.log('Adding group ', groupId, ' to the scene'); // TODO: circle radius should not be hard stored - this.groups.set(groupId, new Circle(groupPositionMessage.position.x, groupPositionMessage.position.y, 48)); + let sprite = new Sprite(this, Math.round(groupPositionMessage.position.x), Math.round(groupPositionMessage.position.y), 'circleSprite'); + sprite.setDisplayOrigin(48, 48); + this.add.existing(sprite); + this.groups.set(groupId, sprite); } } deleteGroup(groupId: string): void { - //console.log('Deleting group ', groupId); + this.groups.get(groupId).destroy(); this.groups.delete(groupId); } }