diff --git a/SpineRuntimes/SpineRuntime21/AnimationState.cs b/SpineRuntimes/SpineRuntime21/AnimationState.cs index 5fc8e03..52692b2 100644 --- a/SpineRuntimes/SpineRuntime21/AnimationState.cs +++ b/SpineRuntimes/SpineRuntime21/AnimationState.cs @@ -29,13 +29,17 @@ *****************************************************************************/ using System; +using System.Collections; using System.Collections.Generic; using System.Text; namespace SpineRuntime21 { public class AnimationState { - private AnimationStateData data; - private List tracks = new List(); + static readonly Animation EmptyAnimation = new Animation("", new List(), 0); + private AnimationStateData data; + + Pool trackEntryPool = new Pool(); + private List tracks = new List(); private List events = new List(); private float timeScale = 1; @@ -43,15 +47,11 @@ namespace SpineRuntime21 { public float TimeScale { get { return timeScale; } set { timeScale = value; } } public List Tracks => tracks; - public delegate void StartEndDelegate(AnimationState state, int trackIndex); - public event StartEndDelegate Start; - public event StartEndDelegate End; + public delegate void TrackEntryDelegate(TrackEntry trackEntry); + public event TrackEntryDelegate Start, End, Complete; - public delegate void EventDelegate(AnimationState state, int trackIndex, Event e); + public delegate void EventDelegate(AnimationState state, int trackIndex, Event e); public event EventDelegate Event; - - public delegate void CompleteDelegate(AnimationState state, int trackIndex, int loopCount); - public event CompleteDelegate Complete; public AnimationState (AnimationStateData data) { if (data == null) throw new ArgumentNullException("data cannot be null."); @@ -77,8 +77,8 @@ namespace SpineRuntime21 { // Check if completed the animation or a loop iteration. if (current.loop ? (current.lastTime % endTime > time % endTime) : (current.lastTime < endTime && time >= endTime)) { int count = (int)(time / endTime); - current.OnComplete(this, i, count); - if (Complete != null) Complete(this, i, count); + current.OnComplete(); + if (Complete != null) Complete(current); } TrackEntry next = current.next; @@ -145,10 +145,17 @@ namespace SpineRuntime21 { TrackEntry current = tracks[trackIndex]; if (current == null) return; - current.OnEnd(this, trackIndex); - if (End != null) End(this, trackIndex); + current.OnEnd(); + if (End != null) End(current); tracks[trackIndex] = null; + + while (current is not null) + { + var tmp = current.next; + trackEntryPool.Free(current); + current = tmp; + } } private TrackEntry ExpandToIndex (int index) { @@ -164,8 +171,8 @@ namespace SpineRuntime21 { TrackEntry previous = current.previous; current.previous = null; - current.OnEnd(this, index); - if (End != null) End(this, index); + current.OnEnd(); + if (End != null) End(current); entry.mixDuration = data.GetMix(current.animation, entry.animation); if (entry.mixDuration > 0) { @@ -180,8 +187,15 @@ namespace SpineRuntime21 { tracks[index] = entry; - entry.OnStart(this, index); - if (Start != null) Start(this, index); + while (current is not null) + { + var tmp = current.next; + trackEntryPool.Free(current); + current = tmp; + } + + entry.OnStart(); + if (Start != null) Start(entry); } public TrackEntry SetAnimation (int trackIndex, String animationName, bool loop) { @@ -193,7 +207,8 @@ namespace SpineRuntime21 { /// Set the current animation. Any queued animations are cleared. public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) { if (animation == null) throw new ArgumentException("animation cannot be null."); - TrackEntry entry = new TrackEntry(); + TrackEntry entry = trackEntryPool.Obtain(); + entry.trackIndex = trackIndex; entry.animation = animation; entry.loop = loop; entry.time = 0; @@ -212,8 +227,9 @@ namespace SpineRuntime21 { /// May be <= 0 to use duration of previous animation minus any mix duration plus the negative delay. public TrackEntry AddAnimation (int trackIndex, Animation animation, bool loop, float delay) { if (animation == null) throw new ArgumentException("animation cannot be null."); - TrackEntry entry = new TrackEntry(); - entry.animation = animation; + TrackEntry entry = trackEntryPool.Obtain(); + entry.trackIndex = trackIndex; + entry.animation = animation; entry.loop = loop; entry.time = 0; entry.endTime = animation.Duration; @@ -237,8 +253,48 @@ namespace SpineRuntime21 { return entry; } - /// May be null. - public TrackEntry GetCurrent (int trackIndex) { + /// + /// Sets an empty animation for a track, discarding any queued animations, and mixes to it over the specified mix duration. + public TrackEntry SetEmptyAnimation(int trackIndex, float mixDuration) + { + TrackEntry entry = SetAnimation(trackIndex, AnimationState.EmptyAnimation, false); + entry.mixDuration = mixDuration; + entry.endTime = mixDuration; + return entry; + } + + /// + /// Adds an empty animation to be played after the current or last queued animation for a track, and mixes to it over the + /// specified mix duration. + /// + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept after . + /// + /// Track number. + /// Mix duration. + /// Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation + /// duration of the previous track minus any mix duration plus the negative delay. + public TrackEntry AddEmptyAnimation(int trackIndex, float mixDuration, float delay) + { + if (delay <= 0) delay -= mixDuration; + TrackEntry entry = AddAnimation(trackIndex, AnimationState.EmptyAnimation, false, delay); + entry.mixDuration = mixDuration; + entry.endTime = mixDuration; + return entry; + } + + /// + /// Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix duration. + public void SetEmptyAnimations(float mixDuration) + { + for (int i = 0, n = tracks.Count; i < n; i++) + { + TrackEntry current = tracks[i]; + if (current != null) SetEmptyAnimation(i, mixDuration); + } + } + + /// May be null. + public TrackEntry GetCurrent (int trackIndex) { if (trackIndex >= tracks.Count) return null; return tracks[trackIndex]; } @@ -254,16 +310,18 @@ namespace SpineRuntime21 { if (buffer.Length == 0) return ""; return buffer.ToString(); } - } + } - public class TrackEntry { + public class TrackEntry : Pool.IPoolable { internal TrackEntry next, previous; - internal Animation animation; + internal int trackIndex; + internal Animation animation; internal bool loop; internal float delay, time, lastTime = -1, endTime, timeScale = 1; internal float mixTime, mixDuration, mix = 1; - public Animation Animation { get { return animation; } } + public int TrackIndex { get { return trackIndex; } } + public Animation Animation { get { return animation; } } public float Delay { get { return delay; } set { delay = value; } } public float Time { get { return time; } set { time = value; } } public float LastTime { get { return lastTime; } set { lastTime = value; } } @@ -272,29 +330,110 @@ namespace SpineRuntime21 { public float Mix { get { return mix; } set { mix = value; } } public bool Loop { get { return loop; } set { loop = value; } } - public event AnimationState.StartEndDelegate Start; - public event AnimationState.StartEndDelegate End; + /// + /// 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 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 less than or equal to 0, note the is set using the mix duration from the + /// + /// + /// + public float MixDuration { get { return mixDuration; } set { mixDuration = value; } } + + /// + /// The animation queued to start after this animation, or null. + public TrackEntry Next { get { return next; } } + + public event AnimationState.TrackEntryDelegate Start, End, Complete; public event AnimationState.EventDelegate Event; - public event AnimationState.CompleteDelegate Complete; - internal void OnStart (AnimationState state, int index) { - if (Start != null) Start(state, index); - } + // IPoolable.Reset() + public void Reset() + { + next = null; + previous = null; + animation = null; - internal void OnEnd (AnimationState state, int index) { - if (End != null) End(state, index); - } + Start = null; + End = null; + Complete = null; + Event = null; + } - internal void OnEvent (AnimationState state, int index, Event e) { + internal void OnStart() { if (Start != null) Start(this); } + internal void OnEnd() { if (End != null) End(this); } + internal void OnComplete() { if (Complete != null) Complete(this); } + + internal void OnEvent (AnimationState state, int index, Event e) { if (Event != null) Event(state, index, e); } - internal void OnComplete (AnimationState state, int index, int loopCount) { - if (Complete != null) Complete(state, index, loopCount); - } - override public String ToString () { return animation == null ? "" : animation.name; } } + + public class Pool where T : class, new() + { + public readonly int max; + readonly Stack freeObjects; + + public int Count { get { return freeObjects.Count; } } + public int Peak { get; private set; } + + public Pool(int initialCapacity = 16, int max = int.MaxValue) + { + freeObjects = new Stack(initialCapacity); + this.max = max; + } + + public T Obtain() + { + return freeObjects.Count == 0 ? new T() : freeObjects.Pop(); + } + + public void Free(T obj) + { + if (obj == null) throw new ArgumentNullException("obj", "obj cannot be null"); + if (freeObjects.Count < max) + { + freeObjects.Push(obj); + Peak = Math.Max(Peak, freeObjects.Count); + } + Reset(obj); + } + + // protected void FreeAll (List objects) { + // if (objects == null) throw new ArgumentNullException("objects", "objects cannot be null."); + // var freeObjects = this.freeObjects; + // int max = this.max; + // for (int i = 0; i < objects.Count; i++) { + // T obj = objects[i]; + // if (obj == null) continue; + // if (freeObjects.Count < max) freeObjects.Push(obj); + // Reset(obj); + // } + // Peak = Math.Max(Peak, freeObjects.Count); + // } + + public void Clear() + { + freeObjects.Clear(); + } + + protected void Reset(T obj) + { + var poolable = obj as IPoolable; + if (poolable != null) poolable.Reset(); + } + + public interface IPoolable + { + void Reset(); + } + } } diff --git a/SpineRuntimes/SpineRuntime21/BoneData.cs b/SpineRuntimes/SpineRuntime21/BoneData.cs index 237522c..405d21d 100644 --- a/SpineRuntimes/SpineRuntime21/BoneData.cs +++ b/SpineRuntimes/SpineRuntime21/BoneData.cs @@ -32,14 +32,18 @@ using System; namespace SpineRuntime21 { public class BoneData { - internal BoneData parent; + internal int index; + internal BoneData parent; internal String name; internal float length, x, y, rotation, scaleX = 1, scaleY = 1; internal bool flipX, flipY; internal bool inheritScale = true, inheritRotation = true; - /// May be null. - public BoneData Parent { get { return parent; } } + /// The index of the bone in Skeleton.Bones + public int Index { get { return index; } } + + /// May be null. + public BoneData Parent { get { return parent; } } public String Name { get { return name; } } public float Length { get { return length; } set { length = value; } } public float X { get { return x; } set { x = value; } } @@ -53,9 +57,10 @@ namespace SpineRuntime21 { public bool InheritRotation { get { return inheritRotation; } set { inheritRotation = value; } } /// May be null. - public BoneData (String name, BoneData parent) { + public BoneData (int index, String name, BoneData parent) { if (name == null) throw new ArgumentNullException("name cannot be null."); - this.name = name; + this.index = index; + this.name = name; this.parent = parent; } diff --git a/SpineRuntimes/SpineRuntime21/SkeletonBinary.cs b/SpineRuntimes/SpineRuntime21/SkeletonBinary.cs index 633b0c6..5d86d7d 100644 --- a/SpineRuntimes/SpineRuntime21/SkeletonBinary.cs +++ b/SpineRuntimes/SpineRuntime21/SkeletonBinary.cs @@ -119,7 +119,7 @@ namespace SpineRuntime21 { BoneData parent = null; int parentIndex = ReadInt(input, true) - 1; if (parentIndex != -1) parent = skeletonData.bones[parentIndex]; - BoneData boneData = new BoneData(name, parent); + BoneData boneData = new BoneData(i, name, parent); boneData.x = ReadFloat(input) * scale; boneData.y = ReadFloat(input) * scale; boneData.scaleX = ReadFloat(input); @@ -149,7 +149,7 @@ namespace SpineRuntime21 { for (int i = 0, n = ReadInt(input, true); i < n; i++) { String slotName = ReadString(input); BoneData boneData = skeletonData.bones[ReadInt(input, true)]; - SlotData slotData = new SlotData(slotName, boneData); + SlotData slotData = new SlotData(i, slotName, boneData); int color = ReadInt(input); slotData.r = ((color & 0xff000000) >> 24) / 255f; slotData.g = ((color & 0x00ff0000) >> 16) / 255f; diff --git a/SpineRuntimes/SpineRuntime21/SkeletonJson.cs b/SpineRuntimes/SpineRuntime21/SkeletonJson.cs index bc995bf..634c456 100644 --- a/SpineRuntimes/SpineRuntime21/SkeletonJson.cs +++ b/SpineRuntimes/SpineRuntime21/SkeletonJson.cs @@ -106,7 +106,7 @@ namespace SpineRuntime21 { if (parent == null) throw new Exception("Parent bone not found: " + boneMap["parent"]); } - var boneData = new BoneData((String)boneMap["name"], parent); + var boneData = new BoneData(skeletonData.Bones.Count, (String)boneMap["name"], parent); boneData.length = GetFloat(boneMap, "length", 0) * Scale; boneData.x = GetFloat(boneMap, "x", 0) * Scale; boneData.y = GetFloat(boneMap, "y", 0) * Scale; @@ -150,7 +150,7 @@ namespace SpineRuntime21 { BoneData boneData = skeletonData.FindBone(boneName); if (boneData == null) throw new Exception("Slot bone not found: " + boneName); - var slotData = new SlotData(slotName, boneData); + var slotData = new SlotData(skeletonData.Slots.Count, slotName, boneData); if (slotMap.ContainsKey("color")) { var color = (String)slotMap["color"]; diff --git a/SpineRuntimes/SpineRuntime21/SlotData.cs b/SpineRuntimes/SpineRuntime21/SlotData.cs index 5edfb34..1723722 100644 --- a/SpineRuntimes/SpineRuntime21/SlotData.cs +++ b/SpineRuntimes/SpineRuntime21/SlotData.cs @@ -32,13 +32,15 @@ using System; namespace SpineRuntime21 { public class SlotData { + internal int index; internal String name; internal BoneData boneData; internal float r = 1, g = 1, b = 1, a = 1; internal String attachmentName; internal bool additiveBlending; - public String Name { get { return name; } } + public int Index { get { return index; } } + public String Name { get { return name; } } public BoneData BoneData { get { return boneData; } } public float R { get { return r; } set { r = value; } } public float G { get { return g; } set { g = value; } } @@ -48,9 +50,10 @@ namespace SpineRuntime21 { public String AttachmentName { get { return attachmentName; } set { attachmentName = value; } } public bool AdditiveBlending { get { return additiveBlending; } set { additiveBlending = value; } } - public SlotData (String name, BoneData boneData) { + public SlotData (int index, String name, BoneData boneData) { if (name == null) throw new ArgumentNullException("name cannot be null."); if (boneData == null) throw new ArgumentNullException("boneData cannot be null."); + this.index = index; this.name = name; this.boneData = boneData; }