diff --git a/e2e/case/.mockForE2E.ts b/e2e/case/.mockForE2E.ts index 89904df856..1218e394dc 100644 --- a/e2e/case/.mockForE2E.ts +++ b/e2e/case/.mockForE2E.ts @@ -1,6 +1,6 @@ import { Camera, Engine, RenderTarget, Texture2D } from "@galacean/engine-core"; -export const updateForE2E = (engine, deltaTime = 100) => { +export const updateForE2E = (engine, deltaTime = 100, loopTime = 10) => { engine._vSyncCount = Infinity; engine._time._lastSystemTime = 0; let times = 0; @@ -8,7 +8,7 @@ export const updateForE2E = (engine, deltaTime = 100) => { times++; return times * deltaTime; }; - for (let i = 0; i < 10; ++i) { + for (let i = 0; i < loopTime; ++i) { engine.update(); } engine._hardwareRenderer._gl.finish(); diff --git a/e2e/case/animator-stateMachine.ts b/e2e/case/animator-stateMachine.ts index d0f495cf8b..5e89c2c47c 100644 --- a/e2e/case/animator-stateMachine.ts +++ b/e2e/case/animator-stateMachine.ts @@ -51,49 +51,70 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { const idleState = animator.findAnimatorState("idle"); const walkState = animator.findAnimatorState("walk"); const runState = animator.findAnimatorState("run"); - { - // handle idle state - const toWalkTransition = new AnimatorStateTransition(); - toWalkTransition.destinationState = walkState; - toWalkTransition.exitTime = 0.5; - toWalkTransition.duration = 0.3; - toWalkTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0); - idleState.addTransition(toWalkTransition); + let idleToWalkTime = 0; + let walkToRunTime = 0; + let runToWalkTime = 0; + let walkToIdleTime = 0; - const exitTransition = idleState.addExitTransition(); - exitTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); - } - { - // handle walk state - const toRunTransition = new AnimatorStateTransition(); - toRunTransition.destinationState = runState; - toRunTransition.duration = 0.3; - toRunTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0.5); - walkState.addTransition(toRunTransition); + // handle idle state + const toWalkTransition = new AnimatorStateTransition(); + toWalkTransition.destinationState = walkState; + toWalkTransition.duration = 0.2; + toWalkTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0); + idleState.addTransition(toWalkTransition); + idleToWalkTime = + //@ts-ignore + toWalkTransition.exitTime * idleState._getDuration() + toWalkTransition.duration * walkState._getDuration(); - const toIdleTransition = new AnimatorStateTransition(); - toIdleTransition.destinationState = idleState; - toIdleTransition.duration = 0.3; - toIdleTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); - walkState.addTransition(toIdleTransition); - } - { - // handle run state - const toWalkTransition = new AnimatorStateTransition(); - toWalkTransition.destinationState = walkState; - toWalkTransition.duration = 0.3; - toWalkTransition.addCondition(AnimatorConditionMode.Less, "playerSpeed", 0.5); - runState.addTransition(toWalkTransition); - } + const exitTransition = idleState.addExitTransition(); + exitTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); + // to walk state + const toRunTransition = new AnimatorStateTransition(); + toRunTransition.destinationState = runState; + toRunTransition.duration = 0.3; + toRunTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0.5); + walkState.addTransition(toRunTransition); + walkToRunTime = + //@ts-ignore + (toRunTransition.exitTime - toWalkTransition.duration) * walkState._getDuration() + + //@ts-ignore + toRunTransition.duration * runState._getDuration(); + const toIdleTransition = new AnimatorStateTransition(); + toIdleTransition.destinationState = idleState; + toIdleTransition.duration = 0.3; + toIdleTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); + walkState.addTransition(toIdleTransition); + walkToIdleTime = + //@ts-ignore + (toIdleTransition.exitTime - toRunTransition.duration) * walkState._getDuration() + + //@ts-ignore + toIdleTransition.duration * idleState._getDuration(); + + // to run state + const RunToWalkTransition = new AnimatorStateTransition(); + RunToWalkTransition.destinationState = walkState; + RunToWalkTransition.duration = 0.3; + RunToWalkTransition.addCondition(AnimatorConditionMode.Less, "playerSpeed", 0.5); + runState.addTransition(RunToWalkTransition); + runToWalkTime = + //@ts-ignore + (RunToWalkTransition.exitTime - toRunTransition.duration) * runState._getDuration() + + //@ts-ignore + RunToWalkTransition.duration * walkState._getDuration(); stateMachine.addEntryStateTransition(idleState); const anyTransition = stateMachine.addAnyStateTransition(idleState); anyTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); anyTransition.duration = 0.3; + let anyToIdleTime = + // @ts-ignore + (anyTransition.exitTime - toIdleTransition.duration) * walkState._getDuration() + + // @ts-ignore + anyTransition.duration * idleState._getDuration(); - updateForE2E(engine, 500); - + engine.time.maximumDeltaTime = 10000; + updateForE2E(engine, (idleToWalkTime + walkToRunTime) * 1000, 1); initScreenshot(engine, camera); }); }); diff --git a/e2e/fixtures/originImage/Animator_animator-stateMachine.jpg b/e2e/fixtures/originImage/Animator_animator-stateMachine.jpg index e4111ab0d2..2db72f67fb 100644 --- a/e2e/fixtures/originImage/Animator_animator-stateMachine.jpg +++ b/e2e/fixtures/originImage/Animator_animator-stateMachine.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf4053216f455ca14fcb2c6b875a5c82892d2b0ce8106f9b0b4d1ef71c33dfb0 -size 40024 +oid sha256:31439e4610988628ccfb01533b00922d1c1b00dd622f6007f1b547083700b4bd +size 41568 diff --git a/packages/core/src/ComponentsManager.ts b/packages/core/src/ComponentsManager.ts index 5b83650cbf..fce0d43de9 100644 --- a/packages/core/src/ComponentsManager.ts +++ b/packages/core/src/ComponentsManager.ts @@ -192,7 +192,7 @@ export class ComponentsManager { callAnimationUpdate(deltaTime: number): void { this._onUpdateAnimations.forEach( (element: Animator) => { - element.engine.time.frameCount > element._playFrameCount && element.update(deltaTime); + element.update(deltaTime); }, (element: Animator, index: number) => { element._onUpdateIndex = index; diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 7e276790e4..4f2ab0e6e1 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -1,3 +1,4 @@ +import { MathUtil } from "@galacean/engine-math"; import { BoolUpdateFlag } from "../BoolUpdateFlag"; import { Component } from "../Component"; import { Entity } from "../Entity"; @@ -7,9 +8,13 @@ import { Logger } from "../base/Logger"; import { assignmentClone, ignoreClone } from "../clone/CloneManager"; import { ClearableObjectPool } from "../utils/ClearableObjectPool"; import { AnimatorController } from "./AnimatorController"; +import { AnimatorControllerLayer } from "./AnimatorControllerLayer"; +import { AnimatorControllerParameter } from "./AnimatorControllerParameter"; import { AnimatorState } from "./AnimatorState"; +import { AnimatorStateMachine } from "./AnimatorStateMachine"; import { AnimatorStateTransition } from "./AnimatorStateTransition"; import { KeyframeValueType } from "./Keyframe"; +import { AnimatorConditionMode } from "./enums/AnimatorConditionMode"; import { AnimatorCullingMode } from "./enums/AnimatorCullingMode"; import { AnimatorLayerBlendingMode } from "./enums/AnimatorLayerBlendingMode"; import { AnimatorStatePlayState } from "./enums/AnimatorStatePlayState"; @@ -20,9 +25,6 @@ import { AnimatorLayerData } from "./internal/AnimatorLayerData"; import { AnimatorStateData } from "./internal/AnimatorStateData"; import { AnimatorStatePlayData } from "./internal/AnimatorStatePlayData"; import { AnimationCurveOwner } from "./internal/animationCurveOwner/AnimationCurveOwner"; -import { AnimatorConditionMode } from "./enums/AnimatorConditionMode"; -import { AnimatorControllerLayer } from "./AnimatorControllerLayer"; -import { AnimatorControllerParameter, AnimatorControllerParameterValueType } from "./AnimatorControllerParameter"; /** * The controller of the animation system. @@ -108,26 +110,15 @@ export class Animator extends Component { } const stateInfo = this._getAnimatorStateInfo(stateName, layerIndex); - const { state, layerIndex: playLayerIndex } = stateInfo; + const { state } = stateInfo; if (!state) { return; } - if (!state.clip) { - Logger.warn(`The state named ${stateName} has no AnimationClip data.`); - return; - } - - const animatorLayerData = this._getAnimatorLayerData(playLayerIndex); - const animatorStateData = this._getAnimatorStateData(stateName, state, animatorLayerData, playLayerIndex); - - this._preparePlay(animatorLayerData, state); - animatorLayerData.layerState = LayerState.Playing; - animatorLayerData.srcPlayData.reset(state, animatorStateData, state._getDuration() * normalizedTimeOffset); - - this._playFrameCount = this.engine.time.frameCount; - this.update(0); + if (this._preparePlay(state, stateInfo.layerIndex, normalizedTimeOffset)) { + this._playFrameCount = this.engine.time.frameCount; + } } /** @@ -143,15 +134,18 @@ export class Animator extends Component { 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; - if (this._crossFadeByTransition(manuallyTransition, playLayerIndex)) { + if (this._prepareCrossFadeByTransition(manuallyTransition, playLayerIndex)) { this._playFrameCount = this.engine.time.frameCount; - this.update(0); } } @@ -160,6 +154,11 @@ export class Animator extends Component { * @param deltaTime - The deltaTime when the animation update */ update(deltaTime: number): void { + // Play or crossFade in script, animation playing from the first frame, deltaTime should be 0 + if (this._playFrameCount === this.engine.time.frameCount) { + deltaTime = 0; + } + let animationUpdate: boolean; if (this.cullingMode === AnimatorCullingMode.Complete) { animationUpdate = false; @@ -174,16 +173,20 @@ export class Animator extends Component { animationUpdate = true; } - const { _animatorController: animatorController } = this; + const animatorController = this._animatorController; if (!animatorController) { return; } + if (this._controllerUpdateFlag?.flag) { + this._reset(); + } + this._updateMark++; - const layers = animatorController.layers; + const { layers } = animatorController; for (let i = 0, n = layers.length; i < n; i++) { - this._updateLayer(i, i === 0, deltaTime, animationUpdate); + this._updateLayer(i, layers[i], deltaTime, animationUpdate); } } @@ -224,7 +227,10 @@ export class Animator extends Component { * @internal */ override _onEnable(): void { - this.animatorController && this._checkEntryState(); + const layersData = this._animatorLayersData; + for (let i = 0, n = layersData.length; i < n; i++) { + layersData[i].layerState = LayerState.Standby; + } this._entity.getComponentsIncludeChildren(Renderer, this._controlledRenderers); } @@ -401,31 +407,31 @@ export class Animator extends Component { } private _prepareCrossFading(animatorLayerData: AnimatorLayerData): void { - // Add src cross curve data. + // Add src cross curve data this._prepareSrcCrossData(animatorLayerData, false); - // Add dest cross curve data. + // Add dest cross curve data this._prepareDestCrossData(animatorLayerData, false); } private _prepareStandbyCrossFading(animatorLayerData: AnimatorLayerData): void { - // Standby have two sub state, one is never play, one is finished, never play srcPlayData.state is null. + // Standby have two sub state, one is never play, one is finished, never play srcPlayData.state is null animatorLayerData.srcPlayData.state && this._prepareSrcCrossData(animatorLayerData, true); - // Add dest cross curve data. + // Add dest cross curve data this._prepareDestCrossData(animatorLayerData, true); } private _prepareFixedPoseCrossFading(animatorLayerData: AnimatorLayerData): void { const { crossLayerOwnerCollection } = animatorLayerData; - // Save current cross curve data owner fixed pose. + // Save current cross curve data owner fixed pose for (let i = crossLayerOwnerCollection.length - 1; i >= 0; i--) { const layerOwner = crossLayerOwnerCollection[i]; if (!layerOwner) continue; layerOwner.curveOwner.saveFixedPoseValue(); - // Reset destCurveIndex When fixed pose crossFading again. + // Reset destCurveIndex When fixed pose crossFading again layerOwner.crossDestCurveIndex = -1; } - // prepare dest AnimatorState cross data. + // Prepare dest AnimatorState cross data this._prepareDestCrossData(animatorLayerData, true); } @@ -462,149 +468,279 @@ export class Animator extends Component { return animatorLayerData; } - private _updateLayer(layerIndex: number, firstLayer: boolean, deltaTime: number, aniUpdate: boolean): void { - let { stateMachine, blendingMode, weight } = this._animatorController.layers[layerIndex]; + private _updateLayer( + layerIndex: number, + layer: AnimatorControllerLayer, + deltaTime: number, + aniUpdate: boolean + ): void { + let { weight } = layer; const layerData = this._getAnimatorLayerData(layerIndex); - const { srcPlayData, destPlayData, layerState } = layerData; - const additive = blendingMode === AnimatorLayerBlendingMode.Additive; - firstLayer && (weight = 1.0); + this._updateState(layerIndex, layerData, layer.stateMachine, deltaTime); - switch (layerState) { - case LayerState.Standby: - const { entryTransitions } = stateMachine; - if (entryTransitions.length) { - this._checkTransitionsByCondition(null, entryTransitions, layerIndex); - } else { - const defaultState = stateMachine.defaultState; - defaultState && this.play(defaultState.name, layerIndex); - } - break; + const { srcPlayData, destPlayData } = layerData; + const additive = layer.blendingMode === AnimatorLayerBlendingMode.Additive; + + layerIndex === 0 && (weight = 1.0); + + switch (layerData.layerState) { case LayerState.Playing: - this._updatePlayingState(srcPlayData, layerData, layerIndex, weight, deltaTime, additive, aniUpdate); + this._evaluatePlayingState(srcPlayData, weight, additive, aniUpdate); break; case LayerState.FixedCrossFading: - this._updateCrossFadeFromPose(destPlayData, layerData, layerIndex, weight, deltaTime, additive, aniUpdate); + this._evaluateCrossFadeFromPoseState(destPlayData, layerData, weight, additive, aniUpdate); break; case LayerState.CrossFading: - this._updateCrossFade(srcPlayData, destPlayData, layerData, layerIndex, weight, deltaTime, additive, aniUpdate); + this._evaluateCrossFadeState(srcPlayData, destPlayData, layerData, weight, additive, aniUpdate); break; case LayerState.Finished: - this._updateFinishedState(srcPlayData, layerIndex, weight, additive, aniUpdate); + this._evaluateFinishedState(srcPlayData, weight, additive, aniUpdate); break; } } - private _updatePlayingState( - playData: AnimatorStatePlayData, + private _updateState( + layerIndex: number, layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, + deltaTime: number + ): void { + switch (layerData.layerState) { + case LayerState.Standby: + this._checkAnyAndEntryState(layerIndex, layerData, stateMachine, deltaTime); + break; + case LayerState.Playing: + this._updatePlayingState(layerIndex, layerData, stateMachine, deltaTime); + break; + case LayerState.Finished: + this._updateFinishedState(layerIndex, layerData, stateMachine, deltaTime); + break; + case LayerState.CrossFading: + this._updateCrossFadeState(layerIndex, layerData, stateMachine, deltaTime); + break; + case LayerState.FixedCrossFading: { + this._updateCrossFadeFromPoseState(layerIndex, layerData, stateMachine, deltaTime); + break; + } + } + } + + private _updatePlayingState( layerIndex: number, - weight: number, - delta: number, - additive: boolean, - aniUpdate: boolean + layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, + deltaTime: number ): void { - const { curveLayerOwner, eventHandlers } = playData.stateData; - const { state, playState: lastPlayState, clipTime: lastClipTime } = playData; - const { _curveBindings: curveBindings } = state.clip; + const { srcPlayData } = layerData; + const { state } = srcPlayData; + const { eventHandlers } = srcPlayData.stateData; + + const actualSpeed = state.speed * this.speed; + const actualDeltaTime = actualSpeed * deltaTime; + + srcPlayData.updateOrientation(actualDeltaTime); + + const { clipTime: lastClipTime, playState: lastPlayState } = srcPlayData; + + // Precalculate to get the transition + srcPlayData.update(actualDeltaTime); + + const { clipTime, playState, isForwards } = srcPlayData; + + const transition = + this._applyTransitionsByCondition(layerIndex, layerData, stateMachine, state, stateMachine.anyStateTransitions) || + this._applyStateTransitions( + layerIndex, + layerData, + stateMachine, + isForwards, + srcPlayData, + state.transitions, + lastClipTime, + clipTime, + actualDeltaTime + ); - const speed = state.speed * this.speed; - playData.frameTime += speed * delta; + let costTime = 0; + if (transition) { + const clipDuration = state.clip.length; + const clipEndTime = state.clipEndTime * clipDuration; + const exitTime = transition.exitTime * state._getDuration(); + + if (isForwards) { + if (exitTime < lastClipTime) { + costTime = exitTime + clipEndTime - lastClipTime; + } else { + costTime = exitTime - lastClipTime; + } + } else { + const startTime = state.clipStartTime * clipDuration; + if (lastClipTime < exitTime) { + costTime = clipEndTime - exitTime + lastClipTime - startTime; + } else { + costTime = lastClipTime - exitTime; + } + costTime = -costTime; + } + // Revert actualDeltaTime and update costTime + srcPlayData.update(costTime - actualDeltaTime); + // Need update whenever has transition + } - playData.update(speed < 0); + if (playState === AnimatorStatePlayState.Finished) { + layerData.layerState = LayerState.Finished; + } - let transition = this._checkStateTransitions( - playData, - state.transitions, - layerIndex, - lastClipTime, - playData.clipTime - ); + eventHandlers.length && this._fireAnimationEvents(srcPlayData, eventHandlers, lastClipTime, clipTime); - if (!transition) { - transition = this._checkTransitionsByCondition( - state, - this._animatorController._layers[layerIndex].stateMachine.anyStateTransitions, - layerIndex - ); + if (lastPlayState === AnimatorStatePlayState.UnStarted) { + this._callAnimatorScriptOnEnter(state, layerIndex); + } + if (playState === AnimatorStatePlayState.Finished) { + this._callAnimatorScriptOnExit(state, layerIndex); + } else { + this._callAnimatorScriptOnUpdate(state, layerIndex); } if (transition) { - return; + const remainDeltaTime = deltaTime - Math.abs(costTime); + this._updateState(layerIndex, layerData, stateMachine, remainDeltaTime); } + } - const { clipTime, playState } = playData; - const finished = playState === AnimatorStatePlayState.Finished; + private _evaluatePlayingState( + playData: AnimatorStatePlayData, + weight: number, + additive: boolean, + aniUpdate: boolean + ): void { + const curveBindings = playData.state.clip._curveBindings; + const finished = playData.playState === AnimatorStatePlayState.Finished; if (aniUpdate || finished) { + const curveLayerOwner = playData.stateData.curveLayerOwner; for (let i = curveBindings.length - 1; i >= 0; i--) { const layerOwner = curveLayerOwner[i]; const owner = layerOwner?.curveOwner; - if (!owner || !layerOwner.isActive) continue; + if (!owner || !layerOwner.isActive) { + continue; + } const curve = curveBindings[i].curve; if (curve.keys.length) { this._checkRevertOwner(owner, additive); - const value = owner.evaluateValue(curve, clipTime, additive); + const value = owner.evaluateValue(curve, playData.clipTime, additive); aniUpdate && owner.applyValue(value, weight, additive); finished && layerOwner.saveFinalValue(); } } } + } - if (playState === AnimatorStatePlayState.Finished) { - layerData.layerState = LayerState.Finished; + private _updateCrossFadeState( + layerIndex: number, + layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, + deltaTime: number + ) { + const { srcPlayData, destPlayData } = layerData; + const { speed } = this; + const { state: srcState, stateData: srcStateData } = srcPlayData; + const { eventHandlers: srcEventHandlers } = srcStateData; + const { state: destState, stateData: destStateData } = destPlayData; + const { eventHandlers: destEventHandlers } = destStateData; + + const destStateDuration = destState._getDuration(); + const transitionDuration = destStateDuration * layerData.crossFadeTransition.duration; + + const actualSrcSpeed = srcState.speed * speed; + const actualDestSpeed = destState.speed * speed; + const actualDestDeltaTime = actualDestSpeed * deltaTime; + + srcPlayData && srcPlayData.updateOrientation(actualSrcSpeed * deltaTime); + destPlayData && destPlayData.updateOrientation(actualDestDeltaTime); + + const { clipTime: lastSrcClipTime, playState: lastSrcPlayState } = srcPlayData; + const { clipTime: lastDestClipTime, playState: lastDstPlayState } = destPlayData; + + let destCostTime = 0; + if (destPlayData.isForwards) { + destCostTime = + lastDestClipTime + actualDestDeltaTime > transitionDuration + ? transitionDuration - lastDestClipTime + : actualDestDeltaTime; + } else { + // The time that has been played + const playedTime = destStateDuration - lastDestClipTime; + destCostTime = + // -actualDestDeltaTime: The time that will be played, negative are meant to make ite be a periods + // > transition: The time that will be played is enough to finish the transition + playedTime - actualDestDeltaTime > transitionDuration + ? // Negative number is used to convert a time period into a reverse deltaTime. + // -(transitionDuration - playedTime) + playedTime - transitionDuration + : actualDestDeltaTime; } - eventHandlers.length && this._fireAnimationEvents(playData, eventHandlers, lastClipTime, clipTime); - if (lastPlayState === AnimatorStatePlayState.UnStarted) { - this._callAnimatorScriptOnEnter(state, layerIndex); + const costTime = actualDestSpeed === 0 ? 0 : destCostTime / actualDestSpeed; + + srcPlayData.update(costTime * actualSrcSpeed); + destPlayData.update(destCostTime); + + const { clipTime: srcClipTime, playState: srcPlayState } = srcPlayData; + const { clipTime: destClipTime, playState: destPlayState, frameTime } = destPlayData; + + let crossWeight = Math.abs(frameTime) / transitionDuration; + // For precision problem, loose judgment, expect to crossFade + (crossWeight >= 1.0 - MathUtil.zeroTolerance || transitionDuration === 0) && (crossWeight = 1.0); + srcEventHandlers.length && this._fireAnimationEvents(srcPlayData, srcEventHandlers, lastSrcClipTime, srcClipTime); + destEventHandlers.length && + this._fireAnimationEvents(destPlayData, destEventHandlers, lastDestClipTime, destClipTime); + + if (lastSrcPlayState === AnimatorStatePlayState.UnStarted) { + this._callAnimatorScriptOnEnter(srcState, layerIndex); } - if (playState === AnimatorStatePlayState.Finished) { - this._callAnimatorScriptOnExit(state, layerIndex); + if (crossWeight === 1.0 || srcPlayState === AnimatorStatePlayState.Finished) { + this._callAnimatorScriptOnExit(srcState, layerIndex); } else { - this._callAnimatorScriptOnUpdate(state, layerIndex); + this._callAnimatorScriptOnUpdate(srcState, layerIndex); } + + if (lastDstPlayState === AnimatorStatePlayState.UnStarted) { + this._callAnimatorScriptOnEnter(destState, layerIndex); + } + if (destPlayState === AnimatorStatePlayState.Finished) { + this._callAnimatorScriptOnExit(destState, layerIndex); + } else { + this._callAnimatorScriptOnUpdate(destState, layerIndex); + } + + crossWeight === 1.0 && this._updateCrossFadeData(layerData); + + const remainDeltaTime = deltaTime - costTime; + // For precision problem, strict judgment, expect not to update + remainDeltaTime > MathUtil.zeroTolerance && this._updateState(layerIndex, layerData, stateMachine, remainDeltaTime); } - private _updateCrossFade( + private _evaluateCrossFadeState( srcPlayData: AnimatorStatePlayData, destPlayData: AnimatorStatePlayData, layerData: AnimatorLayerData, - layerIndex: number, weight: number, - delta: number, additive: boolean, aniUpdate: boolean ) { - const { speed } = this; const { crossLayerOwnerCollection } = layerData; const { _curveBindings: srcCurves } = srcPlayData.state.clip; - const { state: srcState, stateData: srcStateData, playState: lastSrcPlayState } = srcPlayData; - const { eventHandlers: srcEventHandlers } = srcStateData; - const { state: destState, stateData: destStateData, playState: lastDstPlayState } = destPlayData; - const { eventHandlers: destEventHandlers } = destStateData; + const { state: destState } = destPlayData; const { _curveBindings: destCurves } = destState.clip; - const { clipTime: lastSrcClipTime } = srcPlayData; - const { clipTime: lastDestClipTime } = destPlayData; - - const duration = destState._getDuration() * layerData.crossFadeTransition.duration; - let crossWeight = Math.abs(destPlayData.frameTime) / duration; - (crossWeight >= 1.0 || duration === 0) && (crossWeight = 1.0); + const transitionDuration = destState._getDuration() * layerData.crossFadeTransition.duration; + let crossWeight = Math.abs(destPlayData.frameTime) / transitionDuration; + (crossWeight >= 1.0 - MathUtil.zeroTolerance || transitionDuration === 0) && (crossWeight = 1.0); - const srcSpeed = srcState.speed * speed; - const destSpeed = destState.speed * speed; - - srcPlayData.frameTime += srcSpeed * delta; - destPlayData.frameTime += destSpeed * delta; - - srcPlayData.update(srcSpeed < 0); - destPlayData.update(destSpeed < 0); - - const { clipTime: srcClipTime, playState: srcPlayState } = srcPlayData; - const { clipTime: destClipTime, playState: destPlayState } = destPlayData; const finished = destPlayData.playState === AnimatorStatePlayState.Finished; if (aniUpdate || finished) { @@ -622,8 +758,8 @@ export class Animator extends Component { const value = owner.evaluateCrossFadeValue( srcCurveIndex >= 0 ? srcCurves[srcCurveIndex].curve : null, destCurveIndex >= 0 ? destCurves[destCurveIndex].curve : null, - srcClipTime, - destClipTime, + srcPlayData.clipTime, + destPlayData.clipTime, crossWeight, additive ); @@ -631,60 +767,94 @@ export class Animator extends Component { finished && layerOwner.saveFinalValue(); } } + } - this._updateCrossFadeData(layerData, crossWeight); + private _updateCrossFadeFromPoseState( + layerIndex: number, + layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, + deltaTime: number + ) { + const { destPlayData } = layerData; + const { state, stateData } = destPlayData; + const { eventHandlers } = stateData; - srcEventHandlers.length && this._fireAnimationEvents(srcPlayData, srcEventHandlers, lastSrcClipTime, srcClipTime); - destEventHandlers.length && - this._fireAnimationEvents(destPlayData, destEventHandlers, lastDestClipTime, destClipTime); + const stateDuration = state._getDuration(); + const transitionDuration = stateDuration * layerData.crossFadeTransition.duration; - if (lastSrcPlayState === AnimatorStatePlayState.UnStarted) { - this._callAnimatorScriptOnEnter(srcState, layerIndex); - } - if (crossWeight === 1 || srcPlayState === AnimatorStatePlayState.Finished) { - this._callAnimatorScriptOnExit(srcState, layerIndex); + const actualSpeed = state.speed * this.speed; + const actualDeltaTime = actualSpeed * deltaTime; + + destPlayData.updateOrientation(actualDeltaTime); + + const { clipTime: lastDestClipTime, playState: lastPlayState } = destPlayData; + + let destCostTime = 0; + if (destPlayData.isForwards) { + destCostTime = + lastDestClipTime + actualDeltaTime > transitionDuration + ? transitionDuration - lastDestClipTime + : actualDeltaTime; } else { - this._callAnimatorScriptOnUpdate(srcState, layerIndex); + // The time that has been played + const playedTime = stateDuration - lastDestClipTime; + destCostTime = + // -actualDestDeltaTime: The time that will be played, negative are meant to make ite be a periods + // > transition: The time that will be played is enough to finish the transition + playedTime - actualDeltaTime > transitionDuration + ? // Negative number is used to convert a time period into a reverse deltaTime. + // -(transitionDuration - playedTime) + playedTime - transitionDuration + : actualDeltaTime; } - if (lastDstPlayState === AnimatorStatePlayState.UnStarted) { - this._callAnimatorScriptOnEnter(destState, layerIndex); + const costTime = actualSpeed === 0 ? 0 : destCostTime / actualSpeed; + + destPlayData.update(destCostTime); + + const { clipTime, playState, frameTime } = destPlayData; + + let crossWeight = Math.abs(frameTime) / transitionDuration; + // For precision problem, loose judgment, expect to crossFade + (crossWeight >= 1.0 - MathUtil.zeroTolerance || transitionDuration === 0) && (crossWeight = 1.0); + //@todo: srcState is missing the judgment of the most recent period." + eventHandlers.length && this._fireAnimationEvents(destPlayData, eventHandlers, lastDestClipTime, clipTime); + + if (lastPlayState === AnimatorStatePlayState.UnStarted) { + this._callAnimatorScriptOnEnter(state, layerIndex); } - if (destPlayState === AnimatorStatePlayState.Finished) { - this._callAnimatorScriptOnExit(destState, layerIndex); + if (playState === AnimatorStatePlayState.Finished) { + this._callAnimatorScriptOnExit(state, layerIndex); } else { - this._callAnimatorScriptOnUpdate(destState, layerIndex); + this._callAnimatorScriptOnUpdate(state, layerIndex); } + + crossWeight === 1.0 && this._updateCrossFadeData(layerData); + + const remainDeltaTime = deltaTime - costTime; + // For precision problem, strict judgment, expect not to update + remainDeltaTime > MathUtil.zeroTolerance && this._updateState(layerIndex, layerData, stateMachine, remainDeltaTime); } - private _updateCrossFadeFromPose( + private _evaluateCrossFadeFromPoseState( destPlayData: AnimatorStatePlayData, layerData: AnimatorLayerData, - layerIndex: number, weight: number, - delta: number, additive: boolean, aniUpdate: boolean ) { const { crossLayerOwnerCollection } = layerData; - const { state, stateData, playState: lastPlayState } = destPlayData; - const { eventHandlers } = stateData; + const { state } = destPlayData; const { _curveBindings: curveBindings } = state.clip; - const { clipTime: lastDestClipTime } = destPlayData; const duration = state._getDuration() * layerData.crossFadeTransition.duration; let crossWeight = Math.abs(destPlayData.frameTime) / duration; (crossWeight >= 1.0 || duration === 0) && (crossWeight = 1.0); - const speed = state.speed * this.speed; - - destPlayData.frameTime += speed * delta; - destPlayData.update(speed < 0); - const { clipTime: destClipTime, playState } = destPlayData; const finished = playState === AnimatorStatePlayState.Finished; - // When the animator is culled (aniUpdate=false), if the play state has finished, the final value needs to be calculated and saved to be applied directly. + // When the animator is culled (aniUpdate=false), if the play state has finished, the final value needs to be calculated and saved to be applied directly if (aniUpdate || finished) { for (let i = crossLayerOwnerCollection.length - 1; i >= 0; i--) { const layerOwner = crossLayerOwnerCollection[i]; @@ -706,25 +876,45 @@ export class Animator extends Component { finished && layerOwner.saveFinalValue(); } } + } - this._updateCrossFadeData(layerData, crossWeight); - - //@todo: srcState is missing the judgment of the most recent period." - eventHandlers.length && this._fireAnimationEvents(destPlayData, eventHandlers, lastDestClipTime, destClipTime); + private _updateFinishedState( + layerIndex: number, + layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, + deltaTime: number + ): void { + const playData = layerData.srcPlayData; + const { state } = playData; + const transitions = state.transitions; + const actualSpeed = state.speed * this.speed; + const actualDeltaTime = actualSpeed * deltaTime; + + playData.updateOrientation(actualDeltaTime); + + const { clipTime } = playData; + + const transition = + this._applyTransitionsByCondition(layerIndex, layerData, stateMachine, state, stateMachine.anyStateTransitions) || + this._applyStateTransitions( + layerIndex, + layerData, + stateMachine, + actualSpeed < 0, + playData, + transitions, + clipTime, + clipTime, + actualDeltaTime + ); - if (lastPlayState === AnimatorStatePlayState.UnStarted) { - this._callAnimatorScriptOnEnter(state, layerIndex); - } - if (playState === AnimatorStatePlayState.Finished) { - this._callAnimatorScriptOnExit(state, layerIndex); - } else { - this._callAnimatorScriptOnUpdate(state, layerIndex); + if (transition) { + this._updateState(layerIndex, layerData, stateMachine, deltaTime); } } - private _updateFinishedState( + private _evaluateFinishedState( playData: AnimatorStatePlayData, - layerIndex: number, weight: number, additive: boolean, aniUpdate: boolean @@ -733,22 +923,6 @@ export class Animator extends Component { return; } - const { state, clipTime } = playData; - - let transition = this._checkStateTransitions(playData, state.transitions, layerIndex, clipTime, playData.clipTime); - - if (!transition) { - transition = this._checkTransitionsByCondition( - state, - this._animatorController._layers[layerIndex].stateMachine.anyStateTransitions, - layerIndex - ); - } - - if (transition) { - return; - } - const { curveLayerOwner } = playData.stateData; const { _curveBindings: curveBindings } = playData.state.clip; @@ -764,20 +938,18 @@ export class Animator extends Component { } } - private _updateCrossFadeData(layerData: AnimatorLayerData, crossWeight: number): void { + private _updateCrossFadeData(layerData: AnimatorLayerData): void { const { destPlayData } = layerData; - if (crossWeight === 1.0) { - if (destPlayData.playState === AnimatorStatePlayState.Finished) { - layerData.layerState = LayerState.Finished; - } else { - layerData.layerState = LayerState.Playing; - } - layerData.switchPlayData(); - layerData.crossFadeTransition = null; + if (destPlayData.playState === AnimatorStatePlayState.Finished) { + layerData.layerState = LayerState.Finished; + } else { + layerData.layerState = LayerState.Playing; } + layerData.switchPlayData(); + layerData.crossFadeTransition = null; } - private _preparePlay(layerData: AnimatorLayerData, playState: AnimatorState): void { + private _preparePlayOwner(layerData: AnimatorLayerData, playState: AnimatorState): void { if (layerData.layerState === LayerState.Playing) { const srcPlayData = layerData.srcPlayData; if (srcPlayData.state !== playState) { @@ -787,7 +959,6 @@ export class Animator extends Component { } } } else { - // layerState is CrossFading, FixedCrossFading, Standby, Finished const { crossLayerOwnerCollection } = layerData; for (let i = crossLayerOwnerCollection.length - 1; i >= 0; i--) { crossLayerOwnerCollection[i].curveOwner.revertDefaultValue(); @@ -795,62 +966,86 @@ export class Animator extends Component { } } - private _checkStateTransitions( + private _applyStateTransitions( + layerIndex: number, + layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, + isForwards: boolean, playState: AnimatorStatePlayData, transitions: Readonly, - layerIndex: number, lastClipTime: number, - clipTime: number + clipTime: number, + deltaTime: number ): AnimatorStateTransition { const { state } = playState; const clipDuration = state.clip.length; let targetTransition: AnimatorStateTransition = null; - if (this.speed * state.speed >= 0) { - if (clipTime < lastClipTime) { + const startTime = state.clipStartTime * clipDuration; + const endTime = state.clipEndTime * clipDuration; + if (isForwards) { + if (lastClipTime + deltaTime >= endTime) { targetTransition = this._checkSubTransition( + layerIndex, + layerData, + stateMachine, playState, transitions, - layerIndex, lastClipTime, - state.clipEndTime * clipDuration + endTime ); if (!targetTransition) { playState.currentTransitionIndex = 0; targetTransition = this._checkSubTransition( + layerIndex, + layerData, + stateMachine, playState, transitions, - layerIndex, - state.clipStartTime * clipDuration, + startTime, clipTime ); } } else { - targetTransition = this._checkSubTransition(playState, transitions, layerIndex, lastClipTime, clipTime); + targetTransition = this._checkSubTransition( + layerIndex, + layerData, + stateMachine, + playState, + transitions, + lastClipTime, + clipTime + ); } } else { - if (clipTime > lastClipTime) { + if (lastClipTime + deltaTime <= startTime) { targetTransition = this._checkBackwardsSubTransition( + layerIndex, + layerData, + stateMachine, playState, transitions, - layerIndex, lastClipTime, - state.clipStartTime * clipDuration + startTime ); if (!targetTransition) { playState.currentTransitionIndex = transitions.length - 1; targetTransition = this._checkBackwardsSubTransition( + layerIndex, + layerData, + stateMachine, playState, transitions, - layerIndex, clipTime, - state.clipEndTime * clipDuration + endTime ); } } else { targetTransition = this._checkBackwardsSubTransition( + layerIndex, + layerData, + stateMachine, playState, transitions, - layerIndex, lastClipTime, clipTime ); @@ -861,9 +1056,11 @@ export class Animator extends Component { } private _checkSubTransition( + layerIndex: number, + layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, playState: AnimatorStatePlayData, transitions: Readonly, - layerIndex: number, lastClipTime: number, curClipTime: number ): AnimatorStateTransition { @@ -880,7 +1077,7 @@ export class Animator extends Component { if (exitTime >= lastClipTime) { playState.currentTransitionIndex = Math.min(transitionIndex + 1, n - 1); if (this._checkConditions(state, transition)) { - this._applyTransition(transition, layerIndex); + this._applyTransition(layerIndex, layerData, stateMachine, transition); return transition; } } @@ -889,9 +1086,11 @@ export class Animator extends Component { } private _checkBackwardsSubTransition( + layerIndex: number, + layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, playState: AnimatorStatePlayData, transitions: Readonly, - layerIndex: number, lastClipTime: number, curClipTime: number ): AnimatorStateTransition { @@ -908,7 +1107,7 @@ export class Animator extends Component { if (exitTime <= lastClipTime) { playState.currentTransitionIndex = Math.max(transitionIndex - 1, 0); if (this._checkConditions(state, transition)) { - this._applyTransition(transition, layerIndex); + this._applyTransition(layerIndex, layerData, stateMachine, transition); return transition; } } @@ -916,26 +1115,52 @@ export class Animator extends Component { return null; } - private _checkTransitionsByCondition( + private _applyTransitionsByCondition( + layerIndex: number, + layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, state: AnimatorState, - transitions: Readonly, - layerIndex: number + transitions: Readonly ): AnimatorStateTransition { for (let i = 0, n = transitions.length; i < n; i++) { const transition = transitions[i]; if (this._checkConditions(state, transition)) { - this._applyTransition(transition, layerIndex); + this._applyTransition(layerIndex, layerData, stateMachine, transition); return transition; } } } - private _applyTransition(transition: AnimatorStateTransition, layerIndex: number): void { + 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.`); + return false; + } + + const animatorLayerData = this._getAnimatorLayerData(layerIndex); + const animatorStateData = this._getAnimatorStateData(name, state, animatorLayerData, layerIndex); + + this._preparePlayOwner(animatorLayerData, state); + + animatorLayerData.layerState = LayerState.Playing; + animatorLayerData.srcPlayData.reset(state, animatorStateData, state._getDuration() * normalizedTimeOffset); + + return true; + } + + private _applyTransition( + layerIndex: number, + layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, + transition: AnimatorStateTransition + ): void { + // Need prepare first, it should crossFade when to exit + this._prepareCrossFadeByTransition(transition, layerIndex); if (transition.isExit) { - this._checkEntryState(); + this._checkAnyAndEntryState(layerIndex, layerData, stateMachine); return; } - this._crossFadeByTransition(transition, layerIndex); } private _checkConditions(state: AnimatorState, transition: AnimatorStateTransition): boolean { @@ -990,13 +1215,9 @@ export class Animator extends Component { return allPass; } - private _crossFadeByTransition(transition: AnimatorStateTransition, layerIndex: number): boolean { + private _prepareCrossFadeByTransition(transition: AnimatorStateTransition, layerIndex: number): boolean { const crossState = transition.destinationState; - if (this._controllerUpdateFlag?.flag) { - this._reset(); - } - if (!crossState) { return false; } @@ -1006,15 +1227,12 @@ export class Animator extends Component { } const animatorLayerData = this._getAnimatorLayerData(layerIndex); - const layerState = animatorLayerData.layerState; - const { destPlayData } = animatorLayerData; - const animatorStateData = this._getAnimatorStateData(crossState.name, crossState, animatorLayerData, layerIndex); const duration = crossState._getDuration(); const offset = duration * transition.offset; - destPlayData.reset(crossState, animatorStateData, offset); + animatorLayerData.destPlayData.reset(crossState, animatorStateData, offset); - switch (layerState) { + switch (animatorLayerData.layerState) { case LayerState.Standby: case LayerState.Finished: animatorLayerData.layerState = LayerState.FixedCrossFading; @@ -1144,24 +1362,32 @@ export class Animator extends Component { } } - private _checkEntryState(): void { - const { layers } = this._animatorController; - for (let i = 0, n = layers.length; i < n; ++i) { - const stateMachine = layers[i].stateMachine; - - const { entryTransitions } = stateMachine; - const length = entryTransitions.length; - if (length) { - for (let j = 0, m = length; j < m; j++) { - const transition = entryTransitions[j]; - if (this._checkConditions(null, transition)) { - this._crossFadeByTransition(transition, i); - break; - } - } - } else { - const defaultState = stateMachine.defaultState; - defaultState && this.play(defaultState.name, i); + private _checkAnyAndEntryState( + layerIndex: number, + layerData: AnimatorLayerData, + stateMachine: AnimatorStateMachine, + remainDeltaTime = 0 + ): void { + const { anyStateTransitions, entryTransitions } = stateMachine; + let transition: AnimatorStateTransition; + + transition = + anyStateTransitions.length && + this._applyTransitionsByCondition(layerIndex, layerData, stateMachine, null, anyStateTransitions); + + if (!transition) { + transition = + entryTransitions.length && + this._applyTransitionsByCondition(layerIndex, layerData, stateMachine, null, entryTransitions); + } + + if (transition) { + this._updateState(layerIndex, layerData, stateMachine, remainDeltaTime); + } else { + const defaultState = stateMachine.defaultState; + if (defaultState) { + this._preparePlay(defaultState, layerIndex); + this._updateState(layerIndex, layerData, stateMachine, remainDeltaTime); } } } diff --git a/packages/core/src/animation/internal/AnimatorStatePlayData.ts b/packages/core/src/animation/internal/AnimatorStatePlayData.ts index 6422ce442f..81535eecbf 100644 --- a/packages/core/src/animation/internal/AnimatorStatePlayData.ts +++ b/packages/core/src/animation/internal/AnimatorStatePlayData.ts @@ -14,6 +14,9 @@ export class AnimatorStatePlayData { clipTime: number; currentEventIndex: number; currentTransitionIndex: number; + isForwards = true; + + private _changedOrientation = false; reset(state: AnimatorState, stateData: AnimatorStateData, offsetFrameTime: number): void { this.state = state; @@ -23,9 +26,22 @@ export class AnimatorStatePlayData { this.clipTime = state.clipStartTime * state.clip.length; this.currentEventIndex = 0; this.currentTransitionIndex = 0; + this.isForwards = true; + } + + updateOrientation(deltaTime: number): void { + if (deltaTime !== 0) { + const lastIsForwards = this.isForwards; + this.isForwards = deltaTime > 0; + if (this.isForwards !== lastIsForwards) { + this._changedOrientation = true; + this.isForwards || this._correctTime(); + } + } } - update(isBackwards: boolean): void { + update(deltaTime: number): void { + this.frameTime += deltaTime; const state = this.state; let time = this.frameTime; const duration = state._getDuration(); @@ -39,11 +55,19 @@ export class AnimatorStatePlayData { } } - if (isBackwards && time === 0) { + time < 0 && (time += duration); + this.clipTime = time + state.clipStartTime * state.clip.length; + + if (this._changedOrientation) { + !this.isForwards && this._correctTime(); + this._changedOrientation = false; + } + } + + private _correctTime() { + const { state } = this; + if (this.clipTime === 0) { this.clipTime = state.clipEndTime * state.clip.length; - } else { - time < 0 && (time += duration); - this.clipTime = time + state.clipStartTime * state.clip.length; } } } diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index 407cad1006..af53b7b696 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -38,8 +38,6 @@ describe("Animator test", function () { const defaultSceneRoot = resource.defaultSceneRoot; rootEntity.addChild(defaultSceneRoot); animator = defaultSceneRoot.getComponent(Animator); - - engine.run(); }); after(function () { @@ -76,6 +74,8 @@ describe("Animator test", function () { let expectedSpeed = speed * 0.5; animator.speed = expectedSpeed; let lastFrameTime = srcPlayData.frameTime; + // @ts-ignore + animator.engine.time._frameCount++; animator.update(5); expect(animator.speed).to.eq(expectedSpeed); expect(srcPlayData.frameTime).to.eq(lastFrameTime + 5 * expectedSpeed); @@ -191,7 +191,9 @@ describe("Animator test", function () { it("animation cross fade", () => { animator.play("Walk"); animator.crossFade("Run", 0.5); - animator.update(1); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.1); const layerIndex = animator["_tempAnimatorStateInfo"].layerIndex; const animatorLayerData = animator["_animatorLayersData"]; @@ -211,8 +213,11 @@ describe("Animator test", function () { walkState.addTransition(transition); animator.play("Walk"); - + // @ts-ignore + animator.engine.time._frameCount++; animator.update(walkState.clip.length - 0.1); + // @ts-ignore + animator.engine.time._frameCount++; animator.update(0.1); const layerIndex = animator["_tempAnimatorStateInfo"].layerIndex; @@ -225,10 +230,14 @@ describe("Animator test", function () { it("animation fix cross fade", () => { animator.play("Walk"); - animator.update(1); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.1); animator.crossFade("Survey", 5); animator.crossFade("Run", 0.5); - animator.update(10); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.1); const layerIndex = animator["_tempAnimatorStateInfo"].layerIndex; const animatorLayerData = animator["_animatorLayersData"]; @@ -308,66 +317,237 @@ describe("Animator test", function () { animator.animatorController.addParameter("playerSpeed", 1); const stateMachine = animator.animatorController.layers[0].stateMachine; const idleState = animator.findAnimatorState("Survey"); + idleState.clearTransitions(); const walkState = animator.findAnimatorState("Walk"); + walkState.clearTransitions(); const runState = animator.findAnimatorState("Run"); - { - // handle idle state - const toWalkTransition = new AnimatorStateTransition(); - toWalkTransition.destinationState = walkState; - toWalkTransition.exitTime = 0.5; - toWalkTransition.duration = 0.3; - toWalkTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0); - idleState.addTransition(toWalkTransition); - - const exitTransition = idleState.addExitTransition(); - exitTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); - } - { - // handle walk state - const toRunTransition = new AnimatorStateTransition(); - toRunTransition.destinationState = runState; - toRunTransition.duration = 0.3; - toRunTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0.5); - walkState.addTransition(toRunTransition); - - const toIdleTransition = new AnimatorStateTransition(); - toIdleTransition.destinationState = idleState; - toIdleTransition.duration = 0.3; - toIdleTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); - walkState.addTransition(toIdleTransition); - } - { - // handle run state - const toWalkTransition = new AnimatorStateTransition(); - toWalkTransition.destinationState = walkState; - toWalkTransition.duration = 0.3; - toWalkTransition.addCondition(AnimatorConditionMode.Less, "playerSpeed", 0.5); - runState.addTransition(toWalkTransition); - } + runState.clearTransitions(); + let idleToWalkTime = 0; + let walkToRunTime = 0; + let runToWalkTime = 0; + let walkToIdleTime = 0; + + // handle idle state + const toWalkTransition = new AnimatorStateTransition(); + toWalkTransition.destinationState = walkState; + toWalkTransition.duration = 0.2; + toWalkTransition.exitTime = 0.9; + toWalkTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 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); + // to walk state + const toRunTransition = new AnimatorStateTransition(); + toRunTransition.destinationState = runState; + toRunTransition.duration = 0.3; + toRunTransition.exitTime = 0.9; + toRunTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0.5); + walkState.addTransition(toRunTransition); + walkToRunTime = + //@ts-ignore + (toRunTransition.exitTime - toWalkTransition.duration) * walkState._getDuration() + + //@ts-ignore + toRunTransition.duration * runState._getDuration(); + const toIdleTransition = new AnimatorStateTransition(); + toIdleTransition.destinationState = idleState; + toIdleTransition.duration = 0.3; + toIdleTransition.exitTime = 0.9; + toIdleTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); + walkState.addTransition(toIdleTransition); + walkToIdleTime = + //@ts-ignore + (toIdleTransition.exitTime - toRunTransition.duration) * walkState._getDuration() + + //@ts-ignore + toIdleTransition.duration * idleState._getDuration(); + + // to run state + const runToWalkTransition = new AnimatorStateTransition(); + runToWalkTransition.destinationState = walkState; + runToWalkTransition.duration = 0.3; + runToWalkTransition.exitTime = 0.9; + runToWalkTransition.addCondition(AnimatorConditionMode.Less, "playerSpeed", 0.5); + runState.addTransition(runToWalkTransition); + runToWalkTime = + //@ts-ignore + (runToWalkTransition.exitTime - toRunTransition.duration) * runState._getDuration() + + //@ts-ignore + runToWalkTransition.duration * walkState._getDuration(); stateMachine.addEntryStateTransition(idleState); const anyTransition = stateMachine.addAnyStateTransition(idleState); anyTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); anyTransition.duration = 0.3; + anyTransition.exitTime = 0.9; + let anyToIdleTime = + // @ts-ignore + (anyTransition.exitTime - toIdleTransition.duration) * walkState._getDuration() + + // @ts-ignore + anyTransition.duration * idleState._getDuration(); - for (let i = 0; i < 10; ++i) { - animator.update(500); - } + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(idleToWalkTime - 0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Survey"); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Walk"); + + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(walkToRunTime - 0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Walk"); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.001); expect(animator.getCurrentAnimatorState(0).name).to.eq("Run"); animator.animatorController.setParameterValue("playerSpeed", 0.4); - for (let i = 0; i < 10; ++i) { - animator.update(500); - } - + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(runToWalkTime - 0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Run"); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.001); expect(animator.getCurrentAnimatorState(0).name).to.eq("Walk"); animator.animatorController.setParameterValue("playerSpeed", 0); - for (let i = 0; i < 10; ++i) { - animator.update(500); - } + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(anyToIdleTime - 0.001); + // apply any transition not walk to idle + expect(animator.getCurrentAnimatorState(0).name).to.eq("Walk"); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Survey"); + }); + it("stateMachine backwards", () => { + animator.animatorController.addParameter("playerSpeed", 1); + animator.speed = -1; + const stateMachine = animator.animatorController.layers[0].stateMachine; + //@ts-ignore + stateMachine._entryTransitions.length = 0; + //@ts-ignore + stateMachine._anyStateTransitions.length = 0; + + const idleState = animator.findAnimatorState("Survey"); + idleState.clearTransitions(); + const walkState = animator.findAnimatorState("Walk"); + walkState.clearTransitions(); + const runState = animator.findAnimatorState("Run"); + runState.clearTransitions(); + let idleToWalkTime = 0; + let walkToRunTime = 0; + let runToWalkTime = 0; + let walkToIdleTime = 0; + + // handle idle state + const toWalkTransition = new AnimatorStateTransition(); + toWalkTransition.destinationState = walkState; + toWalkTransition.duration = 0.2; + toWalkTransition.exitTime = 0.1; + toWalkTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0); + idleState.addTransition(toWalkTransition); + idleToWalkTime = + //@ts-ignore + (1 - toWalkTransition.exitTime) * idleState._getDuration() + toWalkTransition.duration * walkState._getDuration(); + + const exitTransition = idleState.addExitTransition(); + exitTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); + // to walk state + const toRunTransition = new AnimatorStateTransition(); + toRunTransition.destinationState = runState; + toRunTransition.duration = 0.3; + toRunTransition.exitTime = 0.1; + toRunTransition.addCondition(AnimatorConditionMode.Greater, "playerSpeed", 0.5); + walkState.addTransition(toRunTransition); + walkToRunTime = + //@ts-ignore + (1 - toRunTransition.exitTime - toWalkTransition.duration) * walkState._getDuration() + + //@ts-ignore + toRunTransition.duration * runState._getDuration(); + const toIdleTransition = new AnimatorStateTransition(); + toIdleTransition.destinationState = idleState; + toIdleTransition.duration = 0.3; + toIdleTransition.exitTime = 0.1; + toIdleTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); + walkState.addTransition(toIdleTransition); + walkToIdleTime = + //@ts-ignore + (1 - toIdleTransition.exitTime - toRunTransition.duration) * walkState._getDuration() + + //@ts-ignore + toIdleTransition.duration * idleState._getDuration(); + + // to run state + const runToWalkTransition = new AnimatorStateTransition(); + runToWalkTransition.destinationState = walkState; + runToWalkTransition.duration = 0.3; + runToWalkTransition.exitTime = 0.1; + runToWalkTransition.addCondition(AnimatorConditionMode.Less, "playerSpeed", 0.5); + runState.addTransition(runToWalkTransition); + runToWalkTime = + //@ts-ignore + (1 - runToWalkTransition.exitTime - toRunTransition.duration) * runState._getDuration() + + //@ts-ignore + runToWalkTransition.duration * walkState._getDuration(); + + stateMachine.addEntryStateTransition(idleState); + + const anyTransition = stateMachine.addAnyStateTransition(idleState); + anyTransition.addCondition(AnimatorConditionMode.Equals, "playerSpeed", 0); + 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(); + + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(idleToWalkTime - 0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Survey"); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Walk"); + + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(walkToRunTime - 0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Walk"); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Run"); + + animator.animatorController.setParameterValue("playerSpeed", 0.4); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(runToWalkTime - 0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Run"); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.001); + expect(animator.getCurrentAnimatorState(0).name).to.eq("Walk"); + + animator.animatorController.setParameterValue("playerSpeed", 0); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(anyToIdleTime - 0.001); + // apply any transition not walk to idle + expect(animator.getCurrentAnimatorState(0).name).to.eq("Walk"); + // @ts-ignore + animator.engine.time._frameCount++; + animator.update(0.001); expect(animator.getCurrentAnimatorState(0).name).to.eq("Survey"); }); });