From 4d76cf60120c15e31b37b3e14bbf3c9f07bd9169 Mon Sep 17 00:00:00 2001 From: Marius Kaczmarek Date: Mon, 12 Feb 2024 15:43:05 +0100 Subject: [PATCH] added early stopping behaviour to CharacterNavigator --- .../CharacterAnimator/CharacterAnimator.ts | 2 +- .../CharacterNavigator/CharacterNavigator.ts | 64 +++++++++++++++++-- .../CharacterAnimator.test.ts | 4 +- .../CharacterNavigator.test.ts | 36 +++++++++++ 4 files changed, 97 insertions(+), 9 deletions(-) diff --git a/src/Components/Core/Presentation/Babylon/CharacterAnimator/CharacterAnimator.ts b/src/Components/Core/Presentation/Babylon/CharacterAnimator/CharacterAnimator.ts index b06d3f00f..160b83157 100644 --- a/src/Components/Core/Presentation/Babylon/CharacterAnimator/CharacterAnimator.ts +++ b/src/Components/Core/Presentation/Babylon/CharacterAnimator/CharacterAnimator.ts @@ -262,6 +262,6 @@ export default class CharacterAnimator implements ICharacterAnimator { return; let velocity = this.getCharacterVelocity().length(); - this.walkAnimation.speedRatio = Math.max(velocity, 1); + this.walkAnimation.speedRatio = Math.max(velocity, 0.5); } } diff --git a/src/Components/Core/Presentation/Babylon/CharacterNavigator/CharacterNavigator.ts b/src/Components/Core/Presentation/Babylon/CharacterNavigator/CharacterNavigator.ts index 2e11fc3f2..19afe52fe 100644 --- a/src/Components/Core/Presentation/Babylon/CharacterNavigator/CharacterNavigator.ts +++ b/src/Components/Core/Presentation/Babylon/CharacterNavigator/CharacterNavigator.ts @@ -8,6 +8,7 @@ import { LinesMesh, Nullable, Observer, + Scene, } from "@babylonjs/core"; import bind from "bind-decorator"; import INavigation from "../Navigation/INavigation"; @@ -30,7 +31,7 @@ export default class CharacterNavigator implements ICharacterNavigator { private readonly agentParams: IAgentParameters = { - radius: 0.5, + radius: 0.4, height: 1, maxAcceleration: 5000.0, maxSpeed: 3.0, @@ -39,16 +40,21 @@ export default class CharacterNavigator separationWeight: 1.0, reachRadius: 0.4, // acts as stopping distance }; + private readonly earlyStoppingPatience = 300; // in ms + private readonly earlyStoppingVelocityThreshold = 0.3; private navigation: INavigation; private scenePresenter: IScenePresenter; + private characterAnimator: ICharacterAnimator; private agentIndex: number; private targetReachedObserverRef: Nullable< Observer<{ agentIndex: number; destination: Vector3 }> >; + private targetReachedCallback: (() => void) | null; private parentNode: TransformNode; - private characterAnimator: ICharacterAnimator; private verbose: boolean = false; + private checkEarlyStoppingObserverRef: Nullable>; + private earlyStoppingCounter = 0; private debug_pathLine: LinesMesh; @@ -81,12 +87,28 @@ export default class CharacterNavigator target: Vector3, onTargetReachedCallback?: () => void ): void { + // reset navigation + this.navigation.Crowd.agentTeleport( + this.agentIndex, + this.parentNode.position + ); + this.resetObservers(); + + // get target on navmesh target = this.navigation.Plugin.getClosestPoint(target); + + // start movement this.navigation.Crowd.agentGoto(this.agentIndex, target); this.characterAnimator.transition( CharacterAnimationActions.MovementStarted ); + // setup observers + this.targetReachedCallback = onTargetReachedCallback ?? null; + this.checkEarlyStoppingObserverRef = + this.scenePresenter.Scene.onBeforeRenderObservable.add( + this.checkEarlyStopping + ); this.targetReachedObserverRef = this.navigation.Crowd.onReachTargetObservable.add( (eventData: { agentIndex: number }) => { @@ -97,6 +119,7 @@ export default class CharacterNavigator } ); + // debug drawings this.debug_drawPath(target); } @@ -108,9 +131,7 @@ export default class CharacterNavigator ); this.characterAnimator.transition(CharacterAnimationActions.TargetReached); - this.navigation.Crowd.onReachTargetObservable.remove( - this.targetReachedObserverRef - ); + this.resetObservers(); } @bind @@ -128,13 +149,44 @@ export default class CharacterNavigator this.parentNode ); - // commenting this debug code solves avatar spawn bug this.debug_drawCircle(this.agentParams.radius, Color3.Blue()); this.debug_drawCircle(this.agentParams.reachRadius!, Color3.Red()); this.resolveIsReady(); } + @bind + private checkEarlyStopping(scene: Scene): void { + const velocity = this.navigation.Crowd.getAgentVelocity( + this.agentIndex + ).length(); + + if (velocity < this.earlyStoppingVelocityThreshold) + this.earlyStoppingCounter += scene.deltaTime; + else this.earlyStoppingCounter = 0; + + if (this.earlyStoppingCounter >= this.earlyStoppingPatience) { + if (this.targetReachedCallback) this.targetReachedCallback(); + this.stopMovement(); + } + } + + private resetObservers(): void { + if (this.targetReachedObserverRef !== null) { + this.navigation.Crowd.onReachTargetObservable.remove( + this.targetReachedObserverRef + ); + this.targetReachedObserverRef = null; + } + if (this.checkEarlyStoppingObserverRef !== null) { + this.scenePresenter.Scene.onBeforeRenderObservable.remove( + this.checkEarlyStoppingObserverRef + ); + this.checkEarlyStoppingObserverRef = null; + } + if (this.targetReachedCallback !== null) this.targetReachedCallback = null; + } + private debug_drawPath(target: Vector3): void { if (this.verbose === false) return; diff --git a/src/Components/CoreTest/Presentation/Babylon/CharacterAnimator/CharacterAnimator.test.ts b/src/Components/CoreTest/Presentation/Babylon/CharacterAnimator/CharacterAnimator.test.ts index 156e05972..9677f6344 100644 --- a/src/Components/CoreTest/Presentation/Babylon/CharacterAnimator/CharacterAnimator.test.ts +++ b/src/Components/CoreTest/Presentation/Babylon/CharacterAnimator/CharacterAnimator.test.ts @@ -298,7 +298,7 @@ describe("CharacterAnimator", () => { expect(mockWalkAnimation.speedRatio).toEqual(1); }); - test("setWalkingAnimationSpeed doesn't set the speedRation to less than 1", () => { + test("setWalkingAnimationSpeed doesn't set the speedRation to less than 0.5", () => { systemUnderTest["stateMachine"]["currentState"] = CharacterAnimationStates.Walking; systemUnderTest["getCharacterVelocity"] = () => new Vector3(0.5, 0, 0); @@ -306,6 +306,6 @@ describe("CharacterAnimator", () => { systemUnderTest["setWalkingAnimationSpeed"](); - expect(mockWalkAnimation.speedRatio).toEqual(1); + expect(mockWalkAnimation.speedRatio).toEqual(0.5); }); }); diff --git a/src/Components/CoreTest/Presentation/Babylon/CharacterNavigator/CharacterNavigator.test.ts b/src/Components/CoreTest/Presentation/Babylon/CharacterNavigator/CharacterNavigator.test.ts index 3c8cc0c13..2c1d73cba 100644 --- a/src/Components/CoreTest/Presentation/Babylon/CharacterNavigator/CharacterNavigator.test.ts +++ b/src/Components/CoreTest/Presentation/Babylon/CharacterNavigator/CharacterNavigator.test.ts @@ -136,6 +136,42 @@ describe("CharacterNavigator", () => { ); }); + test("checkEarlyStopping increases earlyStoppingCounter when velocity is under threshold", () => { + navigationMock.Crowd.getAgentVelocity.mockReturnValue( + new Vector3(0.1, 0, 0) + ); + const sceneMock = mockDeep(); + sceneMock.deltaTime = 1; + + systemUnderTest["checkEarlyStopping"](sceneMock); + + expect(systemUnderTest["earlyStoppingCounter"]).toBe(1); + }); + + test("checkEarlyStopping resets earlyStoppingCounter when velocity is above threshold", () => { + navigationMock.Crowd.getAgentVelocity.mockReturnValue(new Vector3(1, 0, 0)); + const sceneMock = mockDeep(); + sceneMock.deltaTime = 1; + + systemUnderTest["checkEarlyStopping"](sceneMock); + + expect(systemUnderTest["earlyStoppingCounter"]).toBe(0); + }); + + test("checkEarlyStopping calls targetReachedCallback when earlyStoppingPatience is exceeded", () => { + const sceneMock = mockDeep(); + sceneMock.deltaTime = 1; + navigationMock.Crowd.getAgentVelocity.mockReturnValue(new Vector3(0, 0, 0)); + + systemUnderTest["earlyStoppingCounter"] = 500; + const targetReachedCallback = jest.fn(); + systemUnderTest["targetReachedCallback"] = targetReachedCallback; + + systemUnderTest["checkEarlyStopping"](sceneMock); + + expect(targetReachedCallback).toHaveBeenCalledTimes(1); + }); + test("debug_drawPath does nothing when verbose is false", () => { systemUnderTest["verbose"] = false;