This commit is contained in:
pharan 2017-05-18 07:17:41 +08:00
parent 18ebc9b447
commit eacb73c913
3 changed files with 134 additions and 166 deletions

View File

@ -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<Timeline> Timelines { get { return timelines; } set { timelines = value; } }
public float Duration { get { return duration; } set { duration = value; } }
public Animation (String name, ExposedList<Timeline> timelines, float duration) {
public Animation (string name, ExposedList<Timeline> 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;

View File

@ -34,6 +34,7 @@ using System.Collections.Generic;
namespace Spine {
public class AnimationState {
static readonly Animation EmptyAnimation = new Animation("<empty>", new ExposedList<Timeline>(), 0);
internal const int SUBSEQUENT = 0, FIRST = 1, DIP = 2;
private AnimationStateData data;
private readonly ExposedList<TrackEntry> tracks = new ExposedList<TrackEntry>();
@ -41,24 +42,8 @@ namespace Spine {
private readonly ExposedList<Event> events = new ExposedList<Event>();
private readonly EventQueue queue;
private readonly ExposedList<TrackEntry> mixingTo = new ExposedList<TrackEntry>();
private bool animationsChanged;
private bool multipleMixing;
/// <summary>
/// <para>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.</para>
/// <para>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.</para>
/// Defaults to false.</summary>
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) {
/// <summary>Returns true when all mixing from entries are complete.</summary>
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;
}
}
}
/// <summary>From last to first mixingFrom entries, sets timelinesFirst to true on last, calls checkTimelineUsage on rest.</summary>
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;
}
}
/// <summary>From last to first mixingFrom entries, calls checkTimelineUsage.</summary>
private void CheckTimelinesFirst (TrackEntry entry) {
if (entry.mixingFrom != null) CheckTimelinesFirst(entry.mixingFrom);
CheckTimelinesUsage(entry, entry.timelinesFirst);
}
private void CheckTimelinesUsage (TrackEntry entry, ExposedList<bool> 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);
}
/// <returns>The track entry for the animation currently playing on the track, or null if no animation is currently playing.</returns>
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<bool> timelinesFirst = new ExposedList<bool>();
internal readonly ExposedList<bool> timelinesLast = new ExposedList<bool>();
internal float alpha, mixTime, mixDuration, interruptAlpha;
internal readonly ExposedList<int> timelineData = new ExposedList<int>();
internal readonly ExposedList<TrackEntry> timelineDipMix = new ExposedList<TrackEntry>();
internal readonly ExposedList<float> timelinesRotation = new ExposedList<float>();
// 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;
}
/// <param name="to">May be null.</param>
internal TrackEntry SetTimelineData (TrackEntry to, ExposedList<TrackEntry> mixingToArray, HashSet<int> 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;
}
/// <summary>The index of the track where this entry is either current or queued.</summary>
public int TrackIndex { get { return trackIndex; } }
@ -870,17 +837,18 @@ namespace Spine {
/// <summary>
/// Seconds from 0 to the mix duration when mixing from the previous animation to this animation. May be slightly more than
/// <see cref="TrackEntry.MixDuration"/>.</summary>
/// <see cref="TrackEntry.MixDuration"/> when the mix is complete.</summary>
public float MixTime { get { return mixTime; } set { mixTime = value; } }
/// <summary>
/// Seconds for mixing from the previous animation to this animation. Defaults to the value provided by
/// <see cref="AnimationStateData"/> based on the animation before this animation (if any).
///
/// The mix duration must be set before <see cref="AnimationState.Update(float)"/> 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 <see cref="AnimationState.Update(float)"/> is next called.
/// <para>
/// When using <seealso cref="AnimationState.AddAnimation(int, Animation, bool, float)"/> with a
/// <code>delay</code> <seealso cref="Delay"/> is set using the mix duration from the <see cref=" AnimationStateData"/>
/// <code>delay</code> less than or equal to 0, note the <seealso cref="Delay"/> is set using the mix duration from the <see cref=" AnimationStateData"/>
/// </para>
///
/// </summary>
@ -888,7 +856,7 @@ namespace Spine {
/// <summary>
/// The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
/// mixing is currently occuring.</summary>
/// mixing is currently occuring. When mixing from multiple animations, MixingFrom makes up a linked list.</summary>
public TrackEntry MixingFrom { get { return mixingFrom; } }
public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete;

View File

@ -49,7 +49,7 @@ namespace Spine {
this.name = name;
}
/// <summary>Adds an attachment to the skin for the specified slot index and name.</summary>
/// <summary>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.</summary>
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;