diff --git a/spine-as3/spine-as3/src/spine/animation/EventTimeline.as b/spine-as3/spine-as3/src/spine/animation/EventTimeline.as index 1f5103d29..45ac7ff1d 100644 --- a/spine-as3/spine-as3/src/spine/animation/EventTimeline.as +++ b/spine-as3/spine-as3/src/spine/animation/EventTimeline.as @@ -55,18 +55,19 @@ public class EventTimeline implements Timeline { events[frameIndex] = event; } + /** Fires events for frames > lastTime and <= time. */ public function apply (skeleton:Skeleton, lastTime:Number, time:Number, firedEvents:Vector., alpha:Number) : void { if (!firedEvents) return; - if (lastTime >= frames[frameCount - 1]) return; // Last time is after last frame. - if (lastTime > time) { // Fire events after last time for looped animations. apply(skeleton, lastTime, int.MAX_VALUE, firedEvents, alpha); - lastTime = 0; - } + lastTime = -1; + } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. + return; + if (time < frames[0]) return; // Time is before first frame. var frameIndex:int; - if (lastTime <= frames[0] || frameCount == 1) + if (lastTime < frames[0]) frameIndex = 0; else { frameIndex = Animation.binarySearch(frames, lastTime, 1); diff --git a/spine-as3/spine-as3/src/spine/animation/TrackEntry.as b/spine-as3/spine-as3/src/spine/animation/TrackEntry.as index 7a4ba9642..fc6f0d8e3 100644 --- a/spine-as3/spine-as3/src/spine/animation/TrackEntry.as +++ b/spine-as3/spine-as3/src/spine/animation/TrackEntry.as @@ -40,7 +40,7 @@ public class TrackEntry { internal var previous:TrackEntry; public var animation:Animation; public var loop:Boolean; - public var delay:Number, time:Number = 0, lastTime:Number = 0, endTime:Number, timeScale:Number = 1; + public var delay:Number, time:Number = 0, lastTime:Number = -1, endTime:Number, timeScale:Number = 1; internal var mixTime:Number, mixDuration:Number; public var onStart:Function, onEnd:Function, onComplete:Function, onEvent:Function; diff --git a/spine-c/src/spine/Animation.c b/spine-c/src/spine/Animation.c index e93ab7249..58dfab822 100644 --- a/spine-c/src/spine/Animation.c +++ b/spine-c/src/spine/Animation.c @@ -514,21 +514,21 @@ void spAttachmentTimeline_setFrame (spAttachmentTimeline* self, int frameIndex, /**/ +/** Fires events for frames > lastTime and <= time. */ void _spEventTimeline_apply (const spTimeline* timeline, spSkeleton* skeleton, float lastTime, float time, spEvent** firedEvents, int* eventCount, float alpha) { spEventTimeline* self = (spEventTimeline*)timeline; int frameIndex; if (!firedEvents) return; - if (lastTime >= self->frames[self->framesLength - 1]) return; /* Last time is after last frame. */ - - if (lastTime > time) { - /* Fire events after last time for looped animations. */ + if (lastTime > time) { /* Fire events after last time for looped animations. */ _spEventTimeline_apply(timeline, skeleton, lastTime, (float)INT_MAX, firedEvents, eventCount, alpha); - lastTime = 0; - } + lastTime = -1; + } else if (lastTime >= self->frames[self->framesLength - 1]) /* Last time is after last frame. */ + return; + if (time < self->frames[0]) return; /* Time is before first frame. */ - if (lastTime <= self->frames[0] || self->framesLength == 1) + if (lastTime < self->frames[0]) frameIndex = 0; else { float frame; diff --git a/spine-c/src/spine/AnimationState.c b/spine-c/src/spine/AnimationState.c index f3ee632ac..3ae0b8859 100644 --- a/spine-c/src/spine/AnimationState.c +++ b/spine-c/src/spine/AnimationState.c @@ -43,6 +43,7 @@ spTrackEntry* _spTrackEntry_create () { spTrackEntry* entry = NEW(spTrackEntry); entry->timeScale = 1; + entry->lastTime = -1; return entry; } @@ -232,7 +233,6 @@ spTrackEntry* spAnimationState_setAnimation (spAnimationState* self, int trackIn entry = _spTrackEntry_create(); entry->animation = animation; entry->loop = loop; - entry->time = 0; entry->endTime = animation->duration; _spAnimationState_setCurrent(self, trackIndex, entry); return entry; @@ -251,7 +251,6 @@ spTrackEntry* spAnimationState_addAnimation (spAnimationState* self, int trackIn spTrackEntry* entry = _spTrackEntry_create(); entry->animation = animation; entry->loop = loop; - entry->time = 0; entry->endTime = animation->duration; last = _spAnimationState_expandToIndex(self, trackIndex); diff --git a/spine-csharp/src/Animation.cs b/spine-csharp/src/Animation.cs index 7a3c3ea66..3e6308a97 100644 --- a/spine-csharp/src/Animation.cs +++ b/spine-csharp/src/Animation.cs @@ -459,20 +459,21 @@ namespace Spine { events[frameIndex] = e; } + /// Fires events for frames > lastTime and <= time. public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { if (firedEvents == null) return; float[] frames = this.frames; int frameCount = frames.Length; - if (lastTime >= frames[frameCount - 1]) return; // Last time is after last frame. - if (lastTime > time) { // Fire events after last time for looped animations. - Apply(skeleton, lastTime, int.MaxValue, firedEvents, alpha); - lastTime = 0; - } + apply(skeleton, lastTime, Integer.MAX_VALUE, firedEvents, alpha); + lastTime = -1f; + } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. + return; + if (time < frames[0]) return; // Time is before first frame. int frameIndex; - if (lastTime <= frames[0] || frameCount == 1) + if (lastTime < frames[0]) frameIndex = 0; else { frameIndex = Animation.binarySearch(frames, lastTime, 1); diff --git a/spine-csharp/src/AnimationState.cs b/spine-csharp/src/AnimationState.cs index db6bd5836..92de2b7cd 100644 --- a/spine-csharp/src/AnimationState.cs +++ b/spine-csharp/src/AnimationState.cs @@ -275,7 +275,7 @@ namespace Spine { internal TrackEntry next, previous; internal Animation animation; internal bool loop; - internal float delay, time, lastTime, endTime, timeScale = 1; + internal float delay, time, lastTime = -1, endTime, timeScale = 1; internal float mixTime, mixDuration; public Animation Animation { get { return animation; } } diff --git a/spine-js/spine.js b/spine-js/spine.js index 22b48a536..436c7708f 100644 --- a/spine-js/spine.js +++ b/spine-js/spine.js @@ -532,20 +532,22 @@ spine.EventTimeline.prototype = { this.frames[frameIndex] = time; this.events[frameIndex] = event; }, + /** Fires events for frames > lastTime and <= time. */ apply: function (skeleton, lastTime, time, firedEvents, alpha) { if (!firedEvents) return; var frames = this.frames; var frameCount = frames.length; - if (lastTime >= frames[frameCount - 1]) return; // Last time is after last frame. if (lastTime > time) { // Fire events after last time for looped animations. - this.apply(skeleton, lastTime, Number.MAX_VALUE, firedEvents, alpha); - lastTime = 0; - } + apply(skeleton, lastTime, Number.MAX_VALUE, firedEvents, alpha); + lastTime = -1f; + } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. + return; + if (time < frames[0]) return; // Time is before first frame. var frameIndex; - if (lastTime <= frames[0] || frameCount == 1) + if (lastTime < frames[0]) frameIndex = 0; else { frameIndex = spine.binarySearch(frames, lastTime, 1); @@ -947,7 +949,7 @@ spine.TrackEntry.prototype = { next: null, previous: null, animation: null, loop: false, - delay: 0, time: 0, lastTime: 0, endTime: 0, + delay: 0, time: 0, lastTime: -1, endTime: 0, timeScale: 1, mixTime: 0, mixDuration: 0, onStart: null, onEnd: null, onComplete: null, onEvent: null diff --git a/spine-libgdx/src/com/esotericsoftware/spine/Animation.java b/spine-libgdx/src/com/esotericsoftware/spine/Animation.java index 521ea740d..179cf94c0 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/Animation.java +++ b/spine-libgdx/src/com/esotericsoftware/spine/Animation.java @@ -533,20 +533,21 @@ public class Animation { events[frameIndex] = event; } + /** Fires events for frames > lastTime and <= time. */ public void apply (Skeleton skeleton, float lastTime, float time, Array firedEvents, float alpha) { if (firedEvents == null) return; float[] frames = this.frames; int frameCount = frames.length; - if (lastTime >= frames[frameCount - 1]) return; // Last time is after last frame. - if (lastTime > time) { // Fire events after last time for looped animations. apply(skeleton, lastTime, Integer.MAX_VALUE, firedEvents, alpha); - lastTime = 0; - } + lastTime = -1f; + } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. + return; + if (time < frames[0]) return; // Time is before first frame. int frameIndex; - if (lastTime <= frames[0] || frameCount == 1) + if (lastTime < frames[0]) frameIndex = 0; else { frameIndex = binarySearch(frames, lastTime, 1); diff --git a/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java b/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java index 5b30c813a..652a95bca 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java @@ -297,7 +297,7 @@ public class AnimationState { animation = null; listener = null; timeScale = 1; - lastTime = 0; + lastTime = -1; time = 0; } diff --git a/spine-libgdx/test/com/esotericsoftware/spine/EventTimelineTests.java b/spine-libgdx/test/com/esotericsoftware/spine/EventTimelineTests.java new file mode 100644 index 000000000..46a7f319c --- /dev/null +++ b/spine-libgdx/test/com/esotericsoftware/spine/EventTimelineTests.java @@ -0,0 +1,198 @@ + +package com.esotericsoftware.spine; + +import com.esotericsoftware.spine.Animation.EventTimeline; + +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.StringBuilder; + +import java.util.Arrays; + +/** Unit tests for {@link EventTimeline}. */ +public class EventTimelineTests { + private final SkeletonData skeletonData; + private final Skeleton skeleton; + private final Array firedEvents = new Array(); + private EventTimeline timeline = new EventTimeline(0); + private char[] events; + private float[] frames; + + public EventTimelineTests () { + skeletonData = new SkeletonData(); + skeleton = new Skeleton(skeletonData); + + test(0); + test(1); + test(1, 1); + test(1, 2); + test(1, 2); + test(1, 2, 3); + test(1, 2, 3); + test(0, 0, 0); + test(0, 0, 1); + test(0, 1, 1); + test(1, 1, 1); + test(1, 2, 3, 4); + test(0, 2, 3, 4); + test(0, 2, 2, 4); + test(0, 0, 0, 0); + test(2, 2, 2, 2); + test(0.1f); + test(0.1f, 0.1f); + test(0.1f, 50f); + test(0.1f, 0.2f, 0.3f, 0.4f); + test(1, 2, 3, 4, 5, 6, 6, 7, 7, 8, 9, 10, 11, 11.01f, 12, 12, 12, 12); + + System.out.println("All tests passed."); + } + + private void test (float... frames) { + int eventCount = frames.length; + + StringBuilder buffer = new StringBuilder(); + for (int i = 0; i < eventCount; i++) + buffer.append((char)('a' + i)); + + this.events = buffer.toString().toCharArray(); + this.frames = frames; + timeline = new EventTimeline(eventCount); + + float maxFrame = 0; + int distinctCount = 0; + float lastFrame = -1; + for (int i = 0; i < eventCount; i++) { + float frame = frames[i]; + Event event = new Event(new EventData("" + events[i])); + timeline.setFrame(i, frame, event); + maxFrame = Math.max(maxFrame, frame); + if (lastFrame != frame) distinctCount++; + lastFrame = frame; + } + + run(0, 99, 0.1f); + run(0, maxFrame, 0.1f); + run(frames[0], 999, 2f); + run(frames[0], maxFrame, 0.1f); + run(0, maxFrame, (float)Math.ceil(maxFrame / 100)); + run(0, 99, 0.1f); + run(0, 999, 100f); + if (distinctCount > 1) { + float epsilon = 0.02f; + // Ending before last. + run(frames[0], maxFrame - epsilon, 0.1f); + run(0, maxFrame - epsilon, 0.1f); + // Starting after first. + run(frames[0] + epsilon, maxFrame, 0.1f); + run(frames[0] + epsilon, 99, 0.1f); + } + } + + private void run (float startTime, float endTime, float timeStep) { + timeStep = Math.max(timeStep, 0.00001f); + boolean loop = false; + try { + fire(startTime, endTime, timeStep, loop, false); + loop = true; + fire(startTime, endTime, timeStep, loop, false); + } catch (FailException ignored) { + try { + fire(startTime, endTime, timeStep, loop, true); + } catch (FailException ex) { + System.out.println(ex.getMessage()); + System.exit(0); + } + } + } + + private void fire (float timeStart, float timeEnd, float timeStep, boolean loop, boolean print) { + if (print) { + System.out.println("events: " + Arrays.toString(events)); + System.out.println("frames: " + Arrays.toString(frames)); + System.out.println("timeStart: " + timeStart); + System.out.println("timeEnd: " + timeEnd); + System.out.println("timeStep: " + timeStep); + System.out.println("loop: " + loop); + } + + // Expected starting event. + int eventIndex = 0; + while (frames[eventIndex] < timeStart) + eventIndex++; + + // Expected number of events when not looping. + int eventsCount = frames.length; + while (frames[eventsCount - 1] > timeEnd) + eventsCount--; + eventsCount -= eventIndex; + + float duration = frames[eventIndex + eventsCount - 1]; + if (loop && duration > 0) { // When looping timeStep can't be > nyquist. + while (timeStep > duration / 2f) + timeStep /= 2f; + } + // duration *= 2; // Everything should still pass with this uncommented. + + firedEvents.clear(); + int i = 0; + float lastTime = timeStart - 0.00001f; + float timeLooped, lastTimeLooped; + while (true) { + float time = Math.min(timeStart + timeStep * i, timeEnd); + lastTimeLooped = lastTime; + timeLooped = time; + if (loop && duration != 0) { + lastTimeLooped %= duration; + timeLooped %= duration; + } + + int beforeCount = firedEvents.size; + Array original = new Array(firedEvents); + timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1); + + while (beforeCount < firedEvents.size) { + char fired = firedEvents.get(beforeCount).getData().getName().charAt(0); + if (loop) { + eventIndex %= events.length; + } else { + if (firedEvents.size > eventsCount) { + if (print) System.out.println(lastTimeLooped + "->" + timeLooped + ": " + fired + " == ?"); + timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1); + fail("Too many events fired."); + } + } + if (print) { + System.out.println(lastTimeLooped + "->" + timeLooped + ": " + fired + " == " + events[eventIndex]); + } + if (fired != events[eventIndex]) { + timeline.apply(skeleton, lastTimeLooped, timeLooped, original, 1); + fail("Wrong event fired."); + } + eventIndex++; + beforeCount++; + } + + if (time >= timeEnd) break; + lastTime = time; + i++; + } + if (firedEvents.size < eventsCount) { + timeline.apply(skeleton, lastTimeLooped, timeLooped, firedEvents, 1); + if (print) System.out.println(firedEvents); + fail("Event not fired: " + events[eventIndex] + ", " + frames[eventIndex]); + } + } + + private void fail (String message) { + throw new FailException(message); + } + + static class FailException extends RuntimeException { + public FailException (String message) { + super(message); + } + } + + static public void main (String[] args) throws Exception { + new EventTimelineTests(); + } +} diff --git a/spine-lua/Animation.lua b/spine-lua/Animation.lua index 4e1c90e5c..7bd10658f 100644 --- a/spine-lua/Animation.lua +++ b/spine-lua/Animation.lua @@ -463,21 +463,23 @@ function Animation.EventTimeline.new () self.events[frameIndex] = event end + -- Fires events for frames > lastTime and <= time. function self:apply (skeleton, lastTime, time, firedEvents, alpha) if not firedEvents then return end local frames = self.frames local frameCount = #frames - if lastTime >= frames[frameCount] then return end -- Last time is after last frame. - frameCount = frameCount + 1 if lastTime > time then -- Fire events after last time for looped animations. self:apply(skeleton, lastTime, 999999, firedEvents, alpha) - lastTime = 0 + lastTime = -1 + elseif lastTime >= frames[frameCount - 1] then -- Last time is after last frame. + return end + if time < frames[0] then return end -- Time is before first frame. local frameIndex - if lastTime <= frames[0] or frameCount == 1 then + if lastTime < frames[0] then frameIndex = 0 else frameIndex = binarySearch(frames, lastTime, 1) diff --git a/spine-lua/AnimationState.lua b/spine-lua/AnimationState.lua index f90ae2c8f..62b872c14 100644 --- a/spine-lua/AnimationState.lua +++ b/spine-lua/AnimationState.lua @@ -225,7 +225,7 @@ function AnimationState.TrackEntry.new (data) next = nil, previous = nil, animation = nil, loop = false, - delay = 0, time = 0, lastTime = 0, endTime = 0, + delay = 0, time = 0, lastTime = -1, endTime = 0, timeScale = 1, mixTime = 0, mixDuration = 0, onStart = nil, onEnd = nil, onComplete = nil, onEvent = nil