UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

686 lines (683 loc) 26.4 kB
import { Debug } from '../../../core/debug.js'; import { Asset } from '../../asset/asset.js'; import { AnimEvaluator } from '../../anim/evaluator/anim-evaluator.js'; import { AnimController } from '../../anim/controller/anim-controller.js'; import { Component } from '../component.js'; import { AnimComponentBinder } from './component-binder.js'; import { AnimComponentLayer } from './component-layer.js'; import { AnimStateGraph } from '../../anim/state-graph/anim-state-graph.js'; import { Entity } from '../../entity.js'; import { ANIM_CONTROL_STATES, ANIM_PARAMETER_FLOAT, ANIM_PARAMETER_INTEGER, ANIM_PARAMETER_BOOLEAN, ANIM_PARAMETER_TRIGGER } from '../../anim/controller/constants.js'; import { AnimTrack } from '../../anim/evaluator/anim-track.js'; /** * The AnimComponent allows an {@link Entity} to playback animations on models and entity * properties. * * @hideconstructor * @category Animation */ class AnimComponent extends Component { set stateGraphAsset(value) { if (value === null) { this.removeStateGraph(); return; } // remove event from previous asset if (this._stateGraphAsset) { const stateGraphAsset = this.system.app.assets.get(this._stateGraphAsset); stateGraphAsset.off('change', this._onStateGraphAssetChangeEvent, this); } let _id; let _asset; if (value instanceof Asset) { _id = value.id; _asset = this.system.app.assets.get(_id); if (!_asset) { this.system.app.assets.add(value); _asset = this.system.app.assets.get(_id); } } else { _id = value; _asset = this.system.app.assets.get(_id); } if (!_asset || this._stateGraphAsset === _id) { return; } if (_asset.resource) { this._stateGraph = _asset.resource; this.loadStateGraph(this._stateGraph); _asset.on('change', this._onStateGraphAssetChangeEvent, this); } else { _asset.once('load', (asset)=>{ this._stateGraph = asset.resource; this.loadStateGraph(this._stateGraph); }); _asset.on('change', this._onStateGraphAssetChangeEvent, this); this.system.app.assets.load(_asset); } this._stateGraphAsset = _id; } get stateGraphAsset() { return this._stateGraphAsset; } /** * Sets whether the animation component will normalize the weights of its layers by their sum total. * * @type {boolean} */ set normalizeWeights(value) { this._normalizeWeights = value; this.unbind(); } /** * Gets whether the animation component will normalize the weights of its layers by their sum total. * * @type {boolean} */ get normalizeWeights() { return this._normalizeWeights; } set animationAssets(value) { this._animationAssets = value; this.loadAnimationAssets(); } get animationAssets() { return this._animationAssets; } /** * Sets the speed multiplier for animation play back speed. 1.0 is playback at normal speed, 0.0 pauses * the animation. * * @type {number} */ set speed(value) { this._speed = value; } /** * Gets the speed multiplier for animation play back speed. * * @type {number} */ get speed() { return this._speed; } /** * Sets whether the first animation will begin playing when the scene is loaded. * * @type {boolean} */ set activate(value) { this._activate = value; } /** * Gets whether the first animation will begin playing when the scene is loaded. * * @type {boolean} */ get activate() { return this._activate; } /** * Sets whether to play or pause all animations in the component. * * @type {boolean} */ set playing(value) { this._playing = value; } /** * Gets whether to play or pause all animations in the component. * * @type {boolean} */ get playing() { return this._playing; } /** * Sets the entity that this anim component should use as the root of the animation hierarchy. * * @type {Entity} */ set rootBone(value) { if (typeof value === 'string') { const entity = this.entity.root.findByGuid(value); Debug.assert(entity, `rootBone entity for supplied guid:${value} cannot be found in the scene`); this._rootBone = entity; } else if (value instanceof Entity) { this._rootBone = value; } else { this._rootBone = null; } this.rebind(); } /** * Gets the entity that this anim component should use as the root of the animation hierarchy. * * @type {Entity} */ get rootBone() { return this._rootBone; } set stateGraph(value) { this._stateGraph = value; } get stateGraph() { return this._stateGraph; } /** * Returns the animation layers available in this anim component. * * @type {AnimComponentLayer[]} */ get layers() { return this._layers; } set layerIndices(value) { this._layerIndices = value; } get layerIndices() { return this._layerIndices; } set parameters(value) { this._parameters = value; } get parameters() { return this._parameters; } set targets(value) { this._targets = value; } get targets() { return this._targets; } /** * Returns whether all component layers are currently playable. * * @type {boolean} */ get playable() { for(let i = 0; i < this._layers.length; i++){ if (!this._layers[i].playable) { return false; } } return true; } /** * Returns the base layer of the state graph. * * @type {AnimComponentLayer|null} */ get baseLayer() { if (this._layers.length > 0) { return this._layers[0]; } return null; } _onStateGraphAssetChangeEvent(asset) { // both animationAssets and layer masks should be maintained when switching AnimStateGraph assets const prevAnimationAssets = this.animationAssets; const prevMasks = this.layers.map((layer)=>layer.mask); // clear the previous state graph this.removeStateGraph(); // load the new state graph this._stateGraph = new AnimStateGraph(asset._data); this.loadStateGraph(this._stateGraph); // assign the previous animation assets this.animationAssets = prevAnimationAssets; this.loadAnimationAssets(); // assign the previous layer masks then rebind all anim targets this.layers.forEach((layer, i)=>{ layer.mask = prevMasks[i]; }); this.rebind(); } dirtifyTargets() { const targets = Object.values(this._targets); for(let i = 0; i < targets.length; i++){ targets[i].dirty = true; } } _addLayer({ name, states, transitions, weight, mask, blendType }) { let graph; if (this.rootBone) { graph = this.rootBone; } else { graph = this.entity; } const layerIndex = this._layers.length; const animBinder = new AnimComponentBinder(this, graph, name, mask, layerIndex); const animEvaluator = new AnimEvaluator(animBinder); const controller = new AnimController(animEvaluator, states, transitions, this._activate, this, this.findParameter, this.consumeTrigger); this._layers.push(new AnimComponentLayer(name, controller, this, weight, blendType)); this._layerIndices[name] = layerIndex; return this._layers[layerIndex]; } /** * Adds a new anim component layer to the anim component. * * @param {string} name - The name of the layer to create. * @param {number} [weight] - The blending weight of the layer. Defaults to 1. * @param {object[]} [mask] - A list of paths to bones in the model which should be animated in * this layer. If omitted the full model is used. Defaults to null. * @param {string} [blendType] - Defines how properties animated by this layer blend with * animations of those properties in previous layers. Defaults to pc.ANIM_LAYER_OVERWRITE. * @returns {AnimComponentLayer} The created anim component layer. */ addLayer(name, weight, mask, blendType) { const layer = this.findAnimationLayer(name); if (layer) return layer; const states = [ { 'name': 'START', 'speed': 1 } ]; const transitions = []; return this._addLayer({ name, states, transitions, weight, mask, blendType }); } _assignParameters(stateGraph) { this._parameters = {}; const paramKeys = Object.keys(stateGraph.parameters); for(let i = 0; i < paramKeys.length; i++){ const paramKey = paramKeys[i]; this._parameters[paramKey] = { type: stateGraph.parameters[paramKey].type, value: stateGraph.parameters[paramKey].value }; } } /** * Initializes component animation controllers using the provided state graph. * * @param {object} stateGraph - The state graph asset to load into the component. Contains the * states, transitions and parameters used to define a complete animation controller. * @example * entity.anim.loadStateGraph({ * "layers": [ * { * "name": layerName, * "states": [ * { * "name": "START", * "speed": 1 * }, * { * "name": "Initial State", * "speed": speed, * "loop": loop, * "defaultState": true * } * ], * "transitions": [ * { * "from": "START", * "to": "Initial State" * } * ] * } * ], * "parameters": {} * }); */ loadStateGraph(stateGraph) { this._stateGraph = stateGraph; this._assignParameters(stateGraph); this._layers = []; let containsBlendTree = false; for(let i = 0; i < stateGraph.layers.length; i++){ const layer = stateGraph.layers[i]; this._addLayer({ ...layer }); if (layer.states.some((state)=>state.blendTree)) { containsBlendTree = true; } } // blend trees do not support the automatic assignment of animation assets if (!containsBlendTree) { this.setupAnimationAssets(); } } setupAnimationAssets() { for(let i = 0; i < this._layers.length; i++){ const layer = this._layers[i]; const layerName = layer.name; for(let j = 0; j < layer.states.length; j++){ const stateName = layer.states[j]; if (ANIM_CONTROL_STATES.indexOf(stateName) === -1) { const stateKey = `${layerName}:${stateName}`; if (!this._animationAssets[stateKey]) { this._animationAssets[stateKey] = { asset: null }; } } } } this.loadAnimationAssets(); } loadAnimationAssets() { for(let i = 0; i < this._layers.length; i++){ const layer = this._layers[i]; for(let j = 0; j < layer.states.length; j++){ const stateName = layer.states[j]; if (ANIM_CONTROL_STATES.indexOf(stateName) !== -1) continue; const animationAsset = this._animationAssets[`${layer.name}:${stateName}`]; if (!animationAsset || !animationAsset.asset) { this.findAnimationLayer(layer.name).assignAnimation(stateName, AnimTrack.EMPTY); continue; } const assetId = animationAsset.asset; const asset = this.system.app.assets.get(assetId); // check whether assigned animation asset still exists if (asset) { if (asset.resource) { this.onAnimationAssetLoaded(layer.name, stateName, asset); } else { asset.once('load', (function(layerName, stateName) { return (function(asset) { this.onAnimationAssetLoaded(layerName, stateName, asset); }).bind(this); }).bind(this)(layer.name, stateName)); this.system.app.assets.load(asset); } } } } } onAnimationAssetLoaded(layerName, stateName, asset) { this.findAnimationLayer(layerName).assignAnimation(stateName, asset.resource); } /** * Removes all layers from the anim component. */ removeStateGraph() { this._stateGraph = null; this._stateGraphAsset = null; this._animationAssets = {}; this._layers = []; this._layerIndices = {}; this._parameters = {}; this._playing = false; this.unbind(); // clear all targets from previous binding this._targets = {}; } /** * Reset all of the components layers and parameters to their initial states. If a layer was * playing before it will continue playing. */ reset() { this._assignParameters(this._stateGraph); for(let i = 0; i < this._layers.length; i++){ const layerPlaying = this._layers[i].playing; this._layers[i].reset(); this._layers[i].playing = layerPlaying; } } unbind() { if (!this._normalizeWeights) { Object.keys(this._targets).forEach((targetKey)=>{ this._targets[targetKey].unbind(); }); } } /** * Rebind all of the components layers. */ rebind() { // clear all targets from previous binding this._targets = {}; // rebind all layers for(let i = 0; i < this._layers.length; i++){ this._layers[i].rebind(); } } /** * Finds an {@link AnimComponentLayer} in this component. * * @param {string} name - The name of the anim component layer to find. * @returns {AnimComponentLayer} Layer. */ findAnimationLayer(name) { const layerIndex = this._layerIndices[name]; return this._layers[layerIndex] || null; } addAnimationState(nodeName, animTrack, speed = 1, loop = true, layerName = 'Base') { if (!this._stateGraph) { this.loadStateGraph(new AnimStateGraph({ 'layers': [ { 'name': layerName, 'states': [ { 'name': 'START', 'speed': 1 }, { 'name': nodeName, 'speed': speed, 'loop': loop, 'defaultState': true } ], 'transitions': [ { 'from': 'START', 'to': nodeName } ] } ], 'parameters': {} })); } const layer = this.findAnimationLayer(layerName); if (layer) { layer.assignAnimation(nodeName, animTrack, speed, loop); } else { this.addLayer(layerName)?.assignAnimation(nodeName, animTrack, speed, loop); } } /** * Associates an animation with a state or blend tree node in the loaded state graph. If all * states are linked and the {@link activate} value was set to true then the component will * begin playing. If no state graph is loaded, a default state graph will be created with a * single state based on the provided nodePath parameter. * * @param {string} nodePath - Either the state name or the path to a blend tree node that this * animation should be associated with. Each section of a blend tree path is split using a * period (`.`) therefore state names should not include this character (e.g "MyStateName" or * "MyStateName.BlendTreeNode"). * @param {AnimTrack} animTrack - The animation track that will be assigned to this state and * played whenever this state is active. * @param {string} [layerName] - The name of the anim component layer to update. If omitted the * default layer is used. If no state graph has been previously loaded this parameter is * ignored. * @param {number} [speed] - Update the speed of the state you are assigning an animation to. * Defaults to 1. * @param {boolean} [loop] - Update the loop property of the state you are assigning an * animation to. Defaults to true. */ assignAnimation(nodePath, animTrack, layerName, speed = 1, loop = true) { if (!this._stateGraph && nodePath.indexOf('.') === -1) { this.loadStateGraph(new AnimStateGraph({ 'layers': [ { 'name': 'Base', 'states': [ { 'name': 'START', 'speed': 1 }, { 'name': nodePath, 'speed': speed, 'loop': loop, 'defaultState': true } ], 'transitions': [ { 'from': 'START', 'to': nodePath } ] } ], 'parameters': {} })); this.baseLayer.assignAnimation(nodePath, animTrack); return; } const layer = layerName ? this.findAnimationLayer(layerName) : this.baseLayer; if (!layer) { Debug.error('assignAnimation: Trying to assign an anim track to a layer that doesn\'t exist'); return; } layer.assignAnimation(nodePath, animTrack, speed, loop); } /** * Removes animations from a node in the loaded state graph. * * @param {string} nodeName - The name of the node that should have its animation tracks removed. * @param {string} [layerName] - The name of the anim component layer to update. If omitted the * default layer is used. */ removeNodeAnimations(nodeName, layerName) { const layer = layerName ? this.findAnimationLayer(layerName) : this.baseLayer; if (!layer) { Debug.error('removeStateAnimations: Trying to remove animation tracks from a state before the state graph has been loaded. Have you called loadStateGraph?'); return; } layer.removeNodeAnimations(nodeName); } getParameterValue(name, type) { const param = this._parameters[name]; if (param && param.type === type) { return param.value; } Debug.log(`Cannot get parameter value. No parameter found in anim controller named "${name}" of type "${type}"`); return undefined; } setParameterValue(name, type, value) { const param = this._parameters[name]; if (param && param.type === type) { param.value = value; return; } Debug.log(`Cannot set parameter value. No parameter found in anim controller named "${name}" of type "${type}"`); } /** * Returns a float parameter value by name. * * @param {string} name - The name of the float to return the value of. * @returns {number} A float. */ getFloat(name) { return this.getParameterValue(name, ANIM_PARAMETER_FLOAT); } /** * Sets the value of a float parameter that was defined in the animation components state graph. * * @param {string} name - The name of the parameter to set. * @param {number} value - The new float value to set this parameter to. */ setFloat(name, value) { this.setParameterValue(name, ANIM_PARAMETER_FLOAT, value); } /** * Returns an integer parameter value by name. * * @param {string} name - The name of the integer to return the value of. * @returns {number} An integer. */ getInteger(name) { return this.getParameterValue(name, ANIM_PARAMETER_INTEGER); } /** * Sets the value of an integer parameter that was defined in the animation components state * graph. * * @param {string} name - The name of the parameter to set. * @param {number} value - The new integer value to set this parameter to. */ setInteger(name, value) { if (typeof value === 'number' && value % 1 === 0) { this.setParameterValue(name, ANIM_PARAMETER_INTEGER, value); } else { Debug.error('Attempting to assign non integer value to integer parameter', name, value); } } /** * Returns a boolean parameter value by name. * * @param {string} name - The name of the boolean to return the value of. * @returns {boolean} A boolean. */ getBoolean(name) { return this.getParameterValue(name, ANIM_PARAMETER_BOOLEAN); } /** * Sets the value of a boolean parameter that was defined in the animation components state * graph. * * @param {string} name - The name of the parameter to set. * @param {boolean} value - The new boolean value to set this parameter to. */ setBoolean(name, value) { this.setParameterValue(name, ANIM_PARAMETER_BOOLEAN, !!value); } /** * Returns a trigger parameter value by name. * * @param {string} name - The name of the trigger to return the value of. * @returns {boolean} A boolean. */ getTrigger(name) { return this.getParameterValue(name, ANIM_PARAMETER_TRIGGER); } /** * Sets the value of a trigger parameter that was defined in the animation components state * graph to true. * * @param {string} name - The name of the parameter to set. * @param {boolean} [singleFrame] - If true, this trigger will be set back to false at the end * of the animation update. Defaults to false. */ setTrigger(name, singleFrame = false) { this.setParameterValue(name, ANIM_PARAMETER_TRIGGER, true); if (singleFrame) { this._consumedTriggers.add(name); } } /** * Resets the value of a trigger parameter that was defined in the animation components state * graph to false. * * @param {string} name - The name of the parameter to set. */ resetTrigger(name) { this.setParameterValue(name, ANIM_PARAMETER_TRIGGER, false); } onBeforeRemove() { if (Number.isFinite(this._stateGraphAsset)) { const stateGraphAsset = this.system.app.assets.get(this._stateGraphAsset); stateGraphAsset.off('change', this._onStateGraphAssetChangeEvent, this); } } update(dt) { for(let i = 0; i < this.layers.length; i++){ this.layers[i].update(dt * this.speed); } this._consumedTriggers.forEach((trigger)=>{ this.parameters[trigger].value = false; }); this._consumedTriggers.clear(); } resolveDuplicatedEntityReferenceProperties(oldAnim, duplicatedIdsMap) { if (oldAnim.rootBone && duplicatedIdsMap[oldAnim.rootBone.getGuid()]) { this.rootBone = duplicatedIdsMap[oldAnim.rootBone.getGuid()]; } else { this.rebind(); } } constructor(...args){ super(...args), /** @private */ this._stateGraphAsset = null, /** @private */ this._animationAssets = {}, /** @private */ this._speed = 1, /** @private */ this._activate = true, /** @private */ this._playing = false, /** @private */ this._rootBone = null, /** @private */ this._stateGraph = null, /** @private */ this._layers = [], /** @private */ this._layerIndices = {}, /** @private */ this._parameters = {}, /** @private */ this._targets = {}, /** @private */ this._consumedTriggers = new Set(), /** @private */ this._normalizeWeights = false, /** * Returns the parameter object for the specified parameter name. This function is anonymous so that it can be passed to the AnimController * while still being called in the scope of the AnimComponent. * * @param {string} name - The name of the parameter to return the value of. * @returns {object} The parameter object. * @private */ this.findParameter = (name)=>{ return this._parameters[name]; }, /** * Sets a trigger parameter as having been used by a transition. This function is anonymous so that it can be passed to the AnimController * while still being called in the scope of the AnimComponent. * * @param {string} name - The name of the trigger to set as consumed. * @private */ this.consumeTrigger = (name)=>{ this._consumedTriggers.add(name); }; } } export { AnimComponent };