AnimationState, javadoc, cleanup, tests.

#621
This commit is contained in:
NathanSweet 2016-08-20 15:47:13 +02:00
parent 0a8896a3e2
commit a39c970a6a
2 changed files with 139 additions and 71 deletions

View File

@ -122,7 +122,7 @@ public class AnimationStateTest {
expect(0, "end", 1, 1.1f) // expect(0, "end", 1, 1.1f) //
); );
state.setAnimation(0, "events1", false); state.setAnimation(0, "events1", false);
run(0.1f, 1000); run(0.1f, 1000, null);
setup("1/60 time step", // 2 setup("1/60 time step", // 2
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -133,7 +133,7 @@ public class AnimationStateTest {
expect(0, "end", 1, 1.017f) // expect(0, "end", 1, 1.017f) //
); );
state.setAnimation(0, "events1", false); state.setAnimation(0, "events1", false);
run(1 / 60f, 1000); run(1 / 60f, 1000, null);
setup("30 time step", // 3 setup("30 time step", // 3
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -144,7 +144,7 @@ public class AnimationStateTest {
expect(0, "end", 30, 60) // expect(0, "end", 30, 60) //
); );
state.setAnimation(0, "events1", false); state.setAnimation(0, "events1", false);
run(30, 1000); run(30, 1000, null);
setup("1 time step", // 4 setup("1 time step", // 4
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -155,7 +155,7 @@ public class AnimationStateTest {
expect(0, "end", 1, 2) // expect(0, "end", 1, 2) //
); );
state.setAnimation(0, "events1", false); state.setAnimation(0, "events1", false);
run(1, 1.01f); run(1, 1.01f, null);
setup("interrupt", // 5 setup("interrupt", // 5
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -188,7 +188,7 @@ public class AnimationStateTest {
state.setAnimation(0, "events1", false); state.setAnimation(0, "events1", false);
state.addAnimation(0, "events2", false, 0); state.addAnimation(0, "events2", false, 0);
state.addAnimation(0, "events1", false, 0); state.addAnimation(0, "events1", false, 0);
run(0.1f, 4f); run(0.1f, 4f, null);
setup("interrupt with delay", // 6 setup("interrupt with delay", // 6
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -208,7 +208,7 @@ public class AnimationStateTest {
); );
state.setAnimation(0, "events1", false); state.setAnimation(0, "events1", false);
state.addAnimation(0, "events2", false, 0.5f); state.addAnimation(0, "events2", false, 0.5f);
run(0.1f, 1000); run(0.1f, 1000, null);
setup("interrupt with delay and mix time", // 7 setup("interrupt with delay and mix time", // 7
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -232,7 +232,7 @@ public class AnimationStateTest {
stateData.setMix("events1", "events2", 0.7f); stateData.setMix("events1", "events2", 0.7f);
state.setAnimation(0, "events1", true); state.setAnimation(0, "events1", true);
state.addAnimation(0, "events2", false, 0.9f); state.addAnimation(0, "events2", false, 0.9f);
run(0.1f, 1000); run(0.1f, 1000, null);
setup("animation 0 events do not fire during mix", // 8 setup("animation 0 events do not fire during mix", // 8
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -255,7 +255,7 @@ public class AnimationStateTest {
stateData.setDefaultMix(0.7f); stateData.setDefaultMix(0.7f);
state.setAnimation(0, "events1", false); state.setAnimation(0, "events1", false);
state.addAnimation(0, "events2", false, 0.4f); state.addAnimation(0, "events2", false, 0.4f);
run(0.1f, 1000); run(0.1f, 1000, null);
setup("event threshold, some animation 0 events fire during mix", // 9 setup("event threshold, some animation 0 events fire during mix", // 9
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -279,7 +279,7 @@ public class AnimationStateTest {
stateData.setMix("events1", "events2", 0.7f); stateData.setMix("events1", "events2", 0.7f);
state.setAnimation(0, "events1", false).setEventThreshold(0.5f); state.setAnimation(0, "events1", false).setEventThreshold(0.5f);
state.addAnimation(0, "events2", false, 0.4f); state.addAnimation(0, "events2", false, 0.4f);
run(0.1f, 1000); run(0.1f, 1000, null);
setup("event threshold, all animation 0 events fire during mix", // 10 setup("event threshold, all animation 0 events fire during mix", // 10
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -306,7 +306,7 @@ public class AnimationStateTest {
); );
state.setAnimation(0, "events1", true).setEventThreshold(1); state.setAnimation(0, "events1", true).setEventThreshold(1);
state.addAnimation(0, "events2", false, 0.8f).setMixDuration(0.7f); state.addAnimation(0, "events2", false, 0.8f).setMixDuration(0.7f);
run(0.1f, 1000); run(0.1f, 1000, null);
setup("looping", // 11 setup("looping", // 11
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -329,7 +329,7 @@ public class AnimationStateTest {
expect(0, "event 0", 4, 4) // expect(0, "event 0", 4, 4) //
); );
state.setAnimation(0, "events1", true); state.setAnimation(0, "events1", true);
run(0.1f, 4); run(0.1f, 4, null);
setup("not looping, update past animation 0 duration", // 12 setup("not looping, update past animation 0 duration", // 12
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -351,7 +351,7 @@ public class AnimationStateTest {
); );
state.setAnimation(0, "events1", false); state.setAnimation(0, "events1", false);
state.addAnimation(0, "events2", false, 2); state.addAnimation(0, "events2", false, 2);
run(0.1f, 4f); run(0.1f, 4f, null);
setup("interrupt animation after first loop complete", // 13 setup("interrupt animation after first loop complete", // 13
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -392,7 +392,7 @@ public class AnimationStateTest {
expect(0, "end", 1, 1.1f) // expect(0, "end", 1, 1.1f) //
); );
state.addAnimation(0, "events1", false, 0); state.addAnimation(0, "events1", false, 0);
run(0.1f, 1.9f); run(0.1f, 1.9f, null);
setup("end time beyond non-looping animation duration", // 15 setup("end time beyond non-looping animation duration", // 15
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -403,7 +403,7 @@ public class AnimationStateTest {
expect(0, "end", 9f, 9.1f) // expect(0, "end", 9f, 9.1f) //
); );
state.setAnimation(0, "events1", false).setTrackEnd(9); state.setAnimation(0, "events1", false).setTrackEnd(9);
run(0.1f, 10); run(0.1f, 10, null);
setup("looping with animation start", // 16 setup("looping with animation start", // 16
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -417,7 +417,7 @@ public class AnimationStateTest {
entry = state.setAnimation(0, "events1", true); entry = state.setAnimation(0, "events1", true);
entry.setAnimationLast(0.6f); entry.setAnimationLast(0.6f);
entry.setAnimationStart(0.6f); entry.setAnimationStart(0.6f);
run(0.1f, 1.4f); run(0.1f, 1.4f, null);
setup("looping with animation start and end", // 17 setup("looping with animation start and end", // 17
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -431,7 +431,7 @@ public class AnimationStateTest {
entry.setAnimationStart(0.2f); entry.setAnimationStart(0.2f);
entry.setAnimationLast(0.2f); entry.setAnimationLast(0.2f);
entry.setAnimationEnd(0.8f); entry.setAnimationEnd(0.8f);
run(0.1f, 1.8f); run(0.1f, 1.8f, null);
setup("non-looping with animation start and end", // 18 setup("non-looping with animation start and end", // 18
expect(0, "start", 0, 0), // expect(0, "start", 0, 0), //
@ -443,7 +443,63 @@ public class AnimationStateTest {
entry.setAnimationStart(0.2f); entry.setAnimationStart(0.2f);
entry.setAnimationLast(0.2f); entry.setAnimationLast(0.2f);
entry.setAnimationEnd(0.8f); entry.setAnimationEnd(0.8f);
run(0.1f, 1.8f); run(0.1f, 1.8f, null);
setup("mix out looping with animation start and end", // 19
expect(0, "start", 0, 0), //
expect(0, "event 14", 0.3f, 0.3f), //
expect(0, "complete", 0.6f, 0.6f), //
expect(1, "start", 0.1f, 0.8f), //
expect(0, "interrupt", 0.8f, 0.8f), //
expect(1, "event 0", 0.1f, 0.8f), //
expect(0, "event 14", 0.9f, 0.9f), //
expect(0, "complete", 1.2f, 1.2f), //
expect(1, "event 14", 0.5f, 1.2f), //
expect(0, "end", 1.4f, 1.4f), //
expect(1, "event 30", 1, 1.7f), //
expect(1, "complete", 1, 1.7f), //
expect(1, "end", 1, 1.8f) //
);
entry = state.setAnimation(0, "events1", true);
entry.setAnimationStart(0.2f);
entry.setAnimationLast(0.2f);
entry.setAnimationEnd(0.8f);
entry.setEventThreshold(1);
state.addAnimation(0, "events2", false, 0.7f).setMixDuration(0.7f);
run(0.1f, 20, null);
setup("setAnimation with track entry mix", // 20
expect(0, "start", 0, 0), //
expect(0, "event 0", 0, 0), //
expect(0, "event 14", 0.5f, 0.5f), //
expect(1, "start", 0.1f, 1), //
expect(0, "interrupt", 1, 1), //
expect(0, "complete", 1, 1), //
expect(1, "event 0", 0.1f, 1), //
expect(1, "event 14", 0.5f, 1.4f), //
expect(0, "end", 1.6f, 1.6f), //
expect(1, "event 30", 1, 1.9f), //
expect(1, "complete", 1, 1.9f), //
expect(1, "end", 1, 2) //
);
state.setAnimation(0, "events1", true);
run(0.1f, 1000, new TestListener() {
public void frame (float time) {
if (MathUtils.isEqual(time, 1f)) state.setAnimation(0, "events2", false).setMixDuration(0.7f);
}
});
System.out.println("AnimationState tests passed."); System.out.println("AnimationState tests passed.");
} }
@ -460,10 +516,6 @@ public class AnimationStateTest {
log(String.format("%-3s%-12s%-7s%-7s%-7s", "#", "EVENT", "TRACK", "TOTAL", "RESULT")); log(String.format("%-3s%-12s%-7s%-7s%-7s", "#", "EVENT", "TRACK", "TOTAL", "RESULT"));
} }
void run (float incr, float endTime) {
run(incr, endTime, null);
}
void run (float incr, float endTime, TestListener listener) { void run (float incr, float endTime, TestListener listener) {
Skeleton skeleton = new Skeleton(skeletonData); Skeleton skeleton = new Skeleton(skeletonData);
state.apply(skeleton); state.apply(skeleton);

View File

@ -42,16 +42,16 @@ import com.esotericsoftware.spine.Animation.Timeline;
/** Stores state for applying one or more animations over time and automatically mixes (crossfades) when animations change. */ /** Stores state for applying one or more animations over time and automatically mixes (crossfades) when animations change. */
public class AnimationState { public class AnimationState {
private AnimationStateData data; private AnimationStateData data;
private Array<TrackEntry> tracks = new Array(); private final Array<TrackEntry> tracks = new Array();
private final Array<Event> events = new Array(); private final Array<Event> events = new Array();
final Array<AnimationStateListener> listeners = new Array(); private final Array<AnimationStateListener> listeners = new Array();
private float timeScale = 1; private final Pool<TrackEntry> trackEntryPool = new Pool() {
final Pool<TrackEntry> trackEntryPool = new Pool() {
protected Object newObject () { protected Object newObject () {
return new TrackEntry(); return new TrackEntry();
} }
}; };
private final EventQueue queue = new EventQueue(listeners, trackEntryPool); private final EventQueue queue = new EventQueue(listeners, trackEntryPool);
private float timeScale = 1;
/** Creates an uninitialized AnimationState. The animation state data must be set before use. */ /** Creates an uninitialized AnimationState. The animation state data must be set before use. */
public AnimationState () { public AnimationState () {
@ -62,7 +62,7 @@ public class AnimationState {
this.data = data; this.data = data;
} }
/** Updates the track entry times. */ /** Increments the track entry times, setting queued animations as current if needed. */
public void update (float delta) { public void update (float delta) {
delta *= timeScale; delta *= timeScale;
for (int i = 0; i < tracks.size; i++) { for (int i = 0; i < tracks.size; i++) {
@ -82,8 +82,8 @@ public class AnimationState {
if (next.mixingFrom != null) next.mixTime += currentDelta; if (next.mixingFrom != null) next.mixTime += currentDelta;
continue; continue;
} }
} else if (!current.loop && current.trackLast >= current.trackEnd) { } else if (current.trackLast >= current.trackEnd) {
// Clear a non-looping animation when it reaches its end time and there is no next entry. // Clear the track when the end time is reached and there is no next entry.
clearTrack(i); clearTrack(i);
continue; continue;
} }
@ -254,17 +254,21 @@ public class AnimationState {
return setAnimation(trackIndex, animation, loop); return setAnimation(trackIndex, animation, loop);
} }
/** Set the current animation. Any queued animations for the track are cleared. /** Sets the current animation for a track. If the track is empty, the new animation is made the current animation immediately.
* Otherwise, any queued animations are discarded and the new animation is queued to become the current animation the next time
* {@link #update(float)} is called.
* @return A track entry to allow further customization of animation playback. References to the track entry must not be kept * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
* after {@link AnimationStateListener#end(TrackEntry)}. */ * after {@link AnimationStateListener#end(TrackEntry)}. */
public TrackEntry setAnimation (int trackIndex, Animation animation, boolean loop) { public TrackEntry setAnimation (int trackIndex, Animation animation, boolean loop) {
if (animation == null) throw new IllegalArgumentException("animation cannot be null."); if (animation == null) throw new IllegalArgumentException("animation cannot be null.");
TrackEntry current = expandToIndex(trackIndex); TrackEntry current = expandToIndex(trackIndex);
if (current != null) freeAll(current.next);
TrackEntry entry = trackEntry(trackIndex, animation, loop, current); TrackEntry entry = trackEntry(trackIndex, animation, loop, current);
setCurrent(trackIndex, entry); if (current != null) {
freeAll(current.next);
current.next = entry;
entry.delay = current.trackLast;
} else
setCurrent(trackIndex, entry);
return entry; return entry;
} }
@ -275,7 +279,7 @@ public class AnimationState {
return addAnimation(trackIndex, animation, loop, delay); return addAnimation(trackIndex, animation, loop, delay);
} }
/** Adds an animation to be played after the current or last queued animation. /** Adds an animation to be played after the current or last queued animation for a track.
* @param delay Seconds to begin this animation after the start of the previous animation. May be <= 0 to use duration of the * @param delay Seconds to begin this animation after the start of the previous animation. May be <= 0 to use duration of the
* previous animation minus any mix duration plus the negative delay. * previous animation minus any mix duration plus the negative delay.
* @return A track entry to allow further customization of animation playback. References to the track entry must not be kept * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
@ -297,13 +301,12 @@ public class AnimationState {
float duration = last.animationEnd - last.animationStart; float duration = last.animationEnd - last.animationStart;
if (duration != 0) if (duration != 0)
delay += duration * (1 + (int)(last.trackTime / duration)) - data.getMix(last.animation, animation); delay += duration * (1 + (int)(last.trackTime / duration)) - data.getMix(last.animation, animation);
else
delay = 0;
} }
} else { entry.delay = delay;
} else
setCurrent(trackIndex, entry); setCurrent(trackIndex, entry);
if (delay <= 0) delay = 0;
}
entry.delay = delay;
return entry; return entry;
} }
@ -319,12 +322,12 @@ public class AnimationState {
entry.drawOrderThreshold = 0; entry.drawOrderThreshold = 0;
entry.delay = 0; entry.delay = 0;
entry.trackTime = 0;
entry.trackEnd = animation.getDuration();
entry.trackLast = -1;
entry.animationStart = 0; entry.animationStart = 0;
entry.animationEnd = entry.trackEnd; entry.animationEnd = animation.getDuration();
entry.animationLast = -1; entry.animationLast = -1;
entry.trackTime = 0;
entry.trackEnd = loop ? Integer.MAX_VALUE : entry.animationEnd;
entry.trackLast = -1;
entry.timeScale = 1; entry.timeScale = 1;
entry.alpha = 1; entry.alpha = 1;
@ -334,13 +337,13 @@ public class AnimationState {
return entry; return entry;
} }
/** @return May be null. */ /** The track entry for the animation currently playing on the track, or null. */
public TrackEntry getCurrent (int trackIndex) { public TrackEntry getCurrent (int trackIndex) {
if (trackIndex >= tracks.size) return null; if (trackIndex >= tracks.size) return null;
return tracks.get(trackIndex); return tracks.get(trackIndex);
} }
/** Adds a listener to receive events for all animations. */ /** Adds a listener to receive events for all track entries. */
public void addListener (AnimationStateListener listener) { public void addListener (AnimationStateListener listener) {
if (listener == null) throw new IllegalArgumentException("listener cannot be null."); if (listener == null) throw new IllegalArgumentException("listener cannot be null.");
listeners.add(listener); listeners.add(listener);
@ -355,14 +358,14 @@ public class AnimationState {
listeners.clear(); listeners.clear();
} }
/** Discards all listener notifications that have not yet been delivered. This can be useful to call from an /** Discards all {@link #addListener(AnimationStateListener) listener} notifications that have not yet been delivered. This can
* {@link AnimationStateListener} when it is known that further notifications that may have been already triggered are not * be useful to call from an {@link AnimationStateListener} when it is known that further notifications that may have been
* wanted, for example because new animations are being set. */ * already queued for delivery are not wanted because new animations are being set. */
public void clearListenerNotifications () { public void clearListenerNotifications () {
queue.clear(); queue.clear();
} }
/** Multiplier for the delta time when the animation state is updated, causing time for all animations to move slower or /** Multiplier for the delta time when the animation state is updated, causing time for all animations to play slower or
* faster. Defaults to 1. */ * faster. Defaults to 1. */
public float getTimeScale () { public float getTimeScale () {
return timeScale; return timeScale;
@ -417,15 +420,6 @@ public class AnimationState {
listener = null; listener = null;
} }
public float getAnimationTime () {
if (loop) {
float duration = animationEnd - animationStart;
if (duration == 0) return 0;
return (trackTime % duration) + animationStart;
}
return Math.min(trackTime + animationStart, animationEnd);
}
public int getTrackIndex () { public int getTrackIndex () {
return trackIndex; return trackIndex;
} }
@ -455,8 +449,8 @@ public class AnimationState {
this.delay = delay; this.delay = delay;
} }
/** Current time in seconds for this track entry. When changing the track time, it often makes sense to also change /** Current time in seconds this track entry has been the current track entry. The track time determines
* {@link #getAnimationLast()} to control when timelines will trigger. Defaults to 0. */ * {@link #getAnimationTime()} and can be set to start the animation at a time other than 0. */
public float getTrackTime () { public float getTrackTime () {
return trackTime; return trackTime;
} }
@ -465,6 +459,9 @@ public class AnimationState {
this.trackTime = trackTime; this.trackTime = trackTime;
} }
/** The track time in seconds when this animation will be removed from the track. If the track end time is reached and no
* other animations are queued for playback, the track is cleared. Defaults to the animation duration for non-looping
* animations and to {@link Integer#MAX_VALUE} for looping animations. */
public float getTrackEnd () { public float getTrackEnd () {
return trackEnd; return trackEnd;
} }
@ -473,7 +470,10 @@ public class AnimationState {
this.trackEnd = trackEnd; this.trackEnd = trackEnd;
} }
/** Seconds when this animation starts, both initially and after looping. Defaults to 0. */ /** Seconds when this animation starts, both initially and after looping. Defaults to 0.
* <p>
* When changing the animation start time, it often makes sense to also change {@link #getAnimationLast()} to control when
* timelines will trigger. */
public float getAnimationStart () { public float getAnimationStart () {
return animationStart; return animationStart;
} }
@ -482,8 +482,8 @@ public class AnimationState {
this.animationStart = animationStart; this.animationStart = animationStart;
} }
/** Seconds when this animation ends, causing the next animation to start or this animation to loop. Defaults to the /** Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animation will
* animation duration. */ * loop back to {@link #getAnimationStart()} at this time. Defaults to the animation duration. */
public float getAnimationEnd () { public float getAnimationEnd () {
return animationEnd; return animationEnd;
} }
@ -503,7 +503,19 @@ public class AnimationState {
this.animationLast = animationLast; this.animationLast = animationLast;
} }
/** Multiplier for the delta time when the animation state is updated, causing time for this animation to move slower or /** Uses the {@link #getTrackTime() track time} to compute the animation time between the {@link #getAnimationStart()
* animation start} and {@link #getAnimationEnd() animation end}. When the track time is 0, the animation time is equal to
* the animation start time. */
public float getAnimationTime () {
if (loop) {
float duration = animationEnd - animationStart;
if (duration == 0) return animationStart;
return (trackTime % duration) + animationStart;
}
return Math.min(trackTime + animationStart, animationEnd);
}
/** Multiplier for the delta time when the animation state is updated, causing time for this animation to play slower or
* faster. Defaults to 1. */ * faster. Defaults to 1. */
public float getTimeScale () { public float getTimeScale () {
return timeScale; return timeScale;
@ -513,7 +525,7 @@ public class AnimationState {
this.timeScale = timeScale; this.timeScale = timeScale;
} }
/** The listener for events generated solely from this track entry, or null. */ /** The listener for events generated by this track entry, or null. */
public AnimationStateListener getListener () { public AnimationStateListener getListener () {
return listener; return listener;
} }
@ -524,8 +536,10 @@ public class AnimationState {
} }
/** Values < 1 mix this animation with the skeleton pose. Defaults to 1, which overwrites the skeleton pose with this /** Values < 1 mix this animation with the skeleton pose. Defaults to 1, which overwrites the skeleton pose with this
* animation. Typically track 0 is used to completely pose the skeleton, then alpha can be used on higher tracks. Generally * animation.
* it doesn't make sense to use alpha on track 0, since the skeleton pose is probably from the last frame render. */ * <p>
* Typically track 0 is used to completely pose the skeleton, then alpha can be used on higher tracks. Generally it doesn't
* make sense to use alpha on track 0, since the skeleton pose is probably from the last frame render. */
public float getAlpha () { public float getAlpha () {
return alpha; return alpha;
} }
@ -576,12 +590,12 @@ public class AnimationState {
this.next = next; this.next = next;
} }
/** Returns true if at least one loop has been completed (ie time >= end time). */ /** Returns true if at least one loop has been completed. */
public boolean isComplete () { public boolean isComplete () {
return trackTime >= trackEnd; return trackTime >= animationEnd - animationStart;
} }
/** Seconds from zero to the mix duration when mixing from the previous animation to this animation. */ /** Seconds from 0 to the mix duration when mixing from the previous animation to this animation. */
public float getMixTime () { public float getMixTime () {
return mixTime; return mixTime;
} }
@ -591,7 +605,9 @@ public class AnimationState {
} }
/** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by /** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by
* {@link AnimationStateData} based on the animation before this animation (if any). */ * {@link AnimationStateData} based on the animation before this animation (if any).
* <p>
* The mix duration must be set before this track entry becomes the current track entry. */
public float getMixDuration () { public float getMixDuration () {
return mixDuration; return mixDuration;
} }