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 4c63c2252d..02cc02684f 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 @@ -63,7 +63,7 @@ export class Animator extends Component { private _tempAnimatorStateInfo: IAnimatorStateInfo = { layerIndex: -1, state: null }; @ignoreClone - private _controlledRenderers: Renderer[] = []; + private _controlledRenderers = new Array(); /** * All layers from the AnimatorController which belongs this Animator. @@ -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 time offset between 0 and 1(default 0) + * @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 { if (this._controllerUpdateFlag?.flag) { @@ -126,31 +126,35 @@ 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 normalizedTransitionDuration - 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 normalizedTimeOffset - The normalized time offset (between 0 and 1, default 0) to start the destination state's animation from */ crossFade( stateName: string, - normalizedTransitionDuration: number, + normalizedDuration: number, layerIndex: number = -1, normalizedTimeOffset: number = 0 ): void { - if (this._controllerUpdateFlag?.flag) { - this._reset(); - } - - const { state, layerIndex: playLayerIndex } = this._getAnimatorStateInfo(stateName, layerIndex); - const { manuallyTransition } = this._getAnimatorLayerData(playLayerIndex); - manuallyTransition.duration = normalizedTransitionDuration; - manuallyTransition.offset = normalizedTimeOffset; - manuallyTransition.destinationState = state; + this._crossFade(stateName, normalizedDuration, layerIndex, normalizedTimeOffset, false); + } - if (this._prepareCrossFadeByTransition(manuallyTransition, playLayerIndex)) { - this._playFrameCount = this.engine.time.frameCount; - } + /** + * 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 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 normalized time offset (between 0 and 1, default 0) to start the destination state's animation from + */ + crossFadeInFixedDuration( + stateName: string, + fixedDuration: number, + layerIndex: number = -1, + normalizedTimeOffset: number = 0 + ): void { + this._crossFade(stateName, fixedDuration, layerIndex, normalizedTimeOffset, true); } /** @@ -321,6 +325,30 @@ export class Animator extends Component { } } + private _crossFade( + stateName: string, + duration: number, + layerIndex: number, + normalizedTimeOffset: number, + isFixedDuration: boolean + ): void { + if (this._controllerUpdateFlag?.flag) { + this._reset(); + } + + const { state, layerIndex: playLayerIndex } = this._getAnimatorStateInfo(stateName, layerIndex); + const { manuallyTransition } = this._getAnimatorLayerData(playLayerIndex); + manuallyTransition.duration = duration; + + manuallyTransition.offset = normalizedTimeOffset; + manuallyTransition.isFixedDuration = isFixedDuration; + manuallyTransition.destinationState = state; + + if (this._prepareCrossFadeByTransition(manuallyTransition, playLayerIndex)) { + this._playFrameCount = this.engine.time.frameCount; + } + } + private _getAnimatorStateInfo(stateName: string, layerIndex: number): IAnimatorStateInfo { const { _animatorController: animatorController, _tempAnimatorStateInfo: stateInfo } = this; let state: AnimatorState = null; @@ -697,8 +725,7 @@ export class Animator extends Component { const { speed } = this; const { state: srcState } = srcPlayData; const { state: destState } = destPlayData; - const destStateDuration = destState._getDuration(); - const transitionDuration = destStateDuration * layerData.crossFadeTransition.duration; + const transitionDuration = layerData.crossFadeTransition._getFixedDuration(); const srcPlaySpeed = srcState.speed * speed; const dstPlaySpeed = destState.speed * speed; @@ -827,8 +854,7 @@ export class Animator extends Component { const { destPlayData } = layerData; const { state } = destPlayData; - const stateDuration = state._getDuration(); - const transitionDuration = stateDuration * layerData.crossFadeTransition.duration; + const transitionDuration = layerData.crossFadeTransition._getFixedDuration(); const playSpeed = state.speed * this.speed; const playDeltaTime = playSpeed * deltaTime; @@ -843,7 +869,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 @@ -1362,15 +1388,14 @@ 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.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 db5e15abb8..651924643f 100644 --- a/packages/core/src/animation/AnimatorStateTransition.ts +++ b/packages/core/src/animation/AnimatorStateTransition.ts @@ -8,25 +8,27 @@ 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. */ - duration: number = 0; + /** 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: number = 0; + offset = 0; /** ExitTime represents the exact time at which the transition can take effect. This is represented in normalized time. */ - exitTime: number = 1.0; + exitTime = 1.0; /** The destination state of the transition. */ destinationState: AnimatorState; /** Mutes the transition. The transition will never occur. */ - mute: boolean = false; + mute = false; + /** Determines whether the duration of the transition is reported in a fixed duration in seconds or as a normalized time. */ + isFixedDuration = false; /** @internal */ _collection: AnimatorStateTransitionCollection; /** @internal */ - _isExit: boolean = false; + _isExit = false; private _conditions: AnimatorCondition[] = []; private _solo = false; - private _hasExitTime: boolean = true; + private _hasExitTime = true; /** * Is the transition destination the exit of the current state machine. @@ -111,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(); + } } diff --git a/packages/loader/src/AnimatorControllerLoader.ts b/packages/loader/src/AnimatorControllerLoader.ts index a541601e6a..08751a9465 100644 --- a/packages/loader/src/AnimatorControllerLoader.ts +++ b/packages/loader/src/AnimatorControllerLoader.ts @@ -120,6 +120,7 @@ class AnimatorControllerLoader extends Loader { private _createTransition(transitionData: ITransitionData, destinationState: AnimatorState): AnimatorStateTransition { const transition = new AnimatorStateTransition(); transition.hasExitTime = transitionData.hasExitTime; + transition.isFixedDuration = transitionData.isFixedDuration; transition.duration = transitionData.duration; transition.offset = transitionData.offset; transition.exitTime = transitionData.exitTime; @@ -161,6 +162,7 @@ interface ITransitionData { isExit: boolean; conditions: IConditionData[]; hasExitTime: boolean; + isFixedDuration: boolean; } interface IConditionData { diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index b41f23feaf..9e9533915e 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -211,6 +211,23 @@ describe("Animator test", function () { expect(layerState).to.eq(2); }); + it("cross fade in fixed time", () => { + const runState = animator.findAnimatorState("Run"); + animator.play("Walk"); + animator.crossFadeInFixedDuration("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"); + // @ts-ignore + expect(srcPlayData.frameTime).to.eq(0.3 + 0.1 * runState._getDuration()); + }); + it("animation cross fade by transition", () => { const walkState = animator.findAnimatorState("Walk"); const runState = animator.findAnimatorState("Run"); @@ -851,4 +868,31 @@ describe("Animator test", function () { 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.addTriggerParameter("triggerRun"); + animatorController.addTriggerParameter("triggerWalk"); + // @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.isFixedDuration = true; + walkToRunTransition.duration = 0.1; + walkToRunTransition.addCondition("triggerRun", AnimatorConditionMode.If, true); + animator.play("Walk"); + animator.activateTriggerParameter("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); + }); });