@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
398 lines (351 loc) • 15.8 kB
text/typescript
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
*/
applyRootMotion: boolean = false;
/**
* Indicates whether this animator contains root motion data
*/
hasRootMotion: boolean = false;
/**
* When enabled, the animator will maintain its state when the component is disabled
*/
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
*/
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);
}
}
}