com.ardor3d.extension.animation.skeletal.AnimationManager Maven / Gradle / Ivy
/**
* Copyright (c) 2008-2012 Ardor Labs, Inc.
*
* This file is part of Ardor3D.
*
* Ardor3D is free software: you can redistribute it and/or modify it
* under the terms of its license which may be found in the accompanying
* LICENSE file or at .
*/
package com.ardor3d.extension.animation.skeletal;
import java.util.List;
import java.util.Map;
import com.ardor3d.extension.animation.skeletal.clip.AnimationClip;
import com.ardor3d.extension.animation.skeletal.clip.AnimationClipInstance;
import com.ardor3d.extension.animation.skeletal.layer.AnimationLayer;
import com.ardor3d.extension.animation.skeletal.state.AbstractFiniteState;
import com.ardor3d.extension.animation.skeletal.util.LoggingMap;
import com.ardor3d.scenegraph.Spatial;
import com.ardor3d.util.ReadOnlyTimer;
import com.ardor3d.util.Timer;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
/**
*
* AnimationManager describes and maintains an animation system. It tracks one or more layered animation state machines
* (AnimationLayer) and uses their combined result to update one or more poses (via a set AnimationApplier.)
* AnimationClips used in these layers are instanced and tracked specifically for this manager.
*
*
* By default, an animation manager has a single base animation layer. Other layers may be added to this. It is
* important that the base layer (the layer at index 0) always has a full set of data to put a skeleton pose into a
* valid state.
*
*/
public class AnimationManager {
public enum AnimationUpdateState {
Play, Pause, Stop
}
/**
* A timer to use as our "global" time keeper. All animation sources under this manager will use this timer as their
* time reference.
*/
protected ReadOnlyTimer _globalTimer;
/** The pose(s) this manager manipulates on update. */
protected List _applyToPoses;
/** The root of a scenegraph we can look for transform animation targets under. */
protected final Spatial _sceneRoot;
/** Local instance information for any clips referenced by the layers/blend trees in this manager. */
protected final Map _clipInstances = new MapMaker().weakKeys().makeMap();
/** A logic object responsible for taking animation data and applying it to skeleton poses. */
protected AnimationApplier _applier;
/** Our animation layers. */
protected final List _layers = Lists.newArrayList();
/**
* A map of key->Double values, allowing control over elements under this manager without needing precise knowledge
* of the layout of those layers, blend trees, etc. Missing keys will return 0.0 and log a warning.
*/
protected final LoggingMap _valuesStore = new LoggingMap();
/**
* The throttle rate of animation. Default is 60fps (1/60.0). Set to 0 to disable throttling.
*/
protected double _updateRate = 1.0 / 60.0;
/**
* The global time we last processed an animation. (To use when checking our throttle.)
*/
protected double _lastUpdate = 0.0;
/**
* Sets the current animationState used to control if animation is playing, pausing or stopped.
*/
protected AnimationUpdateState _currentAnimationState = AnimationUpdateState.Play;
/**
* boolean flag to allow stop state to be updated one last time.
*/
protected boolean _canSetStopState = false;
/**
* boolean flag to reset Clips automatically once they are stopped.
*/
protected boolean _resetClipsOnStop = false;
/**
* Listeners for changes to this manager's AnimationUpdateState.
*/
protected final List _updateStateListeners = Lists.newArrayList();
/**
* Construct a new AnimationManager.
*
* @param globalTimer
* the timer to use for global time keeping.
* @param pose
* a pose to update. Optional if we won't be animating a {@link SkinnedMesh}.
*/
public AnimationManager(final ReadOnlyTimer globalTimer, final SkeletonPose pose) {
this(globalTimer, pose, null);
}
/**
* Construct a new AnimationManager.
*
* @param globalTimer
* the timer to use for global time keeping.
* @param pose
* a pose to update. Optional if we won't be animating a {@link SkinnedMesh}.
* @param sceneRoot
* a root we will use to search for spatials when doing transform animations.
*/
public AnimationManager(final ReadOnlyTimer globalTimer, final SkeletonPose pose, final Spatial sceneRoot) {
_globalTimer = globalTimer;
_sceneRoot = sceneRoot;
// add our base layer
final AnimationLayer layer = new AnimationLayer(AnimationLayer.BASE_LAYER_NAME);
layer.setManager(this);
_layers.add(layer);
if (pose != null) {
_applyToPoses = Lists.newArrayList(pose);
} else {
_applyToPoses = Lists.newArrayList();
}
_valuesStore.setLogOnReplace(false);
_valuesStore.setDefaultValue(0.0);
}
/**
* @return the "local time", in seconds reported by our global timer.
*/
public double getCurrentGlobalTime() {
return _globalTimer.getTimeInSeconds();
}
/**
* @return the timer used by this manager for global time keeping.
*/
public ReadOnlyTimer getGlobalTimer() {
return _globalTimer;
}
/**
* @param timer
* the timer to be used by this manager for global time keeping.
*/
public void setGlobalTimer(final Timer timer) {
_globalTimer = timer;
}
/**
*
* @return True if clips will reset if the currentUpdateState is Stop.
*/
public boolean isResetClipsOnStop() {
return _resetClipsOnStop;
}
/**
* @param resetClipsOnStop
* True if clips are to be reset when currentUpdateState is Stop, false otherwise.
*
*
*/
public void setResetClipsOnStop(final boolean resetClipsOnStop) {
_resetClipsOnStop = resetClipsOnStop;
}
public void play() {
setAnimationUpdateState(AnimationUpdateState.Play);
}
public void pause() {
setAnimationUpdateState(AnimationUpdateState.Pause);
}
public void stop() {
setAnimationUpdateState(AnimationUpdateState.Stop);
}
public boolean isPlaying() {
return _currentAnimationState == AnimationUpdateState.Play;
}
public boolean isPaused() {
return _currentAnimationState == AnimationUpdateState.Pause;
}
public boolean isStopped() {
return _currentAnimationState == AnimationUpdateState.Stop;
}
/**
* @param newAnimationState
* the new animation state in the animation Manager.
*/
public void setAnimationUpdateState(final AnimationUpdateState newAnimationState) {
if (newAnimationState == _currentAnimationState) {
// ignore if unchanged.
return;
}
final double currentTime = _globalTimer.getTimeInSeconds();
if (newAnimationState == AnimationUpdateState.Pause) {
if (_currentAnimationState == AnimationUpdateState.Stop) {
// ignore a non-allowed situation
return;
}
// Keep track of current time so we can resume active clips
_lastUpdate = currentTime;
} else if (newAnimationState == AnimationUpdateState.Play) {
// reset instances
if (_currentAnimationState == AnimationUpdateState.Pause) {
final double offset = currentTime - _lastUpdate;
for (final AnimationClipInstance instance : _clipInstances.values()) {
if (instance.isActive()) {
instance.setStartTime(instance.getStartTime() + offset);
}
}
} else {
// if newState is check if we will restart clips.
if (_resetClipsOnStop) {
for (final AnimationClipInstance instance : _clipInstances.values()) {
if (instance.isActive()) {
instance.setStartTime(currentTime);
}
}
}
}
} else {
for (final AnimationClipInstance instance : _clipInstances.values()) {
if (instance.isActive()) {
instance.setStartTime(currentTime);
}
}
}
final AnimationUpdateState oldState = _currentAnimationState;
_currentAnimationState = newAnimationState;
// Let listeners know we have changed state.
fireAnimationUpdateStateChange(oldState);
}
/**
* Notify any listeners of the state change
*
* @param oldState
* previous state
*/
protected void fireAnimationUpdateStateChange(final AnimationUpdateState oldState) {
for (final AnimationUpdateStateListener listener : _updateStateListeners) {
listener.stateChanged(oldState, _currentAnimationState);
}
}
/**
* Add an AnimationUpdateStateListener to this manager.
*
* @param listener
* the listener to add.
*/
public void addAnimationUpdateStateListener(final AnimationUpdateStateListener listener) {
_updateStateListeners.add(listener);
}
/**
* Remove an AnimationUpdateStateListener from this manager.
*
* @param listener
* the listener to remove.
* @return true if the listener was found
*/
public boolean removeAnimationUpdateStateListener(final AnimationUpdateStateListener listener) {
return _updateStateListeners.remove(listener);
}
/**
* Remove any AnimationUpdateStateListeners registered with this manager.
*/
public void clearAnimationUpdateStateListeners() {
_updateStateListeners.clear();
}
/**
* @return the currentAnimationState.
*/
public AnimationUpdateState getAnimationUpdateState() {
return _currentAnimationState;
}
/**
* @param pose
* a pose to add to be updated by this manager.
*/
public void addPose(final SkeletonPose pose) {
_applyToPoses.add(pose);
}
/**
* @param pose
* the pose to remove from this manager.
* @return true if the pose was found to be removed.
*/
public boolean removePose(final SkeletonPose pose) {
return _applyToPoses.remove(pose);
}
/**
* @param pose
* a pose to look for
* @return true if the pose was found in this manager.
*/
public boolean containsPose(final SkeletonPose pose) {
return _applyToPoses.contains(pose);
}
/**
* @return the number of poses managed by this manager.
*/
public int getPoseCount() {
return _applyToPoses.size();
}
/**
* @param index
* the index to pull the pose from.
* @return pose at the given index
*/
public SkeletonPose getSkeletonPose(final int index) {
return _applyToPoses.get(index);
}
/**
* @return the logic object responsible for taking animation data and applying it to skeleton poses.
*/
public AnimationApplier getApplier() {
return _applier;
}
/**
* @param applier
* a logic object to be responsible for taking animation data and applying it to skeleton poses.
*/
public void setApplier(final AnimationApplier applier) {
_applier = applier;
}
/**
* Move associated layers forward to the current global time and then apply the associated animation data to any
* SkeletonPoses set on the manager.
*/
public void update() {
if (_currentAnimationState != AnimationUpdateState.Play) {
if (_resetClipsOnStop) {
if (_currentAnimationState == AnimationUpdateState.Stop && !_canSetStopState) {
_canSetStopState = true;
} else {
// pause state or reset update has occurred
return;
}
} else {
// stop update without reseting
return;
}
} else {
_canSetStopState = false;
}
// grab current global time
final double globalTime = _globalTimer.getTimeInSeconds();
// check throttle
if (_updateRate != 0.0) {
if (globalTime - _lastUpdate < _updateRate) {
return;
}
// we subtract a bit to maintain our desired rate, even if there are some gc pauses, etc.
_lastUpdate = globalTime - (globalTime - _lastUpdate) % _updateRate;
}
// move the time forward on the layers
for (int i = 0; i < _layers.size(); ++i) {
final AnimationLayer layer = _layers.get(i);
final AbstractFiniteState state = layer.getCurrentState();
if (state != null) {
state.update(globalTime, layer);
}
}
// call apply on blend module, passing in pose
if (!_applyToPoses.isEmpty()) {
for (int i = 0; i < _applyToPoses.size(); ++i) {
final SkeletonPose pose = _applyToPoses.get(i);
_applier.applyTo(pose, this);
}
}
// apply for non-pose related assets
_applier.apply(_sceneRoot, this);
// post update to clear states
for (int i = 0; i < _layers.size(); ++i) {
final AnimationLayer layer = _layers.get(i);
final AbstractFiniteState state = layer.getCurrentState();
if (state != null) {
state.postUpdate(layer);
}
}
}
/**
* Retrieve and track an instance of an animation clip to be used with this manager.
*
* @param clip
* the clip to instance.
* @return our new clip instance.
*/
public AnimationClipInstance getClipInstance(final AnimationClip clip) {
AnimationClipInstance instance = _clipInstances.get(clip);
if (instance == null) {
instance = new AnimationClipInstance();
instance.setStartTime(_globalTimer.getTimeInSeconds());
_clipInstances.put(clip, instance);
}
return instance;
}
/**
* Retrieve an existing clip instance being tracked by this manager.
*
* @param clipName
* the name of the clip to find an existing instance of. Case sensitive.
* @return our existing clip instance, or null if we were not tracking a clip of the given name.
*/
public AnimationClipInstance findClipInstance(final String clipName) {
for (final AnimationClip clip : _clipInstances.keySet()) {
if (clipName.equals(clip.getName())) {
return _clipInstances.get(clip);
}
}
return null;
}
/**
* Retrieve an existing clip tracked by this manager.
*
* @param clipName
* the name of the clip to find. Case sensitive.
* @return our existing clip, or null if we were not tracking a clip of the given name.
*/
public AnimationClip findAnimationClip(final String clipName) {
for (final AnimationClip clip : _clipInstances.keySet()) {
if (clipName.equals(clip.getName())) {
return clip;
}
}
return null;
}
/**
* Rewind and reactivate the clip instance associated with the given clip.
*
* @param clip
* the clip to pull the instance for.
* @param globalStartTime
* the time to set the clip instance's start as.
*/
public void resetClipInstance(final AnimationClip clip, final double globalStartTime) {
final AnimationClipInstance instance = getClipInstance(clip);
if (instance != null) {
instance.setStartTime(globalStartTime);
instance.setActive(true);
}
}
/**
* @param index
* the index of the layer to retrieve.
* @return the animation layer at that index, or null of index is outside the bounds of our list of layers.
*/
public AnimationLayer getAnimationLayer(final int index) {
if (index < 0 || index >= _layers.size()) {
return null;
}
return _layers.get(index);
}
/**
* @param layerName
* the name of the layer to find.
* @return the first animation layer with a matching name, or null of none are found.
*/
public AnimationLayer findAnimationLayer(final String layerName) {
for (final AnimationLayer layer : _layers) {
if (layerName.equals(layer.getName())) {
return layer;
}
}
return null;
}
/**
* Add a new layer to our list of animation layers.
*
* @param layer
* the layer to add.
* @return the index of our added layer in our list of animation layers.
*/
public int addAnimationLayer(final AnimationLayer layer) {
_layers.add(layer);
layer.setManager(this);
return _layers.size() - 1;
}
/**
* Insert a given animation layer into our list of layers.
*
* @param layer
* the layer to insert.
* @param index
* the index to insert at. Moves any layers at that index over by one before inserting.
*/
public void insertAnimationLayer(final AnimationLayer layer, final int index) {
_layers.add(index, layer);
layer.setManager(this);
}
/**
* @param layer
* a layer to remove.
* @return true if the layer is found to remove.
*/
public boolean removeAnimationLayer(final AnimationLayer layer) {
return _layers.remove(layer);
}
/**
* @return our bottom most layer. This layer should always consist of a full skeletal pose data.
*/
public AnimationLayer getBaseAnimationLayer() {
return _layers.get(0);
}
/**
* @return the amount of time in seconds between frame rate updates. (throttle) default is 60fps (1.0/60.0).
*/
public double getUpdateRate() {
return _updateRate;
}
/**
* @param updateRate
* the new throttle rate. Default is 60fps (1.0/60.0). Set to 0 to disable throttling.
*/
public void setUpdateRate(final double updateRate) {
_updateRate = updateRate;
}
/**
* @return the current source data from the layers of this manager.
*/
public Map getCurrentSourceData() {
// set up our layer blending.
for (int i = 0; i < _layers.size() - 1; i++) {
final AnimationLayer layerA = _layers.get(i);
final AnimationLayer layerB = _layers.get(i + 1);
layerB.updateLayerBlending(layerA);
}
return _layers.get(_layers.size() - 1).getCurrentSourceData();
}
public LoggingMap getValuesStore() {
return _valuesStore;
}
/**
* @return the Map containing the AnimationClips and their respective AnimationClipInstances.
*/
public Map getClipInstancesStore() {
return _clipInstances;
}
}