Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

AnimatorStateTransition support fixedDuration #2377

Merged
merged 19 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions e2e/case/animator-stateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
129 changes: 86 additions & 43 deletions packages/core/src/animation/Animator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnimatorLayerData>();
@ignoreClone
private _curveOwnerPool: Record<number, Record<string, AnimationCurveOwner<KeyframeValueType>>> = Object.create(null);
@ignoreClone
Expand All @@ -63,7 +63,7 @@ export class Animator extends Component {
private _tempAnimatorStateInfo: IAnimatorStateInfo = { layerIndex: -1, state: null };

@ignoreClone
private _controlledRenderers: Renderer[] = [];
private _controlledRenderers = new Array<Renderer>();

/**
* All layers from the AnimatorController which belongs this Animator.
Expand Down Expand Up @@ -106,51 +106,52 @@ 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) {
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.
* 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 fixedTimeOffset - The time offset in seconds from the start of the animation
*/
crossFadeInFixedTime(
stateName: string,
fixedDuration: number,
layerIndex: number = -1,
fixedTimeOffset: number = 0
): void {
GuoLei1990 marked this conversation as resolved.
Show resolved Hide resolved
this._crossFade(stateName, fixedDuration, layerIndex, fixedTimeOffset, true);
}

/**
Expand Down Expand Up @@ -321,6 +322,50 @@ 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,
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;
Expand Down Expand Up @@ -697,8 +742,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._fixedDuration;

const srcPlaySpeed = srcState.speed * speed;
const dstPlaySpeed = destState.speed * speed;
Expand Down Expand Up @@ -827,8 +871,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._fixedDuration;

const playSpeed = state.speed * this.speed;
const playDeltaTime = playSpeed * deltaTime;
Expand All @@ -843,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
Expand Down Expand Up @@ -1250,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.`);
Expand All @@ -1263,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;
}
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/animation/AnimatorStateTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,26 @@ import { AnimatorConditionMode } from "./enums/AnimatorConditionMode";
*/
export class AnimatorStateTransition {
/** The duration of the transition. This is represented in normalized time. */
duration: number = 0;
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.
Expand Down Expand Up @@ -67,6 +69,13 @@ 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
Expand Down
2 changes: 2 additions & 0 deletions packages/loader/src/AnimatorControllerLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class AnimatorControllerLoader extends Loader<AnimatorController> {
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;
Expand Down Expand Up @@ -161,6 +162,7 @@ interface ITransitionData {
isExit: boolean;
conditions: IConditionData[];
hasExitTime: boolean;
isFixedDuration: boolean;
}

interface IConditionData {
Expand Down
27 changes: 27 additions & 0 deletions tests/src/core/Animator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -851,4 +851,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);
});
});
Loading