playcanvas
Version:
PlayCanvas WebGL game engine
467 lines (464 loc) • 13.9 kB
JavaScript
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';
class AnimComponent extends Component {
set stateGraphAsset(value) {
if (value === null) {
this.removeStateGraph();
return;
}
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;
}
set normalizeWeights(value) {
this._normalizeWeights = value;
this.unbind();
}
get normalizeWeights() {
return this._normalizeWeights;
}
set animationAssets(value) {
this._animationAssets = value;
this.loadAnimationAssets();
}
get animationAssets() {
return this._animationAssets;
}
set speed(value) {
this._speed = value;
}
get speed() {
return this._speed;
}
set activate(value) {
this._activate = value;
}
get activate() {
return this._activate;
}
set playing(value) {
this._playing = value;
}
get playing() {
return this._playing;
}
set rootBone(value) {
if (typeof value === 'string') {
const entity = this.entity.root.findByGuid(value);
this._rootBone = entity;
} else if (value instanceof Entity) {
this._rootBone = value;
} else {
this._rootBone = null;
}
this.rebind();
}
get rootBone() {
return this._rootBone;
}
set stateGraph(value) {
this._stateGraph = value;
}
get stateGraph() {
return this._stateGraph;
}
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;
}
get playable() {
for(let i = 0; i < this._layers.length; i++){
if (!this._layers[i].playable) {
return false;
}
}
return true;
}
get baseLayer() {
if (this._layers.length > 0) {
return this._layers[0];
}
return null;
}
_onStateGraphAssetChangeEvent(asset) {
const prevAnimationAssets = this.animationAssets;
const prevMasks = this.layers.map((layer)=>layer.mask);
this.removeStateGraph();
this._stateGraph = new AnimStateGraph(asset._data);
this.loadStateGraph(this._stateGraph);
this.animationAssets = prevAnimationAssets;
this.loadAnimationAssets();
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];
}
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
};
}
}
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;
}
}
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);
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);
}
removeStateGraph() {
this._stateGraph = null;
this._stateGraphAsset = null;
this._animationAssets = {};
this._layers = [];
this._layerIndices = {};
this._parameters = {};
this._playing = false;
this.unbind();
this._targets = {};
}
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() {
this._targets = {};
for(let i = 0; i < this._layers.length; i++){
this._layers[i].rebind();
}
}
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);
}
}
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) {
return;
}
layer.assignAnimation(nodePath, animTrack, speed, loop);
}
removeNodeAnimations(nodeName, layerName) {
const layer = layerName ? this.findAnimationLayer(layerName) : this.baseLayer;
if (!layer) {
return;
}
layer.removeNodeAnimations(nodeName);
}
getParameterValue(name, type) {
const param = this._parameters[name];
if (param && param.type === type) {
return param.value;
}
return undefined;
}
setParameterValue(name, type, value) {
const param = this._parameters[name];
if (param && param.type === type) {
param.value = value;
return;
}
}
getFloat(name) {
return this.getParameterValue(name, ANIM_PARAMETER_FLOAT);
}
setFloat(name, value) {
this.setParameterValue(name, ANIM_PARAMETER_FLOAT, value);
}
getInteger(name) {
return this.getParameterValue(name, ANIM_PARAMETER_INTEGER);
}
setInteger(name, value) {
if (typeof value === 'number' && value % 1 === 0) {
this.setParameterValue(name, ANIM_PARAMETER_INTEGER, value);
}
}
getBoolean(name) {
return this.getParameterValue(name, ANIM_PARAMETER_BOOLEAN);
}
setBoolean(name, value) {
this.setParameterValue(name, ANIM_PARAMETER_BOOLEAN, !!value);
}
getTrigger(name) {
return this.getParameterValue(name, ANIM_PARAMETER_TRIGGER);
}
setTrigger(name, singleFrame = false) {
this.setParameterValue(name, ANIM_PARAMETER_TRIGGER, true);
if (singleFrame) {
this._consumedTriggers.add(name);
}
}
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), this._stateGraphAsset = null, this._animationAssets = {}, this._speed = 1, this._activate = true, this._playing = false, this._rootBone = null, this._stateGraph = null, this._layers = [], this._layerIndices = {}, this._parameters = {}, this._targets = {}, this._consumedTriggers = new Set(), this._normalizeWeights = false, this.findParameter = (name)=>{
return this._parameters[name];
}, this.consumeTrigger = (name)=>{
this._consumedTriggers.add(name);
};
}
}
export { AnimComponent };