From eacb73c9133aa3729721e886c7ad308a5ca444dc Mon Sep 17 00:00:00 2001 From: pharan Date: Thu, 18 May 2017 07:17:41 +0800 Subject: [PATCH] [csharp] Ported animation state changes, see https://github.com/EsotericSoftware/spine-runtimes/issues/904 --- spine-csharp/src/Animation.cs | 4 +- spine-csharp/src/AnimationState.cs | 294 +++++++++++++---------------- spine-csharp/src/Skin.cs | 2 +- 3 files changed, 134 insertions(+), 166 deletions(-) diff --git a/spine-csharp/src/Animation.cs b/spine-csharp/src/Animation.cs index cbfe409a3..b2b3d5706 100644 --- a/spine-csharp/src/Animation.cs +++ b/spine-csharp/src/Animation.cs @@ -37,11 +37,11 @@ namespace Spine { internal float duration; internal String name; - public String Name { get { return name; } } + public string Name { get { return name; } } public ExposedList Timelines { get { return timelines; } set { timelines = value; } } public float Duration { get { return duration; } set { duration = value; } } - public Animation (String name, ExposedList timelines, float duration) { + public Animation (string name, ExposedList timelines, float duration) { if (name == null) throw new ArgumentNullException("name", "name cannot be null."); if (timelines == null) throw new ArgumentNullException("timelines", "timelines cannot be null."); this.name = name; diff --git a/spine-csharp/src/AnimationState.cs b/spine-csharp/src/AnimationState.cs index c1c158c6b..f38e5f599 100644 --- a/spine-csharp/src/AnimationState.cs +++ b/spine-csharp/src/AnimationState.cs @@ -34,6 +34,7 @@ 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; private AnimationStateData data; private readonly ExposedList tracks = new ExposedList(); @@ -41,24 +42,8 @@ namespace Spine { private readonly ExposedList events = new ExposedList(); private readonly EventQueue queue; + private readonly ExposedList mixingTo = new ExposedList(); private bool animationsChanged; - private bool multipleMixing; - /// - /// When false, only two animations can be mixed at once. Interrupting a mix by setting a new animation will choose from the - /// two old animations the one that is closest to being fully mixed in and the other is discarded. Discarding an animation in - /// this way may cause keyed values to jump. - /// When true, any number of animations may be mixed at once without causing keyed values to jump. Mixing is done by mixing out - /// one or more animations while mixing in the newest one. When animations key the same value, this may cause "dipping", where - /// the value moves toward the setup pose as the old animation mixes out, then back to the keyed value as the new animation - /// mixes in. - /// Defaults to false. - public bool MultipleMixing { - get { return multipleMixing; } - set { - multipleMixing = value; - animationsChanged = true; - } - } private float timeScale = 1; @@ -122,16 +107,23 @@ namespace Spine { } continue; } - } else { + } else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) { // 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) { - tracksItems[i] = null; - queue.End(current); - DisposeNext(current); - continue; + tracksItems[i] = null; + + queue.End(current); + DisposeNext(current); + continue; + } + if (current.mixingFrom != null && UpdateMixingFrom(current, delta, 2)) { + // End mixing from entries once all have completed. + var from = current.mixingFrom; + current.mixingFrom = null; + while (from != null) { + queue.End(from); + from = from.mixingFrom; } } - UpdateMixingFrom(current, delta); current.trackTime += currentDelta; } @@ -139,22 +131,27 @@ namespace Spine { queue.Drain(); } - private void UpdateMixingFrom (TrackEntry entry, float delta) { + /// Returns true when all mixing from entries are complete. + private bool UpdateMixingFrom (TrackEntry entry, float delta, int animationCount) { TrackEntry from = entry.mixingFrom; - if (from == null) return; + if (from == null) return true; - UpdateMixingFrom(from, delta); + bool finished = UpdateMixingFrom(from, delta, animationCount + 1); - if (entry.mixTime >= entry.mixDuration && from.mixingFrom == null && entry.mixTime > 0) { - entry.mixingFrom = null; - queue.End(from); - return; + // Require mixTime > 0 to ensure the mixing from entry was applied at least once. + if (entry.mixTime > 0 && (entry.mixTime >= entry.mixDuration || entry.timeScale == 0)) { + if (animationCount > 6 && from.mixingFrom == null) { // Limit the mixing from linked list. + entry.mixingFrom = null; + queue.End(from); + } + return finished; } from.animationLast = from.nextAnimationLast; from.trackLast = from.nextTrackLast; from.trackTime += delta * from.timeScale; entry.mixTime += delta * entry.timeScale; + return false; } @@ -188,19 +185,20 @@ namespace Spine { for (int ii = 0; ii < timelineCount; ii++) timelinesItems[ii].Apply(skeleton, animationLast, animationTime, events, 1, true, false); } else { + var timelineData = current.timelineData.Items; + bool firstFrame = current.timelinesRotation.Count == 0; if (firstFrame) current.timelinesRotation.EnsureCapacity(timelines.Count << 1); var timelinesRotation = current.timelinesRotation.Items; - var timelinesFirstItems = current.timelinesFirst.Items; for (int ii = 0; ii < timelineCount; ii++) { Timeline timeline = timelinesItems[ii]; var rotateTimeline = timeline as RotateTimeline; if (rotateTimeline != null) { - ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, mix, timelinesFirstItems[ii], timelinesRotation, ii << 1, + ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, mix, timelineData[ii] > 0, timelinesRotation, ii << 1, firstFrame); } else { - timeline.Apply(skeleton, animationLast, animationTime, events, mix, timelinesFirstItems[ii], false); + timeline.Apply(skeleton, animationLast, animationTime, events, mix, timelineData[ii] > 0, false); } } } @@ -213,15 +211,15 @@ namespace Spine { queue.Drain(); } - private float ApplyMixingFrom (TrackEntry entry, Skeleton skeleton) { - TrackEntry from = entry.mixingFrom; + private float ApplyMixingFrom (TrackEntry to, Skeleton skeleton) { + TrackEntry from = to.mixingFrom; if (from.mixingFrom != null) ApplyMixingFrom(from, skeleton); float mix; - if (entry.mixDuration == 0) // Single frame mix to undo mixingFrom changes. + if (to.mixDuration == 0) // Single frame mix to undo mixingFrom changes. mix = 1; else { - mix = entry.mixTime / entry.mixDuration; + mix = to.mixTime / to.mixDuration; if (mix > 1) mix = 1; } @@ -229,36 +227,50 @@ namespace Spine { bool attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold; float animationLast = from.animationLast, animationTime = from.AnimationTime; var timelines = from.animation.timelines; - var timelinesItems = timelines.Items; int timelineCount = timelines.Count; - var timelinesFirst = from.timelinesFirst; - var timelinesFirstItems = timelinesFirst.Items; - var timelinesLastItems = multipleMixing ? null : from.timelinesLast.Items; - float alphaBase = from.alpha * entry.mixAlpha; - float alphaMix = alphaBase * (1 - mix); + var timelinesItems = timelines.Items; + var timelineData = from.timelineData.Items; + var timelineDipMix = from.timelineDipMix.Items; - bool firstFrame = entry.timelinesRotation.Count == 0; - if (firstFrame) entry.timelinesRotation.EnsureCapacity(timelines.Count << 1); - var timelinesRotation = entry.timelinesRotation.Items; + bool firstFrame = from.timelinesRotation.Count == 0; + if (firstFrame) from.timelinesRotation.Resize(timelines.Count << 1); // from.timelinesRotation.setSize + var timelinesRotation = from.timelinesRotation.Items; + bool first; + float alphaDip = from.alpha * to.interruptAlpha, alphaMix = alphaDip * (1 - mix), alpha; for (int i = 0; i < timelineCount; i++) { Timeline timeline = timelinesItems[i]; - bool setupPose = timelinesFirstItems[i]; - float alpha = timelinesLastItems != null && setupPose && !timelinesLastItems[i] ? alphaBase : alphaMix; + switch (timelineData[i]) { + case SUBSEQUENT: + first = false; + alpha = alphaMix; + break; + case FIRST: + first = true; + alpha = alphaMix; + break; + default: + first = true; + alpha = alphaDip; + var dipMix = timelineDipMix[i]; + if (dipMix != null) alpha *= Math.Max(0, 1 - dipMix.mixTime / dipMix.mixDuration); + break; + } + var rotateTimeline = timeline as RotateTimeline; if (rotateTimeline != null) { - ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, alpha, setupPose, timelinesRotation, i << 1, firstFrame); + ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, alpha, first, timelinesRotation, i << 1, firstFrame); } else { - if (!setupPose) { + if (!first) { if (!attachments && timeline is AttachmentTimeline) continue; if (!drawOrder && timeline is DrawOrderTimeline) continue; } - timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, alpha, setupPose, true); + timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, alpha, first, true); } } - if (entry.mixDuration > 0 ) QueueEvents(from, animationTime); - events.Clear(false); + if (to.mixDuration > 0) QueueEvents(from, animationTime); + this.events.Clear(false); from.nextAnimationLast = animationTime; from.nextTrackLast = from.trackTime; @@ -411,29 +423,9 @@ namespace Spine { current.mixingFrom = from; current.mixTime = 0; - //from.timelinesRotation.Clear(); - var mixingFrom = from.mixingFrom; - - if (mixingFrom != null && from.mixDuration > 0) { - if (multipleMixing) { - // The interrupted mix will mix out from its current percentage to zero. - current.mixAlpha *= Math.Min(from.mixTime / from.mixDuration, 1); - } else { - // A mix was interrupted, mix from the closest animation. - if (from.mixTime / from.mixDuration < 0.5f && mixingFrom.animation != AnimationState.EmptyAnimation) { - current.mixingFrom = mixingFrom; - mixingFrom.mixingFrom = from; - mixingFrom.mixTime = from.mixDuration - from.mixTime; - mixingFrom.mixDuration = from.mixDuration; - from.mixingFrom = null; - from = mixingFrom; - } - - from.mixAlpha = 0; - from.mixTime = 0; - from.mixDuration = 0; - } - } + // Store interrupted mix percentage. + if (from.mixingFrom != null && from.mixDuration > 0) + current.interruptAlpha *= Math.Min(1, from.mixTime / from.mixDuration); from.timelinesRotation.Clear(); // Reset rotation for mixing out, in case entry was mixed in. } @@ -595,7 +587,7 @@ namespace Spine { entry.timeScale = 1; entry.alpha = 1; - entry.mixAlpha = 1; + entry.interruptAlpha = 1; entry.mixTime = 0; entry.mixDuration = (last == null) ? 0 : data.GetMix(last.animation, animation); return entry; @@ -614,88 +606,20 @@ namespace Spine { animationsChanged = false; var propertyIDs = this.propertyIDs; - - // Set timelinesFirst for all entries, from lowest track to highest. - int i = 0, n = tracks.Count; propertyIDs.Clear(); - for (; i < n; i++) { // Find first non-null entry. - TrackEntry entry = tracks.Items[i]; - if (entry == null) continue; - SetTimelinesFirst(entry); - i++; - break; - } - for (; i < n; i++) { // Rest of entries. - TrackEntry entry = tracks.Items[i]; - if (entry != null) CheckTimelinesFirst(entry); - } + var mixingTo = this.mixingTo; - if (multipleMixing) return; - - // Set timelinesLast for mixingFrom entries, from highest track to lowest that has mixingFrom. - propertyIDs.Clear(); - int lowestMixingFrom = n; - for (i = 0; i < n; i++) { // Find lowest track with a mixingFrom entry. - TrackEntry entry = tracks.Items[i]; - if (entry == null || entry.mixingFrom == null) continue; - lowestMixingFrom = i; - break; - } - for (i = n - 1; i >= lowestMixingFrom; i--) { // Find first non-null entry. - TrackEntry entry = tracks.Items[i]; - if (entry == null) continue; - - // Store properties for non-mixingFrom entry but don't set timelinesLast, which is only used for mixingFrom entries. - var timelines = entry.animation.timelines; - var timelinesItems = timelines.Items; - for (int ii = 0, nn = timelines.Count; ii < nn; ii++) - propertyIDs.Add(timelinesItems[ii].PropertyId); - - entry = entry.mixingFrom; - while (entry != null) { - CheckTimelinesUsage(entry, entry.timelinesLast); - entry = entry.mixingFrom; + TrackEntry lastEntry = null; + var tracksItems = tracks.Items; + for (int i = 0, n = tracks.Count; i < n; i++) { + var entry = tracksItems[i]; + if (entry != null) { + entry.SetTimelineData(lastEntry, mixingTo, propertyIDs); + lastEntry = entry; } } } - /// 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; - } - var propertyIDs = this.propertyIDs; - var timelines = entry.animation.timelines; - int n = timelines.Count; - entry.timelinesFirst.EnsureCapacity(n); // entry.timelinesFirst.setSize(n); - var usage = entry.timelinesFirst.Items; - var timelinesItems = timelines.Items; - for (int i = 0; i < n; i++) { - propertyIDs.Add(timelinesItems[i].PropertyId); - usage[i] = true; - } - } - - /// 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, ExposedList usageArray) { - var propertyIDs = this.propertyIDs; - var timelines = entry.animation.timelines; - int n = timelines.Count; - //var usageArray = entry.timelinesFirst; - usageArray.EnsureCapacity(n); - var usage = usageArray.Items; - var timelinesItems = timelines.Items; - for (int i = 0; i < n; i++) - usage[i] = propertyIDs.Add(timelinesItems[i].PropertyId); - } - /// 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]; @@ -731,9 +655,9 @@ namespace Spine { 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, mixAlpha; - internal readonly ExposedList timelinesFirst = new ExposedList(); - internal readonly ExposedList timelinesLast = new ExposedList(); + internal float alpha, mixTime, mixDuration, interruptAlpha; + internal readonly ExposedList timelineData = new ExposedList(); + internal readonly ExposedList timelineDipMix = new ExposedList(); internal readonly ExposedList timelinesRotation = new ExposedList(); // IPoolable.Reset() @@ -741,8 +665,8 @@ namespace Spine { next = null; mixingFrom = null; animation = null; - timelinesFirst.Clear(); - timelinesLast.Clear(); + timelineData.Clear(); + timelineDipMix.Clear(); timelinesRotation.Clear(); Start = null; @@ -753,6 +677,49 @@ namespace Spine { Event = null; } + /// 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.RemoveAt(mixingToArray.Count - 1); // mixingToArray.pop(); + + var mixingTo = mixingToArray.Items; + int mixingToLast = mixingToArray.Count - 1; + var timelines = animation.timelines.Items; + int timelinesCount = animation.timelines.Count; + var timelineDataItems = this.timelineData.Resize(timelinesCount).Items; // timelineData.setSize(timelinesCount); + var timelineDipMixItems = this.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 { + timelineDataItems[i] = AnimationState.DIP; + for (int ii = mixingToLast; ii >= 0; ii--) { + var entry = mixingTo[ii]; + if (!entry.HasTimeline(id)) { + timelineDipMixItems[i] = entry; + goto outer; // continue outer; + } + } + timelineDipMixItems[i] = null; + } + outer: {} + } + return lastEntry; + } + + bool HasTimeline (int id) { + var timelines = animation.timelines.Items; + for (int i = 0, n = animation.timelines.Count; i < n; i++) + if (timelines[i].PropertyId == id) return true; + return false; + } + /// The index of the track where this entry is either current or queued. public int TrackIndex { get { return trackIndex; } } @@ -870,17 +837,18 @@ namespace Spine { /// /// Seconds from 0 to the mix duration when mixing from the previous animation to this animation. May be slightly more than - /// . + /// when the mix is complete. public float MixTime { get { return mixTime; } set { mixTime = value; } } /// /// Seconds for mixing from the previous animation to this animation. Defaults to the value provided by /// based on the animation before this animation (if any). /// - /// The mix duration must be set before is next called. + /// The mix duration can be set manually rather than use the value from AnimationStateData.GetMix. + /// In that case, the mixDuration must be set before is next called. /// /// When using with a - /// delay is set using the mix duration from the + /// delay less than or equal to 0, note the is set using the mix duration from the /// /// /// @@ -888,7 +856,7 @@ namespace Spine { /// /// The track entry for the previous animation when mixing from the previous animation to this animation, or null if no - /// mixing is currently occuring. + /// mixing is currently occuring. When mixing from multiple animations, MixingFrom makes up a linked list. public TrackEntry MixingFrom { get { return mixingFrom; } } public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete; diff --git a/spine-csharp/src/Skin.cs b/spine-csharp/src/Skin.cs index ef743a0c5..4ac70c80f 100644 --- a/spine-csharp/src/Skin.cs +++ b/spine-csharp/src/Skin.cs @@ -49,7 +49,7 @@ namespace Spine { this.name = name; } - /// Adds an attachment to the skin for the specified slot index and name. + /// Adds an attachment to the skin for the specified slot index and name. If the name already exists for the slot, the previous value is replaced. public void AddAttachment (int slotIndex, string name, Attachment attachment) { if (attachment == null) throw new ArgumentNullException("attachment", "attachment cannot be null."); attachments[new AttachmentKeyTuple(slotIndex, name)] = attachment;