playcanvas
Version:
PlayCanvas WebGL game engine
591 lines (588 loc) • 27.8 kB
JavaScript
import { Debug } from '../../../core/debug.js';
import { sortPriority } from '../../../core/sort.js';
import { AnimClip } from '../evaluator/anim-clip.js';
import { ANIM_STATE_START, ANIM_STATE_END, ANIM_STATE_ANY, ANIM_NOT_EQUAL_TO, ANIM_EQUAL_TO, ANIM_LESS_THAN_EQUAL_TO, ANIM_GREATER_THAN_EQUAL_TO, ANIM_LESS_THAN, ANIM_GREATER_THAN, ANIM_INTERRUPTION_NEXT_PREV, ANIM_INTERRUPTION_PREV_NEXT, ANIM_INTERRUPTION_NEXT, ANIM_INTERRUPTION_PREV, ANIM_PARAMETER_TRIGGER, ANIM_CONTROL_STATES, ANIM_INTERRUPTION_NONE } from './constants.js';
import { AnimState } from './anim-state.js';
import { AnimNode } from './anim-node.js';
import { AnimTransition } from './anim-transition.js';
function _extends() {
_extends = Object.assign || function(target) {
for(var i = 1; i < arguments.length; i++){
var source = arguments[i];
for(var key in source){
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
/**
* @import { AnimEvaluator } from '../evaluator/anim-evaluator.js'
* @import { EventHandler } from '../../../core/event-handler.js'
*/ /**
* The AnimController manages the animations for its entity, based on the provided state graph and
* parameters. Its update method determines which state the controller should be in based on the
* current time, parameters and available states / transitions. It also ensures the AnimEvaluator
* is supplied with the correct animations, based on the currently active state.
*
* @ignore
*/ class AnimController {
get animEvaluator() {
return this._animEvaluator;
}
set activeState(stateName) {
this._activeStateName = stateName;
}
get activeState() {
return this._findState(this._activeStateName);
}
get activeStateName() {
return this._activeStateName;
}
get activeStateAnimations() {
return this.activeState.animations;
}
set previousState(stateName) {
this._previousStateName = stateName;
}
get previousState() {
return this._findState(this._previousStateName);
}
get previousStateName() {
return this._previousStateName;
}
get playable() {
var playable = true;
for(var i = 0; i < this._stateNames.length; i++){
if (!this._states[this._stateNames[i]].playable) {
playable = false;
}
}
return playable;
}
set playing(value) {
this._playing = value;
}
get playing() {
return this._playing;
}
get activeStateProgress() {
return this._getActiveStateProgressForTime(this._timeInState);
}
get activeStateDuration() {
if (this._activeStateDurationDirty) {
var maxDuration = 0.0;
for(var i = 0; i < this.activeStateAnimations.length; i++){
var activeClip = this._animEvaluator.findClip(this.activeStateAnimations[i].name);
if (activeClip) {
maxDuration = Math.max(maxDuration, activeClip.track.duration);
}
}
this._activeStateDuration = maxDuration;
this._activeStateDurationDirty = false;
}
return this._activeStateDuration;
}
set activeStateCurrentTime(time) {
this._timeInStateBefore = time;
this._timeInState = time;
for(var i = 0; i < this.activeStateAnimations.length; i++){
var clip = this.animEvaluator.findClip(this.activeStateAnimations[i].name);
if (clip) {
clip.time = time;
}
}
}
get activeStateCurrentTime() {
return this._timeInState;
}
get transitioning() {
return this._isTransitioning;
}
get transitionProgress() {
return this._currTransitionTime / this._totalTransitionTime;
}
get states() {
return this._stateNames;
}
assignMask(mask) {
return this._animEvaluator.assignMask(mask);
}
/**
* @param {string} stateName - The name of the state to find.
* @returns {AnimState} The state with the given name.
* @private
*/ _findState(stateName) {
return this._states[stateName];
}
_getActiveStateProgressForTime(time) {
if (this.activeStateName === ANIM_STATE_START || this.activeStateName === ANIM_STATE_END || this.activeStateName === ANIM_STATE_ANY) {
return 1.0;
}
var activeClip = this._animEvaluator.findClip(this.activeStateAnimations[0].name);
if (activeClip) {
return activeClip.progressForTime(time);
}
return null;
}
/**
* Return all the transitions that have the given stateName as their source state.
*
* @param {string} stateName - The name of the state to find transitions from.
* @returns {AnimTransition[]} The transitions that have the given stateName as their source
* state.
* @private
*/ _findTransitionsFromState(stateName) {
var transitions = this._findTransitionsFromStateCache[stateName];
if (!transitions) {
transitions = this._transitions.filter((transition)=>{
return transition.from === stateName;
});
// sort transitions in priority order
sortPriority(transitions);
this._findTransitionsFromStateCache[stateName] = transitions;
}
return transitions;
}
/**
* Return all the transitions that contain the given source and destination states.
*
* @param {string} sourceStateName - The name of the source state to find transitions from.
* @param {string} destinationStateName - The name of the destination state to find transitions
* to.
* @returns {AnimTransition[]} The transitions that have the given source and destination states.
* @private
*/ _findTransitionsBetweenStates(sourceStateName, destinationStateName) {
var transitions = this._findTransitionsBetweenStatesCache[sourceStateName + "->" + destinationStateName];
if (!transitions) {
transitions = this._transitions.filter((transition)=>{
return transition.from === sourceStateName && transition.to === destinationStateName;
});
// sort transitions in priority order
sortPriority(transitions);
this._findTransitionsBetweenStatesCache[sourceStateName + "->" + destinationStateName] = transitions;
}
return transitions;
}
_transitionHasConditionsMet(transition) {
var conditions = transition.conditions;
for(var i = 0; i < conditions.length; i++){
var condition = conditions[i];
var parameter = this._findParameter(condition.parameterName);
switch(condition.predicate){
case ANIM_GREATER_THAN:
if (!(parameter.value > condition.value)) return false;
break;
case ANIM_LESS_THAN:
if (!(parameter.value < condition.value)) return false;
break;
case ANIM_GREATER_THAN_EQUAL_TO:
if (!(parameter.value >= condition.value)) return false;
break;
case ANIM_LESS_THAN_EQUAL_TO:
if (!(parameter.value <= condition.value)) return false;
break;
case ANIM_EQUAL_TO:
if (!(parameter.value === condition.value)) return false;
break;
case ANIM_NOT_EQUAL_TO:
if (!(parameter.value !== condition.value)) return false;
break;
}
}
return true;
}
_findTransition(from, to) {
var transitions = [];
// If from and to are supplied, find transitions that include the required source and destination states
if (from && to) {
transitions = transitions.concat(this._findTransitionsBetweenStates(from, to));
} else {
// If no transition is active, look for transitions from the active & any states.
if (!this._isTransitioning) {
transitions = transitions.concat(this._findTransitionsFromState(this._activeStateName));
transitions = transitions.concat(this._findTransitionsFromState(ANIM_STATE_ANY));
} else {
// Otherwise look for transitions from the previous and active states based on the current interruption source.
// Accept transitions from the any state unless the interruption source is set to none
switch(this._transitionInterruptionSource){
case ANIM_INTERRUPTION_PREV:
transitions = transitions.concat(this._findTransitionsFromState(this._previousStateName));
transitions = transitions.concat(this._findTransitionsFromState(ANIM_STATE_ANY));
break;
case ANIM_INTERRUPTION_NEXT:
transitions = transitions.concat(this._findTransitionsFromState(this._activeStateName));
transitions = transitions.concat(this._findTransitionsFromState(ANIM_STATE_ANY));
break;
case ANIM_INTERRUPTION_PREV_NEXT:
transitions = transitions.concat(this._findTransitionsFromState(this._previousStateName));
transitions = transitions.concat(this._findTransitionsFromState(this._activeStateName));
transitions = transitions.concat(this._findTransitionsFromState(ANIM_STATE_ANY));
break;
case ANIM_INTERRUPTION_NEXT_PREV:
transitions = transitions.concat(this._findTransitionsFromState(this._activeStateName));
transitions = transitions.concat(this._findTransitionsFromState(this._previousStateName));
transitions = transitions.concat(this._findTransitionsFromState(ANIM_STATE_ANY));
break;
}
}
}
// filter out transitions that don't have their conditions met
transitions = transitions.filter((transition)=>{
// if the transition is moving to the already active state, ignore it
if (transition.to === this.activeStateName) {
return false;
}
// when an exit time is present, we should only exit if it falls within the current frame delta time
if (transition.hasExitTime) {
var progressBefore = this._getActiveStateProgressForTime(this._timeInStateBefore);
var progress = this._getActiveStateProgressForTime(this._timeInState);
// when the exit time is smaller than 1 and the state is looping, we should check for an exit each loop
if (transition.exitTime < 1.0 && this.activeState.loop) {
progressBefore -= Math.floor(progressBefore);
progress -= Math.floor(progress);
}
// if the delta time is 0 and the progress matches the exit time, the exitTime condition has been met
if (progress === progressBefore) {
if (progress !== transition.exitTime) {
return null;
}
// otherwise if the delta time is greater than 0, return false if exit time isn't within the frames delta time
} else if (!(transition.exitTime > progressBefore && transition.exitTime <= progress)) {
return null;
}
}
// if the exitTime condition has been met or is not present, check condition parameters
return this._transitionHasConditionsMet(transition);
});
// return the highest priority transition to use
if (transitions.length > 0) {
var transition = transitions[0];
if (transition.to === ANIM_STATE_END) {
var startTransition = this._findTransitionsFromState(ANIM_STATE_START)[0];
transition.to = startTransition.to;
}
return transition;
}
return null;
}
updateStateFromTransition(transition) {
var state;
var animation;
var clip;
// If transition.from is set, transition from the active state irregardless of the transition.from value (this could be the previous, active or ANY states).
// Otherwise the previousState is cleared.
this.previousState = transition.from ? this.activeStateName : null;
this.activeState = transition.to;
// when transitioning to a new state, we need to recalculate the duration of the active state based on its animations
this._activeStateDurationDirty = true;
// turn off any triggers which were required to activate this transition
for(var i = 0; i < transition.conditions.length; i++){
var condition = transition.conditions[i];
var parameter = this._findParameter(condition.parameterName);
if (parameter.type === ANIM_PARAMETER_TRIGGER) {
this._consumeTrigger(condition.parameterName);
}
}
if (this.previousState) {
if (!this._isTransitioning) {
this._transitionPreviousStates = [];
}
// record the transition source state in the previous states array
this._transitionPreviousStates.push({
name: this._previousStateName,
weight: 1
});
// if this new transition was activated during another transition, update the previous transition state weights based
// on the progress through the previous transition.
var interpolatedTime = Math.min(this._totalTransitionTime !== 0 ? this._currTransitionTime / this._totalTransitionTime : 1, 1.0);
for(var i1 = 0; i1 < this._transitionPreviousStates.length; i1++){
// interpolate the weights of the most recent previous state and all other previous states based on the progress through the previous transition
if (!this._isTransitioning) {
this._transitionPreviousStates[i1].weight = 1.0;
} else if (i1 !== this._transitionPreviousStates.length - 1) {
this._transitionPreviousStates[i1].weight *= 1.0 - interpolatedTime;
} else {
this._transitionPreviousStates[i1].weight = interpolatedTime;
}
state = this._findState(this._transitionPreviousStates[i1].name);
// update the animations of previous states, set their name to include their position in the previous state array
// to uniquely identify animations from the same state that were added during different transitions
for(var j = 0; j < state.animations.length; j++){
animation = state.animations[j];
clip = this._animEvaluator.findClip(animation.name + ".previous." + i1);
if (!clip) {
clip = this._animEvaluator.findClip(animation.name);
clip.name = animation.name + ".previous." + i1;
}
// // pause previous animation clips to reduce their impact on performance
if (i1 !== this._transitionPreviousStates.length - 1) {
clip.pause();
}
}
}
}
this._isTransitioning = true;
this._totalTransitionTime = transition.time;
this._currTransitionTime = 0;
this._transitionInterruptionSource = transition.interruptionSource;
var activeState = this.activeState;
var hasTransitionOffset = transition.transitionOffset && transition.transitionOffset > 0.0 && transition.transitionOffset < 1.0;
// set the time in the new state to 0 or to a value based on transitionOffset if one was given
var timeInState = 0;
var timeInStateBefore = 0;
if (hasTransitionOffset) {
var offsetTime = activeState.timelineDuration * transition.transitionOffset;
timeInState = offsetTime;
timeInStateBefore = offsetTime;
}
this._timeInState = timeInState;
this._timeInStateBefore = timeInStateBefore;
// Add clips to the evaluator for each animation in the new state.
for(var i2 = 0; i2 < activeState.animations.length; i2++){
clip = this._animEvaluator.findClip(activeState.animations[i2].name);
if (!clip) {
var speed = Number.isFinite(activeState.animations[i2].speed) ? activeState.animations[i2].speed : activeState.speed;
clip = new AnimClip(activeState.animations[i2].animTrack, this._timeInState, speed, true, activeState.loop, this._eventHandler);
clip.name = activeState.animations[i2].name;
this._animEvaluator.addClip(clip);
} else {
clip.reset();
}
if (transition.time > 0) {
clip.blendWeight = 0.0;
} else {
clip.blendWeight = activeState.animations[i2].normalizedWeight;
}
clip.play();
if (hasTransitionOffset) {
clip.time = activeState.timelineDuration * transition.transitionOffset;
} else {
var startTime = activeState.speed >= 0 ? 0 : this.activeStateDuration;
clip.time = startTime;
}
}
}
_transitionToState(newStateName) {
if (!this._findState(newStateName)) {
return;
}
// move to the given state, if a transition is present in the state graph use it. Otherwise move instantly to it.
var transition = this._findTransition(this._activeStateName, newStateName);
if (!transition) {
this._animEvaluator.removeClips();
transition = new AnimTransition({
from: null,
to: newStateName
});
}
this.updateStateFromTransition(transition);
}
assignAnimation(pathString, animTrack, speed, loop) {
var path = pathString.split('.');
var state = this._findState(path[0]);
if (!state) {
state = new AnimState(this, path[0], speed);
this._states[path[0]] = state;
this._stateNames.push(path[0]);
}
state.addAnimation(path, animTrack);
this._animEvaluator.updateClipTrack(state.name, animTrack);
if (speed !== undefined) {
state.speed = speed;
}
if (loop !== undefined) {
state.loop = loop;
}
if (!this._playing && this._activate && this.playable) {
this.play();
}
// when a new animation is added, the active state duration needs to be recalculated
this._activeStateDurationDirty = true;
}
removeNodeAnimations(nodeName) {
if (ANIM_CONTROL_STATES.indexOf(nodeName) !== -1) {
return false;
}
var state = this._findState(nodeName);
if (!state) {
Debug.error('Attempting to unassign animation tracks from a state that does not exist.', nodeName);
return false;
}
state.animations = [];
return true;
}
play(stateName) {
if (stateName) {
this._transitionToState(stateName);
}
this._playing = true;
}
pause() {
this._playing = false;
}
reset() {
this._previousStateName = null;
this._activeStateName = ANIM_STATE_START;
this._playing = false;
this._currTransitionTime = 1.0;
this._totalTransitionTime = 1.0;
this._isTransitioning = false;
this._timeInState = 0;
this._timeInStateBefore = 0;
this._animEvaluator.removeClips();
}
rebind() {
this._animEvaluator.rebind();
}
update(dt) {
if (!this._playing) {
return;
}
var state;
var animation;
var clip;
// update time when looping or when the active state is not at the end of its duration
if (this.activeState.loop || this._timeInState < this.activeStateDuration) {
this._timeInStateBefore = this._timeInState;
this._timeInState += dt * this.activeState.speed;
// if the active state is not looping and the time in state is greater than the duration, set the time in state to the state duration
// and update the delta time accordingly
if (!this.activeState.loop && this._timeInState > this.activeStateDuration) {
this._timeInState = this.activeStateDuration;
dt = this.activeStateDuration - this._timeInStateBefore;
}
}
// transition between states if a transition is available from the active state
var transition = this._findTransition(this._activeStateName);
if (transition) {
this.updateStateFromTransition(transition);
}
if (this._isTransitioning) {
this._currTransitionTime += dt;
if (this._currTransitionTime <= this._totalTransitionTime) {
var interpolatedTime = this._totalTransitionTime !== 0 ? this._currTransitionTime / this._totalTransitionTime : 1;
// while transitioning, set all previous state animations to be weighted by (1.0 - interpolationTime).
for(var i = 0; i < this._transitionPreviousStates.length; i++){
state = this._findState(this._transitionPreviousStates[i].name);
var stateWeight = this._transitionPreviousStates[i].weight;
for(var j = 0; j < state.animations.length; j++){
animation = state.animations[j];
clip = this._animEvaluator.findClip(animation.name + ".previous." + i);
if (clip) {
clip.blendWeight = (1.0 - interpolatedTime) * animation.normalizedWeight * stateWeight;
}
}
}
// while transitioning, set active state animations to be weighted by (interpolationTime).
state = this.activeState;
for(var i1 = 0; i1 < state.animations.length; i1++){
animation = state.animations[i1];
this._animEvaluator.findClip(animation.name).blendWeight = interpolatedTime * animation.normalizedWeight;
}
} else {
this._isTransitioning = false;
// when a transition ends, remove all previous state clips from the evaluator
var activeClips = this.activeStateAnimations.length;
var totalClips = this._animEvaluator.clips.length;
for(var i2 = 0; i2 < totalClips - activeClips; i2++){
this._animEvaluator.removeClip(0);
}
this._transitionPreviousStates = [];
// when a transition ends, set the active state clip weights so they sum to 1
state = this.activeState;
for(var i3 = 0; i3 < state.animations.length; i3++){
animation = state.animations[i3];
clip = this._animEvaluator.findClip(animation.name);
if (clip) {
clip.blendWeight = animation.normalizedWeight;
}
}
}
} else {
if (this.activeState._blendTree.constructor !== AnimNode) {
state = this.activeState;
for(var i4 = 0; i4 < state.animations.length; i4++){
animation = state.animations[i4];
clip = this._animEvaluator.findClip(animation.name);
if (clip) {
clip.blendWeight = animation.normalizedWeight;
if (animation.parent.syncAnimations) {
clip.speed = animation.speed;
}
}
}
}
}
this._animEvaluator.update(dt, this.activeState.hasAnimations);
}
/**
* Create a new AnimController.
*
* @param {AnimEvaluator} animEvaluator - The animation evaluator used to blend all current
* playing animation keyframes and update the entities properties based on the current
* animation values.
* @param {object[]} states - The list of states used to form the controller state graph.
* @param {object[]} transitions - The list of transitions used to form the controller state
* graph.
* @param {boolean} activate - Determines whether the anim controller should automatically play
* once all {@link AnimNodes} are assigned animations.
* @param {EventHandler} eventHandler - The event handler which should be notified with anim
* events.
* @param {Function} findParameter - Retrieves a parameter which is used to control the
* transition between states.
* @param {Function} consumeTrigger - Used to set triggers back to their default state after
* they have been consumed by a transition.
*/ constructor(animEvaluator, states, transitions, activate, eventHandler, findParameter, consumeTrigger){
/**
* @type {Object<string, AnimState>}
* @private
*/ this._states = {};
/**
* @type {string[]}
* @private
*/ this._stateNames = [];
/**
* @type {Object<string, AnimTransition[]>}
* @private
*/ this._findTransitionsFromStateCache = {};
/**
* @type {Object<string, AnimTransition[]>}
* @private
*/ this._findTransitionsBetweenStatesCache = {};
/**
* @type {string|null}
* @private
*/ this._previousStateName = null;
/** @private */ this._activeStateName = ANIM_STATE_START;
/** @private */ this._activeStateDuration = 0;
/** @private */ this._activeStateDurationDirty = true;
/** @private */ this._playing = false;
/** @private */ this._currTransitionTime = 1;
/** @private */ this._totalTransitionTime = 1;
/** @private */ this._isTransitioning = false;
/** @private */ this._transitionInterruptionSource = ANIM_INTERRUPTION_NONE;
/** @private */ this._transitionPreviousStates = [];
/** @private */ this._timeInState = 0;
/** @private */ this._timeInStateBefore = 0;
this.findParameter = (name)=>{
return this._findParameter(name);
};
this._animEvaluator = animEvaluator;
this._eventHandler = eventHandler;
this._findParameter = findParameter;
this._consumeTrigger = consumeTrigger;
for(var i = 0; i < states.length; i++){
this._states[states[i].name] = new AnimState(this, states[i].name, states[i].speed, states[i].loop, states[i].blendTree);
this._stateNames.push(states[i].name);
}
this._transitions = transitions.map((transition)=>{
return new AnimTransition(_extends({}, transition));
});
this._activate = activate;
}
}
export { AnimController };