diff --git a/spine-csharp/src/Animation.cs b/spine-csharp/src/Animation.cs index 54b5eb2a3..d47588324 100644 --- a/spine-csharp/src/Animation.cs +++ b/spine-csharp/src/Animation.cs @@ -32,14 +32,13 @@ using System; using System.Collections.Generic; namespace Spine { + + /// + /// A simple container for a list of timelines and a name. public class Animation { + internal String name; internal ExposedList timelines; internal float duration; - internal String 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) { if (name == null) throw new ArgumentNullException("name", "name cannot be null."); @@ -49,9 +48,18 @@ namespace Spine { this.duration = duration; } + public ExposedList Timelines { get { return timelines; } set { timelines = value; } } + + /// The duration of the animation in seconds, which is the highest time of all keys in the timeline. + public float Duration { get { return duration; } set { duration = value; } } + + /// The animation's name, which is unique within the skeleton. + public string Name { get { return name; } } + /// Applies all the animation's timelines to the specified skeleton. /// - public void Apply (Skeleton skeleton, float lastTime, float time, bool loop, ExposedList events, float alpha, MixBlend pose, MixDirection direction) { + public void Apply (Skeleton skeleton, float lastTime, float time, bool loop, ExposedList events, float alpha, MixBlend blend, + MixDirection direction) { if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); if (loop && duration != 0) { @@ -61,10 +69,15 @@ namespace Spine { ExposedList timelines = this.timelines; for (int i = 0, n = timelines.Count; i < n; i++) - timelines.Items[i].Apply(skeleton, lastTime, time, events, alpha, pose, direction); + timelines.Items[i].Apply(skeleton, lastTime, time, events, alpha, blend, direction); + } + + override public string ToString () { + return name; } /// After the first and before the last entry. + /// Index of first value greater than the target. internal static int BinarySearch (float[] values, float target, int step) { int low = 0; int high = values.Length / step - 2; @@ -87,7 +100,7 @@ namespace Spine { if (high == 0) return 1; int current = (int)((uint)high >> 1); while (true) { - if (values[(current + 1)] <= target) + if (values[current + 1] <= target) low = current + 1; else high = current; @@ -103,19 +116,28 @@ namespace Spine { } } + /// + /// The interface for all timelines. public interface Timeline { - /// Sets the value(s) for the specified time. - /// The skeleton the timeline is being applied to. This provides access to the bones, slots, and other skeleton components the timeline may change. - /// lastTime The time this timeline was last applied. Timelines such as EventTimeline trigger only at specific times rather than every frame. In that case, the timeline triggers everything between lastTime (exclusive) and time (inclusive). - /// The time within the animation. Most timelines find the key before and the key after this time so they can interpolate between the keys. - /// If any events are fired, they are added to this list. Can be null to ignore firing events or if the timeline does not fire events. May be null. - /// alpha 0 applies the current or setup pose value (depending on pose parameter). 1 applies the timeline - /// value. Between 0 and 1 applies a value between the current or setup pose and the timeline value. By adjusting - /// alpha over time, an animation can be mixed in or out. alpha can also be useful to - /// apply animations on top of each other (layered). - /// Controls how mixing is applied when alpha is than 1. - /// Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions such as DrawOrderTimeline and AttachmentTimeline. + /// Applies this timeline to the skeleton. + /// The skeleton the timeline is being applied to. This provides access to the bones, slots, and other + /// skeleton components the timeline may change. + /// The time this timeline was last applied. Timelines such as trigger only at specific + /// times rather than every frame. In that case, the timeline triggers everything between lastTime + /// (exclusive) and time (inclusive). + /// The time within the animation. Most timelines find the key before and the key after this time so they can + /// interpolate between the keys. + /// If any events are fired, they are added to this list. Can be null to ignore firing events or if the + /// timeline does not fire events. + /// 0 applies the current or setup value (depending on blend). 1 applies the timeline value. + /// Between 0 and 1 applies a value between the current or setup value and the timeline value. By adjusting + /// alpha over time, an animation can be mixed in or out. alpha can also be useful to + /// apply animations on top of each other (layered). + /// Controls how mixing is applied when alpha < 1. + /// Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions, + /// such as or . void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, MixBlend blend, MixDirection direction); + /// Uniquely encodes both the type of this timeline and the skeleton property that it affects. int PropertyId { get; } } @@ -124,12 +146,15 @@ namespace Spine { /// public enum MixBlend { - /// Transitions from the setup value to the timeline value (the current value is not used). Before the first key, the setup value is set.. + /// Transitions from the setup value to the timeline value (the current value is not used). Before the first key, the setup + /// value is set. Setup, /// - /// Transitions from the current value to the timeline value. Before the first key, transitions from the current value to - /// the setup value. Timelines which perform instant transitions, such as or , use the setup value before the first key. + /// + /// Transitions from the current value to the timeline value. Before the first key, transitions from the current value to + /// the setup value. Timelines which perform instant transitions, such as or + /// , use the setup value before the first key. /// /// First is intended for the first animations applied, not for animations layered on top of those. /// @@ -137,7 +162,8 @@ namespace Spine { /// /// - /// Transitions from the current value to the timeline value. No change is made before the first key (the current value is kept until the first key). + /// Transitions from the current value to the timeline value. No change is made before the first key (the current value is + /// kept until the first key). /// /// Replace is intended for animations layered on top of others, not for the first animations applied. /// @@ -145,7 +171,8 @@ namespace Spine { /// /// - /// Transitions from the current value to the current value plus the timeline value. No change is made before the first key (the current value is kept until the first key). + /// Transitions from the current value to the current value plus the timeline value. No change is made before the first key + /// (the current value is kept until the first key). /// /// Add is intended for animations layered on top of others, not for the first animations applied. /// @@ -153,7 +180,8 @@ namespace Spine { } /// - /// Indicates whether a timeline's alpha is mixing out over time toward 0 (the setup or current pose value) or mixing in toward 1 (the timeline's value). + /// Indicates whether a timeline's alpha is mixing out over time toward 0 (the setup or current pose value) or + /// mixing in toward 1 (the timeline's value). /// public enum MixDirection { In, @@ -169,42 +197,60 @@ namespace Spine { TwoColor } + /// An interface for timelines which change the property of a bone. public interface IBoneTimeline { + /// The index of the bone in that will be changed. int BoneIndex { get; } } + /// An interface for timelines which change the property of a slot. public interface ISlotTimeline { + /// The index of the slot in that will be changed. int SlotIndex { get; } } - /// Base class for frames that use an interpolation bezier curve. + /// The base class for timelines that use interpolation between key frame values. abstract public class CurveTimeline : Timeline { protected const float LINEAR = 0, STEPPED = 1, BEZIER = 2; protected const int BEZIER_SIZE = 10 * 2 - 1; internal float[] curves; // type, x, y, ... + /// The number of key frames for this timeline. public int FrameCount { get { return curves.Length / BEZIER_SIZE + 1; } } public CurveTimeline (int frameCount) { - if (frameCount <= 0) throw new ArgumentException("frameCount must be > 0: " + frameCount, "frameCount"); + if (frameCount <= 0) throw new ArgumentOutOfRangeException("frameCount must be > 0: "); curves = new float[(frameCount - 1) * BEZIER_SIZE]; } - abstract public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend pose, MixDirection direction); + abstract public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction); abstract public int PropertyId { get; } + /// Sets the specified key frame to linear interpolation. public void SetLinear (int frameIndex) { curves[frameIndex * BEZIER_SIZE] = LINEAR; } + /// Sets the specified key frame to stepped interpolation. public void SetStepped (int frameIndex) { curves[frameIndex * BEZIER_SIZE] = STEPPED; } - /// Sets the control handle positions for an interpolation bezier curve used to transition from this keyframe to the next. - /// cx1 and cx2 are from 0 to 1, representing the percent of time between the two keyframes. cy1 and cy2 are the percent of - /// the difference between the keyframe's values. + /// Returns the interpolation type for the specified key frame. + /// Linear is 0, stepped is 1, Bezier is 2. + public float GetCurveType (int frameIndex) { + int index = frameIndex * BEZIER_SIZE; + if (index == curves.Length) return LINEAR; + float type = curves[index]; + if (type == LINEAR) return LINEAR; + if (type == STEPPED) return STEPPED; + return BEZIER; + } + + /// Sets the specified key frame to Bezier interpolation. cx1 and cx2 are from 0 to 1, + /// representing the percent of time between the two key frames. cy1 and cy2 are the percent of the + /// difference between the key frame's values. public void SetCurve (int frameIndex, float cx1, float cy1, float cx2, float cy2) { float tmpx = (-cx1 * 2 + cx2) * 0.03f, tmpy = (-cy1 * 2 + cy2) * 0.03f; float dddfx = ((cx1 - cx2) * 3 + 1) * 0.006f, dddfy = ((cy1 - cy2) * 3 + 1) * 0.006f; @@ -228,6 +274,7 @@ namespace Spine { } } + /// Returns the interpolated percentage for the specified key frame and linear percentage. public float GetCurvePercent (int frameIndex, float percent) { percent = MathUtils.Clamp (percent, 0, 1); float[] curves = this.curves; @@ -240,65 +287,67 @@ namespace Spine { for (int start = i, n = i + BEZIER_SIZE - 1; i < n; i += 2) { x = curves[i]; if (x >= percent) { - float prevX, prevY; - if (i == start) { - prevX = 0; - prevY = 0; - } else { - prevX = curves[i - 2]; - prevY = curves[i - 1]; - } + if (i == start) return curves[i + 1] * percent / x; // First point is 0,0. + float prevX = curves[i - 2], prevY = curves[i - 1]; return prevY + (curves[i + 1] - prevY) * (percent - prevX) / (x - prevX); } } float y = curves[i - 1]; return y + (1 - y) * (percent - x) / (1 - x); // Last point is 1,1. } - public float GetCurveType (int frameIndex) { - return curves[frameIndex * BEZIER_SIZE]; - } } + /// Changes a bone's local . public class RotateTimeline : CurveTimeline, IBoneTimeline { public const int ENTRIES = 2; internal const int PREV_TIME = -2, PREV_ROTATION = -1; internal const int ROTATION = 1; internal int boneIndex; - internal float[] frames; - - public int BoneIndex { get { return boneIndex; } set { boneIndex = value; } } - public float[] Frames { get { return frames; } set { frames = value; } } // time, angle, ... - - override public int PropertyId { - get { return ((int)TimelineType.Rotate << 24) + boneIndex; } - } + internal float[] frames; // time, degrees, ... public RotateTimeline (int frameCount) : base(frameCount) { frames = new float[frameCount << 1]; } - /// Sets the time and value of the specified keyframe. + override public int PropertyId { + get { return ((int)TimelineType.Rotate << 24) + boneIndex; } + } + /// The index of the bone in that will be changed. + public int BoneIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.boneIndex = value; + } + get { + return boneIndex; + } + } + /// The time in seconds and rotation in degrees for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// Sets the time in seconds and the rotation in degrees for the specified key frame. public void SetFrame (int frameIndex, float time, float degrees) { frameIndex <<= 1; frames[frameIndex] = time; frames[frameIndex + ROTATION] = degrees; } - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; float[] frames = this.frames; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. switch (blend) { case MixBlend.Setup: bone.rotation = bone.data.rotation; return; case MixBlend.First: - float r = bone.data.rotation - bone.rotation; - bone.rotation += (r - (16384 - (int)(16384.499999999996 - r / 360)) * 360) * alpha; - return; + float r = bone.data.rotation - bone.rotation; + bone.rotation += (r - (16384 - (int)(16384.499999999996 - r / 360)) * 360) * alpha; + return; } return; } @@ -327,6 +376,7 @@ namespace Spine { float prevRotation = frames[frame + PREV_ROTATION]; float frameTime = frames[frame]; float percent = GetCurvePercent((frame >> 1) - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); + // scope for 'r' to prevent compile error. { float r = frames[frame + ROTATION] - prevRotation; r = prevRotation + (r - (16384 - (int)(16384.499999999996 - r / 360)) * 360) * percent; @@ -343,31 +393,42 @@ namespace Spine { break; } } - } } + /// Changes a bone's local and . public class TranslateTimeline : CurveTimeline, IBoneTimeline { public const int ENTRIES = 3; protected const int PREV_TIME = -3, PREV_X = -2, PREV_Y = -1; protected const int X = 1, Y = 2; internal int boneIndex; - internal float[] frames; - - public int BoneIndex { get { return boneIndex; } set { boneIndex = value; } } - public float[] Frames { get { return frames; } set { frames = value; } } // time, value, value, ... - - override public int PropertyId { - get { return ((int)TimelineType.Translate << 24) + boneIndex; } - } + internal float[] frames; // time, x, y, ... public TranslateTimeline (int frameCount) : base(frameCount) { frames = new float[frameCount * ENTRIES]; } - /// Sets the time and value of the specified keyframe. + override public int PropertyId { + get { return ((int)TimelineType.Translate << 24) + boneIndex; } + } + + /// The index of the bone in that will be changed. + public int BoneIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.boneIndex = value; + } + get { + return boneIndex; + } + } + /// The time in seconds, x, and y values for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + + /// Sets the time in seconds, x, and y values for the specified key frame. public void SetFrame (int frameIndex, float time, float x, float y) { frameIndex *= ENTRIES; frames[frameIndex] = time; @@ -375,11 +436,12 @@ namespace Spine { frames[frameIndex + Y] = y; } - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; float[] frames = this.frames; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. switch (blend) { case MixBlend.Setup: bone.x = bone.data.x; @@ -427,20 +489,22 @@ namespace Spine { } } + /// Changes a bone's local and . public class ScaleTimeline : TranslateTimeline, IBoneTimeline { - override public int PropertyId { - get { return ((int)TimelineType.Scale << 24) + boneIndex; } - } - public ScaleTimeline (int frameCount) : base(frameCount) { } - - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + + override public int PropertyId { + get { return ((int)TimelineType.Scale << 24) + boneIndex; } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; float[] frames = this.frames; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. switch (blend) { case MixBlend.Setup: bone.scaleX = bone.data.scaleX; @@ -530,19 +594,21 @@ namespace Spine { } } + /// Changes a bone's local and . public class ShearTimeline : TranslateTimeline, IBoneTimeline { + public ShearTimeline (int frameCount) + : base(frameCount) { + } + override public int PropertyId { get { return ((int)TimelineType.Shear << 24) + boneIndex; } } - public ShearTimeline (int frameCount) - : base(frameCount) { - } - - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; float[] frames = this.frames; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. switch (blend) { case MixBlend.Setup: bone.shearX = bone.data.shearX; @@ -590,27 +656,38 @@ namespace Spine { } } + /// Changes a slot's . public class ColorTimeline : CurveTimeline, ISlotTimeline { public const int ENTRIES = 5; protected const int PREV_TIME = -5, PREV_R = -4, PREV_G = -3, PREV_B = -2, PREV_A = -1; protected const int R = 1, G = 2, B = 3, A = 4; internal int slotIndex; - internal float[] frames; - - public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } } - public float[] Frames { get { return frames; } set { frames = value; } } // time, r, g, b, a, ... - - override public int PropertyId { - get { return ((int)TimelineType.Color << 24) + slotIndex; } - } + internal float[] frames; // time, r, g, b, a, ... public ColorTimeline (int frameCount) : base(frameCount) { frames = new float[frameCount * ENTRIES]; } - /// Sets the time and value of the specified keyframe. + override public int PropertyId { + get { return ((int)TimelineType.Color << 24) + slotIndex; } + } + + /// The index of the slot in that will be changed. + public int SlotIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.slotIndex = value; + } + get { + return slotIndex; + } + } + /// The time in seconds, red, green, blue, and alpha values for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// Sets the time in seconds, red, green, blue, and alpha for the specified key frame. public void SetFrame (int frameIndex, float time, float r, float g, float b, float a) { frameIndex *= ENTRIES; frames[frameIndex] = time; @@ -620,10 +697,11 @@ namespace Spine { frames[frameIndex + A] = a; } - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; float[] frames = this.frames; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. var slotData = slot.data; switch (blend) { case MixBlend.Setup: @@ -691,6 +769,7 @@ namespace Spine { } } + /// Changes a slot's and for two color tinting. public class TwoColorTimeline : CurveTimeline, ISlotTimeline { public const int ENTRIES = 8; protected const int PREV_TIME = -8, PREV_R = -7, PREV_G = -6, PREV_B = -5, PREV_A = -4; @@ -700,27 +779,29 @@ namespace Spine { internal int slotIndex; internal float[] frames; // time, r, g, b, a, r2, g2, b2, ... - public int SlotIndex { - get { return slotIndex; } - set { - if (value < 0) - throw new ArgumentOutOfRangeException("index must be >= 0."); - slotIndex = value; - } - } - - public float[] Frames { get { return frames; } } - - override public int PropertyId { - get { return ((int)TimelineType.TwoColor << 24) + slotIndex; } - } - public TwoColorTimeline (int frameCount) : base(frameCount) { frames = new float[frameCount * ENTRIES]; } - /// Sets the time and value of the specified keyframe. + override public int PropertyId { + get { return ((int)TimelineType.TwoColor << 24) + slotIndex; } + } + + /// The index of the slot in that will be changed. + public int SlotIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.slotIndex = value; + } + get { + return slotIndex; + } + } + /// The time in seconds, red, green, blue, and alpha values for each key frame. + public float[] Frames { get { return frames; } } + + /// Sets the time in seconds, light, and dark colors for the specified key frame.. public void SetFrame (int frameIndex, float time, float r, float g, float b, float a, float r2, float g2, float b2) { frameIndex *= ENTRIES; frames[frameIndex] = time; @@ -733,7 +814,8 @@ namespace Spine { frames[frameIndex + B2] = b2; } - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; float[] frames = this.frames; if (time < frames[0]) { // Time is before first frame. @@ -834,32 +916,49 @@ namespace Spine { } + /// Changes a slot's . public class AttachmentTimeline : Timeline, ISlotTimeline { internal int slotIndex; - internal float[] frames; + internal float[] frames; // time, ... internal string[] attachmentNames; - public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } } - public float[] Frames { get { return frames; } set { frames = value; } } // time, ... - public string[] AttachmentNames { get { return attachmentNames; } set { attachmentNames = value; } } - public int FrameCount { get { return frames.Length; } } - - public int PropertyId { - get { return ((int)TimelineType.Attachment << 24) + slotIndex; } - } - public AttachmentTimeline (int frameCount) { frames = new float[frameCount]; attachmentNames = new String[frameCount]; } - /// Sets the time and value of the specified keyframe. + public int PropertyId { + get { return ((int)TimelineType.Attachment << 24) + slotIndex; } + } + + /// The number of key frames for this timeline. + public int FrameCount { get { return frames.Length; } } + + /// The index of the slot in that will be changed. + public int SlotIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.slotIndex = value; + } + get { + return slotIndex; + } + } + + /// The time in seconds for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// The attachment name for each key frame. May contain null values to clear the attachment. + public string[] AttachmentNames { get { return attachmentNames; } set { attachmentNames = value; } } + + /// Sets the time in seconds and the attachment name for the specified key frame. public void SetFrame (int frameIndex, float time, String attachmentName) { frames[frameIndex] = time; attachmentNames[frameIndex] = attachmentName; } - public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { string attachmentName; Slot slot = skeleton.slots.Items[slotIndex]; if (direction == MixDirection.Out && blend == MixBlend.Setup) { @@ -881,27 +980,19 @@ namespace Spine { if (time >= frames[frames.Length - 1]) // Time is after last frame. frameIndex = frames.Length - 1; else - frameIndex = Animation.BinarySearch(frames, time, 1) - 1; + frameIndex = Animation.BinarySearch(frames, time) - 1; attachmentName = attachmentNames[frameIndex]; slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slotIndex, attachmentName); } } + /// Changes a slot's to deform a . public class DeformTimeline : CurveTimeline, ISlotTimeline { internal int slotIndex; - internal float[] frames; - internal float[][] frameVertices; internal VertexAttachment attachment; - - public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } } - public float[] Frames { get { return frames; } set { frames = value; } } // time, ... - public float[][] Vertices { get { return frameVertices; } set { frameVertices = value; } } - public VertexAttachment Attachment { get { return attachment; } set { attachment = value; } } - - override public int PropertyId { - get { return ((int)TimelineType.Deform << 24) + attachment.id + slotIndex; } - } + internal float[] frames; // time, ... + internal float[][] frameVertices; public DeformTimeline (int frameCount) : base(frameCount) { @@ -909,13 +1000,39 @@ namespace Spine { frameVertices = new float[frameCount][]; } - /// Sets the time and value of the specified keyframe. + override public int PropertyId { + get { return ((int)TimelineType.Deform << 27) + attachment.id + slotIndex; } + } + + /// The index of the slot in that will be changed. + public int SlotIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.slotIndex = value; + } + get { + return slotIndex; + } + } + /// The attachment that will be deformed. + public VertexAttachment Attachment { get { return attachment; } set { attachment = value; } } + + /// The time in seconds for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// The vertices for each key frame. + public float[][] Vertices { get { return frameVertices; } set { frameVertices = value; } } + + + /// Sets the time in seconds and the vertices for the specified key frame. + /// Vertex positions for an unweighted VertexAttachment, or deform offsets if it has weights. public void SetFrame (int frameIndex, float time, float[] vertices) { frames[frameIndex] = time; frameVertices[frameIndex] = vertices; } - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; VertexAttachment vertexAttachment = slot.attachment as VertexAttachment; if (vertexAttachment == null || !vertexAttachment.ApplyDeform(attachment)) return; @@ -928,7 +1045,7 @@ namespace Spine { float[] frames = this.frames; float[] vertices; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. switch (blend) { case MixBlend.Setup: @@ -1107,31 +1224,38 @@ namespace Spine { } } + /// Fires an when specific animation times are reached. public class EventTimeline : Timeline { - internal float[] frames; + internal float[] frames; // time, ... private Event[] events; - public float[] Frames { get { return frames; } set { frames = value; } } // time, ... - public Event[] Events { get { return events; } set { events = value; } } - public int FrameCount { get { return frames.Length; } } - - public int PropertyId { - get { return ((int)TimelineType.Event << 24); } - } - public EventTimeline (int frameCount) { frames = new float[frameCount]; events = new Event[frameCount]; } - /// Sets the time and value of the specified keyframe. + public int PropertyId { + get { return ((int)TimelineType.Event << 24); } + } + + /// The number of key frames for this timeline. + public int FrameCount { get { return frames.Length; } } + + /// The time in seconds for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// The event for each key frame. + public Event[] Events { get { return events; } set { events = value; } } + + /// Sets the time in seconds and the event for the specified key frame. public void SetFrame (int frameIndex, Event e) { frames[frameIndex] = e.Time; events[frameIndex] = e; } - /// Fires events for frames > lastTime and <= time. - public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + /// Fires events for frames > lastTime and <= time. + public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { if (firedEvents == null) return; float[] frames = this.frames; int frameCount = frames.Length; @@ -1159,31 +1283,40 @@ namespace Spine { } } + /// Changes a skeleton's . public class DrawOrderTimeline : Timeline { - internal float[] frames; + internal float[] frames; // time, ... private int[][] drawOrders; - public float[] Frames { get { return frames; } set { frames = value; } } // time, ... - public int[][] DrawOrders { get { return drawOrders; } set { drawOrders = value; } } - public int FrameCount { get { return frames.Length; } } - - public int PropertyId { - get { return ((int)TimelineType.DrawOrder << 24); } - } - public DrawOrderTimeline (int frameCount) { frames = new float[frameCount]; drawOrders = new int[frameCount][]; } - /// Sets the time and value of the specified keyframe. - /// May be null to use bind pose draw order. + public int PropertyId { + get { return ((int)TimelineType.DrawOrder << 24); } + } + + /// The number of key frames for this timeline. + public int FrameCount { get { return frames.Length; } } + + /// The time in seconds for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } // time, ... + + /// The draw order for each key frame. + /// . + public int[][] DrawOrders { get { return drawOrders; } set { drawOrders = value; } } + + /// Sets the time in seconds and the draw order for the specified key frame. + /// For each slot in the index of the new draw order. May be null to use setup pose + /// draw order.. public void SetFrame (int frameIndex, float time, int[] drawOrder) { frames[frameIndex] = time; drawOrders[frameIndex] = drawOrder; } - public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { ExposedList drawOrder = skeleton.drawOrder; ExposedList slots = skeleton.slots; if (direction == MixDirection.Out && blend == MixBlend.Setup) { @@ -1192,7 +1325,7 @@ namespace Spine { } float[] frames = this.frames; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. if (blend == MixBlend.Setup || blend == MixBlend.First) Array.Copy(slots.Items, 0, drawOrder.Items, 0, slots.Count); return; } @@ -1215,27 +1348,40 @@ namespace Spine { } } + /// Changes an IK constraint's , , + /// , and . public class IkConstraintTimeline : CurveTimeline { public const int ENTRIES = 5; private const int PREV_TIME = -5, PREV_MIX = -4, PREV_BEND_DIRECTION = -3, PREV_COMPRESS = -2, PREV_STRETCH = -1; private const int MIX = 1, BEND_DIRECTION = 2, COMPRESS = 3, STRETCH = 4; internal int ikConstraintIndex; - internal float[] frames; - - public int IkConstraintIndex { get { return ikConstraintIndex; } set { ikConstraintIndex = value; } } - public float[] Frames { get { return frames; } set { frames = value; } } // time, mix, bendDirection, compress, stretch ... - - override public int PropertyId { - get { return ((int)TimelineType.IkConstraint << 24) + ikConstraintIndex; } - } + internal float[] frames; // time, mix, bendDirection, compress, stretch, ... public IkConstraintTimeline (int frameCount) : base(frameCount) { frames = new float[frameCount * ENTRIES]; } - - /// Sets the time, mix, bend direction, compress and stretch of the specified keyframe. + + override public int PropertyId { + get { return ((int)TimelineType.IkConstraint << 24) + ikConstraintIndex; } + } + + /// The index of the IK constraint slot in that will be changed. + public int IkConstraintIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.ikConstraintIndex = value; + } + get { + return ikConstraintIndex; + } + } + + /// The time in seconds, mix, bend direction, compress, and stretch for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } + + /// Sets the time in seconds, mix, bend direction, compress, and stretch for the specified key frame. public void SetFrame (int frameIndex, float time, float mix, int bendDirection, bool compress, bool stretch) { frameIndex *= ENTRIES; frames[frameIndex] = time; @@ -1245,10 +1391,11 @@ namespace Spine { frames[frameIndex + STRETCH] = stretch ? 1 : 0; } - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { IkConstraint constraint = skeleton.ikConstraints.Items[ikConstraintIndex]; float[] frames = this.frames; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. switch (blend) { case MixBlend.Setup: constraint.mix = constraint.data.mix; @@ -1317,26 +1464,39 @@ namespace Spine { } } + /// Changes a transform constraint's mixes. public class TransformConstraintTimeline : CurveTimeline { public const int ENTRIES = 5; private const int PREV_TIME = -5, PREV_ROTATE = -4, PREV_TRANSLATE = -3, PREV_SCALE = -2, PREV_SHEAR = -1; private const int ROTATE = 1, TRANSLATE = 2, SCALE = 3, SHEAR = 4; internal int transformConstraintIndex; - internal float[] frames; - - public int TransformConstraintIndex { get { return transformConstraintIndex; } set { transformConstraintIndex = value; } } - public float[] Frames { get { return frames; } set { frames = value; } } // time, rotate mix, translate mix, scale mix, shear mix, ... - - override public int PropertyId { - get { return ((int)TimelineType.TransformConstraint << 24) + transformConstraintIndex; } - } + internal float[] frames; // time, rotate mix, translate mix, scale mix, shear mix, ... public TransformConstraintTimeline (int frameCount) : base(frameCount) { frames = new float[frameCount * ENTRIES]; } + override public int PropertyId { + get { return ((int)TimelineType.TransformConstraint << 24) + transformConstraintIndex; } + } + + /// The index of the transform constraint slot in that will be changed. + public int TransformConstraintIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.transformConstraintIndex = value; + } + get { + return transformConstraintIndex; + } + } + + /// The time in seconds, rotate mix, translate mix, scale mix, and shear mix for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } // time, rotate mix, translate mix, scale mix, shear mix, ... + + /// The time in seconds, rotate mix, translate mix, scale mix, and shear mix for the specified key frame. public void SetFrame (int frameIndex, float time, float rotateMix, float translateMix, float scaleMix, float shearMix) { frameIndex *= ENTRIES; frames[frameIndex] = time; @@ -1346,11 +1506,12 @@ namespace Spine { frames[frameIndex + SHEAR] = shearMix; } - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { TransformConstraint constraint = skeleton.transformConstraints.Items[transformConstraintIndex]; float[] frames = this.frames; - if (time < frames[0]) { - var data = constraint.data; + if (time < frames[0]) { // Time is before first frame. + TransformConstraintData data = constraint.data; switch (blend) { case MixBlend.Setup: constraint.rotateMix = data.rotateMix; @@ -1406,37 +1567,50 @@ namespace Spine { } } + /// Changes a path constraint's . public class PathConstraintPositionTimeline : CurveTimeline { public const int ENTRIES = 2; protected const int PREV_TIME = -2, PREV_VALUE = -1; protected const int VALUE = 1; internal int pathConstraintIndex; - internal float[] frames; - - override public int PropertyId { - get { return ((int)TimelineType.PathConstraintPosition << 24) + pathConstraintIndex; } - } + internal float[] frames; // time, position, ... public PathConstraintPositionTimeline (int frameCount) : base(frameCount) { frames = new float[frameCount * ENTRIES]; } - public int PathConstraintIndex { get { return pathConstraintIndex; } set { pathConstraintIndex = value; } } - public float[] Frames { get { return frames; } set { frames = value; } } // time, position, ... - - /// Sets the time and value of the specified keyframe. - public void SetFrame (int frameIndex, float time, float value) { - frameIndex *= ENTRIES; - frames[frameIndex] = time; - frames[frameIndex + VALUE] = value; + override public int PropertyId { + get { return ((int)TimelineType.PathConstraintPosition << 24) + pathConstraintIndex; } } - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + /// The index of the path constraint slot in that will be changed. + public int PathConstraintIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.pathConstraintIndex = value; + } + get { + return pathConstraintIndex; + } + } + + /// The time in seconds and path constraint position for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } // time, position, ... + + /// Sets the time in seconds and path constraint position for the specified key frame. + public void SetFrame (int frameIndex, float time, float position) { + frameIndex *= ENTRIES; + frames[frameIndex] = time; + frames[frameIndex + VALUE] = position; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; float[] frames = this.frames; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. switch (blend) { case MixBlend.Setup: constraint.position = constraint.data.position; @@ -1468,19 +1642,21 @@ namespace Spine { } } + /// Changes a path constraint's . public class PathConstraintSpacingTimeline : PathConstraintPositionTimeline { - override public int PropertyId { - get { return ((int)TimelineType.PathConstraintSpacing << 24) + pathConstraintIndex; } - } - public PathConstraintSpacingTimeline (int frameCount) : base(frameCount) { } - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + override public int PropertyId { + get { return ((int)TimelineType.PathConstraintSpacing << 24) + pathConstraintIndex; } + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, MixBlend blend, + MixDirection direction) { PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; float[] frames = this.frames; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. switch (blend) { case MixBlend.Setup: constraint.spacing = constraint.data.spacing; @@ -1513,27 +1689,39 @@ namespace Spine { } } + /// Changes a path constraint's mixes. public class PathConstraintMixTimeline : CurveTimeline { public const int ENTRIES = 3; private const int PREV_TIME = -3, PREV_ROTATE = -2, PREV_TRANSLATE = -1; private const int ROTATE = 1, TRANSLATE = 2; internal int pathConstraintIndex; - internal float[] frames; + internal float[] frames; // time, rotate mix, translate mix, ... - public int PathConstraintIndex { get { return pathConstraintIndex; } set { pathConstraintIndex = value; } } - public float[] Frames { get { return frames; } set { frames = value; } } // time, rotate mix, translate mix, ... + public PathConstraintMixTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * ENTRIES]; + } override public int PropertyId { get { return ((int)TimelineType.PathConstraintMix << 24) + pathConstraintIndex; } } - public PathConstraintMixTimeline (int frameCount) - : base(frameCount) { - frames = new float[frameCount * ENTRIES]; - } + /// The index of the path constraint slot in that will be changed. + public int PathConstraintIndex { + set { + if (value < 0) throw new ArgumentOutOfRangeException("index must be >= 0."); + this.pathConstraintIndex = value; + } + get { + return pathConstraintIndex; + } + } - /// Sets the time and mixes of the specified keyframe. + /// The time in seconds, rotate mix, and translate mix for each key frame. + public float[] Frames { get { return frames; } set { frames = value; } } // time, rotate mix, translate mix, ... + + /// The time in seconds, rotate mix, and translate mix for the specified key frame. public void SetFrame (int frameIndex, float time, float rotateMix, float translateMix) { frameIndex *= ENTRIES; frames[frameIndex] = time; @@ -1541,10 +1729,11 @@ namespace Spine { frames[frameIndex + TRANSLATE] = translateMix; } - override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { + override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, + MixDirection direction) { PathConstraint constraint = skeleton.pathConstraints.Items[pathConstraintIndex]; float[] frames = this.frames; - if (time < frames[0]) { + if (time < frames[0]) { // Time is before first frame. switch (blend) { case MixBlend.Setup: constraint.rotateMix = constraint.data.rotateMix; diff --git a/spine-csharp/src/AnimationState.cs b/spine-csharp/src/AnimationState.cs index 405dcda7d..4141602d9 100644 --- a/spine-csharp/src/AnimationState.cs +++ b/spine-csharp/src/AnimationState.cs @@ -32,27 +32,53 @@ using System; using System.Collections.Generic; namespace Spine { + + /// + /// + /// Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies + /// multiple animations on top of each other (layering). + /// + /// See Applying Animations in the Spine Runtimes Guide. + /// public class AnimationState { static readonly Animation EmptyAnimation = new Animation("", new ExposedList(), 0); - internal const int Subsequent = 0, First = 1, Hold = 2, HoldMix = 3; + + /// 1) A previously applied timeline has set this property. + /// Result: Mix from the current pose to the timeline pose. + internal const int Subsequent = 0; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry applied after this one does not have a timeline to set this property. + /// Result: Mix from the setup pose to the timeline pose. + internal const int First = 1; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry to be applied does have a timeline to set this property. + /// 3) The next track entry after that one does not have a timeline to set this property. + /// Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations + /// that key the same property. A subsequent timeline will set this property using a mix. + internal const int Hold = 2; + /// 1) This is the first timeline to set this property. + /// 2) The next track entry to be applied does have a timeline to set this property. + /// 3) The next track entry after that one does have a timeline to set this property. + /// 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property. + /// Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than + /// 2 track entries in a row have a timeline that sets the same property. + /// Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid + /// "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A + /// (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into + /// place. + internal const int HoldMix = 3; protected AnimationStateData data; - - 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 bool animationsChanged; - - private float timeScale = 1; - - public AnimationStateData Data { get { return data; } } - - /// A list of tracks that have animations, which may contain nulls. - public ExposedList Tracks { get { return tracks; } } - public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + // difference to libgdx reference: delegates are used for event callbacks instead of 'Array listeners'. + internal void OnStart (TrackEntry entry) { if (Start != null) Start(entry); } + internal void OnInterrupt (TrackEntry entry) { if (Interrupt != null) Interrupt(entry); } + internal void OnEnd (TrackEntry entry) { if (End != null) End(entry); } + internal void OnDispose (TrackEntry entry) { if (Dispose != null) Dispose(entry); } + internal void OnComplete (TrackEntry entry) { if (Complete != null) Complete(entry); } + internal void OnEvent (TrackEntry entry, Event e) { if (Event != null) Event(entry, e); } public delegate void TrackEntryDelegate(TrackEntry trackEntry); public event TrackEntryDelegate Start, Interrupt, End, Dispose, Complete; @@ -60,6 +86,13 @@ namespace Spine { public delegate void TrackEntryEventDelegate(TrackEntry trackEntry, Event e); public event TrackEntryEventDelegate Event; + private readonly EventQueue queue; // Initialized by constructor. + private readonly HashSet propertyIDs = new HashSet(); + private bool animationsChanged; + private float timeScale = 1; + + private readonly Pool trackEntryPool = new Pool(); + public AnimationState(AnimationStateData data) { if (data == null) throw new ArgumentNullException("data", "data cannot be null."); this.data = data; @@ -71,7 +104,7 @@ namespace Spine { } /// - /// Increments the track entry trackTimes, setting queued animations as current if needed + /// Increments the track entry , setting queued animations as current if needed. /// delta time public void Update (float delta) { delta *= timeScale; @@ -116,7 +149,7 @@ namespace Spine { } if (current.mixingFrom != null && UpdateMixingFrom(current, delta)) { // End mixing from entries once all have completed. - var from = current.mixingFrom; + TrackEntry from = current.mixingFrom; current.mixingFrom = null; if (from != null) from.mixingTo = null; while (from != null) { @@ -161,6 +194,7 @@ namespace Spine { /// /// 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. + /// True if any animations were applied. public bool Apply (Skeleton skeleton) { if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); if (animationsChanged) AnimationsChanged(); @@ -168,7 +202,7 @@ namespace Spine { var events = this.events; bool applied = false; var tracksItems = tracks.Items; - for (int i = 0, m = tracks.Count; i < m; i++) { + for (int i = 0, n = tracks.Count; i < n; i++) { TrackEntry current = tracksItems[i]; if (current == null || current.delay > 0) continue; applied = true; @@ -203,7 +237,8 @@ namespace Spine { 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); + ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1, + firstFrame); else timeline.Apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, MixDirection.In); } @@ -282,7 +317,8 @@ namespace Spine { var rotateTimeline = timeline as RotateTimeline; if (rotateTimeline != null) { - ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, alpha, timelineBlend, timelinesRotation, i << 1, firstFrame); + ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, alpha, timelineBlend, timelinesRotation, i << 1, + firstFrame); } else { if (timelineBlend == MixBlend.Setup) { if (timeline is AttachmentTimeline) { @@ -351,7 +387,7 @@ namespace Spine { } } - // Mix between rotations using the direction of the shortest route on the first frame while detecting crosses. + // Mix between rotations using the direction of the shortest route on the first frame. float total, diff = r2 - r1; diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360; if (diff == 0) { @@ -391,7 +427,7 @@ namespace Spine { var eventsItems = events.Items; int i = 0, n = events.Count; for (; i < n; i++) { - var e = eventsItems[i]; + Event e = eventsItems[i]; if (e.time < trackLastWrapped) break; if (e.time > animationEnd) continue; // Discard events outside animation start/end. queue.Event(entry, e); @@ -414,9 +450,11 @@ namespace Spine { } /// - /// Removes all animations from all tracks, leaving skeletons in their previous pose. + /// Removes all animations from all tracks, leaving skeletons in their current pose. + /// /// It may be desired to use to mix the skeletons back to the setup pose, - /// rather than leaving them in their previous pose. + /// rather than leaving them in their current pose. + /// public void ClearTracks () { bool oldDrainDisabled = queue.drainDisabled; queue.drainDisabled = true; @@ -429,9 +467,11 @@ namespace Spine { } /// - /// Removes all animations from the tracks, leaving skeletons in their previous pose. - /// It may be desired to use to mix the skeletons back to the setup pose, - /// rather than leaving them in their previous pose. + /// Removes all animations from the track, leaving skeletons in their current pose. + /// + /// It may be desired to use to mix the skeletons back to the setup pose, + /// rather than leaving them in their current pose. + /// public void ClearTrack (int trackIndex) { if (trackIndex >= tracks.Count) return; TrackEntry current = tracks.Items[trackIndex]; @@ -467,7 +507,7 @@ namespace Spine { from.mixingTo = current; current.mixTime = 0; - // Store interrupted mix percentage. + // Store the interrupted mix percentage. if (from.mixingFrom != null && from.mixDuration > 0) current.interruptAlpha *= Math.Min(1, from.mixTime / from.mixDuration); @@ -485,13 +525,12 @@ namespace Spine { return SetAnimation(trackIndex, animation, loop); } - /// Sets the current animation for a track, discarding any queued animations. - /// If true, the animation will repeat. - /// If false, it will not, instead its last frame is applied if played beyond its duration. - /// In either case determines when the track is cleared. - /// - /// A track entry to allow further customization of animation playback. References to the track entry must not be kept - /// after . + /// Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never + /// applied to a skeleton, it is replaced (not mixed from). + /// If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + /// duration. In either case determines when the track is cleared. + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) { if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null."); bool interrupt = true; @@ -504,7 +543,7 @@ namespace Spine { queue.End(current); DisposeNext(current); current = current.mixingFrom; - interrupt = false; + interrupt = false; // mixingFrom is current again, but don't interrupt it twice. } else { DisposeNext(current); } @@ -523,17 +562,16 @@ namespace Spine { return AddAnimation(trackIndex, animation, loop, delay); } - /// Adds an animation to be played delay seconds after the current or last queued animation - /// for a track. If the track is empty, it is equivalent to calling . + /// Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is + /// equivalent to calling . /// - /// delay Seconds to begin this animation after the start of the previous animation. If <= 0, uses the duration of the - /// previous track entry minus any mix duration (from the ) plus the - /// specifieddelay (ie the mix ends at (delay = 0) or before (delay < 0) the previous - /// track entry duration). If the previous entry is looping, its next loop completion is used - /// instead of the duration. + /// If > 0, sets . If <= 0, the delay set is the duration of the previous track entry + /// minus any mix duration (from the {@link AnimationStateData}) plus the specified Delay (ie the mix + /// ends at (Delay = 0) or before (Delay < 0) the previous track entry duration). If the + /// previous entry is looping, its next loop completion is used instead of its duration. /// /// A track entry to allow further customization of animation playback. References to the track entry must not be kept - /// after + /// after the event occurs. public TrackEntry AddAnimation (int trackIndex, Animation animation, bool loop, float delay) { if (animation == null) throw new ArgumentNullException("animation", "animation cannot be null."); @@ -569,7 +607,21 @@ namespace Spine { } /// - /// Sets an empty animation for a track, discarding any queued animations, and mixes to it over the specified mix duration. + /// Sets an empty animation for a track, discarding any queued animations, and sets the track entry's + /// . An empty animation has no timelines and serves as a placeholder for mixing in or out. + /// + /// Mixing out is done by setting an empty animation with a mix duration using either , + /// , or . Mixing to an empty animation causes + /// the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation + /// transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of + /// 0 still mixes out over one frame. + /// + /// Mixing in is done by first setting an empty animation, then adding an animation using + /// and on the returned track entry, set the + /// . Mixing from an empty animation causes the new animation to be applied more and + /// more over the mix duration. Properties keyed in the new animation transition from the value from lower tracks or from the + /// setup pose value if no lower tracks key the property to the value keyed in the new animation. + /// public TrackEntry SetEmptyAnimation (int trackIndex, float mixDuration) { TrackEntry entry = SetAnimation(trackIndex, AnimationState.EmptyAnimation, false); entry.mixDuration = mixDuration; @@ -578,15 +630,19 @@ namespace Spine { } /// - /// 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 . - /// + /// Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's + /// . If the track is empty, it is equivalent to calling + /// . + /// /// 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. + /// If > 0, sets . If <= 0, the delay set is the duration of the previous track entry + /// minus any mix duration plus the specified Delay (ie the mix ends at (Delay = 0) or + /// before (Delay < 0) the previous track entry duration). If the previous entry is looping, its next + /// loop completion is used instead of its duration. + /// A track entry to allow further customization of animation playback. References to the track entry must not be kept + /// after the event occurs. + /// public TrackEntry AddEmptyAnimation (int trackIndex, float mixDuration, float delay) { if (delay <= 0) delay -= mixDuration; TrackEntry entry = AddAnimation(trackIndex, AnimationState.EmptyAnimation, false, delay); @@ -596,13 +652,14 @@ namespace Spine { } /// - /// Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix duration. + /// 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) { bool oldDrainDisabled = queue.drainDisabled; queue.drainDisabled = true; for (int i = 0, n = tracks.Count; i < n; i++) { TrackEntry current = tracks.Items[i]; - if (current != null) SetEmptyAnimation(i, mixDuration); + if (current != null) SetEmptyAnimation(current.trackIndex, mixDuration); } queue.drainDisabled = oldDrainDisabled; queue.Drain(); @@ -610,8 +667,7 @@ namespace Spine { private TrackEntry ExpandToIndex (int index) { if (index < tracks.Count) return tracks.Items[index]; - while (index >= tracks.Count) - tracks.Add(null); + tracks.Resize(index + 1); return null; } @@ -664,7 +720,7 @@ namespace Spine { var tracksItems = tracks.Items; for (int i = 0, n = tracks.Count; i < n; i++) { - var entry = tracksItems[i]; + TrackEntry entry = tracksItems[i]; if (entry == null) continue; // Move to last entry, then iterate in reverse (the order animations are applied). while (entry.mixingFrom != null) @@ -727,9 +783,39 @@ namespace Spine { /// 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]; + if (trackIndex >= tracks.Count) return null; + return tracks.Items[trackIndex]; } + /// Discards all listener notifications that have not yet been delivered. This can be useful to call from an + /// AnimationState event subscriber when it is known that further notifications that may have been already queued for delivery + /// are not wanted because new animations are being set. + public void ClearListenerNotifications () { + queue.Clear(); + } + + /// + /// Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower + /// or faster. Defaults to 1. + /// + /// See TrackEntry for affecting a single animation. + /// + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + /// The AnimationStateData to look up mix durations. + public AnimationStateData Data { + get { + return data; + } + set { + if (data == null) throw new ArgumentNullException("data", "data cannot be null."); + this.data = value; + } + } + + /// A list of tracks that have animations, which may contain nulls. + public ExposedList Tracks { get { return tracks; } } + override public string ToString () { var buffer = new System.Text.StringBuilder(); for (int i = 0, n = tracks.Count; i < n; i++) { @@ -738,22 +824,31 @@ namespace Spine { if (buffer.Length > 0) buffer.Append(", "); buffer.Append(entry.ToString()); } - return buffer.Length == 0 ? "" : buffer.ToString(); + if (buffer.Length == 0) return ""; + return buffer.ToString(); } - - internal void OnStart (TrackEntry entry) { if (Start != null) Start(entry); } - internal void OnInterrupt (TrackEntry entry) { if (Interrupt != null) Interrupt(entry); } - internal void OnEnd (TrackEntry entry) { if (End != null) End(entry); } - internal void OnDispose (TrackEntry entry) { if (Dispose != null) Dispose(entry); } - internal void OnComplete (TrackEntry entry) { if (Complete != null) Complete(entry); } - internal void OnEvent (TrackEntry entry, Event e) { if (Event != null) Event(entry, e); } } - /// State for the playback of an animation. + /// + /// + /// Stores settings and other state for the playback of an animation on an track. + /// + /// References to a track entry must not be kept after the event occurs. + /// public class TrackEntry : Pool.IPoolable { internal Animation animation; internal TrackEntry next, mixingFrom, mixingTo; + // difference to libgdx reference: delegates are used for event callbacks instead of 'AnimationStateListener listener'. + public event AnimationState.TrackEntryDelegate Start, Interrupt, End, Dispose, Complete; + public event AnimationState.TrackEntryEventDelegate Event; + internal void OnStart () { if (Start != null) Start(this); } + internal void OnInterrupt () { if (Interrupt != null) Interrupt(this); } + internal void OnEnd () { if (End != null) End(this); } + internal void OnDispose () { if (Dispose != null) Dispose(this); } + internal void OnComplete () { if (Complete != null) Complete(this); } + internal void OnEvent (Event e) { if (Event != null) Event(this, e); } + internal int trackIndex; internal bool loop, holdPrevious; @@ -772,67 +867,79 @@ namespace Spine { mixingFrom = null; mixingTo = null; animation = null; - timelineMode.Clear(); - timelineHoldMix.Clear(); - timelinesRotation.Clear(); - + // replaces 'listener = null;' since delegates are used for event callbacks Start = null; Interrupt = null; End = null; Dispose = null; Complete = null; Event = null; + timelineMode.Clear(); + timelineHoldMix.Clear(); + timelinesRotation.Clear(); } /// The index of the track where this entry is either current or queued. + /// public int TrackIndex { get { return trackIndex; } } /// The animation to apply for this track entry. public Animation Animation { get { return animation; } } /// - /// If true, the animation will repeat. If false, it will not, instead its last frame is applied if played beyond its duration. + /// If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its + /// duration. public bool Loop { get { return loop; } set { loop = value; } } /// - /// Seconds to postpone playing the animation. When a track entry is the current track entry, delay postpones incrementing - /// the track time. When a track entry is queued, delay is the time from the start of the previous animation to when the - /// track entry will become the current track entry. affects the delay. + /// + /// Seconds to postpone playing the animation. When this track entry is the current track entry, Delay + /// postpones incrementing the . When this track entry is queued, Delay is the time from + /// the start of the previous animation to when this track entry will become the current track entry (ie when the previous + /// track entry >= this track entry's Delay). + /// + /// affects the delay. + /// public float Delay { get { return delay; } set { delay = value; } } /// /// Current time in seconds this track entry has been the current track entry. The track time determines - /// . The track time can be set to start the animation at a time other than 0, without affecting looping. + /// . The track time can be set to start the animation at a time other than 0, without affecting + /// looping. public float TrackTime { get { return trackTime; } set { trackTime = value; } } /// - /// The track time in seconds when this animation will be removed from the track. Defaults to the animation duration for - /// non-looping animations and to for looping animations. If the track end time is reached and no - /// other animations are queued for playback, and mixing from any previous animations is complete, properties keyed by the animation, - /// are set to the setup pose and the track is cleared. - /// - /// It may be desired to use to mix the properties back to the - /// setup pose over time, rather than have it happen instantly. + /// + /// The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float + /// value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time + /// is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the + /// properties keyed by the animation are set to the setup pose and the track is cleared. + /// + /// It may be desired to use rather than have the animation + /// abruptly cease being applied. /// public float TrackEnd { get { return trackEnd; } set { trackEnd = value; } } /// - /// Seconds when this animation starts, both initially and after looping. Defaults to 0. - /// - /// When changing the animation start time, it often makes sense to set to the same value to - /// prevent timeline keys before the start time from triggering. + /// + /// Seconds when this animation starts, both initially and after looping. Defaults to 0. + /// + /// When changing the AnimationStart time, it often makes sense to set to the same + /// value to prevent timeline keys before the start time from triggering. /// public float AnimationStart { get { return animationStart; } set { animationStart = value; } } /// /// Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will - /// loop back to at this time. Defaults to the animation duration. + /// loop back to at this time. Defaults to the animation . + /// public float AnimationEnd { get { return animationEnd; } set { animationEnd = value; } } /// /// The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this - /// animation is applied, event timelines will fire all events between the animation last time (exclusive) and animation time - /// (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation is applied. + /// animation is applied, event timelines will fire all events between the AnimationLast time (exclusive) and + /// AnimationTime (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation + /// is applied. public float AnimationLast { get { return animationLast; } set { @@ -842,8 +949,9 @@ namespace Spine { } /// - /// Uses to compute the animation time between . and - /// . When the track time is 0, the animation time is equal to the animation start time. + /// Uses to compute the AnimationTime, which is between + /// and . When the TrackTime is 0, the AnimationTime is equal to the + /// AnimationStart time. /// public float AnimationTime { get { @@ -857,120 +965,129 @@ namespace Spine { } /// - /// Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower or - /// faster. Defaults to 1. - /// - /// is not affected by track entry time scale, so may need to - // be adjusted to match the animation speed. - /// - /// When using with a delay <= 0, note the + /// + /// Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or + /// faster. Defaults to 1. + /// + /// is not affected by track entry time scale, so may need to be adjusted to + /// match the animation speed. + /// + /// When using with a Delay <= 0, note the /// { is set using the mix duration from the , assuming time scale to be 1. If - /// the time scale is not 1, the delay may need to be adjusted. + /// the time scale is not 1, the delay may need to be adjusted. + /// + /// See AnimationState for affecting all animations. /// public float TimeScale { get { return timeScale; } set { timeScale = value; } } /// - /// Values less than 1 mix this animation with the last skeleton pose. Defaults to 1, which overwrites the last skeleton pose with - /// this animation. - /// - /// Typically track 0 is used to completely pose the skeleton, then alpha can be used on higher tracks. It doesn't make sense - /// to use alpha on track 0 if the skeleton pose is from the last frame render. + /// + /// Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults + /// to 1, which overwrites the skeleton's current pose with this animation. + /// + /// Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to + /// use alpha on track 0 if the skeleton pose is from the last frame render. /// public float Alpha { get { return alpha; } set { alpha = value; } } /// - /// When the mix percentage (mix time / mix duration) is less than the event threshold, event timelines for the animation - /// being mixed out will be applied. Defaults to 0, so event timelines are not applied for an animation being mixed out. + /// When the mix percentage ( / ) is less than the + /// EventThreshold, event timelines are applied while this animation is being mixed out. Defaults to 0, so event + /// timelines are not applied while this animation is being mixed out. + /// public float EventThreshold { get { return eventThreshold; } set { eventThreshold = value; } } /// - /// When the mix percentage (mix time / mix duration) is less than the attachment threshold, attachment timelines for the - /// animation being mixed out will be applied. Defaults to 0, so attachment timelines are not applied for an animation being - /// mixed out. + /// When the mix percentage ( / ) is less than the + /// AttachmentThreshold, attachment timelines are applied while this animation is being mixed out. Defaults to + /// 0, so attachment timelines are not applied while this animation is being mixed out. + /// public float AttachmentThreshold { get { return attachmentThreshold; } set { attachmentThreshold = value; } } /// - /// When the mix percentage (mix time / mix duration) is less than the draw order threshold, draw order timelines for the - /// animation being mixed out will be applied. Defaults to 0, so draw order timelines are not applied for an animation being - /// mixed out. + /// When the mix percentage ( / ) is less than the + /// DrawOrderThreshold, draw order timelines are applied while this animation is being mixed out. Defaults to 0, + /// so draw order timelines are not applied while this animation is being mixed out. /// public float DrawOrderThreshold { get { return drawOrderThreshold; } set { drawOrderThreshold = value; } } /// - /// The animation queued to start after this animation, or null. + /// The animation queued to start after this animation, or null. Next makes up a linked list. public TrackEntry Next { get { return next; } } /// /// Returns true if at least one loop has been completed. + /// public bool IsComplete { get { return trackTime >= animationEnd - animationStart; } } /// - /// 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. + /// Seconds from 0 to the when mixing from the previous animation to this animation. May be + /// slightly more than MixDuration 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 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 , - /// not a mix duration set afterward. - /// - /// + /// Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData + /// based on the animation before this animation (if any). + /// + /// The MixDuration can be set manually rather than use the value from + /// . In that case, the MixDuration can be set for a new + /// track entry only before is first called. + /// + /// When using with a Delay <= 0, note the + /// is set using the mix duration from the , not a mix duration set + /// afterward. /// public float MixDuration { get { return mixDuration; } set { mixDuration = value; } } /// - /// Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}, which - /// replaces the values from the lower tracks with the animation values. {@link MixBlend#add} adds the animation values to - /// the values from the lower tracks. /// - /// The mixBlend can be set for a new track entry only before - /// AnimationState.Apply(Skeleton) is first called. - /// + /// Controls how properties keyed in the animation are mixed with lower tracks. Defaults to , which + /// replaces the values from the lower tracks with the animation values. adds the animation values to + /// the values from the lower tracks. + /// + /// The MixBlend can be set for a new track entry only before is first + /// called. /// public MixBlend MixBlend { get { return mixBlend; } set { mixBlend = value; } } /// /// The track entry for the previous animation when mixing from the previous animation to this animation, or null if no - /// mixing is currently occuring. When mixing from multiple animations, MixingFrom makes up a linked list. + /// 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. + /// The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is + /// currently occuring. When mixing to multiple animations, MixingTo makes up a linked list. + public TrackEntry MixingTo { get { return mixingTo; } } + + /// + /// + /// 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); } - internal void OnInterrupt () { if (Interrupt != null) Interrupt(this); } - internal void OnEnd () { if (End != null) End(this); } - internal void OnDispose () { if (Dispose != null) Dispose(this); } - internal void OnComplete () { if (Complete != null) Complete(this); } - internal void OnEvent (Event e) { if (Event != null) Event(this, e); } - /// /// /// Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the - /// long way around when using and starting animations on other tracks. - /// + /// long way around when using and starting animations on other tracks. /// /// Mixing with involves finding a rotation between two others, which has two possible solutions: - /// the short way or the long way around.The two rotations likely change over time, so which direction is the short or long - /// way also changes.If the short way was always chosen, bones would flip to the other side when that direction became the - /// long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. - /// + /// the short way or the long way around. The two rotations likely change over time, so which direction is the short or long + /// way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the + /// long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. /// public void ResetRotationDirections () { timelinesRotation.Clear();