diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTest.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTest.java index 84583682d..8fe354667 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTest.java +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTest.java @@ -122,7 +122,7 @@ public class AnimationStateTest { expect(0, "end", 1, 1.1f) // ); state.setAnimation(0, "events1", false); - run(0.1f, 1000); + run(0.1f, 1000, null); setup("1/60 time step", // 2 expect(0, "start", 0, 0), // @@ -133,7 +133,7 @@ public class AnimationStateTest { expect(0, "end", 1, 1.017f) // ); state.setAnimation(0, "events1", false); - run(1 / 60f, 1000); + run(1 / 60f, 1000, null); setup("30 time step", // 3 expect(0, "start", 0, 0), // @@ -144,7 +144,7 @@ public class AnimationStateTest { expect(0, "end", 30, 60) // ); state.setAnimation(0, "events1", false); - run(30, 1000); + run(30, 1000, null); setup("1 time step", // 4 expect(0, "start", 0, 0), // @@ -155,7 +155,7 @@ public class AnimationStateTest { expect(0, "end", 1, 2) // ); state.setAnimation(0, "events1", false); - run(1, 1.01f); + run(1, 1.01f, null); setup("interrupt", // 5 expect(0, "start", 0, 0), // @@ -188,7 +188,7 @@ public class AnimationStateTest { state.setAnimation(0, "events1", false); state.addAnimation(0, "events2", false, 0); state.addAnimation(0, "events1", false, 0); - run(0.1f, 4f); + run(0.1f, 4f, null); setup("interrupt with delay", // 6 expect(0, "start", 0, 0), // @@ -208,7 +208,7 @@ public class AnimationStateTest { ); state.setAnimation(0, "events1", false); state.addAnimation(0, "events2", false, 0.5f); - run(0.1f, 1000); + run(0.1f, 1000, null); setup("interrupt with delay and mix time", // 7 expect(0, "start", 0, 0), // @@ -232,7 +232,7 @@ public class AnimationStateTest { stateData.setMix("events1", "events2", 0.7f); state.setAnimation(0, "events1", true); 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 expect(0, "start", 0, 0), // @@ -255,7 +255,7 @@ public class AnimationStateTest { stateData.setDefaultMix(0.7f); state.setAnimation(0, "events1", false); 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 expect(0, "start", 0, 0), // @@ -279,7 +279,7 @@ public class AnimationStateTest { stateData.setMix("events1", "events2", 0.7f); state.setAnimation(0, "events1", false).setEventThreshold(0.5f); 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 expect(0, "start", 0, 0), // @@ -306,7 +306,7 @@ public class AnimationStateTest { ); state.setAnimation(0, "events1", true).setEventThreshold(1); state.addAnimation(0, "events2", false, 0.8f).setMixDuration(0.7f); - run(0.1f, 1000); + run(0.1f, 1000, null); setup("looping", // 11 expect(0, "start", 0, 0), // @@ -329,7 +329,7 @@ public class AnimationStateTest { expect(0, "event 0", 4, 4) // ); state.setAnimation(0, "events1", true); - run(0.1f, 4); + run(0.1f, 4, null); setup("not looping, update past animation 0 duration", // 12 expect(0, "start", 0, 0), // @@ -351,7 +351,7 @@ public class AnimationStateTest { ); state.setAnimation(0, "events1", false); state.addAnimation(0, "events2", false, 2); - run(0.1f, 4f); + run(0.1f, 4f, null); setup("interrupt animation after first loop complete", // 13 expect(0, "start", 0, 0), // @@ -392,7 +392,7 @@ public class AnimationStateTest { expect(0, "end", 1, 1.1f) // ); 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 expect(0, "start", 0, 0), // @@ -403,7 +403,7 @@ public class AnimationStateTest { expect(0, "end", 9f, 9.1f) // ); state.setAnimation(0, "events1", false).setTrackEnd(9); - run(0.1f, 10); + run(0.1f, 10, null); setup("looping with animation start", // 16 expect(0, "start", 0, 0), // @@ -417,7 +417,7 @@ public class AnimationStateTest { entry = state.setAnimation(0, "events1", true); entry.setAnimationLast(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 expect(0, "start", 0, 0), // @@ -431,7 +431,7 @@ public class AnimationStateTest { entry.setAnimationStart(0.2f); entry.setAnimationLast(0.2f); entry.setAnimationEnd(0.8f); - run(0.1f, 1.8f); + run(0.1f, 1.8f, null); setup("non-looping with animation start and end", // 18 expect(0, "start", 0, 0), // @@ -443,7 +443,63 @@ public class AnimationStateTest { entry.setAnimationStart(0.2f); entry.setAnimationLast(0.2f); 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."); } @@ -460,10 +516,6 @@ public class AnimationStateTest { 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) { Skeleton skeleton = new Skeleton(skeletonData); state.apply(skeleton); diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java index 115afa53e..c7a9bf07b 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java @@ -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. */ public class AnimationState { private AnimationStateData data; - private Array tracks = new Array(); + private final Array tracks = new Array(); private final Array events = new Array(); - final Array listeners = new Array(); - private float timeScale = 1; - final Pool trackEntryPool = new Pool() { + private final Array listeners = new Array(); + private final Pool trackEntryPool = new Pool() { protected Object newObject () { return new TrackEntry(); } }; 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. */ public AnimationState () { @@ -62,7 +62,7 @@ public class AnimationState { 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) { delta *= timeScale; for (int i = 0; i < tracks.size; i++) { @@ -82,8 +82,8 @@ public class AnimationState { if (next.mixingFrom != null) next.mixTime += currentDelta; continue; } - } else if (!current.loop && current.trackLast >= current.trackEnd) { - // Clear a non-looping animation when it reaches its end time and there is no next entry. + } else if (current.trackLast >= current.trackEnd) { + // Clear the track when the end time is reached and there is no next entry. clearTrack(i); continue; } @@ -254,17 +254,21 @@ public class AnimationState { 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 * after {@link AnimationStateListener#end(TrackEntry)}. */ public TrackEntry setAnimation (int trackIndex, Animation animation, boolean loop) { if (animation == null) throw new IllegalArgumentException("animation cannot be null."); - TrackEntry current = expandToIndex(trackIndex); - if (current != null) freeAll(current.next); - 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; } @@ -275,7 +279,7 @@ public class AnimationState { 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 * 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 @@ -297,13 +301,12 @@ public class AnimationState { float duration = last.animationEnd - last.animationStart; if (duration != 0) delay += duration * (1 + (int)(last.trackTime / duration)) - data.getMix(last.animation, animation); + else + delay = 0; } - } else { + entry.delay = delay; + } else setCurrent(trackIndex, entry); - if (delay <= 0) delay = 0; - } - - entry.delay = delay; return entry; } @@ -319,12 +322,12 @@ public class AnimationState { entry.drawOrderThreshold = 0; entry.delay = 0; - entry.trackTime = 0; - entry.trackEnd = animation.getDuration(); - entry.trackLast = -1; entry.animationStart = 0; - entry.animationEnd = entry.trackEnd; + entry.animationEnd = animation.getDuration(); entry.animationLast = -1; + entry.trackTime = 0; + entry.trackEnd = loop ? Integer.MAX_VALUE : entry.animationEnd; + entry.trackLast = -1; entry.timeScale = 1; entry.alpha = 1; @@ -334,13 +337,13 @@ public class AnimationState { return entry; } - /** @return May be null. */ + /** The track entry for the animation currently playing on the track, or null. */ public TrackEntry getCurrent (int trackIndex) { if (trackIndex >= tracks.size) return null; 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) { if (listener == null) throw new IllegalArgumentException("listener cannot be null."); listeners.add(listener); @@ -355,14 +358,14 @@ public class AnimationState { listeners.clear(); } - /** Discards all listener notifications that have not yet been delivered. This can be useful to call from an - * {@link AnimationStateListener} when it is known that further notifications that may have been already triggered are not - * wanted, for example because new animations are being set. */ + /** Discards all {@link #addListener(AnimationStateListener) listener} notifications that have not yet been delivered. This can + * be useful to call from an {@link AnimationStateListener} when it is known that further notifications that may have been + * already queued for delivery are not wanted because new animations are being set. */ public void clearListenerNotifications () { 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. */ public float getTimeScale () { return timeScale; @@ -417,15 +420,6 @@ public class AnimationState { 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 () { return trackIndex; } @@ -455,8 +449,8 @@ public class AnimationState { this.delay = delay; } - /** Current time in seconds for this track entry. When changing the track time, it often makes sense to also change - * {@link #getAnimationLast()} to control when timelines will trigger. Defaults to 0. */ + /** Current time in seconds this track entry has been the current track entry. The track time determines + * {@link #getAnimationTime()} and can be set to start the animation at a time other than 0. */ public float getTrackTime () { return trackTime; } @@ -465,6 +459,9 @@ public class AnimationState { 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 () { return trackEnd; } @@ -473,7 +470,10 @@ public class AnimationState { 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. + *

+ * When changing the animation start time, it often makes sense to also change {@link #getAnimationLast()} to control when + * timelines will trigger. */ public float getAnimationStart () { return animationStart; } @@ -482,8 +482,8 @@ public class AnimationState { this.animationStart = animationStart; } - /** Seconds when this animation ends, causing the next animation to start or this animation to loop. Defaults to the - * animation duration. */ + /** Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animation will + * loop back to {@link #getAnimationStart()} at this time. Defaults to the animation duration. */ public float getAnimationEnd () { return animationEnd; } @@ -503,7 +503,19 @@ public class AnimationState { 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. */ public float getTimeScale () { return timeScale; @@ -513,7 +525,7 @@ public class AnimationState { 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 () { 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 - * animation. 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. */ + * animation. + *

+ * 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 () { return alpha; } @@ -576,12 +590,12 @@ public class AnimationState { 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 () { - 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 () { 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 - * {@link AnimationStateData} based on the animation before this animation (if any). */ + * {@link AnimationStateData} based on the animation before this animation (if any). + *

+ * The mix duration must be set before this track entry becomes the current track entry. */ public float getMixDuration () { return mixDuration; }