diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java index ab2587e53..189ba55fc 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java @@ -249,7 +249,7 @@ public class Animation { * apply animations on top of each other (layering). * @param blend Controls how mixing is applied when alpha < 1. * @param direction Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions, - * such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}. */ + * such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}, and others such as {@link ScaleTimeline}. */ abstract public void apply (Skeleton skeleton, float lastTime, float time, @Null Array events, float alpha, MixBlend blend, MixDirection direction); } @@ -985,22 +985,21 @@ public class Animation { Slot slot = skeleton.slots.get(slotIndex); if (!slot.bone.active) return; - if (direction == out && blend == setup) { - String attachmentName = slot.data.attachmentName; - slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName)); + if (direction == out) { + if (blend == setup) setAttachment(skeleton, slot, slot.data.attachmentName); return; } float[] frames = this.frames; if (time < frames[0]) { // Time is before first frame. - if (blend == setup || blend == first) { - String attachmentName = slot.data.attachmentName; - slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName)); - } + if (blend == setup || blend == first) setAttachment(skeleton, slot, slot.data.attachmentName); return; } - String attachmentName = attachmentNames[search(frames, time)]; + setAttachment(skeleton, slot, attachmentNames[search(frames, time)]); + } + + private void setAttachment (Skeleton skeleton, Slot slot, String attachmentName) { slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName)); } } @@ -1364,8 +1363,8 @@ public class Animation { Array drawOrder = skeleton.drawOrder; Array slots = skeleton.slots; - if (direction == out && blend == setup) { - arraycopy(slots.items, 0, drawOrder.items, 0, slots.size); + if (direction == out) { + if (blend == setup) arraycopy(slots.items, 0, drawOrder.items, 0, slots.size); return; } 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 7aaf3b860..0889cd9b7 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java @@ -77,12 +77,12 @@ public class AnimationState { * (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into * place. */ static private final int HOLD_MIX = 3; - /** 1) An attachment timeline in a subsequent track entry sets the attachment for the same slot as this attachment - * timeline.
- * Result: This attachment timeline will not use MixDirection.out, which would otherwise show the setup mode attachment (or - * none if not visible in setup mode). This allows deform timelines to be applied for the subsequent entry to mix from, rather - * than mixing from the setup pose. */ - static private final int NOT_LAST = 4; + /** 1) This is the last attachment timeline to set the attachment for a slot.
+ * Result: Don't apply this timeline when mixing out. Attachment timelines that are not last are applied when mixing out, so + * any deform timelines are applied and subsequent entries can mix from that deform. */ + static private final int LAST = 4; + + static private final int SETUP = 1, CURRENT = 2; private AnimationStateData data; final Array tracks = new Array(); @@ -92,6 +92,7 @@ public class AnimationState { private final ObjectSet propertyIds = new ObjectSet(); boolean animationsChanged; private float timeScale = 1; + private int unkeyedState; final Pool trackEntryPool = new Pool() { protected Object newObject () { @@ -193,8 +194,8 @@ public class AnimationState { return false; } - /** 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. + /** Poses the skeleton using the track entry animations. The animation state is not changed, so can be applied to multiple + * skeletons to pose them identically. * @return True if any animations were applied. */ public boolean apply (Skeleton skeleton) { if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null."); @@ -227,8 +228,13 @@ public class AnimationState { int timelineCount = current.animation.timelines.size; Object[] timelines = current.animation.timelines.items; if ((i == 0 && mix == 1) || blend == MixBlend.add) { - for (int ii = 0; ii < timelineCount; ii++) - ((Timeline)timelines[ii]).apply(skeleton, animationLast, applyTime, applyEvents, mix, blend, MixDirection.in); + for (int ii = 0; ii < timelineCount; ii++) { + Object timeline = timelines[ii]; + if (timeline instanceof AttachmentTimeline) + applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true); + else + ((Timeline)timeline).apply(skeleton, animationLast, applyTime, applyEvents, mix, blend, MixDirection.in); + } } else { int[] timelineMode = current.timelineMode.items; @@ -238,11 +244,13 @@ public class AnimationState { for (int ii = 0; ii < timelineCount; ii++) { Timeline timeline = (Timeline)timelines[ii]; - MixBlend timelineBlend = (timelineMode[ii] & NOT_LAST - 1) == SUBSEQUENT ? blend : MixBlend.setup; + MixBlend timelineBlend = (timelineMode[ii] & LAST - 1) == SUBSEQUENT ? blend : MixBlend.setup; if (timeline instanceof RotateTimeline) { applyRotateTimeline((RotateTimeline)timeline, skeleton, applyTime, mix, timelineBlend, timelinesRotation, ii << 1, firstFrame); - } else + } else if (timeline instanceof AttachmentTimeline) + applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, blend, true); + else timeline.apply(skeleton, animationLast, applyTime, applyEvents, mix, timelineBlend, MixDirection.in); } } @@ -252,6 +260,20 @@ public class AnimationState { current.nextTrackLast = current.trackTime; } + // Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so + // subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or + // the time is before the first key). + int setupState = unkeyedState + SETUP; + Object[] slots = skeleton.slots.items; + for (int i = 0, n = skeleton.slots.size; i < n; i++) { + Slot slot = (Slot)slots[i]; + if (slot.attachmentState == setupState) { + String attachmentName = slot.data.attachmentName; + slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName)); + } + } + unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot. + queue.drain(); return applied; } @@ -299,14 +321,10 @@ public class AnimationState { MixDirection direction = MixDirection.out; MixBlend timelineBlend; float alpha; - switch (timelineMode[i] & NOT_LAST - 1) { + switch (timelineMode[i] & LAST - 1) { case SUBSEQUENT: - timelineBlend = blend; - if (!attachments && timeline instanceof AttachmentTimeline) { - if ((timelineMode[i] & NOT_LAST) == NOT_LAST) continue; - timelineBlend = MixBlend.setup; - } if (!drawOrder && timeline instanceof DrawOrderTimeline) continue; + timelineBlend = blend; alpha = alphaMix; break; case FIRST: @@ -327,14 +345,14 @@ public class AnimationState { if (timeline instanceof RotateTimeline) { applyRotateTimeline((RotateTimeline)timeline, skeleton, applyTime, alpha, timelineBlend, timelinesRotation, i << 1, firstFrame); + } else if (timeline instanceof AttachmentTimeline) { + // If not showing attachments: do nothing if this is the last timeline, else apply the timeline so + // subsequent timelines see any deform, but don't set attachmentState to CURRENT. + if (!attachments && (timelineMode[i] & LAST) != 0) continue; + applyAttachmentTimeline((AttachmentTimeline)timeline, skeleton, applyTime, timelineBlend, attachments); } else { - if (timelineBlend == MixBlend.setup) { - if (timeline instanceof AttachmentTimeline) { - if (attachments || (timelineMode[i] & NOT_LAST) == NOT_LAST) direction = MixDirection.in; - } else if (timeline instanceof DrawOrderTimeline) { - if (drawOrder) direction = MixDirection.in; - } - } + if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend == MixBlend.setup) + direction = MixDirection.in; timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction); } } @@ -348,6 +366,34 @@ public class AnimationState { return mix; } + /** Applies the attachment timeline and sets {@link Slot#attachmentState}. + * @param attachments False when: 1) the attachment timeline is mixing out, 2) mix < attachmentThreshold, and 3) the timeline + * is not the last timeline to set the slot's attachment. In that case the timeline is applied only so subsequent + * timelines see any deform. */ + private void applyAttachmentTimeline (AttachmentTimeline timeline, Skeleton skeleton, float time, MixBlend blend, + boolean attachments) { + + Slot slot = skeleton.slots.get(timeline.slotIndex); + if (!slot.bone.active) return; + + float[] frames = timeline.frames; + if (time < frames[0]) { // Time is before first frame. + if (blend == MixBlend.setup || blend == MixBlend.first) + setAttachment(skeleton, slot, slot.data.attachmentName, attachments); + } else + setAttachment(skeleton, slot, timeline.attachmentNames[Animation.search(frames, time)], attachments); + + // If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later. + if (slot.attachmentState <= unkeyedState) slot.attachmentState = unkeyedState + SETUP; + } + + private void setAttachment (Skeleton skeleton, Slot slot, String attachmentName, boolean attachments) { + slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName)); + if (attachments) slot.attachmentState = unkeyedState + CURRENT; + } + + /** Applies the rotate timeline, mixing with the current pose while keeping the same rotation direction chosen as the shortest + * the first time the mixing was applied. */ private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float time, float alpha, MixBlend blend, float[] timelinesRotation, int i, boolean firstFrame) { @@ -766,7 +812,7 @@ public class AnimationState { for (int i = 0; i < timelinesCount; i++) { if (timelines[i] instanceof AttachmentTimeline) { AttachmentTimeline timeline = (AttachmentTimeline)timelines[i]; - if (!propertyIds.addAll(timeline.getPropertyIds())) timelineMode[i] |= NOT_LAST; + if (propertyIds.addAll(timeline.getPropertyIds())) timelineMode[i] |= LAST; } } } diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java index 742bd3d51..622d771e8 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Slot.java @@ -48,6 +48,8 @@ public class Slot { private float attachmentTime; private FloatArray deform = new FloatArray(); + int attachmentState; + public Slot (SlotData data, Bone bone) { if (data == null) throw new IllegalArgumentException("data cannot be null."); if (bone == null) throw new IllegalArgumentException("bone cannot be null."); diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs index ce177e0d7..69679ff9b 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs @@ -182,7 +182,7 @@ namespace Spine.Unity { /// Progresses the AnimationState according to the given deltaTime, and applies it to the Skeleton. Use Time.deltaTime to update manually. Use deltaTime 0 to update without progressing the time. public void Update (float deltaTime) { - if (!valid) + if (!valid || state == null) return; deltaTime *= timeScale;