import type { AnimationAction, AnimationActionLoopStyles, AnimationMixer } from "three"; import { Mathf } from "../engine/engine_math.js"; import { serializable } from "../engine/engine_serialization_decorator.js"; import { IAnimationComponent } from "../engine/engine_types.js"; import { getParam } from "../engine/engine_utils.js"; import type { AnimatorControllerModel } from "../engine/extensions/NEEDLE_animator_controller_model.js"; import { getObjectAnimated } from "./AnimationUtils.js"; import { AnimatorController } from "./AnimatorController.js"; import { Behaviour } from "./Component.js"; const debug = getParam("debuganimator"); /** * Represents an event emitted by an animation mixer * @category Animation and Sequencing */ export declare class MixerEvent { /** The type of event that occurred */ type: string; /** The animation action that triggered this event */ action: AnimationAction; /** Number of loops completed in this cycle */ loopDelta: number; /** The animation mixer that emitted this event */ target: AnimationMixer; } /** * Configuration options for playing animations * @category Animation and Sequencing */ export declare class PlayOptions { /** Whether the animation should loop, and if so, which loop style to use */ loop?: boolean | AnimationActionLoopStyles; /** Whether the final animation state should be maintained after playback completes */ clampWhenFinished?: boolean; } /** * The Animator component plays and manages animations on a GameObject. * It works with an AnimatorController to handle state transitions and animation blending. * A new AnimatorController can be created from code via `AnimatorController.createFromClips`. * @category Animation and Sequencing * @group Components */ export class Animator extends Behaviour implements IAnimationComponent { /** * Identifies this component as an animation component in the engine */ get isAnimationComponent() { return true; } /** * When enabled, animation will affect the root transform position and rotation */ @serializable() applyRootMotion: boolean = false; /** * Indicates whether this animator contains root motion data */ @serializable() hasRootMotion: boolean = false; /** * When enabled, the animator will maintain its state when the component is disabled */ @serializable() keepAnimatorControllerStateOnDisable: boolean = false; // set from needle animator extension /** * Sets or replaces the animator controller for this component. * Handles binding the controller to this animator instance and ensures * proper initialization when the controller changes. * @param val The animator controller model or instance to use */ @serializable() set runtimeAnimatorController(val: AnimatorControllerModel | AnimatorController | undefined | null) { if (this._animatorController && this._animatorController.model === val) { return; } if (val) { if (!(val instanceof AnimatorController)) { if (debug) console.log("Assign animator controller", val, this); this._animatorController = new AnimatorController(val); if (this.__didAwake) this._animatorController.bind(this); } else { if (val.animator && val.animator !== this) { console.warn("AnimatorController can not be bound to multiple animators", val.model?.name) if (!val.model) { console.error("AnimatorController has no model"); } val = new AnimatorController(val.model); } this._animatorController = val; this._animatorController.bind(this); } } else this._animatorController = null; } /** * Gets the current animator controller instance * @returns The current animator controller or null if none is assigned */ get runtimeAnimatorController(): AnimatorController | undefined | null { return this._animatorController; } /** * Retrieves information about the current animation state * @returns The current state information, or undefined if no state is playing */ getCurrentStateInfo() { return this.runtimeAnimatorController?.getCurrentStateInfo(); } /** * The currently playing animation action that can be used to modify animation properties * @returns The current animation action, or null if no animation is playing */ get currentAction() { return this.runtimeAnimatorController?.currentAction || null; } /** * Indicates whether animation parameters have been modified since the last update * @returns True if parameters have been changed */ get parametersAreDirty() { return this._parametersAreDirty; } private _parametersAreDirty: boolean = false; /** * Indicates whether the animator state has changed since the last update * @returns True if the animator has been changed */ get isDirty() { return this._isDirty; } private _isDirty: boolean = false; /**@deprecated use play() */ Play(name: string | number, layer: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, transitionDurationInSec: number = 0) { this.play(name, layer, normalizedTime, transitionDurationInSec); } /** * Plays an animation on the animator * @param name The name or hash of the animation to play * @param layer The layer to play the animation on (-1 for default layer) * @param normalizedTime The time position to start playing (0-1 range, NEGATIVE_INFINITY for current position) * @param transitionDurationInSec The duration of the blend transition in seconds */ play(name: string | number, layer: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, transitionDurationInSec: number = 0) { this.runtimeAnimatorController?.play(name, layer, normalizedTime, transitionDurationInSec); this._isDirty = true; } /**@deprecated use reset */ Reset() { this.reset(); } /** * Resets the animator controller to its initial state */ reset() { this._animatorController?.reset(); this._isDirty = true; } /**@deprecated use setBool */ SetBool(name: string | number, val: boolean) { this.setBool(name, val); } /** * Sets a boolean parameter in the animator * @param name The name or hash of the parameter * @param value The boolean value to set */ setBool(name: string | number, value: boolean) { if (debug) console.log("setBool", name, value); if (this.runtimeAnimatorController?.getBool(name) !== value) this._parametersAreDirty = true; this.runtimeAnimatorController?.setBool(name, value); } /**@deprecated use getBool */ GetBool(name: string | number) { return this.getBool(name); } /** * Gets a boolean parameter from the animator * @param name The name or hash of the parameter * @returns The value of the boolean parameter, or false if not found */ getBool(name: string | number): boolean { const res = this.runtimeAnimatorController?.getBool(name) ?? false; if (debug) console.log("getBool", name, res); return res; } /** * Toggles a boolean parameter between true and false * @param name The name or hash of the parameter */ toggleBool(name: string | number) { this.setBool(name, !this.getBool(name)); } /**@deprecated use setFloat */ SetFloat(name: string | number, val: number) { this.setFloat(name, val); } /** * Sets a float parameter in the animator * @param name The name or hash of the parameter * @param val The float value to set */ setFloat(name: string | number, val: number) { if (this.runtimeAnimatorController?.getFloat(name) !== val) this._parametersAreDirty = true; if (debug) console.log("setFloat", name, val); this.runtimeAnimatorController?.setFloat(name, val); } /**@deprecated use getFloat */ GetFloat(name: string | number) { return this.getFloat(name); } /** * Gets a float parameter from the animator * @param name The name or hash of the parameter * @returns The value of the float parameter, or -1 if not found */ getFloat(name: string | number): number { const res = this.runtimeAnimatorController?.getFloat(name) ?? -1; if (debug) console.log("getFloat", name, res); return res; } /**@deprecated use setInteger */ SetInteger(name: string | number, val: number) { this.setInteger(name, val); } /** * Sets an integer parameter in the animator * @param name The name or hash of the parameter * @param val The integer value to set */ setInteger(name: string | number, val: number) { if (this.runtimeAnimatorController?.getInteger(name) !== val) this._parametersAreDirty = true; if (debug) console.log("setInteger", name, val); this.runtimeAnimatorController?.setInteger(name, val); } /**@deprecated use getInteger */ GetInteger(name: string | number) { return this.getInteger(name); } /** * Gets an integer parameter from the animator * @param name The name or hash of the parameter * @returns The value of the integer parameter, or -1 if not found */ getInteger(name: string | number): number { const res = this.runtimeAnimatorController?.getInteger(name) ?? -1; if (debug) console.log("getInteger", name, res); return res; } /**@deprecated use setTrigger */ SetTrigger(name: string | number) { this.setTrigger(name); } /** * Activates a trigger parameter in the animator * @param name The name or hash of the trigger parameter */ setTrigger(name: string | number) { this._parametersAreDirty = true; if (debug) console.log("setTrigger", name); this.runtimeAnimatorController?.setTrigger(name); } /**@deprecated use resetTrigger */ ResetTrigger(name: string | number) { this.resetTrigger(name); } /** * Resets a trigger parameter in the animator * @param name The name or hash of the trigger parameter */ resetTrigger(name: string | number) { this._parametersAreDirty = true; if (debug) console.log("resetTrigger", name); this.runtimeAnimatorController?.resetTrigger(name); } /**@deprecated use getTrigger */ GetTrigger(name: string | number) { this.getTrigger(name); } /** * Gets the state of a trigger parameter from the animator * @param name The name or hash of the trigger parameter * @returns The state of the trigger parameter */ getTrigger(name: string | number) { const res = this.runtimeAnimatorController?.getTrigger(name); if (debug) console.log("getTrigger", name, res); return res; } /**@deprecated use isInTransition */ IsInTransition() { return this.isInTransition(); } /** * Checks if the animator is currently in a transition between states * @returns True if the animator is currently blending between animations */ isInTransition(): boolean { return this.runtimeAnimatorController?.isInTransition() ?? false; } /**@deprecated use setSpeed */ SetSpeed(speed: number) { return this.setSpeed(speed); } /** * Sets the playback speed of the animator * @param speed The new playback speed multiplier */ setSpeed(speed: number) { if (speed === this._speed) return; if (debug) console.log("setSpeed", speed); this._speed = speed; if (this._animatorController?.animator == this) this._animatorController.setSpeed(speed); } /** * Sets a random playback speed between the min and max values * @param minMax Object with x (minimum) and y (maximum) speed values */ set minMaxSpeed(minMax: { x: number, y: number }) { this._speed = Mathf.lerp(minMax.x, minMax.y, Math.random()); if (this._animatorController?.animator == this) this._animatorController.setSpeed(this._speed); } /** * Sets a random normalized time offset for animations between min (x) and max (y) values * @param minMax Object with x (min) and y (max) values for the offset range */ set minMaxOffsetNormalized(minMax: { x: number, y: number }) { this._normalizedStartOffset = Mathf.lerp(minMax.x, minMax.y, Math.random()); if (this.runtimeAnimatorController?.animator == this) this.runtimeAnimatorController.normalizedStartOffset = this._normalizedStartOffset; } private _speed: number = 1; private _normalizedStartOffset: number = 0; private _animatorController?: AnimatorController | null = null; awake() { if (debug) console.log("ANIMATOR", this.name, this); if (!this.gameObject) return; this.initializeRuntimeAnimatorController(); } // Why do we jump through hoops like this? It's because of the PlayableDirector and animation tracks // they NEED to use the same mixer when binding/creating the animation clips // so when the playable director runs it takes over updating the mixer for blending and then calls the runtimeAnimatorController.update // so they effectively share the same mixer. There might be cases still where not the same mixer is being used but then the animation track prints an error in dev private _initializeWithRuntimeAnimatorController?: AnimatorController | null; initializeRuntimeAnimatorController(force: boolean = false) { const shouldRun = (force || this.runtimeAnimatorController !== this._initializeWithRuntimeAnimatorController); if (this.runtimeAnimatorController && shouldRun) { const clone = this.runtimeAnimatorController.clone(); this._initializeWithRuntimeAnimatorController = clone; if (clone) { console.assert(this.runtimeAnimatorController !== clone); this.runtimeAnimatorController = clone; console.assert(this.runtimeAnimatorController === clone); this.runtimeAnimatorController.bind(this); this.runtimeAnimatorController.setSpeed(this._speed); this.runtimeAnimatorController.normalizedStartOffset = this._normalizedStartOffset; } else console.warn("Could not clone animator controller", this.runtimeAnimatorController); } } onDisable() { if (!this.keepAnimatorControllerStateOnDisable) this._animatorController?.reset(); } onBeforeRender() { this._isDirty = false; this._parametersAreDirty = false; const isAnimatedExternally = getObjectAnimated(this.gameObject); if (isAnimatedExternally) return; if (this._animatorController) { this._animatorController.update(1); } } }