diff --git a/spine-csharp/src/AnimationState.cs b/spine-csharp/src/AnimationState.cs index 5a3dde2ee..b288efa45 100644 --- a/spine-csharp/src/AnimationState.cs +++ b/spine-csharp/src/AnimationState.cs @@ -34,17 +34,16 @@ using System.Collections.Generic; namespace Spine { public class AnimationState { static readonly Animation EmptyAnimation = new Animation("", new ExposedList(), 0); - internal const int Subsequent = 0, First = 1, Dip = 2, DipMix = 3; + internal const int Subsequent = 0, First = 1, Hold = 2, HoldMix = 3; private AnimationStateData data; - Pool trackEntryPool = new Pool(); + private readonly Pool trackEntryPool = new Pool(); private readonly ExposedList tracks = new ExposedList(); private readonly ExposedList events = new ExposedList(); private readonly EventQueue queue; // Initialized by constructor. private readonly HashSet propertyIDs = new HashSet(); - private readonly ExposedList mixingTo = new ExposedList(); private bool animationsChanged; private float timeScale = 1; @@ -119,6 +118,7 @@ namespace Spine { // End mixing from entries once all have completed. var from = current.mixingFrom; current.mixingFrom = null; + if (from != null) from.mixingTo = null; while (from != null) { queue.End(from); from = from.mixingFrom; @@ -146,6 +146,7 @@ namespace Spine { // Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame). if (from.totalAlpha == 0 || to.mixDuration == 0) { to.mixingFrom = from.mixingFrom; + if (from.mixingFrom != null) from.mixingFrom.mixingTo = to; to.interruptAlpha = from.interruptAlpha; queue.End(from); } @@ -187,11 +188,11 @@ namespace Spine { int timelineCount = current.animation.timelines.Count; var timelines = current.animation.timelines; var timelinesItems = timelines.Items; - if (mix == 1 || blend == MixBlend.Add) { + if (i == 0 && (mix == 1 || blend == MixBlend.Add)) { for (int ii = 0; ii < timelineCount; ii++) timelinesItems[ii].Apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.In); } else { - var timelineData = current.timelineData.Items; + var timelineMode = current.timelineMode.Items; bool firstFrame = current.timelinesRotation.Count == 0; if (firstFrame) current.timelinesRotation.EnsureCapacity(timelines.Count << 1); @@ -199,7 +200,7 @@ namespace Spine { for (int ii = 0; ii < timelineCount; ii++) { Timeline timeline = timelinesItems[ii]; - MixBlend timelineBlend = timelineData[ii] >= AnimationState.Subsequent ? blend : MixBlend.Setup; + MixBlend timelineBlend = timelineMode[ii] >= AnimationState.Subsequent ? blend : MixBlend.Setup; var rotateTimeline = timeline as RotateTimeline; if (rotateTimeline != null) ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1, firstFrame); @@ -237,14 +238,14 @@ namespace Spine { var timelines = from.animation.timelines; int timelineCount = timelines.Count; var timelinesItems = timelines.Items; - float alphaDip = from.alpha * to.interruptAlpha, alphaMix = alphaDip * (1 - mix); + float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix); if (blend == MixBlend.Add) { for (int i = 0; i < timelineCount; i++) (timelinesItems[i]).Apply(skeleton, animationLast, animationTime, eventBuffer, alphaMix, blend, MixDirection.Out); } else { - var timelineData = from.timelineData.Items; - var timelineDipMix = from.timelineDipMix.Items; + var timelineMode = from.timelineMode.Items; + var timelineHoldMix = from.timelineHoldMix.Items; bool firstFrame = from.timelinesRotation.Count == 0; if (firstFrame) from.timelinesRotation.Resize(timelines.Count << 1); // from.timelinesRotation.setSize @@ -255,7 +256,7 @@ namespace Spine { Timeline timeline = timelinesItems[i]; MixBlend timelineBlend; float alpha; - switch (timelineData[i]) { + switch (timelineMode[i]) { case AnimationState.Subsequent: if (!attachments && timeline is AttachmentTimeline) continue; @@ -268,14 +269,14 @@ namespace Spine { timelineBlend = MixBlend.Setup; alpha = alphaMix; break; - case AnimationState.Dip: + case AnimationState.Hold: timelineBlend = MixBlend.Setup; - alpha = alphaDip; + alpha = alphaHold; break; default: timelineBlend = MixBlend.Setup; - TrackEntry dipMix = timelineDipMix[i]; - alpha = alphaDip * Math.Max(0, 1 - dipMix.mixTime / dipMix.mixDuration); + TrackEntry holdMix = timelineHoldMix[i]; + alpha = alphaHold * Math.Max(0, 1 - holdMix.mixTime / holdMix.mixDuration); break; } from.totalAlpha += alpha; @@ -428,6 +429,7 @@ namespace Spine { if (from == null) break; queue.End(from); entry.mixingFrom = null; + entry.mixingTo = null; entry = from; } @@ -444,6 +446,7 @@ namespace Spine { if (from != null) { if (interrupt) queue.Interrupt(from); current.mixingFrom = from; + current.mixingTo = current; current.mixTime = 0; // Store interrupted mix percentage. @@ -599,6 +602,7 @@ namespace Spine { entry.trackIndex = trackIndex; entry.animation = animation; entry.loop = loop; + entry.holdPrevious = false; entry.eventThreshold = 0; entry.attachmentThreshold = 0; @@ -636,17 +640,71 @@ namespace Spine { private void AnimationsChanged () { animationsChanged = false; - var propertyIDs = this.propertyIDs; - propertyIDs.Clear(); - var mixingTo = this.mixingTo; + this.propertyIDs.Clear(); var tracksItems = tracks.Items; for (int i = 0, n = tracks.Count; i < n; i++) { var entry = tracksItems[i]; - if (entry != null && (i == 0 || entry.mixBlend != MixBlend.Add)) entry.SetTimelineData(null, mixingTo, propertyIDs); + if (entry == null) continue; + // Move to last entry, then iterate in reverse (the order animations are applied). + while (entry.mixingFrom != null) + entry = entry.mixingFrom; + + do { + if (entry.mixingTo == null || entry.mixBlend != MixBlend.Add) SetTimelineModes(entry); + entry = entry.mixingTo; + } while (entry != null); + } } + private void SetTimelineModes (TrackEntry entry) { + var to = entry.mixingTo; + var timelines = entry.animation.timelines.Items; + int timelinesCount = entry.animation.timelines.Count; + var timelineMode = entry.timelineMode.Resize(timelinesCount).Items; //timelineMode.setSize(timelinesCount); + entry.timelineHoldMix.Clear(); + var timelineHoldMix = entry.timelineHoldMix.Resize(timelinesCount).Items; //timelineHoldMix.setSize(timelinesCount); + var propertyIDs = this.propertyIDs; + + if (to != null && to.holdPrevious) { + for (int i = 0; i < timelinesCount; i++) { + propertyIDs.Add(timelines[i].PropertyId); + timelineMode[i] = AnimationState.Hold; + } + return; + } + + // outer: + for (int i = 0; i < timelinesCount; i++) { + int id = timelines[i].PropertyId; + if (!propertyIDs.Add(id)) + timelineMode[i] = AnimationState.Subsequent; + else if (to == null || !HasTimeline(to, id)) + timelineMode[i] = AnimationState.First; + else { + for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) { + if (HasTimeline(next, id)) continue; + if (next.mixDuration > 0) { + timelineMode[i] = AnimationState.HoldMix; + timelineHoldMix[i] = next; + goto continue_outer; // continue outer; + } + break; + } + timelineMode[i] = AnimationState.Hold; + } + continue_outer: {} + } + } + + static bool HasTimeline (TrackEntry entry, int id) { + var timelines = entry.animation.timelines.Items; + for (int i = 0, n = entry.animation.timelines.Count; i < n; i++) + if (timelines[i].PropertyId == id) return true; + return false; + } + /// The track entry for the animation currently playing on the track, or null if no animation is currently playing. public TrackEntry GetCurrent (int trackIndex) { return (trackIndex >= tracks.Count) ? null : tracks.Items[trackIndex]; @@ -675,17 +733,17 @@ namespace Spine { public class TrackEntry : Pool.IPoolable { internal Animation animation; - internal TrackEntry next, mixingFrom; + internal TrackEntry next, mixingFrom, mixingTo; internal int trackIndex; - internal bool loop; + internal bool loop, holdPrevious; internal float eventThreshold, attachmentThreshold, drawOrderThreshold; internal float animationStart, animationEnd, animationLast, nextAnimationLast; internal float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale = 1f; internal float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha; internal MixBlend mixBlend = MixBlend.Replace; - internal readonly ExposedList timelineData = new ExposedList(); - internal readonly ExposedList timelineDipMix = new ExposedList(); + internal readonly ExposedList timelineMode = new ExposedList(); + internal readonly ExposedList timelineHoldMix = new ExposedList(); internal readonly ExposedList timelinesRotation = new ExposedList(); // IPoolable.Reset() @@ -693,8 +751,8 @@ namespace Spine { next = null; mixingFrom = null; animation = null; - timelineData.Clear(); - timelineDipMix.Clear(); + timelineMode.Clear(); + timelineHoldMix.Clear(); timelinesRotation.Clear(); Start = null; @@ -705,47 +763,6 @@ namespace Spine { Event = null; } - /// Sets the timeline data. - /// May be null. - internal TrackEntry SetTimelineData (TrackEntry to, ExposedList mixingToArray, HashSet propertyIDs) { - if (to != null) mixingToArray.Add(to); - var lastEntry = mixingFrom != null ? mixingFrom.SetTimelineData(this, mixingToArray, propertyIDs) : this; - if (to != null) mixingToArray.Pop(); - - var mixingTo = mixingToArray.Items; - int mixingToLast = mixingToArray.Count - 1; - var timelines = animation.timelines.Items; - int timelinesCount = animation.timelines.Count; - var timelineDataItems = timelineData.Resize(timelinesCount).Items; // timelineData.setSize(timelinesCount); - timelineDipMix.Clear(); - var timelineDipMixItems = timelineDipMix.Resize(timelinesCount).Items; //timelineDipMix.setSize(timelinesCount); - - // outer: - for (int i = 0; i < timelinesCount; i++) { - int id = timelines[i].PropertyId; - if (!propertyIDs.Add(id)) { - timelineDataItems[i] = AnimationState.Subsequent; - } else if (to == null || !to.HasTimeline(id)) { - timelineDataItems[i] = AnimationState.First; - } else { - for (int ii = mixingToLast; ii >= 0; ii--) { - var entry = mixingTo[ii]; - if (!entry.HasTimeline(id)) { - if (entry.mixDuration > 0) { - timelineDataItems[i] = AnimationState.DipMix; - timelineDipMixItems[i] = entry; - goto continue_outer; // continue outer; - } - break; - } - } - timelineDataItems[i] = AnimationState.Dip; - } - continue_outer: {} - } - return lastEntry; - } - bool HasTimeline (int id) { var timelines = animation.timelines.Items; for (int i = 0, n = animation.timelines.Count; i < n; i++) @@ -903,6 +920,15 @@ namespace Spine { /// mixing is currently occuring. When mixing from multiple animations, MixingFrom makes up a linked list. public TrackEntry MixingFrom { get { return mixingFrom; } } + /// + /// If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead of being mixed out. + /// + /// When mixing between animations that key the same property, if a lower track also keys that property then the value will briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0% while the second animation mixes from 0% to 100%. Setting HoldPrevious to true applies the first animation at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which keys the property, only when a higher track also keys the property. + /// + /// Snapping will occur if HoldPrevious is true and this animation does not key all the same properties as the previous animation. + /// + public bool HoldPrevious { get { return holdPrevious; } set { holdPrevious = value; } } + public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete; public event AnimationState.TrackEntryEventDelegate Event; internal void OnStart () { if (Start != null) Start(this); } 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 69626a8b5..233a2c0aa 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java @@ -666,7 +666,6 @@ public class AnimationState { private void animationsChanged () { animationsChanged = false; - IntSet propertyIDs = this.propertyIDs; propertyIDs.clear(2048); for (int i = 0, n = tracks.size; i < n; i++) {