From 0a2b927219e07be584e3db593a08d835afbad5d4 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 16 Aug 2024 10:45:32 +0800 Subject: [PATCH 01/17] fix: animation error when set state startTime and transition duration is 0 --- packages/core/src/animation/Animator.ts | 80 +++++++++++++------------ 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index ecf0da396d..9321cb192d 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -551,43 +551,49 @@ export class Animator extends Component { const { transitions } = state; const { anyStateTransitions } = layer.stateMachine; - const transition = - (anyStateTransitions.length && - this._applyTransitionsByCondition(layerIndex, layerData, layer, state, anyStateTransitions, aniUpdate)) || - (transitions.length && - this._applyStateTransitions( - layerIndex, - layerData, - layer, - isForwards, - srcPlayData, - transitions, - lastClipTime, - clipTime, - playDeltaTime, - aniUpdate - )); - + const anyTransition = + anyStateTransitions.length && + this._applyTransitionsByCondition(layerIndex, layerData, layer, state, anyStateTransitions, aniUpdate); + const stateTransition = + transitions.length && + this._applyStateTransitions( + layerIndex, + layerData, + layer, + isForwards, + srcPlayData, + transitions, + lastClipTime, + clipTime, + playDeltaTime, + aniUpdate + ); + + const transition = anyTransition || stateTransition; let playCostTime: number; 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; - } else { - playCostTime = exitTime - lastClipTime; - } + if (anyTransition) { + playCostTime = 0; } else { - const startTime = state.clipStartTime * clipDuration; - if (lastClipTime < exitTime) { - playCostTime = clipEndTime - exitTime + lastClipTime - startTime; + 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; + } else { + playCostTime = exitTime - lastClipTime; + } } else { - playCostTime = lastClipTime - exitTime; + const startTime = state.clipStartTime * clipDuration; + if (lastClipTime < exitTime) { + playCostTime = clipEndTime - exitTime + lastClipTime - startTime; + } else { + playCostTime = lastClipTime - exitTime; + } + playCostTime = -playCostTime; } - playCostTime = -playCostTime; } // Revert actualDeltaTime and update playCostTime srcPlayData.update(playCostTime - playDeltaTime); @@ -667,15 +673,15 @@ export class Animator extends Component { let dstPlayCostTime: number; if (destPlayData.isForwards) { + // The time that has been played + const playedTime = destState.clipStartTime * destState.clip.length - lastDestClipTime; 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 + // -actualDestDeltaTime: 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. From 3f8e9503a0d2feb2abf3f8e9c38cfc1ef4fed97c Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 16 Aug 2024 11:57:30 +0800 Subject: [PATCH 02/17] test: add ut --- tests/src/core/Animator.test.ts | 43 +++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index eb17c66e7c..1bf56d6049 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -16,7 +16,8 @@ import { AnimatorController, WrapMode, StateMachineScript, - AnimatorState + AnimatorState, + Entity } from "@galacean/engine-core"; import { GLTFResource } from "@galacean/engine-loader"; import { Quaternion } from "@galacean/engine-math"; @@ -396,8 +397,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; @@ -522,8 +521,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; @@ -568,6 +565,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); @@ -644,6 +643,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); @@ -709,4 +710,36 @@ 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._anyStateTransitions.length = 0; + const walkState = animator.findAnimatorState("Run"); + 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); + 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"); + // @ts-ignore + expect(layerData.srcPlayData.frameTime).to.eq(0.5); + }); }); From 58f4e03202bdae3e76adf1fb93e061ab630fbd4d Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 16 Aug 2024 11:59:32 +0800 Subject: [PATCH 03/17] feat: opt comment --- packages/core/src/animation/Animator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 9321cb192d..27ef5247d9 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -806,7 +806,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 + // -actualDestDeltaTime: 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. From 4bff236cd87c1a3a087c68f8df51e4ed0259968a Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Wed, 21 Aug 2024 10:20:08 +0800 Subject: [PATCH 04/17] feat: add hasExitTime, fixedDuration, setTrigger --- packages/core/src/animation/Animator.ts | 91 ++++++++++++++----- .../core/src/animation/AnimatorController.ts | 8 +- .../animation/AnimatorControllerParameter.ts | 3 + packages/core/src/animation/AnimatorState.ts | 19 ++-- .../src/animation/AnimatorStateMachine.ts | 1 + .../src/animation/AnimatorStateTransition.ts | 4 + 6 files changed, 94 insertions(+), 32 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index ecf0da396d..3a83f8277f 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -125,15 +125,17 @@ export class Animator extends Component { /** * Create a cross fade from the current state to another state. * @param stateName - The state name - * @param normalizedTransitionDuration - The duration of the transition (normalized) + * @param duration - The duration of the transition (normalized) * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name * @param normalizedTimeOffset - The time offset between 0 and 1(default 0) + * @param fixedDuration - The duration is fixed or normalized(default normalized) */ crossFade( stateName: string, - normalizedTransitionDuration: number, + duration: number, layerIndex: number = -1, - normalizedTimeOffset: number = 0 + normalizedTimeOffset: number = 0, + fixedDuration: boolean = false ): void { if (this._controllerUpdateFlag?.flag) { this._reset(); @@ -141,7 +143,8 @@ export class Animator extends Component { const { state, layerIndex: playLayerIndex } = this._getAnimatorStateInfo(stateName, layerIndex); const { manuallyTransition } = this._getAnimatorLayerData(playLayerIndex); - manuallyTransition.duration = normalizedTransitionDuration; + manuallyTransition.hasFixedDuration = fixedDuration; + manuallyTransition.duration = duration; manuallyTransition.offset = normalizedTimeOffset; manuallyTransition.destinationState = state; @@ -250,6 +253,30 @@ export class Animator extends Component { } } + /** + * Set the 'true' value of the given parameter. + * @param name - The name of the parameter + */ + setTrigger(name: string) { + const parameter = this._animatorController?._parametersMap[name]; + + if (parameter?._isTrigger) { + this._parametersValueMap[name] = true; + } + } + + /** + * Set the 'false' value of the given parameter. + * @param name - The name of the parameter + */ + resetTrigger(name: string) { + const parameter = this._animatorController?._parametersMap[name]; + + if (parameter?._isTrigger) { + this._parametersValueMap[name] = false; + } + } + /** * @internal */ @@ -572,22 +599,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); @@ -648,12 +680,14 @@ export class Animator extends Component { deltaTime: number, aniUpdate: boolean ) { - const { srcPlayData, destPlayData } = layerData; + const { srcPlayData, destPlayData, crossFadeTransition } = layerData; const { speed } = this; const { state: srcState } = srcPlayData; const { state: destState } = destPlayData; const destStateDuration = destState._getDuration(); - const transitionDuration = destStateDuration * layerData.crossFadeTransition.duration; + const transitionDuration = crossFadeTransition.hasFixedDuration + ? crossFadeTransition.duration + : destStateDuration * crossFadeTransition.duration; const srcPlaySpeed = srcState.speed * speed; const dstPlaySpeed = destState.speed * speed; @@ -779,11 +813,13 @@ export class Animator extends Component { deltaTime: number, aniUpdate: boolean ) { - const { destPlayData } = layerData; + const { destPlayData, crossFadeTransition } = layerData; const { state } = destPlayData; const stateDuration = state._getDuration(); - const transitionDuration = stateDuration * layerData.crossFadeTransition.duration; + const transitionDuration = crossFadeTransition.hasFixedDuration + ? crossFadeTransition.duration + : stateDuration * crossFadeTransition.duration; const playSpeed = state.speed * this.speed; const playDeltaTime = playSpeed * deltaTime; @@ -1089,12 +1125,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)) { @@ -1123,12 +1160,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)) { @@ -1213,6 +1251,11 @@ export class Animator extends Component { return false; } + const parameter = this.getParameter(name); + if (parameter._isTrigger) { + this._parametersValueMap[name] = false; + } + switch (mode) { case AnimatorConditionMode.Equals: if (parameterValue === threshold) { diff --git a/packages/core/src/animation/AnimatorController.ts b/packages/core/src/animation/AnimatorController.ts index 8bb96bc8ef..47322dcea7 100644 --- a/packages/core/src/animation/AnimatorController.ts +++ b/packages/core/src/animation/AnimatorController.ts @@ -53,8 +53,13 @@ export class AnimatorController extends ReferResource { * Add a parameter to the controller. * @param name - The name of the parameter * @param defaultValue - The default value of the parameter + * @param isTrigger - Is the parameter a trigger, if true, the parameter will act as a trigger, trigger work mostly like bool parameter, but their values are reset to false after check a Transition. */ - addParameter(name: string, defaultValue?: AnimatorControllerParameterValue): AnimatorControllerParameter { + addParameter( + name: string, + defaultValue?: AnimatorControllerParameterValue, + isTrigger: boolean = false + ): AnimatorControllerParameter { if (this._parametersMap[name]) { console.warn(`Parameter ${name} already exists.`); return null; @@ -62,6 +67,7 @@ export class AnimatorController extends ReferResource { const param = new AnimatorControllerParameter(); param.name = name; param.defaultValue = defaultValue; + param._isTrigger = isTrigger; param._onNameChanged = (oldName, newName) => { delete this._parametersMap[oldName]; this._parametersMap[newName] = param as AnimatorControllerParameter; diff --git a/packages/core/src/animation/AnimatorControllerParameter.ts b/packages/core/src/animation/AnimatorControllerParameter.ts index a0fab7fb41..4f67239b32 100644 --- a/packages/core/src/animation/AnimatorControllerParameter.ts +++ b/packages/core/src/animation/AnimatorControllerParameter.ts @@ -10,6 +10,9 @@ export class AnimatorControllerParameter { /** @internal */ _onNameChanged: (oldName: string, newName: string) => void = null; + /** @internal */ + _isTrigger: boolean = false; + private _name: string; /** diff --git a/packages/core/src/animation/AnimatorState.ts b/packages/core/src/animation/AnimatorState.ts index b9cdb987d0..e26d73c732 100644 --- a/packages/core/src/animation/AnimatorState.ts +++ b/packages/core/src/animation/AnimatorState.ts @@ -112,14 +112,19 @@ 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) { + const time = transition.exitTime; + 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); + } } else { - let index = count; - while (--index >= 0 && time < transitions[index].exitTime); - transitions.splice(index + 1, 0, transition); + transitions.unshift(transition); } transition.solo && !this._hasSoloTransition && this._updateSoloTransition(true); diff --git a/packages/core/src/animation/AnimatorStateMachine.ts b/packages/core/src/animation/AnimatorStateMachine.ts index 65b6f6bd6f..acd2715ed9 100644 --- a/packages/core/src/animation/AnimatorStateMachine.ts +++ b/packages/core/src/animation/AnimatorStateMachine.ts @@ -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; diff --git a/packages/core/src/animation/AnimatorStateTransition.ts b/packages/core/src/animation/AnimatorStateTransition.ts index e4d4a34d3f..784b589669 100644 --- a/packages/core/src/animation/AnimatorStateTransition.ts +++ b/packages/core/src/animation/AnimatorStateTransition.ts @@ -17,6 +17,10 @@ export class AnimatorStateTransition { destinationState: AnimatorState; /** Mutes the transition. The transition will never occur. */ mute: boolean = false; + /** When active the transition will have an exit time condition. */ + hasExitTime: boolean = true; + /** Determines whether the duration of the transition is reported in a fixed duration in seconds or as a normalized time. */ + hasFixedDuration: boolean = false; /** @internal */ _srcState: AnimatorState; From c4ba1e3a12e96f3b454e9eb2fdf5fe20de4e25be Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Wed, 21 Aug 2024 10:27:44 +0800 Subject: [PATCH 05/17] feat: opt code --- packages/core/src/animation/Animator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 27ef5247d9..262475e844 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -895,7 +895,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; From 5a4243bc59aecc6be7ea47086280d24062c0fd09 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Wed, 21 Aug 2024 11:04:31 +0800 Subject: [PATCH 06/17] feat: opt code --- packages/core/src/animation/Animator.ts | 47 +++++++++++++++---------- tests/src/core/Animator.test.ts | 25 ++++++++++++- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index bd24630259..3c2ac44606 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -578,25 +578,34 @@ export class Animator extends Component { const { transitions } = state; const { anyStateTransitions } = layer.stateMachine; - const anyTransition = - anyStateTransitions.length && - this._applyTransitionsByCondition(layerIndex, layerData, layer, state, anyStateTransitions, aniUpdate); - const stateTransition = - transitions.length && - this._applyStateTransitions( - layerIndex, - layerData, - layer, - isForwards, - srcPlayData, - transitions, - lastClipTime, - clipTime, - playDeltaTime, - aniUpdate - ); - - const transition = anyTransition || stateTransition; + const transition = + (anyStateTransitions.length && + this._applyStateTransitions( + layerIndex, + layerData, + layer, + isForwards, + srcPlayData, + anyStateTransitions, + lastClipTime, + clipTime, + playDeltaTime, + aniUpdate + )) || + (transitions.length && + this._applyStateTransitions( + layerIndex, + layerData, + layer, + isForwards, + srcPlayData, + transitions, + lastClipTime, + clipTime, + playDeltaTime, + aniUpdate + )); + let playCostTime: number; if (transition) { const clipDuration = state.clip.length; diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index 1bf56d6049..de9a23f632 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -720,6 +720,7 @@ describe("Animator test", function () { //@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 { @@ -730,6 +731,7 @@ describe("Animator test", function () { ); 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); @@ -739,7 +741,28 @@ describe("Animator test", function () { animator.update(0.5); expect(layerData.srcPlayData.state.name).to.eq("Run"); - // @ts-ignore 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; + const stateMachine = animatorController.layers[0].stateMachine; + const walkState = animator.findAnimatorState("Walk"); + walkState.clipStartTime = 0.5; + const runState = animator.findAnimatorState("Run"); + runState.clipStartTime = 0.5; + const transition = new AnimatorStateTransition(); + transition.destinationState = runState; + transition.exitTime = 0.5; + transition.hasExitTime = true; + walkState.addTransition(transition); + + animator.play("Walk"); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.5); + + expect(animator.getCurrentAnimatorState(0).name).to.eq("Run"); }); }); From 2618b1415002170e14c45a918c99fcfd0ad27e63 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Wed, 21 Aug 2024 13:50:43 +0800 Subject: [PATCH 07/17] test: add ut --- packages/core/src/animation/Animator.ts | 12 ++ .../core/src/animation/AnimatorController.ts | 10 ++ .../src/animation/AnimatorStateMachine.ts | 18 ++- tests/src/core/Animator.test.ts | 128 ++++++++++++++++-- 4 files changed, 154 insertions(+), 14 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 3c2ac44606..2734c69665 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -58,6 +58,8 @@ export class Animator extends Component { @ignoreClone private _tempAnimatorStateInfo: IAnimatorStateInfo = { layerIndex: -1, state: null }; + @ignoreClone + private _tempTriggerParametersName: string[] = []; @ignoreClone private _controlledRenderers: Renderer[] = []; @@ -179,6 +181,7 @@ export class Animator extends Component { const animatorController = this._animatorController; if (!animatorController) { + this._resetTriggerParameters(); return; } @@ -193,6 +196,8 @@ export class Animator extends Component { const layerData = this._getAnimatorLayerData(i); this._updateState(i, layerData, layers[i], deltaTime, animationUpdate); } + + this._resetTriggerParameters(); } /** @@ -262,6 +267,7 @@ export class Animator extends Component { if (parameter?._isTrigger) { this._parametersValueMap[name] = true; + this._tempTriggerParametersName.push(name); } } @@ -1509,6 +1515,12 @@ export class Animator extends Component { this._callAnimatorScriptOnUpdate(state, layerIndex); } } + + private _resetTriggerParameters(): void { + for (let i = 0, n = this._tempTriggerParametersName.length; i < n; i++) { + this._parametersValueMap[this._tempTriggerParametersName[i]] = false; + } + } } interface IAnimatorStateInfo { diff --git a/packages/core/src/animation/AnimatorController.ts b/packages/core/src/animation/AnimatorController.ts index 47322dcea7..55f6f72e7f 100644 --- a/packages/core/src/animation/AnimatorController.ts +++ b/packages/core/src/animation/AnimatorController.ts @@ -90,6 +90,16 @@ export class AnimatorController extends ReferResource { } } + /** + * Clear parameters. + */ + clearParameters(): void { + this._parameters.length = 0; + for (let name in this._parametersMap) { + delete this._parametersMap[name]; + } + } + /** * Get the parameter by name. * @param name - The name of the parameter diff --git a/packages/core/src/animation/AnimatorStateMachine.ts b/packages/core/src/animation/AnimatorStateMachine.ts index acd2715ed9..77d2a044d0 100644 --- a/packages/core/src/animation/AnimatorStateMachine.ts +++ b/packages/core/src/animation/AnimatorStateMachine.ts @@ -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 */ @@ -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; @@ -135,6 +135,20 @@ export class AnimatorStateMachine { this._removeTransition(transition, this._anyStateTransitions); } + /** + * Clears all entry transitions from the state. + */ + clearEntryTransitions(): void { + this._entryTransitions.length = 0; + } + + /** + * Clears all any transitions from the state. + */ + clearAnyStateTransitions(): void { + this._anyStateTransitions.length = 0; + } + private _addTransition( transitionOrAnimatorState: AnimatorStateTransition | AnimatorState, transitions: AnimatorStateTransition[] diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index de9a23f632..be290132ab 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -16,7 +16,6 @@ import { AnimatorController, WrapMode, StateMachineScript, - AnimatorState, Entity } from "@galacean/engine-core"; import { GLTFResource } from "@galacean/engine-loader"; @@ -717,8 +716,8 @@ describe("Animator test", function () { const layerData = animator._getAnimatorLayerData(0); animatorController.addParameter("playRun", 0); const stateMachine = animatorController.layers[0].stateMachine; - //@ts-ignore - stateMachine._anyStateTransitions.length = 0; + stateMachine.clearEntryTransitions(); + stateMachine.clearAnyStateTransitions(); const walkState = animator.findAnimatorState("Run"); // For test clipStartTime is not 0 and transition duration is 0 walkState.clipStartTime = 0.5; @@ -747,22 +746,127 @@ describe("Animator test", function () { it("hasExitTime", () => { const { animatorController } = animator; + animatorController.clearParameters(); + animatorController.addParameter("triggerIdle", false); + // @ts-ignore + const layerData = animator._getAnimatorLayerData(0); const stateMachine = animatorController.layers[0].stateMachine; + stateMachine.clearEntryTransitions(); + stateMachine.clearAnyStateTransitions(); + const idleState = animator.findAnimatorState("Survey"); + idleState.speed = 1; + idleState.clearTransitions(); const walkState = animator.findAnimatorState("Walk"); - walkState.clipStartTime = 0.5; + walkState.clipStartTime = 0; + walkState.clearTransitions(); const runState = animator.findAnimatorState("Run"); - runState.clipStartTime = 0.5; - const transition = new AnimatorStateTransition(); - transition.destinationState = runState; - transition.exitTime = 0.5; - transition.hasExitTime = true; - walkState.addTransition(transition); + 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(0.5); + 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); + }); - expect(animator.getCurrentAnimatorState(0).name).to.eq("Run"); + it("setTrigger", () => { + const { animatorController } = animator; + animatorController.clearParameters(); + animatorController.addParameter("triggerRun", false, true); + animatorController.addParameter("triggerWalk", false, true); + // @ts-ignore + const layerData = animator._getAnimatorLayerData(0); + const stateMachine = animatorController.layers[0].stateMachine; + stateMachine.clearEntryTransitions(); + stateMachine.clearAnyStateTransitions(); + const walkState = animator.findAnimatorState("Walk"); + walkState.clearTransitions(); + const runState = animator.findAnimatorState("Run"); + runState.clipStartTime = 0; + runState.clearTransitions(); + const walkToRunTransition = walkState.addTransition(runState); + walkToRunTransition.hasExitTime = false; + walkToRunTransition.duration = 0.1; + walkToRunTransition.addCondition(AnimatorConditionMode.If, "triggerRun"); + + const runToWalkTransition = runState.addTransition(walkState); + runToWalkTransition.hasExitTime = true; + runToWalkTransition.exitTime = 0.7; + runToWalkTransition.duration = 0.3; + + animator.play("Walk"); + animator.setTrigger("triggerRun"); + animator.setTrigger("triggerWalk"); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.1); + expect(layerData.srcPlayData.state.name).to.eq("Walk"); + expect(layerData.srcPlayData.frameTime).to.eq(0.1); + expect(layerData.destPlayData.state.name).to.eq("Run"); + expect(layerData.destPlayData.frameTime).to.eq(0.1); + expect(animator.getParameterValue("triggerRun")).to.eq(false); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(runState.clip.length * 0.1 - 0.1); + expect(layerData.srcPlayData.state.name).to.eq("Run"); + expect(layerData.srcPlayData.frameTime).to.eq(runState.clip.length * 0.1); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(runState.clip.length * 0.6); + expect(layerData.destPlayData.state.name).to.eq("Walk"); + expect(layerData.destPlayData.frameTime).to.eq(0); + expect(animator.getParameterValue("triggerWalk")).to.eq(false); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(walkState.clip.length * 0.3); + expect(layerData.srcPlayData.state.name).to.eq("Walk"); + expect(layerData.srcPlayData.frameTime).to.eq(walkState.clip.length * 0.3); + }); + + it("fixedDuration", () => { + const { animatorController } = animator; + animatorController.clearParameters(); + animatorController.addParameter("triggerRun", false, true); + animatorController.addParameter("triggerWalk", false, true); + // @ts-ignore + const layerData = animator._getAnimatorLayerData(0); + const walkState = animator.findAnimatorState("Walk"); + walkState.clearTransitions(); + const runState = animator.findAnimatorState("Run"); + runState.clipStartTime = runState.clipEndTime = 0; + runState.clearTransitions(); + const walkToRunTransition = walkState.addTransition(runState); + walkToRunTransition.hasExitTime = false; + walkToRunTransition.hasFixedDuration = true; + walkToRunTransition.duration = 0.1; + walkToRunTransition.addCondition(AnimatorConditionMode.If, "triggerRun"); + animator.play("Walk"); + animator.setTrigger("triggerRun"); + // @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); + expect(layerData.srcPlayData.clipTime).to.eq(0); }); }); From 0dce44fed9ec5d75e88436ab5b487ac6e31bd68f Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Wed, 21 Aug 2024 13:58:15 +0800 Subject: [PATCH 08/17] feat: opt code --- packages/core/src/animation/AnimatorStateMachine.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/animation/AnimatorStateMachine.ts b/packages/core/src/animation/AnimatorStateMachine.ts index 77d2a044d0..19c1f46f9b 100644 --- a/packages/core/src/animation/AnimatorStateMachine.ts +++ b/packages/core/src/animation/AnimatorStateMachine.ts @@ -136,14 +136,14 @@ export class AnimatorStateMachine { } /** - * Clears all entry transitions from the state. + * Clears all entry transitions from the stateMachine. */ clearEntryTransitions(): void { this._entryTransitions.length = 0; } /** - * Clears all any transitions from the state. + * Clears all any transitions from the stateMachine. */ clearAnyStateTransitions(): void { this._anyStateTransitions.length = 0; From bcca797c9dcbaeb01b6567b0bfa9cd035db02a08 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Wed, 21 Aug 2024 14:07:47 +0800 Subject: [PATCH 09/17] feat: update animatorController loader --- packages/loader/src/AnimatorControllerLoader.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/loader/src/AnimatorControllerLoader.ts b/packages/loader/src/AnimatorControllerLoader.ts index 06728a6f7a..2f9fced87f 100644 --- a/packages/loader/src/AnimatorControllerLoader.ts +++ b/packages/loader/src/AnimatorControllerLoader.ts @@ -11,8 +11,7 @@ import { AnimatorState, AnimatorConditionMode, AnimatorControllerParameterValue, - WrapMode, - AnimatorControllerParameter + WrapMode } from "@galacean/engine-core"; @resourceLoader(AssetType.AnimatorController, ["json"], false) @@ -100,7 +99,7 @@ class AnimatorControllerLoader extends Loader { animatorController.addLayer(layer); }); parameters.forEach((parameterData) => { - animatorController.addParameter(parameterData.name, parameterData.defaultValue); + animatorController.addParameter(parameterData.name, parameterData.defaultValue, parameterData.isTrigger); }); Promise.all(promises).then((clipData) => { clipData.forEach((data) => { @@ -116,6 +115,8 @@ class AnimatorControllerLoader extends Loader { private _createTransition(transitionData: ITransitionData, destinationState: AnimatorState): AnimatorStateTransition { const transition = new AnimatorStateTransition(); + transition.hasExitTime = transitionData.hasExitTime; + transition.hasFixedDuration = transitionData.hasFixedDuration; transition.duration = transitionData.duration; transition.offset = transitionData.offset; transition.exitTime = transitionData.exitTime; @@ -156,6 +157,8 @@ interface ITransitionData { mute: boolean; isExit: boolean; conditions: IConditionData[]; + hasExitTime: boolean; + hasFixedDuration: boolean; } interface IConditionData { From 9805cb787be3db3020e135ecb72bb6a7d6444782 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 13 Sep 2024 10:49:57 +0800 Subject: [PATCH 10/17] refactor: opt code --- e2e/case/animator-stateMachine.ts | 12 ++++++------ packages/core/src/animation/Animator.ts | 14 ++++++-------- packages/core/src/animation/AnimatorController.ts | 1 - 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/e2e/case/animator-stateMachine.ts b/e2e/case/animator-stateMachine.ts index 5e89c2c47c..b8a51636b8 100644 --- a/e2e/case/animator-stateMachine.ts +++ b/e2e/case/animator-stateMachine.ts @@ -60,19 +60,19 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { const toWalkTransition = new AnimatorStateTransition(); toWalkTransition.destinationState = walkState; toWalkTransition.duration = 0.2; - toWalkTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0); + toWalkTransition.addCondition("playerSpeed", AnimatorConditionMode.Greater, 0); idleState.addTransition(toWalkTransition); idleToWalkTime = //@ts-ignore toWalkTransition.exitTime * idleState._getDuration() + toWalkTransition.duration * walkState._getDuration(); const exitTransition = idleState.addExitTransition(); - exitTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); + exitTransition.addCondition("playerSpeed", AnimatorConditionMode.Equals, 0); // to walk state const toRunTransition = new AnimatorStateTransition(); toRunTransition.destinationState = runState; toRunTransition.duration = 0.3; - toRunTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0.5); + toRunTransition.addCondition("playerSpeed", AnimatorConditionMode.Greater, 0.5); walkState.addTransition(toRunTransition); walkToRunTime = //@ts-ignore @@ -82,7 +82,7 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { const toIdleTransition = new AnimatorStateTransition(); toIdleTransition.destinationState = idleState; toIdleTransition.duration = 0.3; - toIdleTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); + toIdleTransition.addCondition("playerSpeed", AnimatorConditionMode.Equals, 0); walkState.addTransition(toIdleTransition); walkToIdleTime = //@ts-ignore @@ -94,7 +94,7 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { const RunToWalkTransition = new AnimatorStateTransition(); RunToWalkTransition.destinationState = walkState; RunToWalkTransition.duration = 0.3; - RunToWalkTransition.addCondition(AnimatorConditionMode.Less, "playerSpeed", 0.5); + RunToWalkTransition.addCondition("playerSpeed", AnimatorConditionMode.Less, 0.5); runState.addTransition(RunToWalkTransition); runToWalkTime = //@ts-ignore @@ -105,7 +105,7 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { stateMachine.addEntryStateTransition(idleState); const anyTransition = stateMachine.addAnyStateTransition(idleState); - anyTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); + anyTransition.addCondition("playerSpeed", AnimatorConditionMode.Equals, 0); anyTransition.duration = 0.3; let anyToIdleTime = // @ts-ignore diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 38f213a1bf..ddfe471859 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -36,22 +36,22 @@ export class Animator extends Component { cullingMode: AnimatorCullingMode = AnimatorCullingMode.None; /** The playback speed of the Animator, 1.0 is normal playback speed. */ @assignmentClone - speed: number = 1.0; + speed = 1.0; /** @internal */ - _playFrameCount: number = -1; + _playFrameCount = -1; /** @internal */ - _onUpdateIndex: number = -1; + _onUpdateIndex = -1; protected _animatorController: AnimatorController; @ignoreClone protected _controllerUpdateFlag: BoolUpdateFlag; @ignoreClone - protected _updateMark: number = 0; + protected _updateMark = 0; @ignoreClone - private _animatorLayersData: AnimatorLayerData[] = []; + private _animatorLayersData = new Array(); @ignoreClone private _curveOwnerPool: Record>> = Object.create(null); @ignoreClone @@ -61,11 +61,9 @@ export class Animator extends Component { @ignoreClone private _tempAnimatorStateInfo: IAnimatorStateInfo = { layerIndex: -1, state: null }; - @ignoreClone - private _tempTriggerParametersName: string[] = []; @ignoreClone - private _controlledRenderers: Renderer[] = []; + private _controlledRenderers = new Array(); /** * All layers from the AnimatorController which belongs this Animator. diff --git a/packages/core/src/animation/AnimatorController.ts b/packages/core/src/animation/AnimatorController.ts index 4430e1b17a..02f1c45ce9 100644 --- a/packages/core/src/animation/AnimatorController.ts +++ b/packages/core/src/animation/AnimatorController.ts @@ -53,7 +53,6 @@ export class AnimatorController extends ReferResource { * Add a parameter to the controller. * @param name - The name of the parameter * @param defaultValue - The default value of the parameter - * @param isTrigger - Is the parameter a trigger, if true, the parameter will act as a trigger, trigger work mostly like bool parameter, but their values are reset to false after check a Transition. */ addParameter(name: string, defaultValue?: AnimatorControllerParameterValue): AnimatorControllerParameter { return this._addParameter(name, defaultValue, false); From 1fed3300a653b1466c63091bd58f4e9a4cc7b9fd Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 13 Sep 2024 11:17:23 +0800 Subject: [PATCH 11/17] refactor: opt code --- packages/core/src/animation/Animator.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index ddfe471859..4292345b77 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -126,7 +126,7 @@ export class Animator extends Component { } /** - * Create a cross fade from the current state to another state. + * Create a cross fade from the current state to another state with a normalized duration. * @param stateName - The state name * @param normalizedDuration - The duration of the transition (normalized) * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name @@ -142,6 +142,13 @@ export class Animator extends Component { this._crossFade(stateName, normalizedDuration, layerIndex, normalizedTimeOffset, false); } + /** + * Create a cross fade from the current state to another state with a fixed duration. + * @param stateName - The state name + * @param fixedDuration - The duration of the transition (fixed) + * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name + * @param normalizedTimeOffset - The time offset between 0 and 1(default 0) + */ crossFadeInFixedDuration( stateName: string, fixedDuration: number, From f05d9ce5c88cf0292fa5861c685c7ae842050793 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 13 Sep 2024 14:04:23 +0800 Subject: [PATCH 12/17] refactor: opt code --- packages/core/src/animation/Animator.ts | 81 +++++++++++-------- .../src/animation/AnimatorStateTransition.ts | 4 + 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 4292345b77..f81f69000c 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -106,32 +106,28 @@ export class Animator extends Component { * Play a state by name. * @param stateName - The state name * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name - * @param normalizedTimeOffset - The time offset between 0 and 1(default 0) + * @param normalizedTimeOffset - The normalized time offset (0-1) relative to the state's duration */ play(stateName: string, layerIndex: number = -1, normalizedTimeOffset: number = 0): void { - if (this._controllerUpdateFlag?.flag) { - this._reset(); - } - - const stateInfo = this._getAnimatorStateInfo(stateName, layerIndex); - const { state } = stateInfo; - - if (!state) { - return; - } + this._play(stateName, layerIndex, normalizedTimeOffset, false); + } - if (this._preparePlay(state, stateInfo.layerIndex, normalizedTimeOffset)) { - this._playFrameCount = this.engine.time.frameCount; - } + /** + * Play a state by name with a fixed time offset. + * @param stateName - The state name + * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name + * @param fixedTimeOffset - The time offset in seconds from the start of the animation + */ + playInFixedTime(stateName: string, layerIndex: number = -1, fixedTimeOffset: number = 0): void { + this._play(stateName, layerIndex, fixedTimeOffset, true); } /** * Create a cross fade from the current state to another state with a normalized duration. * @param stateName - The state name - * @param normalizedDuration - The duration of the transition (normalized) + * @param normalizedDuration - The normalized duration of the transition, relative to the destination state's duration (range: 0 to 1) * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name - * @param normalizedTimeOffset - The time offset between 0 and 1(default 0) - * @param fixedDuration - The duration is fixed or normalized(default normalized) + * @param normalizedTimeOffset - The normalized time offset between 0 and 1 (default 0) to start the destination state's animation from */ crossFade( stateName: string, @@ -145,17 +141,17 @@ export class Animator extends Component { /** * Create a cross fade from the current state to another state with a fixed duration. * @param stateName - The state name - * @param fixedDuration - The duration of the transition (fixed) + * @param fixedDuration - The duration of the transition in seconds * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name - * @param normalizedTimeOffset - The time offset between 0 and 1(default 0) + * @param fixedTimeOffset - The time offset in seconds from the start of the animation */ - crossFadeInFixedDuration( + crossFadeInFixedTime( stateName: string, fixedDuration: number, layerIndex: number = -1, - normalizedTimeOffset: number = 0 + fixedTimeOffset: number = 0 ): void { - this._crossFade(stateName, fixedDuration, layerIndex, normalizedTimeOffset, true); + this._crossFade(stateName, fixedDuration, layerIndex, fixedTimeOffset, true); } /** @@ -326,6 +322,27 @@ export class Animator extends Component { } } + _play(stateName: string, layerIndex: number = -1, timeOffset: number = 0, isFixedTime: boolean): void { + if (this._controllerUpdateFlag?.flag) { + this._reset(); + } + + const stateInfo = this._getAnimatorStateInfo(stateName, layerIndex); + const { state } = stateInfo; + + if (!state) { + return; + } + + if (!isFixedTime) { + timeOffset = timeOffset * state._getDuration(); + } + + if (this._preparePlay(state, stateInfo.layerIndex, timeOffset)) { + this._playFrameCount = this.engine.time.frameCount; + } + } + private _crossFade( stateName: string, duration: number, @@ -721,14 +738,11 @@ export class Animator extends Component { deltaTime: number, aniUpdate: boolean ) { - const { srcPlayData, destPlayData, crossFadeTransition } = layerData; + const { srcPlayData, destPlayData } = layerData; const { speed } = this; const { state: srcState } = srcPlayData; const { state: destState } = destPlayData; - const destStateDuration = destState._getDuration(); - const transitionDuration = crossFadeTransition.isFixedDuration - ? crossFadeTransition.duration - : destStateDuration * crossFadeTransition.duration; + const transitionDuration = layerData.crossFadeTransition.fixedDuration; const srcPlaySpeed = srcState.speed * speed; const dstPlaySpeed = destState.speed * speed; @@ -854,13 +868,10 @@ export class Animator extends Component { deltaTime: number, aniUpdate: boolean ) { - const { destPlayData, crossFadeTransition } = layerData; + const { destPlayData } = layerData; const { state } = destPlayData; - const stateDuration = state._getDuration(); - const transitionDuration = crossFadeTransition.isFixedDuration - ? crossFadeTransition.duration - : stateDuration * crossFadeTransition.duration; + const transitionDuration = layerData.crossFadeTransition.fixedDuration; const playSpeed = state.speed * this.speed; const playDeltaTime = playSpeed * deltaTime; @@ -875,7 +886,7 @@ export class Animator extends Component { lastDestClipTime + playDeltaTime > transitionDuration ? transitionDuration - lastDestClipTime : playDeltaTime; } else { // The time that has been played - const playedTime = stateDuration - lastDestClipTime; + const playedTime = state._getDuration() - lastDestClipTime; dstPlayCostTime = // -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 @@ -1282,7 +1293,7 @@ export class Animator extends Component { } } - private _preparePlay(state: AnimatorState, layerIndex: number, normalizedTimeOffset: number = 0): boolean { + private _preparePlay(state: AnimatorState, layerIndex: number, timeOffset: number = 0): boolean { const name = state.name; if (!state.clip) { Logger.warn(`The state named ${name} has no AnimationClip data.`); @@ -1295,7 +1306,7 @@ export class Animator extends Component { this._preparePlayOwner(animatorLayerData, state); animatorLayerData.layerState = LayerState.Playing; - animatorLayerData.srcPlayData.reset(state, animatorStateData, state._getDuration() * normalizedTimeOffset); + animatorLayerData.srcPlayData.reset(state, animatorStateData, timeOffset); return true; } diff --git a/packages/core/src/animation/AnimatorStateTransition.ts b/packages/core/src/animation/AnimatorStateTransition.ts index 6e99fe551c..c808cb290e 100644 --- a/packages/core/src/animation/AnimatorStateTransition.ts +++ b/packages/core/src/animation/AnimatorStateTransition.ts @@ -69,6 +69,10 @@ export class AnimatorStateTransition { this._collection?.updateTransitionsIndex(this, value); } + get fixedDuration(): number { + return this.isFixedDuration ? this.duration : this.duration * this.destinationState._getDuration(); + } + /** * Add a condition to a transition. * @param parameterName - The name of the parameter From 8b72f5c3f8e5c7a333e2c1bb63ee8a9684d95a2e Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 13 Sep 2024 14:16:47 +0800 Subject: [PATCH 13/17] refactor: opt code --- packages/core/src/animation/Animator.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index f81f69000c..f997cd9c1c 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -106,7 +106,7 @@ export class Animator extends Component { * Play a state by name. * @param stateName - The state name * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name - * @param normalizedTimeOffset - The normalized time offset (0-1) relative to the state's duration + * @param normalizedTimeOffset - The normalized time offset (between 0 and 1, default 0) to start the state's animation from */ play(stateName: string, layerIndex: number = -1, normalizedTimeOffset: number = 0): void { this._play(stateName, layerIndex, normalizedTimeOffset, false); @@ -127,7 +127,7 @@ export class Animator extends Component { * @param stateName - The state name * @param normalizedDuration - The normalized duration of the transition, relative to the destination state's duration (range: 0 to 1) * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name - * @param normalizedTimeOffset - The normalized time offset between 0 and 1 (default 0) to start the destination state's animation from + * @param normalizedTimeOffset - The normalized time offset (between 0 and 1, default 0) to start the destination state's animation from */ crossFade( stateName: string, @@ -322,7 +322,7 @@ export class Animator extends Component { } } - _play(stateName: string, layerIndex: number = -1, timeOffset: number = 0, isFixedTime: boolean): void { + private _play(stateName: string, layerIndex: number, timeOffset: number, isFixedTime: boolean): void { if (this._controllerUpdateFlag?.flag) { this._reset(); } @@ -346,9 +346,9 @@ export class Animator extends Component { private _crossFade( stateName: string, duration: number, - layerIndex: number = -1, - normalizedTimeOffset: number = 0, - isFixedDuration: boolean = false + layerIndex: number, + normalizedTimeOffset: number, + isFixedDuration: boolean ): void { if (this._controllerUpdateFlag?.flag) { this._reset(); From cff70f0e71d6d0182be4d3f2de72f27d13b4d450 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 13 Sep 2024 14:21:21 +0800 Subject: [PATCH 14/17] refactor: opt code --- packages/core/src/animation/Animator.ts | 4 ++-- packages/core/src/animation/AnimatorStateTransition.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index f997cd9c1c..4f46e8edb4 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -742,7 +742,7 @@ export class Animator extends Component { const { speed } = this; const { state: srcState } = srcPlayData; const { state: destState } = destPlayData; - const transitionDuration = layerData.crossFadeTransition.fixedDuration; + const transitionDuration = layerData.crossFadeTransition._fixedDuration; const srcPlaySpeed = srcState.speed * speed; const dstPlaySpeed = destState.speed * speed; @@ -871,7 +871,7 @@ export class Animator extends Component { const { destPlayData } = layerData; const { state } = destPlayData; - const transitionDuration = layerData.crossFadeTransition.fixedDuration; + const transitionDuration = layerData.crossFadeTransition._fixedDuration; const playSpeed = state.speed * this.speed; const playDeltaTime = playSpeed * deltaTime; diff --git a/packages/core/src/animation/AnimatorStateTransition.ts b/packages/core/src/animation/AnimatorStateTransition.ts index c808cb290e..e2ec4605ab 100644 --- a/packages/core/src/animation/AnimatorStateTransition.ts +++ b/packages/core/src/animation/AnimatorStateTransition.ts @@ -69,7 +69,10 @@ export class AnimatorStateTransition { this._collection?.updateTransitionsIndex(this, value); } - get fixedDuration(): number { + /** + * @internal + */ + get _fixedDuration(): number { return this.isFixedDuration ? this.duration : this.duration * this.destinationState._getDuration(); } From 23d52b27c027ca726ae7ad9aa41ca73e730a1728 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 13 Sep 2024 14:31:50 +0800 Subject: [PATCH 15/17] refactor: opt code --- packages/core/src/animation/Animator.ts | 4 ++-- .../core/src/animation/AnimatorStateTransition.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 4f46e8edb4..8ee22398ef 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -742,7 +742,7 @@ export class Animator extends Component { const { speed } = this; const { state: srcState } = srcPlayData; const { state: destState } = destPlayData; - const transitionDuration = layerData.crossFadeTransition._fixedDuration; + const transitionDuration = layerData.crossFadeTransition._getFixedDuration(); const srcPlaySpeed = srcState.speed * speed; const dstPlaySpeed = destState.speed * speed; @@ -871,7 +871,7 @@ export class Animator extends Component { const { destPlayData } = layerData; const { state } = destPlayData; - const transitionDuration = layerData.crossFadeTransition._fixedDuration; + const transitionDuration = layerData.crossFadeTransition._getFixedDuration(); const playSpeed = state.speed * this.speed; const playDeltaTime = playSpeed * deltaTime; diff --git a/packages/core/src/animation/AnimatorStateTransition.ts b/packages/core/src/animation/AnimatorStateTransition.ts index e2ec4605ab..eb7f021b3e 100644 --- a/packages/core/src/animation/AnimatorStateTransition.ts +++ b/packages/core/src/animation/AnimatorStateTransition.ts @@ -69,13 +69,6 @@ export class AnimatorStateTransition { this._collection?.updateTransitionsIndex(this, value); } - /** - * @internal - */ - get _fixedDuration(): number { - return this.isFixedDuration ? this.duration : this.duration * this.destinationState._getDuration(); - } - /** * Add a condition to a transition. * @param parameterName - The name of the parameter @@ -120,4 +113,11 @@ export class AnimatorStateTransition { const index = this._conditions.indexOf(condition); index !== -1 && this._conditions.splice(index, 1); } + + /** + * @internal + */ + _getFixedDuration(): number { + return this.isFixedDuration ? this.duration : this.duration * this.destinationState._getDuration(); + } } From 2f6708cc907179ebd29a5fa96057a905221c775a Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 13 Sep 2024 15:07:20 +0800 Subject: [PATCH 16/17] refactor: opt code --- packages/core/src/animation/Animator.ts | 16 +++++----- .../src/animation/AnimatorStateTransition.ts | 7 +++++ tests/src/core/Animator.test.ts | 29 +++++++++++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 8ee22398ef..e363b9f562 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -347,8 +347,8 @@ export class Animator extends Component { stateName: string, duration: number, layerIndex: number, - normalizedTimeOffset: number, - isFixedDuration: boolean + timeOffset: number, + isFixedTime: boolean ): void { if (this._controllerUpdateFlag?.flag) { this._reset(); @@ -357,8 +357,10 @@ export class Animator extends Component { const { state, layerIndex: playLayerIndex } = this._getAnimatorStateInfo(stateName, layerIndex); const { manuallyTransition } = this._getAnimatorLayerData(playLayerIndex); manuallyTransition.duration = duration; - manuallyTransition.offset = normalizedTimeOffset; - manuallyTransition.isFixedDuration = isFixedDuration; + + const stateDuration = state._getDuration(); + manuallyTransition.offset = isFixedTime ? (stateDuration === 0 ? 0 : timeOffset / stateDuration) : timeOffset; + manuallyTransition.isFixedDuration = isFixedTime; manuallyTransition.destinationState = state; if (this._prepareCrossFadeByTransition(manuallyTransition, playLayerIndex)) { @@ -1405,15 +1407,13 @@ export class Animator extends Component { return false; } if (!crossState.clip) { - Logger.warn(`The state named ${name} has no AnimationClip data.`); + Logger.warn(`The state named ${crossState.name} has no AnimationClip data.`); return false; } const animatorLayerData = this._getAnimatorLayerData(layerIndex); const animatorStateData = this._getAnimatorStateData(crossState.name, crossState, animatorLayerData, layerIndex); - const duration = crossState._getDuration(); - const offset = duration * transition.offset; - animatorLayerData.destPlayData.reset(crossState, animatorStateData, offset); + animatorLayerData.destPlayData.reset(crossState, animatorStateData, transition._getFixedTimeOffset()); switch (animatorLayerData.layerState) { case LayerState.Standby: diff --git a/packages/core/src/animation/AnimatorStateTransition.ts b/packages/core/src/animation/AnimatorStateTransition.ts index eb7f021b3e..49867e86f5 100644 --- a/packages/core/src/animation/AnimatorStateTransition.ts +++ b/packages/core/src/animation/AnimatorStateTransition.ts @@ -120,4 +120,11 @@ export class AnimatorStateTransition { _getFixedDuration(): number { return this.isFixedDuration ? this.duration : this.duration * this.destinationState._getDuration(); } + + /** + * @internal + */ + _getFixedTimeOffset(): number { + return this.offset * this.destinationState._getDuration(); + } } diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index 4411364419..28138bfd97 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -211,6 +211,35 @@ describe("Animator test", function () { expect(layerState).to.eq(2); }); + it("play in fixed time", () => { + animator.playInFixedTime("Walk", 0, 0.3); + // @ts-ignore + animator.engine.time._frameCount++; + // @ts-ignore + animator.update(0.3); + + // @ts-ignore + const layerData = animator._getAnimatorLayerData(0); + const srcPlayData = layerData.srcPlayData; + expect(srcPlayData.frameTime).to.eq(0.6); + }); + + it("cross fade in fixed time", () => { + const runState = animator.findAnimatorState("Run"); + animator.play("Walk"); + animator.crossFadeInFixedTime("Run", 0.3, 0, 0.1); + // @ts-ignore + animator.engine.time._frameCount++; + // @ts-ignore + animator.update(0.3); + + // @ts-ignore + const layerData = animator._getAnimatorLayerData(0); + const srcPlayData = layerData.srcPlayData; + expect(srcPlayData.state.name).to.eq("Run"); + expect(srcPlayData.frameTime).to.eq(0.4); + }); + it("animation cross fade by transition", () => { const walkState = animator.findAnimatorState("Walk"); const runState = animator.findAnimatorState("Run"); From 7183a07571892e75ea2a6853367e03a766bb5633 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 13 Sep 2024 15:43:01 +0800 Subject: [PATCH 17/17] refactor: opt code --- packages/core/src/animation/Animator.ts | 68 +++++++------------ .../src/animation/AnimatorStateTransition.ts | 9 +-- tests/src/core/Animator.test.ts | 18 +---- 3 files changed, 29 insertions(+), 66 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index e363b9f562..02cc02684f 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -109,17 +109,20 @@ export class Animator extends Component { * @param normalizedTimeOffset - The normalized time offset (between 0 and 1, default 0) to start the state's animation from */ play(stateName: string, layerIndex: number = -1, normalizedTimeOffset: number = 0): void { - this._play(stateName, layerIndex, normalizedTimeOffset, false); - } + if (this._controllerUpdateFlag?.flag) { + this._reset(); + } - /** - * Play a state by name with a fixed time offset. - * @param stateName - The state name - * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name - * @param fixedTimeOffset - The time offset in seconds from the start of the animation - */ - playInFixedTime(stateName: string, layerIndex: number = -1, fixedTimeOffset: number = 0): void { - this._play(stateName, layerIndex, fixedTimeOffset, true); + const stateInfo = this._getAnimatorStateInfo(stateName, layerIndex); + const { state } = stateInfo; + + if (!state) { + return; + } + + if (this._preparePlay(state, stateInfo.layerIndex, normalizedTimeOffset)) { + this._playFrameCount = this.engine.time.frameCount; + } } /** @@ -143,15 +146,15 @@ export class Animator extends Component { * @param stateName - The state name * @param fixedDuration - The duration of the transition in seconds * @param layerIndex - The layer index(default -1). If layer is -1, play the first state with the given state name - * @param fixedTimeOffset - The time offset in seconds from the start of the animation + * @param normalizedTimeOffset - The normalized time offset (between 0 and 1, default 0) to start the destination state's animation from */ - crossFadeInFixedTime( + crossFadeInFixedDuration( stateName: string, fixedDuration: number, layerIndex: number = -1, - fixedTimeOffset: number = 0 + normalizedTimeOffset: number = 0 ): void { - this._crossFade(stateName, fixedDuration, layerIndex, fixedTimeOffset, true); + this._crossFade(stateName, fixedDuration, layerIndex, normalizedTimeOffset, true); } /** @@ -322,33 +325,12 @@ export class Animator extends Component { } } - private _play(stateName: string, layerIndex: number, timeOffset: number, isFixedTime: boolean): void { - if (this._controllerUpdateFlag?.flag) { - this._reset(); - } - - const stateInfo = this._getAnimatorStateInfo(stateName, layerIndex); - const { state } = stateInfo; - - if (!state) { - return; - } - - if (!isFixedTime) { - timeOffset = timeOffset * state._getDuration(); - } - - if (this._preparePlay(state, stateInfo.layerIndex, timeOffset)) { - this._playFrameCount = this.engine.time.frameCount; - } - } - private _crossFade( stateName: string, duration: number, layerIndex: number, - timeOffset: number, - isFixedTime: boolean + normalizedTimeOffset: number, + isFixedDuration: boolean ): void { if (this._controllerUpdateFlag?.flag) { this._reset(); @@ -358,9 +340,8 @@ export class Animator extends Component { const { manuallyTransition } = this._getAnimatorLayerData(playLayerIndex); manuallyTransition.duration = duration; - const stateDuration = state._getDuration(); - manuallyTransition.offset = isFixedTime ? (stateDuration === 0 ? 0 : timeOffset / stateDuration) : timeOffset; - manuallyTransition.isFixedDuration = isFixedTime; + manuallyTransition.offset = normalizedTimeOffset; + manuallyTransition.isFixedDuration = isFixedDuration; manuallyTransition.destinationState = state; if (this._prepareCrossFadeByTransition(manuallyTransition, playLayerIndex)) { @@ -1295,7 +1276,7 @@ export class Animator extends Component { } } - private _preparePlay(state: AnimatorState, layerIndex: number, timeOffset: number = 0): boolean { + private _preparePlay(state: AnimatorState, layerIndex: number, normalizedTimeOffset: number = 0): boolean { const name = state.name; if (!state.clip) { Logger.warn(`The state named ${name} has no AnimationClip data.`); @@ -1308,7 +1289,7 @@ export class Animator extends Component { this._preparePlayOwner(animatorLayerData, state); animatorLayerData.layerState = LayerState.Playing; - animatorLayerData.srcPlayData.reset(state, animatorStateData, timeOffset); + animatorLayerData.srcPlayData.reset(state, animatorStateData, state._getDuration() * normalizedTimeOffset); return true; } @@ -1413,7 +1394,8 @@ export class Animator extends Component { const animatorLayerData = this._getAnimatorLayerData(layerIndex); const animatorStateData = this._getAnimatorStateData(crossState.name, crossState, animatorLayerData, layerIndex); - animatorLayerData.destPlayData.reset(crossState, animatorStateData, transition._getFixedTimeOffset()); + + animatorLayerData.destPlayData.reset(crossState, animatorStateData, transition.offset * crossState._getDuration()); switch (animatorLayerData.layerState) { case LayerState.Standby: diff --git a/packages/core/src/animation/AnimatorStateTransition.ts b/packages/core/src/animation/AnimatorStateTransition.ts index 49867e86f5..651924643f 100644 --- a/packages/core/src/animation/AnimatorStateTransition.ts +++ b/packages/core/src/animation/AnimatorStateTransition.ts @@ -8,7 +8,7 @@ import { AnimatorConditionMode } from "./enums/AnimatorConditionMode"; * Transitions define when and how the state machine switch from on state to another. AnimatorTransition always originate from a StateMachine or a StateMachine entry. */ export class AnimatorStateTransition { - /** The duration of the transition. This is represented in normalized time. */ + /** The duration of the transition. The duration is in normalized time by default. To set it to be in seconds, set isFixedDuration to true. */ duration = 0; /** The time at which the destination state will start. This is represented in normalized time. */ offset = 0; @@ -120,11 +120,4 @@ export class AnimatorStateTransition { _getFixedDuration(): number { return this.isFixedDuration ? this.duration : this.duration * this.destinationState._getDuration(); } - - /** - * @internal - */ - _getFixedTimeOffset(): number { - return this.offset * this.destinationState._getDuration(); - } } diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index 28138bfd97..9e9533915e 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -211,23 +211,10 @@ describe("Animator test", function () { expect(layerState).to.eq(2); }); - it("play in fixed time", () => { - animator.playInFixedTime("Walk", 0, 0.3); - // @ts-ignore - animator.engine.time._frameCount++; - // @ts-ignore - animator.update(0.3); - - // @ts-ignore - const layerData = animator._getAnimatorLayerData(0); - const srcPlayData = layerData.srcPlayData; - expect(srcPlayData.frameTime).to.eq(0.6); - }); - it("cross fade in fixed time", () => { const runState = animator.findAnimatorState("Run"); animator.play("Walk"); - animator.crossFadeInFixedTime("Run", 0.3, 0, 0.1); + animator.crossFadeInFixedDuration("Run", 0.3, 0, 0.1); // @ts-ignore animator.engine.time._frameCount++; // @ts-ignore @@ -237,7 +224,8 @@ describe("Animator test", function () { const layerData = animator._getAnimatorLayerData(0); const srcPlayData = layerData.srcPlayData; expect(srcPlayData.state.name).to.eq("Run"); - expect(srcPlayData.frameTime).to.eq(0.4); + // @ts-ignore + expect(srcPlayData.frameTime).to.eq(0.3 + 0.1 * runState._getDuration()); }); it("animation cross fade by transition", () => {