diff --git a/spine-csharp/spine-csharp_xna.csproj b/spine-csharp/spine-csharp_xna.csproj
index 514a4cd93..85f5f33ca 100644
--- a/spine-csharp/spine-csharp_xna.csproj
+++ b/spine-csharp/spine-csharp_xna.csproj
@@ -105,6 +105,8 @@
+
+
diff --git a/spine-csharp/src/Animation.cs b/spine-csharp/src/Animation.cs
index 39d66ba03..b11df2ac2 100644
--- a/spine-csharp/src/Animation.cs
+++ b/spine-csharp/src/Animation.cs
@@ -98,6 +98,22 @@ namespace Spine {
}
}
+ /// After the first and before the last entry.
+ internal static int binarySearch (float[] values, float target) {
+ int low = 0;
+ int high = values.Length - 2;
+ if (high == 0) return 1;
+ int current = (int)((uint)high >> 1);
+ while (true) {
+ if (values[(current + 1)] <= target)
+ low = current + 1;
+ else
+ high = current;
+ if (low == high) return (low + 1);
+ current = (int)((uint)(low + high) >> 1);
+ }
+ }
+
internal static int linearSearch (float[] values, float target, int step) {
for (int i = 0, last = values.Length - step; i <= last; i += step)
if (values[i] > target) return i;
@@ -107,78 +123,51 @@ namespace Spine {
public interface Timeline {
/// Sets the value(s) for the specified time.
- void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha);
+ /// May be null to not collect fired events.
+ void Apply (Skeleton skeleton, float lastTime, float time, List events, float alpha);
}
/// Base class for frames that use an interpolation bezier curve.
abstract public class CurveTimeline : Timeline {
- static protected float LINEAR = 0;
- static protected float STEPPED = -1;
- static protected int BEZIER_SEGMENTS = 10;
+ protected const float LINEAR = 0, STEPPED = 1, BEZIER = 2;
+ protected const int BEZIER_SEGMENTS = 10, BEZIER_SIZE = BEZIER_SEGMENTS * 2 - 1;
- private float[] curves; // dfx, dfy, ddfx, ddfy, dddfx, dddfy, ...
- public int FrameCount { get { return curves.Length / 6 + 1; } }
+ private float[] curves; // type, x, y, ...
+ public int FrameCount { get { return curves.Length / BEZIER_SIZE + 1; } }
public CurveTimeline (int frameCount) {
- curves = new float[(frameCount - 1) * 6];
+ curves = new float[(frameCount - 1) * BEZIER_SIZE];
}
abstract public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha);
public void SetLinear (int frameIndex) {
- curves[frameIndex * 6] = LINEAR;
+ curves[frameIndex * BEZIER_SIZE] = LINEAR;
}
public void SetStepped (int frameIndex) {
- curves[frameIndex * 6] = STEPPED;
+ 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.
public void SetCurve (int frameIndex, float cx1, float cy1, float cx2, float cy2) {
- float subdiv_step = 1f / BEZIER_SEGMENTS;
- float subdiv_step2 = subdiv_step * subdiv_step;
- float subdiv_step3 = subdiv_step2 * subdiv_step;
- float pre1 = 3 * subdiv_step;
- float pre2 = 3 * subdiv_step2;
- float pre4 = 6 * subdiv_step2;
- float pre5 = 6 * subdiv_step3;
- float tmp1x = -cx1 * 2 + cx2;
- float tmp1y = -cy1 * 2 + cy2;
- float tmp2x = (cx1 - cx2) * 3 + 1;
- float tmp2y = (cy1 - cy2) * 3 + 1;
- int i = frameIndex * 6;
- float[] curves = this.curves;
- curves[i] = cx1 * pre1 + tmp1x * pre2 + tmp2x * subdiv_step3;
- curves[i + 1] = cy1 * pre1 + tmp1y * pre2 + tmp2y * subdiv_step3;
- curves[i + 2] = tmp1x * pre4 + tmp2x * pre5;
- curves[i + 3] = tmp1y * pre4 + tmp2y * pre5;
- curves[i + 4] = tmp2x * pre5;
- curves[i + 5] = tmp2y * pre5;
- }
+ float subdiv1 = 1f / BEZIER_SEGMENTS, subdiv2 = subdiv1 * subdiv1, subdiv3 = subdiv2 * subdiv1;
+ float pre1 = 3 * subdiv1, pre2 = 3 * subdiv2, pre4 = 6 * subdiv2, pre5 = 6 * subdiv3;
+ float tmp1x = -cx1 * 2 + cx2, tmp1y = -cy1 * 2 + cy2, tmp2x = (cx1 - cx2) * 3 + 1, tmp2y = (cy1 - cy2) * 3 + 1;
+ float dfx = cx1 * pre1 + tmp1x * pre2 + tmp2x * subdiv3, dfy = cy1 * pre1 + tmp1y * pre2 + tmp2y * subdiv3;
+ float ddfx = tmp1x * pre4 + tmp2x * pre5, ddfy = tmp1y * pre4 + tmp2y * pre5;
+ float dddfx = tmp2x * pre5, dddfy = tmp2y * pre5;
- public float GetCurvePercent (int frameIndex, float percent) {
- int curveIndex = frameIndex * 6;
+ int i = frameIndex * BEZIER_SIZE;
float[] curves = this.curves;
- float dfx = curves[curveIndex];
- if (dfx == LINEAR) return percent;
- if (dfx == STEPPED) return 0;
- float dfy = curves[curveIndex + 1];
- float ddfx = curves[curveIndex + 2];
- float ddfy = curves[curveIndex + 3];
- float dddfx = curves[curveIndex + 4];
- float dddfy = curves[curveIndex + 5];
+ curves[i++] = BEZIER;
+
float x = dfx, y = dfy;
- int i = BEZIER_SEGMENTS - 2;
- while (true) {
- if (x >= percent) {
- float lastX = x - dfx;
- float lastY = y - dfy;
- return lastY + (y - lastY) * (percent - lastX) / (x - lastX);
- }
- if (i == 0) break;
- i--;
+ for (int n = i + BEZIER_SIZE - 1; i < n; i += 2) {
+ curves[i] = x;
+ curves[i + 1] = y;
dfx += ddfx;
dfy += ddfy;
ddfx += dddfx;
@@ -186,13 +175,38 @@ namespace Spine {
x += dfx;
y += dfy;
}
+ }
+
+ public float GetCurvePercent (int frameIndex, float percent) {
+ float[] curves = this.curves;
+ int i = frameIndex * BEZIER_SIZE;
+ float type = curves[i];
+ if (type == LINEAR) return percent;
+ if (type == STEPPED) return 0;
+ i++;
+ float x = 0;
+ 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];
+ }
+ 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 class RotateTimeline : CurveTimeline {
- static protected int LAST_FRAME_TIME = -2;
- static protected int FRAME_VALUE = 1;
+ protected const int LAST_FRAME_TIME = -2;
+ protected const int FRAME_VALUE = 1;
internal int boneIndex;
internal float[] frames;
@@ -235,7 +249,7 @@ namespace Spine {
float lastFrameValue = frames[frameIndex - 1];
float frameTime = frames[frameIndex];
float percent = 1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime);
- percent = GetCurvePercent(frameIndex / 2 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
+ percent = GetCurvePercent((frameIndex >> 1) - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
amount = frames[frameIndex + FRAME_VALUE] - lastFrameValue;
while (amount > 180)
@@ -252,9 +266,9 @@ namespace Spine {
}
public class TranslateTimeline : CurveTimeline {
- static protected int LAST_FRAME_TIME = -3;
- static protected int FRAME_X = 1;
- static protected int FRAME_Y = 2;
+ protected const int LAST_FRAME_TIME = -3;
+ protected const int FRAME_X = 1;
+ protected const int FRAME_Y = 2;
internal int boneIndex;
internal float[] frames;
@@ -330,11 +344,11 @@ namespace Spine {
}
public class ColorTimeline : CurveTimeline {
- static protected int LAST_FRAME_TIME = -5;
- static protected int FRAME_R = 1;
- static protected int FRAME_G = 2;
- static protected int FRAME_B = 3;
- static protected int FRAME_A = 4;
+ protected const int LAST_FRAME_TIME = -5;
+ protected const int FRAME_R = 1;
+ protected const int FRAME_G = 2;
+ protected const int FRAME_B = 3;
+ protected const int FRAME_A = 4;
internal int slotIndex;
internal float[] frames;
@@ -423,13 +437,14 @@ namespace Spine {
public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) {
float[] frames = this.frames;
- if (time < frames[0]) return; // Time is before first frame.
+ if (time < frames[0]) {
+ if (lastTime > time) Apply(skeleton, lastTime, int.MaxValue, null, 0);
+ return;
+ } else if (lastTime > time) //
+ lastTime = -1;
- int frameIndex;
- if (time >= frames[frames.Length - 1]) // Time is after last frame.
- frameIndex = frames.Length - 1;
- else
- frameIndex = Animation.binarySearch(frames, time, 1) - 1;
+ int frameIndex = time >= frames[frames.Length - 1] ? frames.Length - 1 : Animation.binarySearch(frames, time) - 1;
+ if (frames[frameIndex] <= lastTime) return;
String attachmentName = attachmentNames[frameIndex];
skeleton.slots[slotIndex].Attachment =
@@ -473,7 +488,7 @@ namespace Spine {
if (lastTime < frames[0])
frameIndex = 0;
else {
- frameIndex = Animation.binarySearch(frames, lastTime, 1);
+ frameIndex = Animation.binarySearch(frames, lastTime);
float frame = frames[frameIndex];
while (frameIndex > 0) { // Fire multiple events with the same frame.
if (frames[frameIndex - 1] != frame) break;
@@ -513,7 +528,7 @@ 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;
List drawOrder = skeleton.drawOrder;
List slots = skeleton.slots;
@@ -583,7 +598,7 @@ namespace Spine {
}
// Interpolate between the previous frame and the current frame.
- int frameIndex = Animation.binarySearch(frames, time, 1);
+ int frameIndex = Animation.binarySearch(frames, time);
float frameTime = frames[frameIndex];
float percent = 1 - (time - frameTime) / (frames[frameIndex - 1] - frameTime);
percent = GetCurvePercent(frameIndex - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
@@ -604,4 +619,57 @@ namespace Spine {
}
}
}
+
+ public class IkConstraintTimeline : CurveTimeline {
+ private const int PREV_FRAME_TIME = -3;
+ private const int FRAME_MIX = 1;
+ private const int FRAME_BEND_DIRECTION = 2;
+
+ 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, ...
+
+ public IkConstraintTimeline (int frameCount)
+ : base(frameCount) {
+ frames = new float[frameCount * 3];
+ }
+
+ public float[] getFrames () {
+ return frames;
+ }
+
+ /** Sets the time, mix and bend direction of the specified keyframe. */
+ public void setFrame (int frameIndex, float time, float mix, int bendDirection) {
+ frameIndex *= 3;
+ frames[frameIndex] = time;
+ frames[frameIndex + 1] = mix;
+ frames[frameIndex + 2] = bendDirection;
+ }
+
+ override public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) {
+ float[] frames = this.frames;
+ if (time < frames[0]) return; // Time is before first frame.
+
+ IkConstraint ikConstraint = skeleton.ikConstraints[ikConstraintIndex];
+
+ if (time >= frames[frames.Length - 3]) { // Time is after last frame.
+ ikConstraint.mix += (frames[frames.Length - 2] - ikConstraint.mix) * alpha;
+ ikConstraint.bendDirection = (int)frames[frames.Length - 1];
+ return;
+ }
+
+ // Interpolate between the previous frame and the current frame.
+ int frameIndex = Animation.binarySearch(frames, time, 3);
+ float prevFrameMix = frames[frameIndex - 2];
+ float frameTime = frames[frameIndex];
+ float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime);
+ percent = GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent));
+
+ float mix = prevFrameMix + (frames[frameIndex + FRAME_MIX] - prevFrameMix) * percent;
+ ikConstraint.mix += (mix - ikConstraint.mix) * alpha;
+ ikConstraint.bendDirection = (int)frames[frameIndex + FRAME_BEND_DIRECTION];
+ }
+ }
}
diff --git a/spine-csharp/src/Bone.cs b/spine-csharp/src/Bone.cs
index 7a9fb556a..bc63853fb 100644
--- a/spine-csharp/src/Bone.cs
+++ b/spine-csharp/src/Bone.cs
@@ -36,15 +36,19 @@ namespace Spine {
internal BoneData data;
internal Bone parent;
- internal float x, y, rotation, scaleX, scaleY;
+ internal float x, y, rotation, rotationIK, scaleX, scaleY;
internal float m00, m01, m10, m11;
internal float worldX, worldY, worldRotation, worldScaleX, worldScaleY;
+ internal bool flipX, flipY;
public BoneData Data { get { return data; } }
public Bone Parent { get { return parent; } }
public float X { get { return x; } set { x = value; } }
public float Y { get { return y; } set { y = value; } }
+ /// The forward kinetics rotation.
public float Rotation { get { return rotation; } set { rotation = value; } }
+ /// The inverse kinetics rotation, as calculated by any IK constraints.
+ public float RotationIK { get { return rotationIK; } set { rotationIK = value; } }
public float ScaleX { get { return scaleX; } set { scaleX = value; } }
public float ScaleY { get { return scaleY; } set { scaleY = value; } }
@@ -67,8 +71,9 @@ namespace Spine {
}
/// Computes the world SRT using the parent bone and the local SRT.
- public void UpdateWorldTransform (bool flipX, bool flipY) {
+ public void UpdateWorldTransform () {
Bone parent = this.parent;
+ float x = this.x, y = this.y;
if (parent != null) {
worldX = x * parent.m00 + y * parent.m01 + parent.worldX;
worldY = x * parent.m10 + y * parent.m11 + parent.worldY;
@@ -79,28 +84,30 @@ namespace Spine {
worldScaleX = scaleX;
worldScaleY = scaleY;
}
- worldRotation = data.inheritRotation ? parent.worldRotation + rotation : rotation;
+ worldRotation = data.inheritRotation ? parent.worldRotation + rotationIK : rotationIK;
} else {
worldX = flipX ? -x : x;
worldY = flipY != yDown ? -y : y;
worldScaleX = scaleX;
worldScaleY = scaleY;
- worldRotation = rotation;
+ worldRotation = rotationIK;
}
float radians = worldRotation * (float)Math.PI / 180;
float cos = (float)Math.Cos(radians);
float sin = (float)Math.Sin(radians);
- m00 = cos * worldScaleX;
- m10 = sin * worldScaleX;
- m01 = -sin * worldScaleY;
- m11 = cos * worldScaleY;
if (flipX) {
- m00 = -m00;
- m01 = -m01;
+ m00 = -cos * worldScaleX;
+ m01 = sin * worldScaleY;
+ } else {
+ m00 = cos * worldScaleX;
+ m01 = -sin * worldScaleY;
}
if (flipY != yDown) {
- m10 = -m10;
- m11 = -m11;
+ m10 = -sin * worldScaleX;
+ m11 = -cos * worldScaleY;
+ } else {
+ m10 = sin * worldScaleX;
+ m11 = cos * worldScaleY;
}
}
@@ -109,10 +116,28 @@ namespace Spine {
x = data.x;
y = data.y;
rotation = data.rotation;
+ rotationIK = rotation;
scaleX = data.scaleX;
scaleY = data.scaleY;
}
+ public void worldToLocal (float worldX, float worldY, out float localX, out float localY) {
+ float dx = worldX - this.worldX, dy = worldY - this.worldY;
+ float m00 = this.m00, m10 = this.m10, m01 = this.m01, m11 = this.m11;
+ if (flipX != (flipY != yDown)) {
+ m00 *= -1;
+ m11 *= -1;
+ }
+ float invDet = 1 / (m00 * m11 - m01 * m10);
+ localX = (dx * m00 * invDet - dy * m01 * invDet);
+ localY = (dy * m11 * invDet - dx * m10 * invDet);
+ }
+
+ public void localToWorld (float localX, float localY, out float worldX, out float worldY) {
+ worldX = localX * m00 + localY * m01 + this.worldX;
+ worldY = localX * m10 + localY * m11 + this.worldY;
+ }
+
override public String ToString () {
return data.name;
}
diff --git a/spine-csharp/src/EventData.cs b/spine-csharp/src/EventData.cs
index 832d3de83..4f5d745ff 100644
--- a/spine-csharp/src/EventData.cs
+++ b/spine-csharp/src/EventData.cs
@@ -32,14 +32,16 @@ using System;
namespace Spine {
public class EventData {
- public String Name { get; private set; }
+ internal String name;
+
+ public String Name { get { return name; } }
public int Int { get; set; }
public float Float { get; set; }
public String String { get; set; }
public EventData (String name) {
if (name == null) throw new ArgumentNullException("name cannot be null.");
- Name = name;
+ this.name = name;
}
override public String ToString () {
diff --git a/spine-csharp/src/IkConstraint.cs b/spine-csharp/src/IkConstraint.cs
new file mode 100644
index 000000000..6b27c4a03
--- /dev/null
+++ b/spine-csharp/src/IkConstraint.cs
@@ -0,0 +1,148 @@
+/******************************************************************************
+ * Spine Runtimes Software License
+ * Version 2.1
+ *
+ * Copyright (c) 2013, Esoteric Software
+ * All rights reserved.
+ *
+ * You are granted a perpetual, non-exclusive, non-sublicensable and
+ * non-transferable license to install, execute and perform the Spine Runtimes
+ * Software (the "Software") solely for internal use. Without the written
+ * permission of Esoteric Software (typically granted by licensing Spine), you
+ * may not (a) modify, translate, adapt or otherwise create derivative works,
+ * improvements of the Software or develop new applications using the Software
+ * or (b) remove, delete, alter or obscure any trademarks or any copyright,
+ * trademark, patent or other intellectual property or proprietary rights
+ * notices on or in the Software, including any copy thereof. Redistributions
+ * in binary or source form must include this license and terms.
+ *
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using System;
+using System.Collections.Generic;
+
+namespace Spine {
+ public class IkConstraint {
+ private const float radDeg = 180 / (float)Math.PI;
+
+ internal IkConstraintData data;
+ internal List bones = new List();
+ internal Bone target;
+ internal int bendDirection = 1;
+ internal float mix = 1;
+
+ public IkConstraintData Data { get { return data; } }
+ public List Bones { get { return bones; } }
+ public Bone Target { get { return target; } set { target = value; } }
+ public int BendDirection { get { return bendDirection; } set { bendDirection = value; } }
+ public float Mix { get { return mix; } set { mix = value; } }
+
+ public IkConstraint (IkConstraintData data, Skeleton skeleton) {
+ this.data = data;
+ mix = data.mix;
+ bendDirection = data.bendDirection;
+
+ bones = new List(data.bones.Count);
+ if (skeleton != null) {
+ foreach (BoneData boneData in data.bones)
+ bones.Add(skeleton.FindBone(boneData.name));
+ target = skeleton.FindBone(data.target.name);
+ }
+ }
+
+ public void apply () {
+ Bone target = this.target;
+ List bones = this.bones;
+ switch (bones.Count) {
+ case 1:
+ apply(bones[0], target.worldX, target.worldY, mix);
+ break;
+ case 2:
+ apply(bones[0], bones[1], target.worldX, target.worldY, bendDirection, mix);
+ break;
+ }
+ }
+
+ override public String ToString () {
+ return data.name;
+ }
+
+ /// Adjusts the bone rotation so the tip is as close to the target position as possible. The target is specified
+ /// in the world coordinate system.
+ static public void apply (Bone bone, float targetX, float targetY, float alpha) {
+ float parentRotation = (!bone.data.inheritRotation || bone.parent == null) ? 0 : bone.parent.worldRotation;
+ float rotation = bone.rotation;
+ float rotationIK = (float)Math.Atan2(targetY - bone.worldY, targetX - bone.worldX) * radDeg - parentRotation;
+ bone.rotationIK = rotation + (rotationIK - rotation) * alpha;
+ }
+
+ /// Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as
+ /// possible. The target is specified in the world coordinate system.
+ /// Any descendant bone of the parent.
+ static public void apply (Bone parent, Bone child, float targetX, float targetY, int bendDirection, float alpha) {
+ float childRotation = child.rotation, parentRotation = parent.rotation;
+ if (alpha == 0) {
+ child.rotationIK = childRotation;
+ parent.rotationIK = parentRotation;
+ return;
+ }
+ float positionX, positionY;
+ Bone parentParent = parent.parent;
+ if (parentParent != null) {
+ parentParent.worldToLocal(targetX, targetY, out positionX, out positionY);
+ targetX = (positionX - parent.x) * parentParent.worldScaleX;
+ targetY = (positionY - parent.y) * parentParent.worldScaleY;
+ } else {
+ targetX -= parent.x;
+ targetY -= parent.y;
+ }
+ if (child.parent == parent) {
+ positionX = child.x;
+ positionY = child.y;
+ } else {
+ child.parent.localToWorld(child.x, child.y, out positionX, out positionY);
+ parent.worldToLocal(positionX, positionY, out positionX, out positionY);
+ }
+ float childX = positionX * parent.worldScaleX, childY = positionY * parent.worldScaleY;
+ float offset = (float)Math.Atan2(childY, childX);
+ float len1 = (float)Math.Sqrt(childX * childX + childY * childY), len2 = child.data.length * child.worldScaleX;
+ // Based on code by Ryan Juckett with permission: Copyright (c) 2008-2009 Ryan Juckett, http://www.ryanjuckett.com/
+ float cosDenom = 2 * len1 * len2;
+ if (cosDenom < 0.0001f) {
+ child.rotationIK = childRotation + ((float)Math.Atan2(targetY, targetX) * radDeg - parentRotation - childRotation)
+ * alpha;
+ return;
+ }
+ float cos = (targetX * targetX + targetY * targetY - len1 * len1 - len2 * len2) / cosDenom;
+ if (cos < -1)
+ cos = -1;
+ else if (cos > 1)
+ cos = 1;
+ float childAngle = (float)Math.Acos(cos) * bendDirection;
+ float adjacent = len1 + len2 * cos, opposite = len2 * (float)Math.Sin(childAngle);
+ float parentAngle = (float)Math.Atan2(targetY * adjacent - targetX * opposite, targetX * adjacent + targetY * opposite);
+ float rotation = (parentAngle - offset) * radDeg - parentRotation;
+ if (rotation > 180)
+ rotation -= 360;
+ else if (rotation < -180) //
+ rotation += 360;
+ parent.rotationIK = parentRotation + rotation * alpha;
+ rotation = (childAngle + offset) * radDeg - childRotation;
+ if (rotation > 180)
+ rotation -= 360;
+ else if (rotation < -180) //
+ rotation += 360;
+ child.rotationIK = childRotation + (rotation + parent.worldRotation - child.parent.worldRotation) * alpha;
+ }
+ }
+}
diff --git a/spine-csharp/src/IkConstraintData.cs b/spine-csharp/src/IkConstraintData.cs
new file mode 100644
index 000000000..9919a61e9
--- /dev/null
+++ b/spine-csharp/src/IkConstraintData.cs
@@ -0,0 +1,57 @@
+/******************************************************************************
+ * Spine Runtimes Software License
+ * Version 2.1
+ *
+ * Copyright (c) 2013, Esoteric Software
+ * All rights reserved.
+ *
+ * You are granted a perpetual, non-exclusive, non-sublicensable and
+ * non-transferable license to install, execute and perform the Spine Runtimes
+ * Software (the "Software") solely for internal use. Without the written
+ * permission of Esoteric Software (typically granted by licensing Spine), you
+ * may not (a) modify, translate, adapt or otherwise create derivative works,
+ * improvements of the Software or develop new applications using the Software
+ * or (b) remove, delete, alter or obscure any trademarks or any copyright,
+ * trademark, patent or other intellectual property or proprietary rights
+ * notices on or in the Software, including any copy thereof. Redistributions
+ * in binary or source form must include this license and terms.
+ *
+ * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *****************************************************************************/
+
+using System;
+using System.Collections.Generic;
+
+namespace Spine {
+ public class IkConstraintData {
+ internal String name;
+ internal List bones = new List();
+ internal BoneData target;
+ internal int bendDirection = 1;
+ internal float mix = 1;
+
+ public String Name { get { return name; } }
+ public List Bones { get { return bones; } }
+ public BoneData Target { get { return target; } set { target = value; } }
+ public int BendDirection { get { return bendDirection; } set { bendDirection = value; } }
+ public float Mix { get { return mix; } set { mix = value; } }
+
+ public IkConstraintData (String name) {
+ if (name == null) throw new ArgumentNullException("name cannot be null.");
+ this.name = name;
+ }
+
+ override public String ToString () {
+ return name;
+ }
+ }
+}
diff --git a/spine-csharp/src/Skeleton.cs b/spine-csharp/src/Skeleton.cs
index 07cb95413..699cd6a33 100644
--- a/spine-csharp/src/Skeleton.cs
+++ b/spine-csharp/src/Skeleton.cs
@@ -37,6 +37,8 @@ namespace Spine {
internal List bones;
internal List slots;
internal List drawOrder;
+ internal List ikConstraints = new List();
+ private List> updateBonesCache = new List>();
internal Skin skin;
internal float r = 1, g = 1, b = 1, a = 1;
internal float time;
@@ -47,17 +49,38 @@ namespace Spine {
public List Bones { get { return bones; } }
public List Slots { get { return slots; } }
public List DrawOrder { get { return drawOrder; } }
+ public List IkConstraints { get { return ikConstraints; } set { ikConstraints = value; } }
public Skin Skin { get { return skin; } set { skin = value; } }
public float R { get { return r; } set { r = value; } }
public float G { get { return g; } set { g = value; } }
public float B { get { return b; } set { b = value; } }
public float A { get { return a; } set { a = value; } }
public float Time { get { return time; } set { time = value; } }
- public bool FlipX { get { return flipX; } set { flipX = value; } }
- public bool FlipY { get { return flipY; } set { flipY = value; } }
public float X { get { return x; } set { x = value; } }
public float Y { get { return y; } set { y = value; } }
+ public bool FlipX {
+ get { return flipX; }
+ set {
+ if (flipX == value) return;
+ flipX = value;
+ List bones = this.bones;
+ for (int i = 0, n = bones.Count; i < n; i++)
+ bones[i].flipX = value;
+ }
+ }
+
+ public bool FlipY {
+ get { return flipY; }
+ set {
+ if (flipY == value) return;
+ flipY = value;
+ List bones = this.bones;
+ for (int i = 0, n = bones.Count; i < n; i++)
+ bones[i].flipY = value;
+ }
+ }
+
public Bone RootBone {
get {
return bones.Count == 0 ? null : bones[0];
@@ -82,15 +105,73 @@ namespace Spine {
slots.Add(slot);
drawOrder.Add(slot);
}
+
+ ikConstraints = new List(data.ikConstraints.Count);
+ foreach (IkConstraintData ikConstraintData in data.ikConstraints)
+ ikConstraints.Add(new IkConstraint(ikConstraintData, this));
+
+ UpdateCache();
}
- /// Updates the world transform for each bone.
+ /// Caches information about bones and IK constraints. Must be called if bones or IK constraints are added or
+ /// removed.
+ public void UpdateCache () {
+ List> updateBonesCache = this.updateBonesCache;
+ List ikConstraints = this.ikConstraints;
+ int ikConstraintsCount = ikConstraints.Count;
+
+ int arrayCount = ikConstraintsCount + 1;
+ if (updateBonesCache.Count > arrayCount) updateBonesCache.RemoveRange(arrayCount, updateBonesCache.Count - arrayCount);
+ for (int i = 0, n = updateBonesCache.Count; i < n; i++)
+ updateBonesCache[i].Clear();
+ while (updateBonesCache.Count < arrayCount)
+ updateBonesCache.Add(new List());
+
+ List nonIkBones = updateBonesCache[0];
+
+ for (int i = 0, n = bones.Count; i < n; i++) {
+ Bone bone = bones[i];
+ Bone current = bone;
+ do {
+ for (int ii = 0; ii < ikConstraintsCount; ii++) {
+ IkConstraint ikConstraint = ikConstraints[ii];
+ Bone parent = ikConstraint.bones[0];
+ Bone child = ikConstraint.bones[ikConstraint.bones.Count - 1];
+ while (true) {
+ if (current == child) {
+ updateBonesCache[ii].Add(bone);
+ updateBonesCache[ii + 1].Add(bone);
+ goto outer;
+ }
+ if (child == parent) break;
+ child = child.parent;
+ }
+ }
+ current = current.parent;
+ } while (current != null);
+ nonIkBones.Add(bone);
+ outer: {}
+ }
+ }
+
+ /// Updates the world transform for each bone and applies IK constraints.
public void UpdateWorldTransform () {
- bool flipX = this.flipX;
- bool flipY = this.flipY;
List bones = this.bones;
- for (int i = 0, n = bones.Count; i < n; i++)
- bones[i].UpdateWorldTransform(flipX, flipY);
+ for (int ii = 0, nn = bones.Count; ii < nn; ii++) {
+ Bone bone = bones[ii];
+ bone.rotationIK = bone.rotation;
+ }
+ List> updateBonesCache = this.updateBonesCache;
+ List ikConstraints = this.ikConstraints;
+ int i = 0, last = updateBonesCache.Count - 1;
+ while (true) {
+ List updateBones = updateBonesCache[i];
+ for (int ii = 0, nn = updateBones.Count; ii < nn; ii++)
+ updateBones[ii].UpdateWorldTransform();
+ if (i == last) break;
+ ikConstraints[i].apply();
+ i++;
+ }
}
/// Sets the bones and slots to their setup pose values.
@@ -103,6 +184,13 @@ namespace Spine {
List bones = this.bones;
for (int i = 0, n = bones.Count; i < n; i++)
bones[i].SetToSetupPose();
+
+ List ikConstraints = this.ikConstraints;
+ for (int i = 0, n = ikConstraints.Count; i < n; i++) {
+ IkConstraint ikConstraint = ikConstraints[i];
+ ikConstraint.bendDirection = ikConstraint.data.bendDirection;
+ ikConstraint.mix = ikConstraint.data.mix;
+ }
}
public void SetSlotsToSetupPose () {
@@ -218,6 +306,17 @@ namespace Spine {
throw new Exception("Slot not found: " + slotName);
}
+ /** @return May be null. */
+ public IkConstraint FindIkConstraint (String ikConstraintName) {
+ if (ikConstraintName == null) throw new ArgumentNullException("ikConstraintName cannot be null.");
+ List ikConstraints = this.ikConstraints;
+ for (int i = 0, n = ikConstraints.Count; i < n; i++) {
+ IkConstraint ikConstraint = ikConstraints[i];
+ if (ikConstraint.data.name == ikConstraintName) return ikConstraint;
+ }
+ return null;
+ }
+
public void Update (float delta) {
time += delta;
}
diff --git a/spine-csharp/src/SkeletonData.cs b/spine-csharp/src/SkeletonData.cs
index 60a5d537a..edc5d648b 100644
--- a/spine-csharp/src/SkeletonData.cs
+++ b/spine-csharp/src/SkeletonData.cs
@@ -40,6 +40,9 @@ namespace Spine {
internal Skin defaultSkin;
internal List events = new List();
internal List animations = new List();
+ internal List ikConstraints = new List();
+ internal float width, height;
+ internal String version, hash;
public String Name { get { return name; } set { name = value; } }
public List Bones { get { return bones; } } // Ordered parents first.
@@ -49,15 +52,15 @@ namespace Spine {
public Skin DefaultSkin { get { return defaultSkin; } set { defaultSkin = value; } }
public List Events { get { return events; } set { events = value; } }
public List Animations { get { return animations; } set { animations = value; } }
+ public List IkConstraints { get { return ikConstraints; } set { ikConstraints = value; } }
+ public float Width { get { return width; } set { width = value; } }
+ public float Height { get { return height; } set { height = value; } }
+ /// The Spine version used to export this data.
+ public String Version { get { return version; } set { version = value; } }
+ public String Hash { get { return hash; } set { hash = value; } }
// --- Bones.
- public void AddBone (BoneData bone) {
- if (bone == null) throw new ArgumentNullException("bone cannot be null.");
- bones.Add(bone);
- }
-
-
/// May be null.
public BoneData FindBone (String boneName) {
if (boneName == null) throw new ArgumentNullException("boneName cannot be null.");
@@ -80,11 +83,6 @@ namespace Spine {
// --- Slots.
- public void AddSlot (SlotData slot) {
- if (slot == null) throw new ArgumentNullException("slot cannot be null.");
- slots.Add(slot);
- }
-
/// May be null.
public SlotData FindSlot (String slotName) {
if (slotName == null) throw new ArgumentNullException("slotName cannot be null.");
@@ -106,12 +104,7 @@ namespace Spine {
}
// --- Skins.
-
- public void AddSkin (Skin skin) {
- if (skin == null) throw new ArgumentNullException("skin cannot be null.");
- skins.Add(skin);
- }
-
+
/// May be null.
public Skin FindSkin (String skinName) {
if (skinName == null) throw new ArgumentNullException("skinName cannot be null.");
@@ -122,33 +115,36 @@ namespace Spine {
// --- Events.
- public void AddEvent (EventData eventData) {
- if (eventData == null) throw new ArgumentNullException("eventData cannot be null.");
- events.Add(eventData);
- }
-
/// May be null.
public EventData FindEvent (String eventDataName) {
if (eventDataName == null) throw new ArgumentNullException("eventDataName cannot be null.");
foreach (EventData eventData in events)
- if (eventData.Name == eventDataName) return eventData;
+ if (eventData.name == eventDataName) return eventData;
return null;
}
// --- Animations.
-
- public void AddAnimation (Animation animation) {
- if (animation == null) throw new ArgumentNullException("animation cannot be null.");
- animations.Add(animation);
- }
-
+
/// May be null.
public Animation FindAnimation (String animationName) {
if (animationName == null) throw new ArgumentNullException("animationName cannot be null.");
List animations = this.animations;
for (int i = 0, n = animations.Count; i < n; i++) {
Animation animation = animations[i];
- if (animation.Name == animationName) return animation;
+ if (animation.name == animationName) return animation;
+ }
+ return null;
+ }
+
+ // --- IK
+
+ /// May be null.
+ public IkConstraintData FindIkConstraint (String ikConstraintName) {
+ if (ikConstraintName == null) throw new ArgumentNullException("ikConstraintName cannot be null.");
+ List ikConstraints = this.ikConstraints;
+ for (int i = 0, n = ikConstraints.Count; i < n; i++) {
+ IkConstraintData ikConstraint = ikConstraints[i];
+ if (ikConstraint.name == ikConstraintName) return ikConstraint;
}
return null;
}
diff --git a/spine-csharp/src/SkeletonJson.cs b/spine-csharp/src/SkeletonJson.cs
index 11c951419..e97102c3d 100644
--- a/spine-csharp/src/SkeletonJson.cs
+++ b/spine-csharp/src/SkeletonJson.cs
@@ -90,6 +90,15 @@ namespace Spine {
var root = Json.Deserialize(reader) as Dictionary;
if (root == null) throw new Exception("Invalid JSON.");
+ // Skeleton.
+ var skeletonMap = (Dictionary)root["skeleton"];
+ if (skeletonMap != null) {
+ skeletonData.version = (String)skeletonMap["spine"];
+ skeletonData.hash = (String)skeletonMap["hash"];
+ skeletonData.width = GetFloat(skeletonMap, "width", 0);
+ skeletonData.height = GetFloat(skeletonMap, "width", 1);
+ }
+
// Bones.
foreach (Dictionary boneMap in (List