From 4171a97cdd824bb1c01fa93f8bf21dad05207f05 Mon Sep 17 00:00:00 2001 From: luzhuang <364439895@qq.com> Date: Fri, 19 Jul 2024 16:59:58 +0800 Subject: [PATCH] feat: update controller loader to enhance stateMachine and fix crossfade bug when dest state has no clip (#2228) * feat: update controller loader to enhance stateMachine --- packages/core/src/animation/Animator.ts | 32 +++-- packages/core/src/animation/index.ts | 3 + .../loader/src/AnimatorControllerLoader.ts | 114 ++++++++++++++---- 3 files changed, 118 insertions(+), 31 deletions(-) diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index cd53d53931..fec6493dac 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -1078,8 +1078,11 @@ export class Animator extends Component { if (exitTime >= lastClipTime) { playState.currentTransitionIndex = Math.min(transitionIndex + 1, n - 1); if (this._checkConditions(state, transition)) { - this._applyTransition(layerIndex, layerData, stateMachine, transition); - return transition; + if (this._applyTransition(layerIndex, layerData, stateMachine, transition)) { + return transition; + } else { + return null; + } } } } @@ -1108,8 +1111,11 @@ export class Animator extends Component { if (exitTime <= lastClipTime) { playState.currentTransitionIndex = Math.max(transitionIndex - 1, 0); if (this._checkConditions(state, transition)) { - this._applyTransition(layerIndex, layerData, stateMachine, transition); - return transition; + if (this._applyTransition(layerIndex, layerData, stateMachine, transition)) { + return transition; + } else { + return null; + } } } } @@ -1126,8 +1132,11 @@ export class Animator extends Component { for (let i = 0, n = transitions.length; i < n; i++) { const transition = transitions[i]; if (this._checkConditions(state, transition)) { - this._applyTransition(layerIndex, layerData, stateMachine, transition); - return transition; + if (this._applyTransition(layerIndex, layerData, stateMachine, transition)) { + return transition; + } else { + return null; + } } } } @@ -1155,13 +1164,14 @@ export class Animator extends Component { layerData: AnimatorLayerData, stateMachine: AnimatorStateMachine, transition: AnimatorStateTransition - ): void { + ): boolean { // Need prepare first, it should crossFade when to exit - this._prepareCrossFadeByTransition(transition, layerIndex); + const success = this._prepareCrossFadeByTransition(transition, layerIndex); if (transition.isExit) { this._checkAnyAndEntryState(layerIndex, layerData, stateMachine); - return; + return true; } + return success; } private _checkConditions(state: AnimatorState, transition: AnimatorStateTransition): boolean { @@ -1176,6 +1186,10 @@ export class Animator extends Component { let pass = false; const { mode, parameterName: name, threshold } = conditions[i]; const parameter = this.getParameter(name); + if (!parameter) { + return false; + } + switch (mode) { case AnimatorConditionMode.Equals: if (parameter.value === threshold) { diff --git a/packages/core/src/animation/index.ts b/packages/core/src/animation/index.ts index 7c3bf8229f..b829ffde54 100644 --- a/packages/core/src/animation/index.ts +++ b/packages/core/src/animation/index.ts @@ -21,3 +21,6 @@ export { WrapMode } from "./enums/WrapMode"; export * from "./Keyframe"; export * from "./AnimatorLayerMask"; export { StateMachineScript } from "./StateMachineScript"; +export { AnimatorCondition } from "./AnimatorCondition"; +export * from "./AnimatorControllerParameter"; +export { LayerPathMask } from "./LayerPathMask"; diff --git a/packages/loader/src/AnimatorControllerLoader.ts b/packages/loader/src/AnimatorControllerLoader.ts index 023a446a50..737a5e37b8 100644 --- a/packages/loader/src/AnimatorControllerLoader.ts +++ b/packages/loader/src/AnimatorControllerLoader.ts @@ -7,8 +7,12 @@ import { ResourceManager, AnimatorController, AnimatorControllerLayer, - AnimatorStateMachine, - AnimatorStateTransition + AnimatorStateTransition, + AnimatorState, + AnimatorConditionMode, + AnimatorControllerParameterValueType, + WrapMode, + AnimatorControllerParameter } from "@galacean/engine-core"; @resourceLoader(AssetType.AnimatorController, ["json"], false) @@ -21,7 +25,7 @@ class AnimatorControllerLoader extends Loader { }) .then((data) => { const animatorController = new AnimatorController(); - const { layers } = data; + const { layers, parameters } = data; const promises = []; layers.forEach((layerData, layerIndex: number) => { const { name, blendingMode, weight, stateMachine: stateMachineData } = layerData; @@ -29,29 +33,30 @@ class AnimatorControllerLoader extends Loader { layer.blendingMode = blendingMode; layer.weight = weight; if (stateMachineData) { - const { states } = stateMachineData; - const stateMachine = (layer.stateMachine = new AnimatorStateMachine()); - states.forEach((stateData, stateIndex: number) => { + const { states, transitions, entryTransitions, anyTransitions } = stateMachineData; + const stateMachine = layer.stateMachine; + const statesMap: Record = {}; + const transitionsMap: Record = {}; + states.forEach((stateData: IStateData, stateIndex: number) => { const { + id, name, speed, wrapMode, clipStartNormalizedTime, clipEndNormalizedTime, - isDefaultState, clip: clipData, scripts } = stateData; const state = stateMachine.addState(name); - isDefaultState && (stateMachine.defaultState = state); state.speed = speed; state.wrapMode = wrapMode; state.clipStartTime = clipStartNormalizedTime; state.clipEndTime = clipEndNormalizedTime; - const scriptsObject = JSON.parse(scripts); - scriptsObject?.forEach((script) => { + scripts.forEach((script) => { state.addStateMachineScript(Loader.getClass(script)); }); + statesMap[id] = state; if (clipData) { promises.push( new Promise((resolve) => { @@ -67,23 +72,39 @@ class AnimatorControllerLoader extends Loader { ); } }); - states.forEach((stateData) => { - const { name, transitions } = stateData; - transitions.forEach((transitionData) => { - const { targetStateName, duration, offset, exitTime } = transitionData; - const sourceState = stateMachine.findStateByName(name); - const destState = stateMachine.findStateByName(targetStateName); - const transition = new AnimatorStateTransition(); - transition.destinationState = destState; - transition.duration = duration; - transition.exitTime = exitTime; - transition.offset = offset; - sourceState.addTransition(transition); + transitions.forEach((transitionData: ITransitionData) => { + const transition = this._createTransition(transitionData, statesMap[transitionData.destinationStateId]); + transitionsMap[transitionData.id] = transition; + }); + + states.forEach((stateData: IStateData) => { + const { id, transitions } = stateData; + transitions.forEach((transitionId) => { + const transition = transitionsMap[transitionId]; + transition && statesMap[id].addTransition(transition); }); }); + + entryTransitions.forEach((entryTransitionData: ITransitionData) => { + stateMachine.addEntryStateTransition( + this._createTransition(entryTransitionData, statesMap[entryTransitionData.destinationStateId]) + ); + }); + + anyTransitions.forEach((anyTransitionData: ITransitionData) => { + stateMachine.addAnyStateTransition( + this._createTransition(anyTransitionData, statesMap[anyTransitionData.destinationStateId]) + ); + }); } animatorController.addLayer(layer); }); + parameters.forEach((parameterData) => { + const parameter = new AnimatorControllerParameter(); + parameter.name = parameterData.name; + parameter.value = parameterData.value; + animatorController.addParameter(parameter); + }); Promise.all(promises).then((clipData) => { clipData.forEach((data) => { const { layerIndex, stateIndex, clip } = data; @@ -95,4 +116,53 @@ class AnimatorControllerLoader extends Loader { .catch(reject); }); } + + private _createTransition(transitionData: ITransitionData, destinationState: AnimatorState): AnimatorStateTransition { + const transition = new AnimatorStateTransition(); + transition.duration = transitionData.duration; + transition.offset = transitionData.offset; + transition.exitTime = transitionData.exitTime; + transition.solo = transitionData.solo; + transition.mute = transitionData.mute; + // @ts-ignore + transition._isExit = transitionData.isExit; + transition.destinationState = destinationState; + transitionData.conditions.forEach((conditionData) => { + transition.addCondition(conditionData.mode, conditionData.parameterName, conditionData.threshold); + }); + return transition; + } +} + +interface IStateData { + id?: string; + name: string; + speed: number; + wrapMode: WrapMode; + clipStartNormalizedTime: number; + clipEndNormalizedTime: number; + clip: any; + transitions: string[]; + scripts: string[]; + isEntryState: boolean; + isExitState: boolean; + isAnyState: boolean; +} + +interface ITransitionData { + id?: string; + duration: number; + offset: number; + exitTime: number; + destinationStateId: string; + solo: boolean; + mute: boolean; + isExit: boolean; + conditions: IConditionData[]; +} + +interface IConditionData { + mode: AnimatorConditionMode; + parameterName: string; + threshold?: AnimatorControllerParameterValueType; }