Skip to content

Commit

Permalink
added early stopping behaviour to CharacterNavigator
Browse files Browse the repository at this point in the history
  • Loading branch information
DerKatsche committed Feb 12, 2024
1 parent 5574d9c commit 4d76cf6
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
LinesMesh,
Nullable,
Observer,
Scene,
} from "@babylonjs/core";
import bind from "bind-decorator";
import INavigation from "../Navigation/INavigation";
Expand All @@ -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,
Expand All @@ -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<Observer<Scene>>;
private earlyStoppingCounter = 0;

private debug_pathLine: LinesMesh;

Expand Down Expand Up @@ -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 }) => {
Expand All @@ -97,6 +119,7 @@ export default class CharacterNavigator
}
);

// debug drawings
this.debug_drawPath(target);
}

Expand All @@ -108,9 +131,7 @@ export default class CharacterNavigator
);
this.characterAnimator.transition(CharacterAnimationActions.TargetReached);

this.navigation.Crowd.onReachTargetObservable.remove(
this.targetReachedObserverRef
);
this.resetObservers();
}

@bind
Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,14 +298,14 @@ 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);
mockWalkAnimation.speedRatio = 0;

systemUnderTest["setWalkingAnimationSpeed"]();

expect(mockWalkAnimation.speedRatio).toEqual(1);
expect(mockWalkAnimation.speedRatio).toEqual(0.5);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<Scene>();
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<Scene>();
sceneMock.deltaTime = 1;

systemUnderTest["checkEarlyStopping"](sceneMock);

expect(systemUnderTest["earlyStoppingCounter"]).toBe(0);
});

test("checkEarlyStopping calls targetReachedCallback when earlyStoppingPatience is exceeded", () => {
const sceneMock = mockDeep<Scene>();
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;

Expand Down

0 comments on commit 4d76cf6

Please sign in to comment.