Explorar o código

Multiple mixingFrom animations.

NathanSweet %!s(int64=9) %!d(string=hai) anos
pai
achega
0ecef9b2c5

+ 7 - 6
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTest.java → spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/AnimationStateTests.java

@@ -43,7 +43,7 @@ import com.esotericsoftware.spine.attachments.MeshAttachment;
 import com.esotericsoftware.spine.attachments.PathAttachment;
 import com.esotericsoftware.spine.attachments.RegionAttachment;
 
-public class AnimationStateTest {
+public class AnimationStateTests {
 	final SkeletonJson json = new SkeletonJson(new AttachmentLoader() {
 		public RegionAttachment newRegionAttachment (Skin skin, String name, String path) {
 			return null;
@@ -112,7 +112,7 @@ public class AnimationStateTest {
 	boolean fail;
 	int test;
 
-	AnimationStateTest () {
+	AnimationStateTests () {
 		skeletonData = json.readSkeletonData(new LwjglFileHandle("test/test.json", FileType.Internal));
 
 		TrackEntry entry;
@@ -645,11 +645,12 @@ public class AnimationStateTest {
 
 			expect(-1, "start", 0, 0.7f), //
 			expect(-1, "complete", 0.1f, 0.8f), //
-			expect(-1, "end", 0.1f, 0.9f), //
-			expect(-1, "dispose", 0.1f, 0.9f), //
 
 			expect(0, "end", 0.8f, 0.9f), //
-			expect(0, "dispose", 0.8f, 0.9f) //
+			expect(0, "dispose", 0.8f, 0.9f), //
+			
+			expect(-1, "end", 0.1f, 0.9f), //
+			expect(-1, "dispose", 0.1f, 0.9f) //
 		);
 		state.addAnimation(0, "events1", false, 0);
 		run(0.1f, 10, new TestListener() {
@@ -779,6 +780,6 @@ public class AnimationStateTest {
 	}
 
 	static public void main (String[] args) throws Exception {
-		new AnimationStateTest();
+		new AnimationStateTests();
 	}
 }

+ 129 - 111
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java

@@ -95,47 +95,62 @@ public class AnimationState {
 				current.delay = 0;
 			}
 
-			TrackEntry next = current.next, mixingFrom = current.mixingFrom;
+			TrackEntry next = current.next;
 			if (next != null) {
-				// When the next entry's delay is passed, change to the next entry.
+				// When the next entry's delay is passed, change to the next entry, preserving leftover time.
 				float nextTime = current.trackLast - next.delay;
 				if (nextTime >= 0) {
 					next.delay = 0;
 					next.trackTime = nextTime + delta * next.timeScale;
 					current.trackTime += currentDelta;
 					setCurrent(i, next);
-					if (next.mixingFrom != null) next.mixTime += currentDelta;
+					while (next.mixingFrom != null) {
+						next.mixTime += currentDelta;
+						next = next.mixingFrom;
+					}
+					continue;
+				}
+				updateMixingFrom(current, delta);
+			} else {
+				updateMixingFrom(current, delta);
+				// Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom.
+				if (current.trackLast >= current.trackEnd && current.mixingFrom == null) {
+					tracks.set(i, null);
+					queue.end(current);
+					disposeNext(current);
 					continue;
 				}
-			} else if (current.trackLast >= current.trackEnd) {
-				// Clear the track when the track end time is reached and there is no next entry.
-				tracks.set(i, null);
-				queue.end(current);
-				disposeNext(current);
-				if (mixingFrom != null) queue.end(mixingFrom);
-				continue;
 			}
 
 			current.trackTime += currentDelta;
-
-			// Update mixing from entry.
-			if (mixingFrom != null) {
-				if (current.mixTime >= current.mixDuration && current.mixTime > 0) {
-					current.mixingFrom = null;
-					queue.end(mixingFrom);
-				} else {
-					mixingFrom.animationLast = mixingFrom.nextAnimationLast;
-					mixingFrom.trackLast = mixingFrom.nextTrackLast;
-					float mixingFromDelta = delta * mixingFrom.timeScale;
-					mixingFrom.trackTime += mixingFromDelta;
-					current.mixTime += mixingFromDelta;
-				}
-			}
 		}
 
 		queue.drain();
 	}
 
+	private void updateMixingFrom (TrackEntry entry, float delta) {
+		TrackEntry from = entry.mixingFrom;
+		if (from == null) return;
+
+		if (entry.mixTime >= entry.mixDuration && entry.mixTime > 0) {
+			queue.end(from);
+			TrackEntry newFrom = from.mixingFrom;
+			entry.mixingFrom = newFrom;
+			if (newFrom == null) return;
+			entry.mixTime = from.mixTime;
+			entry.mixDuration = from.mixDuration;
+			from = newFrom;
+		}
+
+		from.animationLast = from.nextAnimationLast;
+		from.trackLast = from.nextTrackLast;
+		float mixingFromDelta = delta * from.timeScale;
+		from.trackTime += mixingFromDelta;
+		entry.mixTime += mixingFromDelta;
+
+		updateMixingFrom(from, delta);
+	}
+
 	/** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the
 	 * animation state can be applied to multiple skeletons to pose them identically. */
 	public void apply (Skeleton skeleton) {
@@ -146,21 +161,11 @@ public class AnimationState {
 
 		for (int i = 0; i < tracks.size; i++) {
 			TrackEntry current = tracks.get(i);
-			if (current == null) continue;
-			if (current.delay > 0) continue;
+			if (current == null || current.delay > 0) continue;
 
+			// Apply mixing from entries first.
 			float mix = current.alpha;
-
-			// Apply mixing from entry first.
-			if (current.mixingFrom != null) {
-				if (current.mixDuration == 0)
-					mix = 1;
-				else {
-					mix *= current.mixTime / current.mixDuration;
-					if (mix > 1) mix = 1;
-				}
-				applyMixingFrom(current.mixingFrom, skeleton, mix);
-			}
+			if (current.mixingFrom != null) mix = applyMixingFrom(current, skeleton, mix);
 
 			// Apply current entry.
 			float animationLast = current.animationLast, animationTime = current.getAnimationTime();
@@ -189,46 +194,68 @@ public class AnimationState {
 		}
 
 		queue.drain();
+
+		System.out.println();
 	}
 
-	private void applyMixingFrom (TrackEntry entry, Skeleton skeleton, float mix) {
-		Array<Event> events = mix < entry.eventThreshold ? this.events : null;
-		boolean attachments = mix < entry.attachmentThreshold, drawOrder = mix < entry.drawOrderThreshold;
+	private float applyMixingFrom (TrackEntry entry, Skeleton skeleton, float alpha) {
+		float mix;
+		if (entry.mixDuration == 0) // Single frame mix to undo mixingFrom changes.
+			mix = 1;
+		else {
+			mix = alpha * entry.mixTime / entry.mixDuration;
+			if (mix > 1) mix = 1;
+		}
+
+		TrackEntry from = entry.mixingFrom;
+		if (from.mixingFrom != null) applyMixingFrom(from, skeleton, alpha);
 
-		float animationLast = entry.animationLast, animationTime = entry.getAnimationTime();
-		Array<Timeline> timelines = entry.animation.timelines;
+		Array<Event> events = mix < from.eventThreshold ? this.events : null;
+		boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
+
+		float animationLast = from.animationLast, animationTime = from.getAnimationTime();
+		Array<Timeline> timelines = from.animation.timelines;
 		int timelineCount = timelines.size;
-		boolean[] timelinesFirst = entry.timelinesFirst.items, timelinesLast = entry.timelinesLast.items;
-		float alphaFull = entry.alpha, alphaMix = alphaFull * (1 - mix);
+		boolean[] timelinesFirst = from.timelinesFirst.items, timelinesLast = from.timelinesLast.items;
+		float alphaFull = from.alpha, alphaMix = alphaFull * (1 - mix);
+
+		boolean firstFrame = from.timelinesRotation.size == 0;
+		if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1);
+		float[] timelinesRotation = from.timelinesRotation.items;
 
-		boolean firstFrame = entry.timelinesRotation.size == 0;
-		if (firstFrame) entry.timelinesRotation.setSize(timelineCount << 1);
-		float[] timelinesRotation = entry.timelinesRotation.items;
+		System.out.println(entry.mixingFrom + " -> " + entry + ": " + entry.mixTime / entry.mixDuration);
 
 		for (int i = 0; i < timelineCount; i++) {
 			Timeline timeline = timelines.get(i);
 			boolean setupPose = timelinesFirst[i];
-			float alpha = timelinesLast[i] ? alphaMix : alphaFull;
-			if (timeline instanceof RotateTimeline && alpha < 1) {
-				applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, alpha, setupPose,
-					setupPose, timelinesRotation, i << 1, firstFrame);
+			float a = timelinesLast[i] ? alphaMix : alphaFull;
+			if (timeline instanceof RotateTimeline) {
+				applyRotateTimeline((RotateTimeline)timeline, skeleton, animationLast, animationTime, events, a, setupPose, setupPose,
+					timelinesRotation, i << 1, firstFrame);
 			} else {
 				if (!setupPose) {
 					if (!attachments && timeline instanceof AttachmentTimeline) continue;
 					if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
 				}
-				timeline.apply(skeleton, animationLast, animationTime, events, alpha, setupPose, setupPose);
+				timeline.apply(skeleton, animationLast, animationTime, events, a, setupPose, setupPose);
 			}
 		}
 
-		queueEvents(entry, animationTime);
-		entry.nextAnimationLast = animationTime;
-		entry.nextTrackLast = entry.trackTime;
+		queueEvents(from, animationTime);
+		from.nextAnimationLast = animationTime;
+		from.nextTrackLast = from.trackTime;
+
+		return mix;
 	}
 
 	/** @param events May be null. */
 	private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float lastTime, float time, Array<Event> events,
 		float alpha, boolean setupPose, boolean mixingOut, float[] timelinesRotation, int i, boolean firstFrame) {
+		if (alpha == 1) {
+			timeline.apply(skeleton, lastTime, time, events, 1, setupPose, setupPose);
+			return;
+		}
+
 		float[] frames = timeline.frames;
 		if (time < frames[0]) return; // Time is before first frame.
 
@@ -342,10 +369,13 @@ public class AnimationState {
 
 		disposeNext(current);
 
-		TrackEntry mixingFrom = current.mixingFrom;
-		if (mixingFrom != null) {
-			current.mixingFrom = null;
-			queue.end(mixingFrom);
+		TrackEntry entry = current;
+		while (true) {
+			TrackEntry from = entry.mixingFrom;
+			if (from == null) break;
+			queue.end(from);
+			entry.mixingFrom = null;
+			entry = from;
 		}
 
 		tracks.set(current.trackIndex, null);
@@ -358,21 +388,10 @@ public class AnimationState {
 		tracks.set(index, entry);
 
 		if (current != null) {
-			TrackEntry mixingFrom = current.mixingFrom;
-			current.mixingFrom = null;
-
 			queue.interrupt(current);
-
-			// If a mix is in progress, mix from the closest animation.
-			if (mixingFrom != null && mixingFrom.animation != emptyAnimation
-				&& (current.mixDuration == 0 || current.mixTime / current.mixDuration < 0.5f)) {
-				entry.mixingFrom = mixingFrom;
-				mixingFrom = current;
-			} else
-				entry.mixingFrom = current;
-			entry.mixingFrom.timelinesRotation.clear();
-
-			if (mixingFrom != null) queue.end(mixingFrom);
+			entry.mixingFrom = current;
+			entry.mixTime = Math.max(0, entry.mixDuration - current.trackTime);
+			current.timelinesRotation.clear(); // BOZO - Needed? Recursive?
 		}
 
 		queue.start(entry);
@@ -531,31 +550,27 @@ public class AnimationState {
 	private void animationsChanged () {
 		animationsChanged = false;
 
-		// Compute timelinesFirst.
+		IntSet propertyIDs = this.propertyIDs;
+
+		// Compute timelinesFirst from lowest to highest track entries.
 		int i = 0, n = tracks.size;
 		propertyIDs.clear();
-		for (; i < n; i++) {
+		for (; i < n; i++) { // Find first non-null entry.
 			TrackEntry entry = tracks.get(i);
 			if (entry == null) continue;
-			if (entry.mixingFrom != null) {
-				setTimelineUsage(entry.mixingFrom, entry.mixingFrom.timelinesFirst);
-				checkTimelineUsage(entry, entry.timelinesFirst);
-			} else
-				setTimelineUsage(entry, entry.timelinesFirst);
+			setTimelinesFirst(entry);
 			i++;
 			break;
 		}
-		for (; i < n; i++) {
+		for (; i < n; i++) { // Rest of entries.
 			TrackEntry entry = tracks.get(i);
-			if (entry == null) continue;
-			if (entry.mixingFrom != null) checkTimelineUsage(entry.mixingFrom, entry.mixingFrom.timelinesFirst);
-			checkTimelineUsage(entry, entry.timelinesFirst);
+			if (entry != null) checkTimelinesFirst(entry);
 		}
 
-		// Compute timelinesLast from highest to lowest track that has mixingFrom.
+		// Compute timelinesLast from highest to lowest track entries that have mixingFrom.
 		propertyIDs.clear();
 		int lowestMixingFrom = n;
-		for (i = 0; i < n; i++) {
+		for (i = 0; i < n; i++) { // Find lowest with a mixingFrom entry.
 			TrackEntry entry = tracks.get(i);
 			if (entry == null) continue;
 			if (entry.mixingFrom != null) {
@@ -566,34 +581,43 @@ public class AnimationState {
 		for (i = n - 1; i >= lowestMixingFrom; i--) {
 			TrackEntry entry = tracks.get(i);
 			if (entry == null) continue;
-			if (entry.mixingFrom != null) {
-				addTimelineUsage(entry);
-				checkTimelineUsage(entry.mixingFrom, entry.mixingFrom.timelinesLast);
-			} else
-				addTimelineUsage(entry);
-			i--;
-			break;
-		}
-		for (; i >= lowestMixingFrom; i--) {
-			TrackEntry entry = tracks.get(i);
-			if (entry == null) continue;
-			addTimelineUsage(entry);
-			if (entry.mixingFrom != null) checkTimelineUsage(entry.mixingFrom, entry.mixingFrom.timelinesLast);
+
+			Array<Timeline> timelines = entry.animation.timelines;
+			for (int ii = 0, nn = timelines.size; ii < nn; ii++)
+				propertyIDs.add(timelines.get(ii).getPropertyId());
+
+			entry = entry.mixingFrom;
+			while (entry != null) {
+				checkTimelinesUsage(entry, entry.timelinesLast);
+				entry = entry.mixingFrom;
+			}
 		}
 	}
 
-	private void setTimelineUsage (TrackEntry entry, BooleanArray usageArray) {
+	/** From last to first mixingFrom entries, sets timelinesFirst to true on last, calls checkTimelineUsage on rest. */
+	private void setTimelinesFirst (TrackEntry entry) {
+		if (entry.mixingFrom != null) {
+			setTimelinesFirst(entry.mixingFrom);
+			checkTimelinesUsage(entry, entry.timelinesFirst);
+			return;
+		}
 		IntSet propertyIDs = this.propertyIDs;
 		Array<Timeline> timelines = entry.animation.timelines;
 		int n = timelines.size;
-		boolean[] usage = usageArray.setSize(n);
+		boolean[] usage = entry.timelinesFirst.setSize(n);
 		for (int i = 0; i < n; i++) {
 			propertyIDs.add(timelines.get(i).getPropertyId());
 			usage[i] = true;
 		}
 	}
 
-	private void checkTimelineUsage (TrackEntry entry, BooleanArray usageArray) {
+	/** From last to first mixingFrom entries, calls checkTimelineUsage. */
+	private void checkTimelinesFirst (TrackEntry entry) {
+		if (entry.mixingFrom != null) checkTimelinesFirst(entry.mixingFrom);
+		checkTimelinesUsage(entry, entry.timelinesFirst);
+	}
+
+	private void checkTimelinesUsage (TrackEntry entry, BooleanArray usageArray) {
 		IntSet propertyIDs = this.propertyIDs;
 		Array<Timeline> timelines = entry.animation.timelines;
 		int n = timelines.size;
@@ -602,13 +626,6 @@ public class AnimationState {
 			usage[i] = propertyIDs.add(timelines.get(i).getPropertyId());
 	}
 
-	private void addTimelineUsage (TrackEntry entry) {
-		IntSet propertyIDs = this.propertyIDs;
-		Array<Timeline> timelines = entry.animation.timelines;
-		for (int i = 0, n = timelines.size; i < n; i++)
-			propertyIDs.add(timelines.get(i).getPropertyId());
-	}
-
 	/** Returns the track entry for the animation currently playing on the track, or null. */
 	public TrackEntry getCurrent (int trackIndex) {
 		if (trackIndex >= tracks.size) return null;
@@ -740,8 +757,9 @@ public class AnimationState {
 		}
 
 		/** The track time in seconds when this animation will be removed from the track. Defaults to the animation duration for
-		 * non-looping animations and to {@link Integer#MAX_VALUE} for looping animations. If the track end time is reached and no
-		 * other animations are queued for playback then the track is cleared, leaving skeletons in their last pose.
+		 * non-looping animations and to {@link Integer#MAX_VALUE} for looping animations. If the track end time is reached, no
+		 * other animations are queued for playback, and mixing from any previous animations is complete, then the track is cleared,
+		 * leaving skeletons in their last pose.
 		 * <p>
 		 * It may be desired to use {@link AnimationState#addEmptyAnimation(int, float, float)} to mix the skeletons back to the
 		 * setup pose, rather than leaving them in their last pose. */

+ 6 - 5
spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java

@@ -329,9 +329,9 @@ public class SkeletonViewer extends ApplicationAdapter {
 		List<String> skinList = new List(skin);
 		CheckBox loopCheckbox = new CheckBox("Loop", skin);
 		CheckBox premultipliedCheckbox = new CheckBox("Premultiplied", skin);
-		Slider mixSlider = new Slider(0f, 2, 0.01f, false, skin);
+		Slider mixSlider = new Slider(0, 4, 0.01f, false, skin);
 		Label mixLabel = new Label("0.3", skin);
-		Slider speedSlider = new Slider(0.1f, 3, 0.01f, false, skin);
+		Slider speedSlider = new Slider(0, 3, 0.01f, false, skin);
 		Label speedLabel = new Label("1.0", skin);
 		CheckBox flipXCheckbox = new CheckBox("X", skin);
 		CheckBox flipYCheckbox = new CheckBox("Y", skin);
@@ -359,12 +359,13 @@ public class SkeletonViewer extends ApplicationAdapter {
 			loopCheckbox.setChecked(true);
 
 			scaleSlider.setValue(1);
-			scaleSlider.setSnapToValues(new float[] {1}, 0.1f);
+			scaleSlider.setSnapToValues(new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f}, 0.01f);
 
 			mixSlider.setValue(0.3f);
+			mixSlider.setSnapToValues(new float[] {1, 1.5f, 2, 2.5f, 3, 3.5f}, 0.1f);
 
 			speedSlider.setValue(1);
-			speedSlider.setSnapToValues(new float[] {1}, 0.1f);
+			speedSlider.setSnapToValues(new float[] {0.5f, 0.75f, 1, 1.25f, 1.5f, 2, 2.5f}, 0.1f);
 
 			window.setMovable(false);
 			window.setResizable(false);
@@ -621,7 +622,7 @@ public class SkeletonViewer extends ApplicationAdapter {
 			prefs.putInteger("x", skeletonX);
 			prefs.putInteger("y", skeletonY);
 			if (animationList.getSelected() != null) prefs.putString("animationName", animationList.getSelected());
-			if (skinList.getSelected() != null)prefs.putString("skinName", skinList.getSelected());
+			if (skinList.getSelected() != null) prefs.putString("skinName", skinList.getSelected());
 			prefs.flush();
 		}