From 7da8a6138f1e684b11f613901461fc157557100b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 28 Apr 2020 23:01:56 +0200 Subject: [PATCH 1/7] Adding a (failing test) for disconnecting users --- back/tests/WorldTest.ts | 52 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index 1f5affc8..4511efc3 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -12,7 +12,7 @@ describe("World", () => { connectCalled = true; } let disconnect = (user1: string, user2: string): void => { - + } let world = new World(connect, disconnect); @@ -53,14 +53,58 @@ describe("World", () => { })); expect(connectCalled).toBe(false); }); - /** + + it("should disconnect user1 and user2", () => { + let connectCalled: boolean = false; + let disconnectCalled: boolean = false; + let connect = (user1: string, user2: string): void => { + connectCalled = true; + } + let disconnect = (user1: string, user2: string): void => { + disconnectCalled = true; + } + + let world = new World(connect, disconnect); + + world.join(new MessageUserPosition({ + userId: "foo", + roomId: 1, + position: new Point(100, 100) + })); + + world.join(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(259, 100) + })); + + expect(connectCalled).toBe(false); + + world.updatePosition(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(261, 100) + })); + + expect(disconnectCalled).toBe(true); + + disconnectCalled = false; + world.updatePosition(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(262, 100) + })); + expect(disconnectCalled).toBe(false); + }); + + /** it('Should return the distances between all users', () => { let connectCalled: boolean = false; let connect = (user1: string, user2: string): void => { connectCalled = true; } let disconnect = (user1: string, user2: string): void => { - + } let world = new World(connect, disconnect); @@ -100,4 +144,4 @@ describe("World", () => { //expect(distances).toBe([]); }) **/ -}) \ No newline at end of file +}) From abeac558fdaa7e16d27c52e8eaf0daecad3c5ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 28 Apr 2020 23:23:50 +0200 Subject: [PATCH 2/7] Adding TODO comment --- back/src/Model/World.ts | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 804a176b..d1fe3709 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -14,13 +14,13 @@ export class World { private connectCallback: (user1: string, user2: string) => void; private disconnectCallback: (user1: string, user2: string) => void; - constructor(connectCallback: (user1: string, user2: string) => void, disconnectCallback: (user1: string, user2: string) => void) + constructor(connectCallback: (user1: string, user2: string) => void, disconnectCallback: (user1: string, user2: string) => void) { this.users = new Map(); this.groups = []; this.connectCallback = connectCallback; this.disconnectCallback = disconnectCallback; - } + } public join(userPosition: MessageUserPosition): void { this.users.set(userPosition.userId, { @@ -55,9 +55,17 @@ export class World { closestUser.group.join(user); } } - + + } else { + // If the user is part of a group: + // should we split the group? + + // TODO: analyze the group of the user: + // => take one user. "walk the tree of users, each branch being a link 0) { - + context.groups.forEach(group => { if(group.isPartOfGroup(userPosition)) { // Is the user in a group ? if(group.isStillIn(userPosition)) { // Is the user leaving the group ? (is the user at more than max distance of each player) - + // Should we split the group? (is each player reachable from the current player?) // This is needed if // A <==> B <==> C <===> D @@ -136,7 +144,7 @@ export class World { } */ } - + }, this.users); return matchingUser; @@ -164,7 +172,7 @@ export class World { } }); }); - + distances.sort(World.compareDistances); return distances; @@ -190,7 +198,7 @@ export class World { // Detecte le ou les users qui se sont fait sortir du groupe let difference = users.filter(x => !groupTmp.includes(x)); - // TODO : Notify users un difference that they have left the group + // TODO : Notify users un difference that they have left the group } let newgroup = new Group(groupTmp); @@ -207,4 +215,4 @@ export class World { } return 0; }*/ -} \ No newline at end of file +} From 2a8e3ea3238682fd22319991f51ef37ee7ec0760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 29 Apr 2020 22:41:48 +0200 Subject: [PATCH 3/7] Switching connection to a barycenter approach --- back/src/Model/Group.ts | 21 ++++++++- back/src/Model/PositionInterface.ts | 4 ++ back/src/Model/World.ts | 71 +++++++++++++++-------------- 3 files changed, 60 insertions(+), 36 deletions(-) create mode 100644 back/src/Model/PositionInterface.ts diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index 795c0e8e..9f5c48c5 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -1,6 +1,6 @@ -import {MessageUserPosition} from "./Websocket/MessageUserPosition"; import { World } from "./World"; import { UserInterface } from "./UserInterface"; +import {PositionInterface} from "_Model/PositionInterface"; export class Group { static readonly MAX_PER_GROUP = 4; @@ -24,6 +24,25 @@ export class Group { return this.users; } + /** + * Returns the barycenter of all users (i.e. the center of the group) + */ + getPosition(): PositionInterface { + let x = 0; + let y = 0; + // Let's compute the barycenter of all users. + this.users.forEach((user: UserInterface) => { + x += user.position.x; + y += user.position.y; + }); + x /= this.users.length; + y /= this.users.length; + return { + x, + y + }; + } + isFull(): boolean { return this.users.length >= Group.MAX_PER_GROUP; } diff --git a/back/src/Model/PositionInterface.ts b/back/src/Model/PositionInterface.ts new file mode 100644 index 00000000..d3b0dd47 --- /dev/null +++ b/back/src/Model/PositionInterface.ts @@ -0,0 +1,4 @@ +export interface PositionInterface { + x: number, + y: number +} diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 4a31e49b..32e1b383 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -4,6 +4,7 @@ import {Group} from "./Group"; import {Distance} from "./Distance"; import {UserInterface} from "./UserInterface"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import {PositionInterface} from "_Model/PositionInterface"; export class World { static readonly MIN_DISTANCE = 160; @@ -47,17 +48,18 @@ export class World { if (typeof user.group === 'undefined') { // If the user is not part of a group: // should he join a group? - let closestUser: UserInterface|null = this.searchClosestAvailableUser(user); + let closestItem: UserInterface|Group|null = this.searchClosestAvailableUserOrGroup(user); - if (closestUser !== null) { - // Is the closest user part of a group? - if (typeof closestUser.group === 'undefined') { + if (closestItem !== null) { + if (closestItem instanceof Group) { + // Let's join the group! + closestItem.join(user); + } else { + let closestUser : UserInterface = closestItem; let group: Group = new Group([ user, closestUser ], this.connectCallback, this.disconnectCallback); - } else { - closestUser.group.join(user); } } @@ -78,32 +80,16 @@ export class World { * - close enough (distance <= MIN_DISTANCE) * - not in a group OR in a group that is not full */ - private searchClosestAvailableUser(user: UserInterface): UserInterface|null + private searchClosestAvailableUserOrGroup(user: UserInterface): UserInterface|Group|null { -/* - let sortedUsersByDistance: UserInteface[] = Array.from(this.users.values()).sort((user1: UserInteface, user2: UserInteface): number => { - let distance1 = World.computeDistance(user, user1); - let distance2 = World.computeDistance(user, user2); - return distance1 - distance2; - }); - - // The first element should be the current user (distance 0). Let's remove it. - if (sortedUsersByDistance[0] === user) { - sortedUsersByDistance.shift(); - } - - for(let i = 0; i < sortedUsersByDistance.length; i++) { - let currentUser = sortedUsersByDistance[i]; - let distance = World.computeDistance(currentUser, user); - if(distance > World.MIN_DISTANCE) { - return; - } - } -*/ let usersToBeGroupedWith: Distance[] = []; let minimumDistanceFound: number = World.MIN_DISTANCE; - let matchingUser: UserInterface | null = null; + let matchingItem: UserInterface | Group | null = null; this.users.forEach(function(currentUser, userId) { + // Let's only check users that are not part of a group + if (typeof currentUser.group !== 'undefined') { + return; + } if(currentUser === user) { return; } @@ -111,13 +97,13 @@ export class World { let distance = World.computeDistance(user, currentUser); // compute distance between peers. if(distance <= minimumDistanceFound) { - - if (typeof currentUser.group === 'undefined' || !currentUser.group.isFull()) { + minimumDistanceFound = distance; + matchingItem = currentUser; + } + /*if (typeof currentUser.group === 'undefined' || !currentUser.group.isFull()) { // We found a user we can bind to. - minimumDistanceFound = distance; - matchingUser = currentUser; return; - } + }*/ /* if(context.groups.length > 0) { @@ -148,11 +134,21 @@ export class World { usersToBeGroupedWith.push(dist); } */ + }); + + this.groups.forEach(function(group: Group) { + if (group.isFull()) { + return; } + let distance = World.computeDistanceBetweenPositions(user.position, group.getPosition()); - }, this.users); + if(distance <= minimumDistanceFound) { + minimumDistanceFound = distance; + matchingItem = group; + } + }); - return matchingUser; + return matchingItem; } public static computeDistance(user1: UserInterface, user2: UserInterface): number @@ -160,6 +156,11 @@ export class World { return Math.sqrt(Math.pow(user2.position.x - user1.position.x, 2) + Math.pow(user2.position.y - user1.position.y, 2)); } + public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number + { + return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2)); + } + /*getDistancesBetweenGroupUsers(group: Group): Distance[] { let i = 0; From bf0fa516d49486a68b7e747bda839c254bec60ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 29 Apr 2020 23:12:55 +0200 Subject: [PATCH 4/7] First working version with disconnection --- back/src/Model/Group.ts | 33 ++++++++++++++++++++++++++- back/src/Model/World.ts | 34 +++++++++++++++++++++++----- back/tests/WorldTest.ts | 49 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 107 insertions(+), 9 deletions(-) diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index 9f5c48c5..caf9b926 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -47,6 +47,10 @@ export class Group { return this.users.length >= Group.MAX_PER_GROUP; } + isEmpty(): boolean { + return this.users.length <= 1; + } + join(user: UserInterface): void { // Broadcast on the right event @@ -79,7 +83,7 @@ export class Group { return stillIn; } - removeFromGroup(users: UserInterface[]): void + /*removeFromGroup(users: UserInterface[]): void { for(let i = 0; i < users.length; i++){ let user = users[i]; @@ -88,5 +92,32 @@ export class Group { this.users.splice(index, 1); } } + }*/ + + leave(user: UserInterface): void + { + const index = this.users.indexOf(user, 0); + if (index === -1) { + throw new Error("Could not find user in the group"); + } + + this.users.splice(index, 1); + user.group = undefined; + + // Broadcast on the right event + this.users.forEach((groupUser: UserInterface) => { + this.disconnectCallback(user.id, groupUser.id); + }); + } + + /** + * Let's kick everybody out. + * Usually used when there is only one user left. + */ + destroy(): void + { + this.users.forEach((user: UserInterface) => { + this.leave(user); + }) } } diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 32e1b383..06bcf884 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -29,6 +29,8 @@ export class World { id: userPosition.userId, position: userPosition.position }); + // Let's call update position to trigger the join / leave room + this.updatePosition(userPosition); } public leave(user : ExSocketInterface){ @@ -60,18 +62,39 @@ export class World { user, closestUser ], this.connectCallback, this.disconnectCallback); + this.groups.push(group); } } } else { // If the user is part of a group: - // should we split the group? + // should he leave the group? + let distance = World.computeDistanceBetweenPositions(user.position, user.group.getPosition()); + if (distance > World.MIN_DISTANCE) { + this.leaveGroup(user); + } + } + } - // TODO: analyze the group of the user: - // => take one user. "walk the tree of users, each branch being a link { expect(connectCalled).toBe(false); }); + it("should connect 3 users", () => { + let connectCalled: boolean = false; + let connect = (user1: string, user2: string): void => { + connectCalled = true; + } + let disconnect = (user1: string, user2: string): void => { + + } + + let world = new World(connect, disconnect); + + world.join(new MessageUserPosition({ + userId: "foo", + roomId: 1, + position: new Point(100, 100) + })); + + world.join(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(200, 100) + })); + + expect(connectCalled).toBe(true); + connectCalled = false; + + // baz joins at the outer limit of the group + world.join(new MessageUserPosition({ + userId: "baz", + roomId: 1, + position: new Point(311, 100) + })); + + expect(connectCalled).toBe(false); + + world.updatePosition(new MessageUserPosition({ + userId: "baz", + roomId: 1, + position: new Point(309, 100) + })); + + expect(connectCalled).toBe(true); + }); + it("should disconnect user1 and user2", () => { let connectCalled: boolean = false; let disconnectCalled: boolean = false; @@ -78,12 +122,13 @@ describe("World", () => { position: new Point(259, 100) })); - expect(connectCalled).toBe(false); + expect(connectCalled).toBe(true); + expect(disconnectCalled).toBe(false); world.updatePosition(new MessageUserPosition({ userId: "bar", roomId: 1, - position: new Point(261, 100) + position: new Point(100+160+160+1, 100) })); expect(disconnectCalled).toBe(true); From c778afcbca777412e091890a49182b9be8301e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 29 Apr 2020 23:18:42 +0200 Subject: [PATCH 5/7] Adding support for leaving the group if the socket is closed --- back/src/Model/World.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 06bcf884..795cc8be 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -34,7 +34,10 @@ export class World { } public leave(user : ExSocketInterface){ - /*TODO leaver user in group*/ + let userObj = this.users.get(user.id); + if (userObj !== undefined && typeof userObj.group !== 'undefined') { + this.leaveGroup(user); + } this.users.delete(user.userId); } From 62d2498e34724d48471acce057cb4fb32bf54f4a Mon Sep 17 00:00:00 2001 From: kharhamel Date: Thu, 30 Apr 2020 19:18:35 +0200 Subject: [PATCH 6/7] remove the GameSceneInterface from LoginScene --- front/src/Phaser/Game/GameScene.ts | 4 ++-- front/src/Phaser/Login/LogincScene.ts | 16 +++------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index e36fe809..f7fbd7c3 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -6,8 +6,8 @@ import Tile = Phaser.Tilemaps.Tile; import {ITiledMap, ITiledTileSet} from "../Map/ITiledMap"; import {cypressAsserter} from "../../Cypress/CypressAsserter"; +export const GameSceneName = "GameScene"; export enum Textures { - Rock = 'rock', Player = 'playerModel', Map = 'map' } @@ -32,7 +32,7 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ constructor() { super({ - key: "GameScene" + key: GameSceneName }); this.GameManager = gameManager; this.Terrains = []; diff --git a/front/src/Phaser/Login/LogincScene.ts b/front/src/Phaser/Login/LogincScene.ts index 1aa1e0af..15ead519 100644 --- a/front/src/Phaser/Login/LogincScene.ts +++ b/front/src/Phaser/Login/LogincScene.ts @@ -2,8 +2,7 @@ import {gameManager} from "../Game/GameManager"; import {TextField} from "../Components/TextField"; import {TextInput} from "../Components/TextInput"; import {ClickButton} from "../Components/ClickButton"; -import {GameSceneInterface} from "../Game/GameScene"; -import {MessageUserPositionInterface} from "../../Connexion"; +import {GameSceneName} from "../Game/GameScene"; //todo: put this constants in a dedicated file export const LoginSceneName = "LoginScene"; @@ -11,7 +10,7 @@ enum LoginTextures { playButton = "play_button", } -export class LogincScene extends Phaser.Scene implements GameSceneInterface { +export class LogincScene extends Phaser.Scene { private emailInput: TextInput; private textField: TextField; private playButton: ClickButton; @@ -47,16 +46,7 @@ export class LogincScene extends Phaser.Scene implements GameSceneInterface { let email = this.emailInput.text; if (!email) return; gameManager.connect(email).then(() => { - this.scene.start("GameScene"); + this.scene.start(GameSceneName); }); } - - Map: Phaser.Tilemaps.Tilemap; - RoomId: string; - - createCurrentPlayer(UserId: string): void { - } - - shareUserPosition(UsersPosition: Array): void { - } } \ No newline at end of file From dd0744387ffbf30c5475d9ccbebb7105423a56dd Mon Sep 17 00:00:00 2001 From: kharhamel Date: Thu, 30 Apr 2020 19:36:28 +0200 Subject: [PATCH 7/7] reenabled diagonal movement --- front/src/Enum/EnvironmentVariable.ts | 2 +- front/src/Phaser/Entity/PlayableCaracter.ts | 15 +++++------ front/src/Phaser/Player/Player.ts | 28 ++++++++++----------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 6c0226a2..f44f717b 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -1,4 +1,4 @@ -const DEBUG_MODE: boolean = !!process.env.DEBUG_MODE || false; +const DEBUG_MODE: boolean = process.env.DEBUG_MODE as any === true; const API_URL = process.env.API_URL || "http://api.workadventure.localhost"; const ROOM = [process.env.ROOM || "THECODINGMACHINE"]; const RESOLUTION = 4; diff --git a/front/src/Phaser/Entity/PlayableCaracter.ts b/front/src/Phaser/Entity/PlayableCaracter.ts index b84d2dd8..987d6bd3 100644 --- a/front/src/Phaser/Entity/PlayableCaracter.ts +++ b/front/src/Phaser/Entity/PlayableCaracter.ts @@ -22,18 +22,15 @@ export class PlayableCaracter extends Phaser.Physics.Arcade.Sprite { this.setVelocity(x, y); - //todo improve animations to better account for diagonal movement - if (this.body.velocity.x > 0) { //moving right - this.play(PlayerAnimationNames.WalkRight, true); - } - if (this.body.velocity.x < 0) { //moving left - this.anims.playReverse(PlayerAnimationNames.WalkLeft, true); - } + //up or down animationss are prioritized over left and right if (this.body.velocity.y < 0) { //moving up this.play(PlayerAnimationNames.WalkUp, true); - } - if (this.body.velocity.y > 0) { //moving down + } else if (this.body.velocity.y > 0) { //moving down this.play(PlayerAnimationNames.WalkDown, true); + } else if (this.body.velocity.x > 0) { //moving right + this.play(PlayerAnimationNames.WalkRight, true); + } else if (this.body.velocity.x < 0) { //moving left + this.anims.playReverse(PlayerAnimationNames.WalkLeft, true); } if(this.bubble) { diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index a7798720..60326a27 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -66,27 +66,25 @@ export class Player extends PlayableCaracter implements CurrentGamerInterface, G let speedMultiplier = activeEvents.get(UserInputEvent.SpeedUp) ? 25 : 9; let moveAmount = speedMultiplier * delta; + let x = 0; + let y = 0; if (activeEvents.get(UserInputEvent.MoveUp)) { - this.move(0, -moveAmount); - haveMove = true; + y = - moveAmount; direction = PlayerAnimationNames.WalkUp; - } - if (activeEvents.get(UserInputEvent.MoveLeft)) { - this.move(-moveAmount, 0); - haveMove = true; - direction = PlayerAnimationNames.WalkLeft; - } - if (activeEvents.get(UserInputEvent.MoveDown)) { - this.move(0, moveAmount); - haveMove = true; + } else if (activeEvents.get(UserInputEvent.MoveDown)) { + y = moveAmount; direction = PlayerAnimationNames.WalkDown; } - if (activeEvents.get(UserInputEvent.MoveRight)) { - this.move(moveAmount, 0); - haveMove = true; + if (activeEvents.get(UserInputEvent.MoveLeft)) { + x = -moveAmount; + direction = PlayerAnimationNames.WalkLeft; + } else if (activeEvents.get(UserInputEvent.MoveRight)) { + x = moveAmount; direction = PlayerAnimationNames.WalkRight; } - if (!haveMove) { + if (x !== 0 || y !== 0) { + this.move(x, y); + } else { direction = PlayerAnimationNames.None; this.stop(); }