From d72e60610e1442c63613e56f0a07a55c862cb422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 2 Jun 2020 13:44:42 +0200 Subject: [PATCH] Adding PlayersPositionInterpolator to interpolate/extrapolate players positions --- front/src/Phaser/Game/GameScene.ts | 30 ++++++++++----- front/src/Phaser/Game/PlayerMovement.ts | 17 ++++++++- .../Game/PlayersPositionInterpolator.ts | 26 ++++++++++++- front/tests/Phaser/Game/PlayerMovementTest.ts | 38 ++++++++++++++++++- 4 files changed, 97 insertions(+), 14 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 26978c4b..60acf98d 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -13,6 +13,8 @@ import Sprite = Phaser.GameObjects.Sprite; import CanvasTexture = Phaser.Textures.CanvasTexture; import {AddPlayerInterface} from "./AddPlayerInterface"; import {PlayerAnimationNames} from "../Player/Animation"; +import {PlayerMovement} from "./PlayerMovement"; +import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; export enum Textures { Player = "male1" @@ -37,6 +39,7 @@ export class GameScene extends Phaser.Scene { startY = 32; // 1 case circleTexture: CanvasTexture; initPosition: PositionInterface; + private playersPositionInterpolator = new PlayersPositionInterpolator(); MapKey: string; MapUrlFile: string; @@ -381,6 +384,17 @@ export class GameScene extends Phaser.Scene { update(time: number, delta: number) : void { this.currentTick = time; this.CurrentPlayer.moveUser(delta); + + // Let's move all users + let updatedPlayersPositions = this.playersPositionInterpolator.getUpdatedPositions(time); + updatedPlayersPositions.forEach((moveEvent: HasMovedEvent, userId: string) => { + let player : GamerInterface | undefined = this.MapPlayersByKey.get(userId); + if (player === undefined) { + throw new Error('Cannot find player with ID "' + userId +'"'); + } + player.updatePosition(moveEvent); + }); + let nextSceneKey = this.checkToExit(); if(nextSceneKey){ this.scene.start(nextSceneKey.key); @@ -424,15 +438,6 @@ export class GameScene extends Phaser.Scene { }); } - private findPlayerInMap(UserId : string) : GamerInterface | null{ - return this.MapPlayersByKey.get(UserId); - /*let player = this.MapPlayers.getChildren().find((player: Player) => UserId === player.userId); - if(!player){ - return null; - } - return (player as GamerInterface);*/ - } - /** * Create new player */ @@ -475,6 +480,7 @@ export class GameScene extends Phaser.Scene { player.destroy(); this.MapPlayers.remove(player); this.MapPlayersByKey.delete(userId); + this.playersPositionInterpolator.removePlayer(userId); } updatePlayerPosition(message: MessageUserMovedInterface): void { @@ -482,7 +488,11 @@ export class GameScene extends Phaser.Scene { if (player === undefined) { throw new Error('Cannot find player with ID "' + message.userId +'"'); } - player.updatePosition(message.position); + + // We do not update the player position directly (because it is sent only every 200ms). + // Instead we use the PlayersPositionInterpolator that will do a smooth animation over the next 200ms. + let playerMovement = new PlayerMovement({ x: player.x, y: player.y }, this.currentTick, message.position, this.currentTick + POSITION_DELAY); + this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement); } shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) { diff --git a/front/src/Phaser/Game/PlayerMovement.ts b/front/src/Phaser/Game/PlayerMovement.ts index aa7a2d46..1ed2b745 100644 --- a/front/src/Phaser/Game/PlayerMovement.ts +++ b/front/src/Phaser/Game/PlayerMovement.ts @@ -1,15 +1,28 @@ import {HasMovedEvent} from "./GameManager"; import {MAX_EXTRAPOLATION_TIME} from "../../Enum/EnvironmentVariable"; +import {PositionInterface} from "../../Connection"; export class PlayerMovement { - public constructor(private startPosition: HasMovedEvent, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) { + public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) { } public isOutdated(tick: number): boolean { + //console.log(tick, this.endTick, MAX_EXTRAPOLATION_TIME) + + // If the endPosition is NOT moving, no extrapolation needed. + if (this.endPosition.moving === false && tick > this.endTick) { + return true; + } + return tick > this.endTick + MAX_EXTRAPOLATION_TIME; } public getPosition(tick: number): HasMovedEvent { + // Special case: end position reached and end position is not moving + if (tick >= this.endTick && this.endPosition.moving === false) { + return this.endPosition; + } + let x = (this.endPosition.x - this.startPosition.x) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.x; let y = (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + this.startPosition.y; @@ -17,7 +30,7 @@ export class PlayerMovement { x, y, direction: this.endPosition.direction, - moving: this.endPosition.moving + moving: true } } } diff --git a/front/src/Phaser/Game/PlayersPositionInterpolator.ts b/front/src/Phaser/Game/PlayersPositionInterpolator.ts index 4cc47547..19e0f7bc 100644 --- a/front/src/Phaser/Game/PlayersPositionInterpolator.ts +++ b/front/src/Phaser/Game/PlayersPositionInterpolator.ts @@ -2,6 +2,30 @@ * This class is in charge of computing the position of all players. * Player movement is delayed by 200ms so position depends on ticks. */ -export class PlayersPositionInterpolator { +import {PlayerMovement} from "./PlayerMovement"; +import {HasMovedEvent} from "./GameManager"; +export class PlayersPositionInterpolator { + playerMovements: Map = new Map(); + + updatePlayerPosition(userId: string, playerMovement: PlayerMovement) : void { + this.playerMovements.set(userId, playerMovement); + } + + removePlayer(userId: string): void { + this.playerMovements.delete(userId); + } + + getUpdatedPositions(tick: number) : Map { + let positions = new Map(); + this.playerMovements.forEach((playerMovement: PlayerMovement, userId: string) => { + if (playerMovement.isOutdated(tick)) { + //console.log("outdated") + this.playerMovements.delete(userId); + } + //console.log("moving") + positions.set(userId, playerMovement.getPosition(tick)) + }); + return positions; + } } diff --git a/front/tests/Phaser/Game/PlayerMovementTest.ts b/front/tests/Phaser/Game/PlayerMovementTest.ts index f317207a..e65dbec8 100644 --- a/front/tests/Phaser/Game/PlayerMovementTest.ts +++ b/front/tests/Phaser/Game/PlayerMovementTest.ts @@ -4,7 +4,7 @@ import {PlayerMovement} from "../../../src/Phaser/Game/PlayerMovement"; describe("Interpolation / Extrapolation", () => { it("should interpolate", () => { let playerMovement = new PlayerMovement({ - x: 100, y: 200, moving: true, direction: "right" + x: 100, y: 200 }, 42000, { x: 200, y: 100, moving: true, direction: "up" @@ -37,4 +37,40 @@ describe("Interpolation / Extrapolation", () => { moving: true }); }); + + it("should not extrapolate if we stop", () => { + let playerMovement = new PlayerMovement({ + x: 100, y: 200 + }, 42000, + { + x: 200, y: 100, moving: false, direction: "up" + }, + 42200 + ); + + expect(playerMovement.getPosition(42300)).toEqual({ + x: 200, + y: 100, + direction: 'up', + moving: false + }); + }); + + it("should should keep moving until it stops", () => { + let playerMovement = new PlayerMovement({ + x: 100, y: 200 + }, 42000, + { + x: 200, y: 100, moving: false, direction: "up" + }, + 42200 + ); + + expect(playerMovement.getPosition(42100)).toEqual({ + x: 150, + y: 150, + direction: 'up', + moving: true + }); + }); })