Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hasExitTime and fix error when set state startTime and transition duration is 0 #2359

Merged
merged 27 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0a2b927
fix: animation error when set state startTime and transition duration…
luzhuang Aug 16, 2024
3f8e950
test: add ut
luzhuang Aug 16, 2024
58f4e03
feat: opt comment
luzhuang Aug 16, 2024
4bff236
feat: add hasExitTime, fixedDuration, setTrigger
luzhuang Aug 21, 2024
c4ba1e3
feat: opt code
luzhuang Aug 21, 2024
9e9d6da
Merge commit refs/pull/2337/head of github.com:oasis-engine/engine in…
luzhuang Aug 21, 2024
5a4243b
feat: opt code
luzhuang Aug 21, 2024
2618b14
test: add ut
luzhuang Aug 21, 2024
0dce44f
feat: opt code
luzhuang Aug 21, 2024
bcca797
feat: update animatorController loader
luzhuang Aug 21, 2024
e127b27
feat: add hasExitTime and fix error when set state startTime and tran…
luzhuang Aug 26, 2024
8e7e20a
feat: opt code
luzhuang Aug 26, 2024
2dfed41
feat: opt code
luzhuang Aug 26, 2024
b4712af
feat: opt code
luzhuang Aug 26, 2024
562b900
feat: opt code
luzhuang Aug 28, 2024
64d825e
fix: stateMachine transition index error and solo check error
luzhuang Aug 28, 2024
5ddc513
feat: opt code
luzhuang Aug 28, 2024
66bc753
feat: opt code
luzhuang Aug 28, 2024
9f59d49
feat: opt code
luzhuang Aug 29, 2024
e3ef464
feat: opt code
GuoLei1990 Sep 10, 2024
69ee2c4
feat: opt code
GuoLei1990 Sep 10, 2024
2e63e45
refactor: opt code
GuoLei1990 Sep 10, 2024
c7cc55a
refactor: opt code
GuoLei1990 Sep 10, 2024
0027bbe
refactor: opt code
luzhuang Sep 11, 2024
ee324ad
refactor: opt code
GuoLei1990 Sep 11, 2024
e7f3805
refactor: opt code
GuoLei1990 Sep 11, 2024
e684bd1
refactor: opt code
GuoLei1990 Sep 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 41 additions & 24 deletions packages/core/src/animation/Animator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,18 @@ export class Animator extends Component {

const transition =
(anyStateTransitions.length &&
this._applyTransitionsByCondition(layerIndex, layerData, layer, state, anyStateTransitions, aniUpdate)) ||
this._applyStateTransitions(
layerIndex,
layerData,
layer,
isForwards,
srcPlayData,
anyStateTransitions,
lastClipTime,
clipTime,
playDeltaTime,
aniUpdate
)) ||
GuoLei1990 marked this conversation as resolved.
Show resolved Hide resolved
(transitions.length &&
this._applyStateTransitions(
layerIndex,
Expand All @@ -572,22 +583,27 @@ export class Animator extends Component {
if (transition) {
const clipDuration = state.clip.length;
const clipEndTime = state.clipEndTime * clipDuration;
const exitTime = transition.exitTime * state._getDuration();

if (isForwards) {
if (exitTime < lastClipTime) {
playCostTime = exitTime + clipEndTime - lastClipTime;
if (transition.hasExitTime) {
const exitTime = transition.exitTime * state._getDuration();

if (isForwards) {
if (exitTime < lastClipTime) {
playCostTime = exitTime + clipEndTime - lastClipTime;
} else {
playCostTime = exitTime - lastClipTime;
}
} else {
playCostTime = exitTime - lastClipTime;
const startTime = state.clipStartTime * clipDuration;
if (lastClipTime < exitTime) {
playCostTime = clipEndTime - exitTime + lastClipTime - startTime;
} else {
playCostTime = lastClipTime - exitTime;
}
playCostTime = -playCostTime;
}
} else {
const startTime = state.clipStartTime * clipDuration;
if (lastClipTime < exitTime) {
playCostTime = clipEndTime - exitTime + lastClipTime - startTime;
} else {
playCostTime = lastClipTime - exitTime;
}
playCostTime = -playCostTime;
playCostTime = 0;
}
// Revert actualDeltaTime and update playCostTime
srcPlayData.update(playCostTime - playDeltaTime);
Expand Down Expand Up @@ -667,15 +683,15 @@ export class Animator extends Component {

let dstPlayCostTime: number;
if (destPlayData.isForwards) {
// The time that has been played
const playedTime = lastDestClipTime - destState.clipStartTime * destState.clip.length;
GuoLei1990 marked this conversation as resolved.
Show resolved Hide resolved
dstPlayCostTime =
lastDestClipTime + dstPlayDeltaTime > transitionDuration
? transitionDuration - lastDestClipTime
: dstPlayDeltaTime;
playedTime + dstPlayDeltaTime > transitionDuration ? transitionDuration - playedTime : dstPlayDeltaTime;
} else {
// The time that has been played
const playedTime = destStateDuration - lastDestClipTime;
const playedTime = destState.clipEndTime * destState.clip.length - lastDestClipTime;
dstPlayCostTime =
// -actualDestDeltaTime: The time that will be played, negative are meant to make ite be a periods
// -dstPlayDeltaTime: The time that will be played, negative are meant to make it be a periods
// > transition: The time that will be played is enough to finish the transition
playedTime - dstPlayDeltaTime > transitionDuration
? // Negative number is used to convert a time period into a reverse deltaTime.
Expand Down Expand Up @@ -800,7 +816,7 @@ export class Animator extends Component {
// The time that has been played
const playedTime = stateDuration - lastDestClipTime;
dstPlayCostTime =
// -actualDestDeltaTime: The time that will be played, negative are meant to make ite be a periods
// -playDeltaTime: The time that will be played, negative are meant to make it be a periods
// > transition: The time that will be played is enough to finish the transition
playedTime - playDeltaTime > transitionDuration
? // Negative number is used to convert a time period into a reverse deltaTime.
Expand Down Expand Up @@ -889,7 +905,6 @@ export class Animator extends Component {
deltaTime: number,
aniUpdate: boolean
): void {
const { stateMachine } = layer;
const playData = layerData.srcPlayData;
const { state } = playData;
const actualSpeed = state.speed * this.speed;
Expand Down Expand Up @@ -1089,12 +1104,13 @@ export class Animator extends Component {
const duration = state._getDuration();
for (let n = transitions.length; transitionIndex < n; transitionIndex++) {
const transition = transitions[transitionIndex];
const hasExitTime = transition.hasExitTime;
const exitTime = transition.exitTime * duration;
if (exitTime > curClipTime) {
if (hasExitTime && exitTime > curClipTime) {
break;
}

if (exitTime >= lastClipTime) {
if (exitTime >= lastClipTime || !hasExitTime) {
playState.currentTransitionIndex = Math.min(transitionIndex + 1, n - 1);
if (this._checkConditions(state, transition)) {
if (this._applyTransition(layerIndex, layerData, layer, transition, aniUpdate)) {
Expand Down Expand Up @@ -1123,12 +1139,13 @@ export class Animator extends Component {
const duration = playState.state._getDuration();
for (; transitionIndex >= 0; transitionIndex--) {
const transition = transitions[transitionIndex];
const hasExitTime = transition.hasExitTime;
const exitTime = transition.exitTime * duration;
if (exitTime < curClipTime) {
if (hasExitTime && exitTime < curClipTime) {
break;
}

if (exitTime <= lastClipTime) {
if (exitTime <= lastClipTime || !hasExitTime) {
playState.currentTransitionIndex = Math.max(transitionIndex - 1, 0);
if (this._checkConditions(state, transition)) {
if (this._applyTransition(layerIndex, layerData, layer, transition, aniUpdate)) {
Expand Down
38 changes: 30 additions & 8 deletions packages/core/src/animation/AnimatorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,11 @@ export class AnimatorState {
}
transition._srcState = this;
const transitions = this._transitions;
const count = transitions.length;
const time = transition.exitTime;
const maxExitTime = count ? transitions[count - 1].exitTime : 0;
if (time >= maxExitTime) {
transitions.push(transition);

if (transition.hasExitTime) {
this._addHasExitTimeTransition(transition);
} else {
let index = count;
while (--index >= 0 && time < transitions[index].exitTime);
transitions.splice(index + 1, 0, transition);
transitions.unshift(transition);
GuoLei1990 marked this conversation as resolved.
Show resolved Hide resolved
}

transition.solo && !this._hasSoloTransition && this._updateSoloTransition(true);
Expand Down Expand Up @@ -232,4 +228,30 @@ export class AnimatorState {
}
}
}

/**
* @internal
*/
_updateTransitionsIndex(transition: AnimatorStateTransition, hasExitTime: boolean): void {
this._transitions.splice(this._transitions.indexOf(transition), 1);
if (hasExitTime) {
this._addHasExitTimeTransition(transition);
} else {
this._transitions.unshift(transition);
}
}

private _addHasExitTimeTransition(transition: AnimatorStateTransition): void {
const time = transition.exitTime;
const transitions = this._transitions;
const count = transitions.length;
const maxExitTime = count ? transitions[count - 1].exitTime : 0;
if (time >= maxExitTime) {
transitions.push(transition);
} else {
let index = count;
while (--index >= 0 && time < transitions[index].exitTime);
transitions.splice(index + 1, 0, transition);
}
}
}
5 changes: 3 additions & 2 deletions packages/core/src/animation/AnimatorStateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class AnimatorStateMachine {
*/
addEntryStateTransition(transition: AnimatorStateTransition): AnimatorStateTransition;
/**
* Add an entry transition to the destination state.
* Add an entry transition to the destination state, the default value of entry transition's hasExitTime is false.
* @param animatorState - The destination state
*/

Expand All @@ -118,7 +118,7 @@ export class AnimatorStateMachine {
*/
addAnyStateTransition(transition: AnimatorStateTransition): AnimatorStateTransition;
/**
* Add an any transition to the destination state.
* Add an any transition to the destination state, the default value of any transition's hasExitTime is false.
* @param animatorState - The destination state
*/
addAnyStateTransition(animatorState: AnimatorState): AnimatorStateTransition;
Expand All @@ -142,6 +142,7 @@ export class AnimatorStateMachine {
let transition: AnimatorStateTransition;
if (transitionOrAnimatorState instanceof AnimatorState) {
transition = new AnimatorStateTransition();
transition.hasExitTime = false;
transition.destinationState = transitionOrAnimatorState;
} else {
transition = transitionOrAnimatorState;
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/animation/AnimatorStateTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class AnimatorStateTransition {

private _conditions: AnimatorCondition[] = [];
private _solo = false;
private _hasExitTime: boolean = true;

/**
* Is the transition destination the exit of the current state machine.
Expand Down Expand Up @@ -52,6 +53,19 @@ export class AnimatorStateTransition {
return this._conditions;
}

/**
* When active the transition will have an exit time condition.
*/
get hasExitTime(): boolean {
return this._hasExitTime;
}

set hasExitTime(value: boolean) {
if (this._hasExitTime === value) return;
this._hasExitTime = value;
this._srcState?._updateTransitionsIndex(this, value);
}

/**
* Add a condition to a transition.
* @param mode - The AnimatorCondition mode of the condition
Expand Down
5 changes: 3 additions & 2 deletions packages/loader/src/AnimatorControllerLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import {
AnimatorState,
AnimatorConditionMode,
AnimatorControllerParameterValue,
WrapMode,
AnimatorControllerParameter
WrapMode
} from "@galacean/engine-core";

@resourceLoader(AssetType.AnimatorController, ["json"], false)
Expand Down Expand Up @@ -116,6 +115,7 @@ class AnimatorControllerLoader extends Loader<AnimatorController> {

private _createTransition(transitionData: ITransitionData, destinationState: AnimatorState): AnimatorStateTransition {
const transition = new AnimatorStateTransition();
transition.hasExitTime = transitionData.hasExitTime;
transition.duration = transitionData.duration;
transition.offset = transitionData.offset;
transition.exitTime = transitionData.exitTime;
Expand Down Expand Up @@ -156,6 +156,7 @@ interface ITransitionData {
mute: boolean;
isExit: boolean;
conditions: IConditionData[];
hasExitTime: boolean;
}

interface IConditionData {
Expand Down
96 changes: 91 additions & 5 deletions tests/src/core/Animator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
AnimatorController,
WrapMode,
StateMachineScript,
AnimatorState
Entity
} from "@galacean/engine-core";
import { GLTFResource } from "@galacean/engine-loader";
import { Quaternion } from "@galacean/engine-math";
Expand Down Expand Up @@ -396,8 +396,6 @@ describe("Animator test", function () {
anyTransition.duration = 0.3;
anyTransition.exitTime = 0.9;
let anyToIdleTime =
// @ts-ignore
(anyTransition.exitTime - toIdleTransition.duration) * walkState._getDuration() +
// @ts-ignore
(anyTransition.duration * idleState._getDuration()) / idleSpeed;

Expand Down Expand Up @@ -522,8 +520,6 @@ describe("Animator test", function () {
anyTransition.duration = 0.3;
anyTransition.exitTime = 0.1;
let anyToIdleTime =
// @ts-ignore
(1 - anyTransition.exitTime - toIdleTransition.duration) * walkState._getDuration() +
// @ts-ignore
(anyTransition.duration * idleState._getDuration()) / idleSpeed;

Expand Down Expand Up @@ -568,6 +564,8 @@ describe("Animator test", function () {
});

it("change state in one update", () => {
const entity = new Entity(engine);
const animator = entity.addComponent(Animator);
const animatorController = new AnimatorController(engine);
const layer = new AnimatorControllerLayer("layer");
animatorController.addLayer(layer);
Expand Down Expand Up @@ -644,6 +642,8 @@ describe("Animator test", function () {
});

it("stateMachineScript", () => {
const entity = new Entity(engine);
const animator = entity.addComponent(Animator);
const animatorController = new AnimatorController(engine);
const layer = new AnimatorControllerLayer("layer");
animatorController.addLayer(layer);
Expand Down Expand Up @@ -709,4 +709,90 @@ describe("Animator test", function () {
expect(onStateEnter2Spy).to.have.been.called.exactly(1);
expect(onStateExit2Spy).to.have.been.called.exactly(1);
});

it("anyTransition", () => {
const { animatorController } = animator;
// @ts-ignore
const layerData = animator._getAnimatorLayerData(0);
animatorController.addParameter("playRun", 0);
const stateMachine = animatorController.layers[0].stateMachine;
// @ts-ignore
stateMachine._entryTransitions.length = 0;
// @ts-ignore
stateMachine._anyStateTransitions.length = 0;
const walkState = animator.findAnimatorState("Run");
// For test clipStartTime is not 0 and transition duration is 0
walkState.clipStartTime = 0.5;
walkState.addStateMachineScript(
class extends StateMachineScript {
onStateEnter(animator) {
animator.setParameterValue("playRun", 0);
}
}
);
const transition = stateMachine.addAnyStateTransition(animator.findAnimatorState("Run"));
transition.addCondition(AnimatorConditionMode.Equals, "playRun", 1);
// For test clipStartTime is not 0 and transition duration is 0
transition.duration = 0;
animator.setParameterValue("playRun", 1);

animator.play("Walk");
// @ts-ignore
animator.engine.time._frameCount++;
animator.update(0.5);

expect(layerData.srcPlayData.state.name).to.eq("Run");
expect(layerData.srcPlayData.frameTime).to.eq(0.5);
expect(layerData.srcPlayData.clipTime).to.eq(walkState.clip.length * 0.5 + 0.5);
});

it("hasExitTime", () => {
const { animatorController } = animator;
// @ts-ignore
animatorController._parameters.length = 0;
// @ts-ignore
animatorController._parametersMap = Object.create(null);
animatorController.addParameter("triggerIdle", false);
// @ts-ignore
const layerData = animator._getAnimatorLayerData(0);
const stateMachine = animatorController.layers[0].stateMachine;
// @ts-ignore
stateMachine._entryTransitions.length = 0;
// @ts-ignore
stateMachine._anyStateTransitions.length = 0;
const idleState = animator.findAnimatorState("Survey");
idleState.speed = 1;
idleState.clearTransitions();
const walkState = animator.findAnimatorState("Walk");
walkState.clipStartTime = 0;
walkState.clearTransitions();
const runState = animator.findAnimatorState("Run");
runState.clearTransitions();
const walkToRunTransition = walkState.addTransition(runState);
walkToRunTransition.hasExitTime = true;
walkToRunTransition.exitTime = 0.5;
walkToRunTransition.duration = 0;

animator.play("Walk");
// @ts-ignore
animator.engine.time._frameCount++;
animator.update(walkState.clip.length * 0.5);
expect(layerData.destPlayData.state.name).to.eq("Run");
expect(layerData.destPlayData.frameTime).to.eq(0);
const anyToIdleTransition = stateMachine.addAnyStateTransition(idleState);
anyToIdleTransition.hasExitTime = false;
anyToIdleTransition.duration = 0.2;
anyToIdleTransition.addCondition(AnimatorConditionMode.If, "triggerIdle");
animator.setParameterValue("triggerIdle", true);
// @ts-ignore
animator.engine.time._frameCount++;
animator.update(0.1);
expect(layerData.srcPlayData.state.name).to.eq("Run");
expect(layerData.srcPlayData.frameTime).to.eq(0.1);
// @ts-ignore
animator.engine.time._frameCount++;
animator.update(idleState.clip.length * 0.2 - 0.1);
expect(layerData.srcPlayData.state.name).to.eq("Survey");
expect(layerData.srcPlayData.clipTime).to.eq(idleState.clip.length * 0.2);
});
});
Loading