Adding PlayersPositionInterpolator to interpolate/extrapolate players positions
authorDavid Négrier <d.negrier@thecodingmachine.com>
Tue, 2 Jun 2020 11:44:42 +0000 (13:44 +0200)
committerDavid Négrier <d.negrier@thecodingmachine.com>
Tue, 2 Jun 2020 11:44:42 +0000 (13:44 +0200)
front/src/Phaser/Game/GameScene.ts
front/src/Phaser/Game/PlayerMovement.ts
front/src/Phaser/Game/PlayersPositionInterpolator.ts
front/tests/Phaser/Game/PlayerMovementTest.ts

index 26978c4b8fa4eb9f5b58676f41fc930d6be7bfc4..60acf98d26cde4dfb3754eddb21476bcac46010d 100644 (file)
@@ -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) {
index aa7a2d46721c9da6104bfbd05c5cdf58dd86b64d..1ed2b74546caf78081ce5167a82cc2f1508f6ebe 100644 (file)
@@ -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
         }
     }
 }
index 4cc47547e19160dee1868971bb7cc642d1ef29ba..19e0f7bc723802abdc876a0130934b285671b539 100644 (file)
@@ -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.
  */
+import {PlayerMovement} from "./PlayerMovement";
+import {HasMovedEvent} from "./GameManager";
+
 export class PlayersPositionInterpolator {
+    playerMovements: Map<string, PlayerMovement> = new Map<string, PlayerMovement>();
+
+    updatePlayerPosition(userId: string, playerMovement: PlayerMovement) : void {
+        this.playerMovements.set(userId, playerMovement);
+    }
+
+    removePlayer(userId: string): void {
+        this.playerMovements.delete(userId);
+    }
 
+    getUpdatedPositions(tick: number) : Map<string, HasMovedEvent> {
+        let positions = new Map<string, HasMovedEvent>();
+        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;
+    }
 }
index f317207acbe08199ef5553db7adb91d5a30995b6..e65dbec8496ca2b6818d6ca0578c9f05afcbdce6 100644 (file)
@@ -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
+        });
+    });
 })