diff --git a/.gitignore b/.gitignore index 3ac6da826..9c2bb7107 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,12 @@ spine-cocos2d-iphone/libs/* !spine-cocos2d-iphone/libs/cocos2d/Place cocos2d here.txt !spine-cocos2d-iphone/libs/CocosDenshion/Place CocosDenshion here.txt !spine-cocos2d-iphone/libs/kazmath/Place kazmath here.txt + +*.user +forums +spine-csharp/bin +spine-csharp/obj +spine-xna/bin +spine-xna/obj +spine-xna/example/bin +spine-xna/example/obj \ No newline at end of file diff --git a/spine-csharp/Properties/AssemblyInfo.cs b/spine-csharp/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..871268e17 --- /dev/null +++ b/spine-csharp/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("spine-csharp")] +[assembly: AssemblyProduct("spine-csharp")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. Only Windows +// assemblies support COM. +[assembly: ComVisible(false)] + +// On Windows, the following GUID is for the ID of the typelib if this +// project is exposed to COM. On other platforms, it unique identifies the +// title storage container when deploying this assembly to the device. +[assembly: Guid("3ac8567e-9ae8-4624-87b9-a84f0101c629")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] diff --git a/spine-csharp/spine-csharp.csproj b/spine-csharp/spine-csharp.csproj new file mode 100644 index 000000000..79a02999e --- /dev/null +++ b/spine-csharp/spine-csharp.csproj @@ -0,0 +1,89 @@ + + + + {94144E22-2431-4A8F-AC04-DEC22F7EDD8F} + {6D335F3A-9D43-41b4-9D22-F6F17C4BE596};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Debug + x86 + Library + Properties + Spine + spine-csharp + v4.0 + Client + v4.0 + Windows + HiDef + 99dfd52d-8beb-4e5c-a68b-365be39e8064 + Library + + + true + full + false + bin\x86\Debug + DEBUG;TRACE;WINDOWS + prompt + 4 + true + false + x86 + false + + + pdbonly + true + bin\x86\Release + TRACE;WINDOWS + prompt + 4 + true + false + x86 + true + + + + False + + + False + + + 4.0 + False + + + False + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spine-csharp/src/Animation.cs b/spine-csharp/src/Animation.cs new file mode 100644 index 000000000..bd027bfff --- /dev/null +++ b/spine-csharp/src/Animation.cs @@ -0,0 +1,390 @@ +using System; +using System.Collections.Generic; + +namespace Spine { + public class Animation { + public String Name { get; private set; } + public List Timelines { get; set; } + public float Duration { get; set; } + + public Animation (String name, List timelines, float duration) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + if (timelines == null) throw new ArgumentNullException("timelines cannot be null."); + Name = name; + Timelines = timelines; + Duration = duration; + } + + /** Poses the skeleton at the specified time for this animation. */ + public void Apply (Skeleton skeleton, float time, bool loop) { + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + + if (loop && Duration != 0) time %= Duration; + + List timelines = Timelines; + for (int i = 0, n = timelines.Count; i < n; i++) + timelines[i].Apply(skeleton, time, 1); + } + + /** Poses the skeleton at the specified time for this animation mixed with the current pose. + * @param alpha The amount of this animation that affects the current pose. */ + public void Mix (Skeleton skeleton, float time, bool loop, float alpha) { + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + + if (loop && Duration != 0) time %= Duration; + + List timelines = Timelines; + for (int i = 0, n = timelines.Count; i < n; i++) + timelines[i].Apply(skeleton, time, alpha); + } + + /** @param target After the first and before the last entry. */ + internal static int binarySearch (float[] values, float target, int step) { + int low = 0; + int high = values.Length / step - 2; + if (high == 0) return step; + int current = (int)((uint)high >> 1); + while (true) { + if (values[(current + 1) * step] <= target) + low = current + 1; + else + high = current; + if (low == high) return (low + 1) * step; + 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; + return -1; + } + } + + public interface Timeline { + /** Sets the value(s) for the specified time. */ + void Apply (Skeleton skeleton, float time, 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; + + private float[] curves; // dfx, dfy, ddfx, ddfy, dddfx, dddfy, ... + public int FrameCount { + get { + return curves.Length / 6 + 1; + } + } + + public CurveTimeline (int frameCount) { + curves = new float[(frameCount - 1) * 6]; + } + + abstract public void Apply (Skeleton skeleton, float time, float alpha); + + public void SetLinear (int frameIndex) { + curves[frameIndex * 6] = LINEAR; + } + + public void SetStepped (int frameIndex) { + curves[frameIndex * 6] = 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; + } + + public float GetCurvePercent (int frameIndex, float percent) { + int curveIndex = frameIndex * 6; + 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]; + 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--; + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + x += dfx; + y += dfy; + } + 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; + + public int BoneIndex { get; set; } + public float[] Frames { get; private set; } // time, value, ... + + public RotateTimeline (int frameCount) + : base(frameCount) { + Frames = new float[frameCount * 2]; + } + + /** Sets the time and value of the specified keyframe. */ + public void SetFrame (int frameIndex, float time, float angle) { + frameIndex *= 2; + Frames[frameIndex] = time; + Frames[frameIndex + 1] = angle; + } + + override public void Apply (Skeleton skeleton, float time, float alpha) { + float[] frames = Frames; + if (time < frames[0]) return; // Time is before first frame. + + Bone bone = skeleton.Bones[BoneIndex]; + + float amount; + + if (time >= frames[frames.Length - 2]) { // Time is after last frame. + amount = bone.Data.Rotation + frames[frames.Length - 1] - bone.Rotation; + while (amount > 180) + amount -= 360; + while (amount < -180) + amount += 360; + bone.Rotation += amount * alpha; + return; + } + + // Interpolate between the last frame and the current frame. + int frameIndex = Animation.binarySearch(frames, time, 2); + 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)); + + amount = frames[frameIndex + FRAME_VALUE] - lastFrameValue; + while (amount > 180) + amount -= 360; + while (amount < -180) + amount += 360; + amount = bone.Data.Rotation + (lastFrameValue + amount * percent) - bone.Rotation; + while (amount > 180) + amount -= 360; + while (amount < -180) + amount += 360; + bone.Rotation += amount * alpha; + } + } + + public class TranslateTimeline : CurveTimeline { + static protected int LAST_FRAME_TIME = -3; + static protected int FRAME_X = 1; + static protected int FRAME_Y = 2; + + public int BoneIndex { get; set; } + public float[] Frames { get; private set; } // time, value, value, ... + + public TranslateTimeline (int frameCount) + : base(frameCount) { + Frames = new float[frameCount * 3]; + } + + /** Sets the time and value of the specified keyframe. */ + public void SetFrame (int frameIndex, float time, float x, float y) { + frameIndex *= 3; + Frames[frameIndex] = time; + Frames[frameIndex + 1] = x; + Frames[frameIndex + 2] = y; + } + + override public void Apply (Skeleton skeleton, float time, float alpha) { + float[] frames = Frames; + if (time < frames[0]) return; // Time is before first frame. + + Bone bone = skeleton.Bones[BoneIndex]; + + if (time >= frames[frames.Length - 3]) { // Time is after last frame. + bone.X += (bone.Data.X + frames[frames.Length - 2] - bone.X) * alpha; + bone.Y += (bone.Data.Y + frames[frames.Length - 1] - bone.Y) * alpha; + return; + } + + // Interpolate between the last frame and the current frame. + int frameIndex = Animation.binarySearch(frames, time, 3); + float lastFrameX = frames[frameIndex - 2]; + float lastFrameY = frames[frameIndex - 1]; + float frameTime = frames[frameIndex]; + float percent = 1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime); + percent = GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); + + bone.X += (bone.Data.X + lastFrameX + (frames[frameIndex + FRAME_X] - lastFrameX) * percent - bone.X) * alpha; + bone.Y += (bone.Data.Y + lastFrameY + (frames[frameIndex + FRAME_Y] - lastFrameY) * percent - bone.Y) * alpha; + } + } + + public class ScaleTimeline : TranslateTimeline { + public ScaleTimeline (int frameCount) + : base(frameCount) { + } + + override public void Apply (Skeleton skeleton, float time, float alpha) { + float[] frames = Frames; + if (time < frames[0]) return; // Time is before first frame. + + Bone bone = skeleton.Bones[BoneIndex]; + if (time >= frames[frames.Length - 3]) { // Time is after last frame. + bone.ScaleX += (bone.Data.ScaleX - 1 + frames[frames.Length - 2] - bone.ScaleX) * alpha; + bone.ScaleY += (bone.Data.ScaleY - 1 + frames[frames.Length - 1] - bone.ScaleY) * alpha; + return; + } + + // Interpolate between the last frame and the current frame. + int frameIndex = Animation.binarySearch(frames, time, 3); + float lastFrameX = frames[frameIndex - 2]; + float lastFrameY = frames[frameIndex - 1]; + float frameTime = frames[frameIndex]; + float percent = 1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime); + percent = GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); + + bone.ScaleX += (bone.Data.ScaleX - 1 + lastFrameX + (frames[frameIndex + FRAME_X] - lastFrameX) * percent - bone.ScaleX) * alpha; + bone.ScaleY += (bone.Data.ScaleY - 1 + lastFrameY + (frames[frameIndex + FRAME_Y] - lastFrameY) * percent - bone.ScaleY) * alpha; + } + } + + 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; + + public int SlotIndex { get; set; } + public float[] Frames { get; private set; } // time, r, g, b, a, ... + + public ColorTimeline (int frameCount) + : base(frameCount) { + Frames = new float[frameCount * 5]; + } + + /** Sets the time and value of the specified keyframe. */ + public void setFrame (int frameIndex, float time, float r, float g, float b, float a) { + frameIndex *= 5; + Frames[frameIndex] = time; + Frames[frameIndex + 1] = r; + Frames[frameIndex + 2] = g; + Frames[frameIndex + 3] = b; + Frames[frameIndex + 4] = a; + } + + override public void Apply (Skeleton skeleton, float time, float alpha) { + float[] frames = Frames; + if (time < frames[0]) return; // Time is before first frame. + + Slot slot = skeleton.Slots[SlotIndex]; + + if (time >= frames[frames.Length - 5]) { // Time is after last frame. + int i = frames.Length - 1; + slot.R = frames[i - 3]; + slot.G = frames[i - 2]; + slot.B = frames[i - 1]; + slot.A = frames[i]; + return; + } + + // Interpolate between the last frame and the current frame. + int frameIndex = Animation.binarySearch(frames, time, 5); + float lastFrameR = frames[frameIndex - 4]; + float lastFrameG = frames[frameIndex - 3]; + float lastFrameB = frames[frameIndex - 2]; + float lastFrameA = frames[frameIndex - 1]; + float frameTime = frames[frameIndex]; + float percent = 1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime); + percent = GetCurvePercent(frameIndex / 5 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); + + float r = lastFrameR + (frames[frameIndex + FRAME_R] - lastFrameR) * percent; + float g = lastFrameG + (frames[frameIndex + FRAME_G] - lastFrameG) * percent; + float b = lastFrameB + (frames[frameIndex + FRAME_B] - lastFrameB) * percent; + float a = lastFrameA + (frames[frameIndex + FRAME_A] - lastFrameA) * percent; + if (alpha < 1) { + slot.R += (r - slot.R) * alpha; + slot.G += (g - slot.G) * alpha; + slot.B += (b - slot.B) * alpha; + slot.A += (a - slot.A) * alpha; + } else { + slot.R = r; + slot.G = g; + slot.B = b; + slot.A = a; + } + } + } + + public class AttachmentTimeline : Timeline { + public int SlotIndex { get; set; } + public float[] Frames { get; private set; } // time, ... + public String[] AttachmentNames { get; private set; } + public int FrameCount { + get { + return Frames.Length; + } + } + + public AttachmentTimeline (int frameCount) { + Frames = new float[frameCount]; + AttachmentNames = new String[frameCount]; + } + + /** Sets the time and value of the specified keyframe. */ + public void setFrame (int frameIndex, float time, String attachmentName) { + Frames[frameIndex] = time; + AttachmentNames[frameIndex] = attachmentName; + } + + public void Apply (Skeleton skeleton, float time, float alpha) { + float[] frames = Frames; + if (time < frames[0]) return; // Time is before first frame. + + 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; + + String attachmentName = AttachmentNames[frameIndex]; + skeleton.Slots[SlotIndex].Attachment = + attachmentName == null ? null : skeleton.GetAttachment(SlotIndex, attachmentName); + } + } +} diff --git a/spine-csharp/src/Attachments/AtlasAttachmentLoader.cs b/spine-csharp/src/Attachments/AtlasAttachmentLoader.cs new file mode 100644 index 000000000..02e5f975a --- /dev/null +++ b/spine-csharp/src/Attachments/AtlasAttachmentLoader.cs @@ -0,0 +1,24 @@ +using System; + +namespace Spine { + public class AtlasAttachmentLoader : AttachmentLoader { + private BaseAtlas atlas; + + public AtlasAttachmentLoader (BaseAtlas atlas) { + if (atlas == null) throw new ArgumentNullException("atlas cannot be null."); + this.atlas = atlas; + } + + public Attachment NewAttachment (AttachmentType type, String name) { + switch (type) { + case AttachmentType.region: + AtlasRegion region = atlas.FindRegion(name); + if (region == null) throw new Exception("Region not found in atlas: " + name + " (" + type + ")"); + RegionAttachment attachment = new RegionAttachment(name); + attachment.Region = region; + return attachment; + } + throw new Exception("Unknown attachment type: " + type); + } + } +} diff --git a/spine-csharp/src/Attachments/Attachment.cs b/spine-csharp/src/Attachments/Attachment.cs new file mode 100644 index 000000000..c8c8aab52 --- /dev/null +++ b/spine-csharp/src/Attachments/Attachment.cs @@ -0,0 +1,16 @@ +using System; + +namespace Spine { + abstract public class Attachment { + public String Name { get; private set; } + + public Attachment (String name) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + Name = name; + } + + override public String ToString () { + return Name; + } + } +} diff --git a/spine-csharp/src/Attachments/AttachmentLoader.cs b/spine-csharp/src/Attachments/AttachmentLoader.cs new file mode 100644 index 000000000..28393aef2 --- /dev/null +++ b/spine-csharp/src/Attachments/AttachmentLoader.cs @@ -0,0 +1,8 @@ +using System; + +namespace Spine { + public interface AttachmentLoader { + /** @return May be null to not load any attachment. */ + Attachment NewAttachment (AttachmentType type, String name); + } +} diff --git a/spine-csharp/src/Attachments/AttachmentType.cs b/spine-csharp/src/Attachments/AttachmentType.cs new file mode 100644 index 000000000..49d93f775 --- /dev/null +++ b/spine-csharp/src/Attachments/AttachmentType.cs @@ -0,0 +1,6 @@ + +namespace Spine { + public enum AttachmentType { + region, regionSequence + } +} diff --git a/spine-csharp/src/Attachments/RegionAttachment.cs b/spine-csharp/src/Attachments/RegionAttachment.cs new file mode 100644 index 000000000..86d5c291a --- /dev/null +++ b/spine-csharp/src/Attachments/RegionAttachment.cs @@ -0,0 +1,134 @@ +using System; + +namespace Spine { + /** Attachment that displays a texture region. */ + public class RegionAttachment : Attachment { + public const int X1 = 0; + public const int Y1 = 1; + public const int X2 = 2; + public const int Y2 = 3; + public const int X3 = 4; + public const int Y3 = 5; + public const int X4 = 6; + public const int Y4 = 7; + + public float X { get; set; } + public float Y { get; set; } + public float ScaleX { get; set; } + public float ScaleY { get; set; } + public float Rotation { get; set; } + public float Width { get; set; } + public float Height { get; set; } + + public float[] Offset { get; private set; } + public float[] Vertices { get; private set; } + public float[] UVs { get; private set; } + + private AtlasRegion region; + public AtlasRegion Region { + get { + return region; + } + set { + region = value; + float[] uvs = UVs; + if (value.Rotate) { + uvs[X2] = value.U; + uvs[Y2] = value.V2; + uvs[X3] = value.U; + uvs[Y3] = value.V; + uvs[X4] = value.U2; + uvs[Y4] = value.V; + uvs[X1] = value.U2; + uvs[Y1] = value.V2; + } else { + uvs[X1] = value.U; + uvs[Y1] = value.V2; + uvs[X2] = value.U; + uvs[Y2] = value.V; + uvs[X3] = value.U2; + uvs[Y3] = value.V; + uvs[X4] = value.U2; + uvs[Y4] = value.V2; + } + } + } + + public RegionAttachment (string name) + : base(name) { + Offset = new float[8]; + Vertices = new float[8]; + UVs = new float[8]; + ScaleX = 1; + ScaleY = 1; + } + + public void UpdateOffset () { + float width = Width; + float height = Height; + float localX2 = width / 2; + float localY2 = height / 2; + float localX = -localX2; + float localY = -localY2; + AtlasRegion region = Region; + if (region.Rotate) { + localX += region.OffsetX / region.OriginalWidth * height; + localY += region.OffsetY / region.OriginalHeight * width; + localX2 -= (region.OriginalWidth - region.OffsetX - region.Height) / region.OriginalWidth * width; + localY2 -= (region.OriginalHeight - region.OffsetY - region.Width) / region.OriginalHeight * height; + } else { + localX += region.OffsetX / region.OriginalWidth * width; + localY += region.OffsetY / region.OriginalHeight * height; + localX2 -= (region.OriginalWidth - region.OffsetX - region.Width) / region.OriginalWidth * width; + localY2 -= (region.OriginalHeight - region.OffsetY - region.Height) / region.OriginalHeight * height; + } + float scaleX = ScaleX; + float scaleY = ScaleY; + localX *= scaleX; + localY *= scaleY; + localX2 *= scaleX; + localY2 *= scaleY; + float radians = Rotation * (float)Math.PI / 180; + float cos = (float)Math.Cos(radians); + float sin = (float)Math.Sin(radians); + float x = X; + float y = Y; + float localXCos = localX * cos + x; + float localXSin = localX * sin; + float localYCos = localY * cos + y; + float localYSin = localY * sin; + float localX2Cos = localX2 * cos + x; + float localX2Sin = localX2 * sin; + float localY2Cos = localY2 * cos + y; + float localY2Sin = localY2 * sin; + float[] offset = Offset; + offset[X1] = localXCos - localYSin; + offset[Y1] = localYCos + localXSin; + offset[X2] = localXCos - localY2Sin; + offset[Y2] = localY2Cos + localXSin; + offset[X3] = localX2Cos - localY2Sin; + offset[Y3] = localY2Cos + localX2Sin; + offset[X4] = localX2Cos - localYSin; + offset[Y4] = localYCos + localX2Sin; + } + + public void UpdateVertices (Bone bone) { + float x = bone.WorldX; + float y = bone.WorldY; + float m00 = bone.M00; + float m01 = bone.M01; + float m10 = bone.M10; + float m11 = bone.M11; + float[] vertices = Vertices; + float[] offset = Offset; + vertices[X1] = offset[X1] * m00 + offset[Y1] * m01 + x; + vertices[Y1] = offset[X1] * m10 + offset[Y1] * m11 + y; + vertices[X2] = offset[X2] * m00 + offset[Y2] * m01 + x; + vertices[Y2] = offset[X2] * m10 + offset[Y2] * m11 + y; + vertices[X3] = offset[X3] * m00 + offset[Y3] * m01 + x; + vertices[Y3] = offset[X3] * m10 + offset[Y3] * m11 + y; + vertices[X4] = offset[X4] * m00 + offset[Y4] * m01 + x; + vertices[Y4] = offset[X4] * m10 + offset[Y4] * m11 + y; + } + } +} diff --git a/spine-csharp/src/BaseAtlas.cs b/spine-csharp/src/BaseAtlas.cs new file mode 100644 index 000000000..5ebc4ce1f --- /dev/null +++ b/spine-csharp/src/BaseAtlas.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Spine { + abstract public class BaseAtlas { + List pages = new List(); + List regions = new List(); + + abstract protected AtlasPage NewAtlasPage (String path); + + public void load (StreamReader reader, String imagesDir) { + String[] tuple = new String[4]; + AtlasPage page = null; + while (true) { + String line = reader.ReadLine(); + if (line == null) break; + if (line.Trim().Length == 0) + page = null; + else if (page == null) { + page = NewAtlasPage(Path.Combine(imagesDir, line)); + + page.Format = (Format)Enum.Parse(typeof(Format), readValue(reader), false); + + readTuple(reader, tuple); + page.MinFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[0]); + page.MagFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[1]); + + String direction = readValue(reader); + page.UWrap = TextureWrap.ClampToEdge; + page.VWrap = TextureWrap.ClampToEdge; + if (direction == "x") + page.UWrap = TextureWrap.Repeat; + else if (direction == "y") + page.VWrap = TextureWrap.Repeat; + else if (direction == "xy") + page.UWrap = page.VWrap = TextureWrap.Repeat; + + pages.Add(page); + + } else { + AtlasRegion region = new AtlasRegion(); + region.Name = line; + region.Page = page; + + region.Rotate = Boolean.Parse(readValue(reader)); + + readTuple(reader, tuple); + int x = int.Parse(tuple[0]); + int y = int.Parse(tuple[1]); + + readTuple(reader, tuple); + int width = int.Parse(tuple[0]); + int height = int.Parse(tuple[1]); + + float invTexWidth = 1f / page.GetTextureWidth(); + float invTexHeight = 1f / page.GetTextureHeight(); + region.U = x * invTexWidth; + region.V = y * invTexHeight; + region.U2 = (x + width) * invTexWidth; + region.V2 = (y + height) * invTexHeight; + region.Width = Math.Abs(width); + region.Height = Math.Abs(height); + + if (readTuple(reader, tuple) == 4) { // split is optional + region.Splits = new int[] {int.Parse(tuple[0]), int.Parse(tuple[1]), + int.Parse(tuple[2]), int.Parse(tuple[3])}; + + if (readTuple(reader, tuple) == 4) { // pad is optional, but only present with splits + region.Pads = new int[] {int.Parse(tuple[0]), int.Parse(tuple[1]), + int.Parse(tuple[2]), int.Parse(tuple[3])}; + + readTuple(reader, tuple); + } + } + + region.OriginalWidth = int.Parse(tuple[0]); + region.OriginalHeight = int.Parse(tuple[1]); + + readTuple(reader, tuple); + region.OffsetX = int.Parse(tuple[0]); + region.OffsetY = int.Parse(tuple[1]); + + region.Index = int.Parse(readValue(reader)); + + regions.Add(region); + } + } + } + + static String readValue (StreamReader reader) { + String line = reader.ReadLine(); + int colon = line.IndexOf(':'); + if (colon == -1) throw new Exception("Invalid line: " + line); + return line.Substring(colon + 1).Trim(); + } + + /** Returns the number of tuple values read (2 or 4). */ + static int readTuple (StreamReader reader, String[] tuple) { + String line = reader.ReadLine(); + int colon = line.IndexOf(':'); + if (colon == -1) throw new Exception("Invalid line: " + line); + int i = 0, lastMatch = colon + 1; + for (i = 0; i < 3; i++) { + int comma = line.IndexOf(',', lastMatch); + if (comma == -1) { + if (i == 0) throw new Exception("Invalid line: " + line); + break; + } + tuple[i] = line.Substring(lastMatch, comma - lastMatch).Trim(); + lastMatch = comma + 1; + } + tuple[i] = line.Substring(lastMatch).Trim(); + return i + 1; + } + + /** Returns the first region found with the specified name. This method uses string comparison to find the region, so the result + * should be cached rather than calling this method multiple times. + * @return The region, or null. */ + public AtlasRegion FindRegion (String name) { + for (int i = 0, n = regions.Count; i < n; i++) + if (regions[i].Name == name) return regions[i]; + return null; + } + } + + public enum Format { + Alpha, + Intensity, + LuminanceAlpha, + RGB565, + RGBA4444, + RGB888, + RGBA8888 + } + + public enum TextureFilter { + Nearest, + Linear, + MipMap, + MipMapNearestNearest, + MipMapLinearNearest, + MipMapNearestLinear, + MipMapLinearLinear + } + + public enum TextureWrap { + MirroredRepeat, + ClampToEdge, + Repeat + } + + abstract public class AtlasPage { + public Format Format; + public TextureFilter MinFilter; + public TextureFilter MagFilter; + public TextureWrap UWrap; + public TextureWrap VWrap; + + abstract public int GetTextureWidth (); + abstract public int GetTextureHeight (); + } + + public class AtlasRegion { + public AtlasPage Page; + public float U, V; + public float U2, V2; + public int Width, Height; + public int Index; + public String Name; + public float OffsetX, OffsetY; + public int OriginalWidth, OriginalHeight; + public bool Rotate; + public int[] Splits; + public int[] Pads; + } +} diff --git a/spine-csharp/src/Bone.cs b/spine-csharp/src/Bone.cs new file mode 100644 index 000000000..7bd04fe26 --- /dev/null +++ b/spine-csharp/src/Bone.cs @@ -0,0 +1,83 @@ +using System; + +namespace Spine { + public class Bone { + static public bool yDown; + + public BoneData Data { get; private set; } + public Bone Parent { get; private set; } + public float X { get; set; } + public float Y { get; set; } + public float Rotation { get; set; } + public float ScaleX { get; set; } + public float ScaleY { get; set; } + + public float M00 { get; private set; } + public float M01 { get; private set; } + public float M10 { get; private set; } + public float M11 { get; private set; } + public float WorldX { get; private set; } + public float WorldY { get; private set; } + public float WorldRotation { get; private set; } + public float WorldScaleX { get; private set; } + public float WorldScaleY { get; private set; } + + /** @param parent May be null. */ + public Bone (BoneData data, Bone parent) { + if (data == null) throw new ArgumentNullException("data cannot be null."); + Data = data; + Parent = parent; + SetToBindPose(); + } + + /** Computes the world SRT using the parent bone and the local SRT. */ + public void UpdateWorldTransform (bool flipX, bool flipY) { + Bone parent = Parent; + if (parent != null) { + WorldX = X * parent.M00 + Y * parent.M01 + parent.WorldX; + WorldY = X * parent.M10 + Y * parent.M11 + parent.WorldY; + WorldScaleX = parent.WorldScaleX * ScaleX; + WorldScaleY = parent.WorldScaleY * ScaleY; + WorldRotation = parent.WorldRotation + Rotation; + } else { + WorldX = X; + WorldY = Y; + WorldScaleX = ScaleX; + WorldScaleY = ScaleY; + WorldRotation = Rotation; + } + 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; + } + if (flipY) { + M10 = -M10; + M11 = -M11; + } + if (yDown) { + M10 = -M10; + M11 = -M11; + } + } + + public void SetToBindPose () { + BoneData data = Data; + X = data.X; + Y = data.Y; + Rotation = data.Rotation; + ScaleX = data.ScaleX; + ScaleY = data.ScaleY; + } + + override public String ToString () { + return Data.Name; + } + } +} diff --git a/spine-csharp/src/BoneData.cs b/spine-csharp/src/BoneData.cs new file mode 100644 index 000000000..f3a4add51 --- /dev/null +++ b/spine-csharp/src/BoneData.cs @@ -0,0 +1,28 @@ +using System; + +namespace Spine { + public class BoneData { + /** May be null. */ + public BoneData Parent { get; private set; } + public String Name { get; private set; } + public float Length { get; set; } + public float X { get; set; } + public float Y { get; set; } + public float Rotation { get; set; } + public float ScaleX { get; set; } + public float ScaleY { get; set; } + + /** @param parent May be null. */ + public BoneData (String name, BoneData parent) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + Name = name; + Parent = parent; + ScaleX = 1; + ScaleY = 1; + } + + override public String ToString () { + return Name; + } + } +} diff --git a/spine-csharp/src/Json.cs b/spine-csharp/src/Json.cs new file mode 100644 index 000000000..25b67be9d --- /dev/null +++ b/spine-csharp/src/Json.cs @@ -0,0 +1,551 @@ +/* + * Copyright (c) 2012 Calvin Rien + * + * Based on the JSON parser by Patrick van Bergen + * http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html + * + * Simplified it so that it doesn't throw exceptions + * and can be used in Unity iPhone with maximum code stripping. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Spine +{ + // Example usage: + // + // using UnityEngine; + // using System.Collections; + // using System.Collections.Generic; + // using MiniJSON; + // + // public class MiniJSONTest : MonoBehaviour { + // void Start () { + // var jsonString = "{ \"array\": [1.44,2,3], " + + // "\"object\": {\"key1\":\"value1\", \"key2\":256}, " + + // "\"string\": \"The quick brown fox \\\"jumps\\\" over the lazy dog \", " + + // "\"unicode\": \"\\u3041 Men\u00fa sesi\u00f3n\", " + + // "\"int\": 65536, " + + // "\"float\": 3.1415926, " + + // "\"bool\": true, " + + // "\"null\": null }"; + // + // var dict = Json.Deserialize(jsonString) as Dictionary; + // + // Debug.Log("deserialized: " + dict.GetType()); + // Debug.Log("dict['array'][0]: " + ((List) dict["array"])[0]); + // Debug.Log("dict['string']: " + (string) dict["string"]); + // Debug.Log("dict['float']: " + (float) dict["float"]); + // Debug.Log("dict['int']: " + (long) dict["int"]); // ints come out as longs + // Debug.Log("dict['unicode']: " + (string) dict["unicode"]); + // + // var str = Json.Serialize(dict); + // + // Debug.Log("serialized: " + str); + // } + // } + + /// + /// This class encodes and decodes JSON strings. + /// Spec. details, see http://www.json.org/ + /// + /// JSON uses Arrays and Objects. These correspond here to the datatypes IList and IDictionary. + /// All numbers are parsed to floats. + /// + public static class Json { + /// + /// Parses the string json into a value + /// + /// A JSON string. + /// An List<object>, a Dictionary<string, object>, a float, an integer,a string, null, true, or false + public static object Deserialize(string json) { + // save the string for debug information + if (json == null) { + return null; + } + + return Parser.Parse(json); + } + + sealed class Parser : IDisposable { + const string WHITE_SPACE = " \t\n\r"; + const string WORD_BREAK = " \t\n\r{}[],:\""; + + enum TOKEN { + NONE, + CURLY_OPEN, + CURLY_CLOSE, + SQUARED_OPEN, + SQUARED_CLOSE, + COLON, + COMMA, + STRING, + NUMBER, + TRUE, + FALSE, + NULL + }; + + StringReader json; + + Parser(string jsonString) { + json = new StringReader(jsonString); + } + + public static object Parse(string jsonString) { + using (var instance = new Parser(jsonString)) { + return instance.ParseValue(); + } + } + + public void Dispose() { + json.Dispose(); + json = null; + } + + Dictionary ParseObject() { + Dictionary table = new Dictionary(); + + // ditch opening brace + json.Read(); + + // { + while (true) { + switch (NextToken) { + case TOKEN.NONE: + return null; + case TOKEN.COMMA: + continue; + case TOKEN.CURLY_CLOSE: + return table; + default: + // name + string name = ParseString(); + if (name == null) { + return null; + } + + // : + if (NextToken != TOKEN.COLON) { + return null; + } + // ditch the colon + json.Read(); + + // value + table[name] = ParseValue(); + break; + } + } + } + + List ParseArray() { + List array = new List(); + + // ditch opening bracket + json.Read(); + + // [ + var parsing = true; + while (parsing) { + TOKEN nextToken = NextToken; + + switch (nextToken) { + case TOKEN.NONE: + return null; + case TOKEN.COMMA: + continue; + case TOKEN.SQUARED_CLOSE: + parsing = false; + break; + default: + object value = ParseByToken(nextToken); + + array.Add(value); + break; + } + } + + return array; + } + + object ParseValue() { + TOKEN nextToken = NextToken; + return ParseByToken(nextToken); + } + + object ParseByToken(TOKEN token) { + switch (token) { + case TOKEN.STRING: + return ParseString(); + case TOKEN.NUMBER: + return ParseNumber(); + case TOKEN.CURLY_OPEN: + return ParseObject(); + case TOKEN.SQUARED_OPEN: + return ParseArray(); + case TOKEN.TRUE: + return true; + case TOKEN.FALSE: + return false; + case TOKEN.NULL: + return null; + default: + return null; + } + } + + string ParseString() { + StringBuilder s = new StringBuilder(); + char c; + + // ditch opening quote + json.Read(); + + bool parsing = true; + while (parsing) { + + if (json.Peek() == -1) { + parsing = false; + break; + } + + c = NextChar; + switch (c) { + case '"': + parsing = false; + break; + case '\\': + if (json.Peek() == -1) { + parsing = false; + break; + } + + c = NextChar; + switch (c) { + case '"': + case '\\': + case '/': + s.Append(c); + break; + case 'b': + s.Append('\b'); + break; + case 'f': + s.Append('\f'); + break; + case 'n': + s.Append('\n'); + break; + case 'r': + s.Append('\r'); + break; + case 't': + s.Append('\t'); + break; + case 'u': + var hex = new StringBuilder(); + + for (int i=0; i< 4; i++) { + hex.Append(NextChar); + } + + s.Append((char) Convert.ToInt32(hex.ToString(), 16)); + break; + } + break; + default: + s.Append(c); + break; + } + } + + return s.ToString(); + } + + object ParseNumber() { + string number = NextWord; + + //NO!! always serialize to a float + //if (number.IndexOf('.') == -1) { + // long parsedInt; + // Int64.TryParse(number, out parsedInt); + // return parsedInt; + //} + + float parsedFloat; + float.TryParse(number, out parsedFloat); + return parsedFloat; + } + + void EatWhitespace() { + while (WHITE_SPACE.IndexOf(PeekChar) != -1) { + json.Read(); + + if (json.Peek() == -1) { + break; + } + } + } + + char PeekChar { + get { + return Convert.ToChar(json.Peek()); + } + } + + char NextChar { + get { + return Convert.ToChar(json.Read()); + } + } + + string NextWord { + get { + StringBuilder word = new StringBuilder(); + + while (WORD_BREAK.IndexOf(PeekChar) == -1) { + word.Append(NextChar); + + if (json.Peek() == -1) { + break; + } + } + + return word.ToString(); + } + } + + TOKEN NextToken { + get { + EatWhitespace(); + + if (json.Peek() == -1) { + return TOKEN.NONE; + } + + char c = PeekChar; + switch (c) { + case '{': + return TOKEN.CURLY_OPEN; + case '}': + json.Read(); + return TOKEN.CURLY_CLOSE; + case '[': + return TOKEN.SQUARED_OPEN; + case ']': + json.Read(); + return TOKEN.SQUARED_CLOSE; + case ',': + json.Read(); + return TOKEN.COMMA; + case '"': + return TOKEN.STRING; + case ':': + return TOKEN.COLON; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return TOKEN.NUMBER; + } + + string word = NextWord; + + switch (word) { + case "false": + return TOKEN.FALSE; + case "true": + return TOKEN.TRUE; + case "null": + return TOKEN.NULL; + } + + return TOKEN.NONE; + } + } + } + + /// + /// Converts a IDictionary / IList object or a simple type (string, int, etc.) into a JSON string + /// + /// A Dictionary<string, object> / List<object> + /// A JSON encoded string, or null if object 'json' is not serializable + public static string Serialize(object obj) { + return Serializer.Serialize(obj); + } + + sealed class Serializer { + StringBuilder builder; + + Serializer() { + builder = new StringBuilder(); + } + + public static string Serialize(object obj) { + var instance = new Serializer(); + + instance.SerializeValue(obj); + + return instance.builder.ToString(); + } + + void SerializeValue(object value) { + IList asList; + IDictionary asDict; + string asStr; + + if (value == null) { + builder.Append("null"); + } + else if ((asStr = value as string) != null) { + SerializeString(asStr); + } + else if (value is bool) { + builder.Append(value.ToString().ToLower()); + } + else if ((asList = value as IList) != null) { + SerializeArray(asList); + } + else if ((asDict = value as IDictionary) != null) { + SerializeObject(asDict); + } + else if (value is char) { + SerializeString(value.ToString()); + } + else { + SerializeOther(value); + } + } + + void SerializeObject(IDictionary obj) { + bool first = true; + + builder.Append('{'); + + foreach (object e in obj.Keys) { + if (!first) { + builder.Append(','); + } + + SerializeString(e.ToString()); + builder.Append(':'); + + SerializeValue(obj[e]); + + first = false; + } + + builder.Append('}'); + } + + void SerializeArray(IList anArray) { + builder.Append('['); + + bool first = true; + + foreach (object obj in anArray) { + if (!first) { + builder.Append(','); + } + + SerializeValue(obj); + + first = false; + } + + builder.Append(']'); + } + + void SerializeString(string str) { + builder.Append('\"'); + + char[] charArray = str.ToCharArray(); + foreach (var c in charArray) { + switch (c) { + case '"': + builder.Append("\\\""); + break; + case '\\': + builder.Append("\\\\"); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + int codepoint = Convert.ToInt32(c); + if ((codepoint >= 32) && (codepoint <= 126)) { + builder.Append(c); + } + else { + builder.Append("\\u" + Convert.ToString(codepoint, 16).PadLeft(4, '0')); + } + break; + } + } + + builder.Append('\"'); + } + + void SerializeOther(object value) { + if (value is float + || value is int + || value is uint + || value is long + || value is float + || value is sbyte + || value is byte + || value is short + || value is ushort + || value is ulong + || value is decimal) { + builder.Append(value.ToString()); + } + else { + SerializeString(value.ToString()); + } + } + } + } +} \ No newline at end of file diff --git a/spine-csharp/src/Skeleton.cs b/spine-csharp/src/Skeleton.cs new file mode 100644 index 000000000..30917078a --- /dev/null +++ b/spine-csharp/src/Skeleton.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; + +namespace Spine { + public class Skeleton { + public SkeletonData Data { get; private set; } + public List Bones { get; private set; } + public List Slots { get; private set; } + public List DrawOrder { get; private set; } + public Skin Skin { get; set; } + public float R { get; set; } + public float G { get; set; } + public float B { get; set; } + public float A { get; set; } + public float Time { get; set; } + public bool FlipX { get; set; } + public bool FlipY { get; set; } + public Bone RootBone { + get { + return Bones.Count == 0 ? null : Bones[0]; + } + } + + public Skeleton (SkeletonData data) { + if (data == null) throw new ArgumentNullException("data cannot be null."); + Data = data; + + Bones = new List(Data.Bones.Count); + foreach (BoneData boneData in Data.Bones) { + Bone parent = boneData.Parent == null ? null : Bones[Data.Bones.IndexOf(boneData.Parent)]; + Bones.Add(new Bone(boneData, parent)); + } + + Slots = new List(Data.Slots.Count); + DrawOrder = new List(Data.Slots.Count); + foreach (SlotData slotData in Data.Slots) { + Bone bone = Bones[Data.Bones.IndexOf(slotData.BoneData)]; + Slot slot = new Slot(slotData, this, bone); + Slots.Add(slot); + DrawOrder.Add(slot); + } + + R = 1; + G = 1; + B = 1; + A = 1; + } + + /** Updates the world transform for each bone. */ + public void UpdateWorldTransform () { + bool flipX = FlipX; + bool flipY = FlipY; + List bones = Bones; + for (int i = 0, n = bones.Count; i < n; i++) + bones[i].UpdateWorldTransform(flipX, flipY); + } + + /** Sets the bones and slots to their bind pose values. */ + public void SetToBindPose () { + SetBonesToBindPose(); + SetSlotsToBindPose(); + } + + public void SetBonesToBindPose () { + List bones = this.Bones; + for (int i = 0, n = bones.Count; i < n; i++) + bones[i].SetToBindPose(); + } + + public void SetSlotsToBindPose () { + List slots = this.Slots; + for (int i = 0, n = slots.Count; i < n; i++) + slots[i].SetToBindPose(i); + } + + /** @return May be null. */ + public Bone FindBone (String boneName) { + if (boneName == null) throw new ArgumentNullException("boneName cannot be null."); + List bones = this.Bones; + for (int i = 0, n = bones.Count; i < n; i++) { + Bone bone = bones[i]; + if (bone.Data.Name == boneName) return bone; + } + return null; + } + + /** @return -1 if the bone was not found. */ + public int FindBoneIndex (String boneName) { + if (boneName == null) throw new ArgumentNullException("boneName cannot be null."); + List bones = this.Bones; + for (int i = 0, n = bones.Count; i < n; i++) + if (bones[i].Data.Name == boneName) return i; + return -1; + } + + /** @return May be null. */ + public Slot FindSlot (String slotName) { + if (slotName == null) throw new ArgumentNullException("slotName cannot be null."); + List slots = this.Slots; + for (int i = 0, n = slots.Count; i < n; i++) { + Slot slot = slots[i]; + if (slot.Data.Name == slotName) return slot; + } + return null; + } + + /** @return -1 if the bone was not found. */ + public int FindSlotIndex (String slotName) { + if (slotName == null) throw new ArgumentNullException("slotName cannot be null."); + List slots = this.Slots; + for (int i = 0, n = slots.Count; i < n; i++) + if (slots[i].Data.Name.Equals(slotName)) return i; + return -1; + } + + /** Sets a skin by name. + * @see #setSkin(Skin) */ + public void SetSkin (String skinName) { + Skin skin = Data.FindSkin(skinName); + if (skin == null) throw new ArgumentException("Skin not found: " + skinName); + SetSkin(skin); + } + + /** Sets the skin used to look up attachments not found in the {@link SkeletonData#getDefaultSkin() default skin}. Attachments + * from the new skin are attached if the corresponding attachment from the old skin was attached. + * @param newSkin May be null. */ + public void SetSkin (Skin newSkin) { + if (Skin != null && newSkin != null) newSkin.AttachAll(this, Skin); + Skin = newSkin; + } + + /** @return May be null. */ + public Attachment GetAttachment (String slotName, String attachmentName) { + return GetAttachment(Data.FindSlotIndex(slotName), attachmentName); + } + + /** @return May be null. */ + public Attachment GetAttachment (int slotIndex, String attachmentName) { + if (attachmentName == null) throw new ArgumentNullException("attachmentName cannot be null."); + if (Skin != null) { + Attachment attachment = Skin.GetAttachment(slotIndex, attachmentName); + if (attachment != null) return attachment; + } + if (Data.DefaultSkin != null) return Data.DefaultSkin.GetAttachment(slotIndex, attachmentName); + return null; + } + + /** @param attachmentName May be null. */ + public void SetAttachment (String slotName, String attachmentName) { + if (slotName == null) throw new ArgumentNullException("slotName cannot be null."); + List slots = Slots; + for (int i = 0, n = slots.Count; i < n; i++) { + Slot slot = slots[i]; + if (slot.Data.Name == slotName) { + Attachment attachment = null; + if (attachmentName != null) { + attachment = GetAttachment(i, attachmentName); + if (attachment == null) throw new ArgumentNullException("Attachment not found: " + attachmentName + ", for slot: " + slotName); + } + slot.Attachment = attachment; + return; + } + } + throw new Exception("Slot not found: " + slotName); + } + + public void Update (float delta) { + Time += delta; + } + } +} diff --git a/spine-csharp/src/SkeletonData.cs b/spine-csharp/src/SkeletonData.cs new file mode 100644 index 000000000..014f239ad --- /dev/null +++ b/spine-csharp/src/SkeletonData.cs @@ -0,0 +1,111 @@ + +using System; +using System.Collections.Generic; + +namespace Spine { + public class SkeletonData { + public String Name { get; set; } + public List Bones { get; private set; } // Ordered parents first. + public List Slots { get; private set; } // Bind pose draw order. + public List Skins { get; private set; } + /** May be null. */ + public Skin DefaultSkin; + public List Animations { get; private set; } + + public SkeletonData () { + Bones = new List(); + Slots = new List(); + Skins = new List(); + Animations = new List(); + } + + // --- Bones. + + public void AddBone (BoneData bone) { + if (bone == null) throw new ArgumentNullException("bone cannot be null."); + Bones.Add(bone); + } + + + /** @return May be null. */ + public BoneData FindBone (String boneName) { + if (boneName == null) throw new ArgumentNullException("boneName cannot be null."); + for (int i = 0, n = Bones.Count; i < n; i++) { + BoneData bone = Bones[i]; + if (bone.Name == boneName) return bone; + } + return null; + } + + /** @return -1 if the bone was not found. */ + public int FindBoneIndex (String boneName) { + if (boneName == null) throw new ArgumentNullException("boneName cannot be null."); + for (int i = 0, n = Bones.Count; i < n; i++) + if (Bones[i].Name == boneName) return i; + return -1; + } + + // --- Slots. + + public void AddSlot (SlotData slot) { + if (slot == null) throw new ArgumentNullException("slot cannot be null."); + Slots.Add(slot); + } + + /** @return May be null. */ + public SlotData FindSlot (String slotName) { + if (slotName == null) throw new ArgumentNullException("slotName cannot be null."); + for (int i = 0, n = Slots.Count; i < n; i++) { + SlotData slot = Slots[i]; + if (slot.Name == slotName) return slot; + } + return null; + } + + /** @return -1 if the bone was not found. */ + public int FindSlotIndex (String slotName) { + if (slotName == null) throw new ArgumentNullException("slotName cannot be null."); + for (int i = 0, n = Slots.Count; i < n; i++) + if (Slots[i].Name == slotName) return i; + return -1; + } + + // --- Skins. + + public void AddSkin (Skin skin) { + if (skin == null) throw new ArgumentNullException("skin cannot be null."); + Skins.Add(skin); + } + + /** @return May be null. */ + public Skin FindSkin (String skinName) { + if (skinName == null) throw new ArgumentNullException("skinName cannot be null."); + foreach (Skin skin in Skins) + if (skin.Name == skinName) return skin; + return null; + } + + // --- Animations. + + public void AddAnimation (Animation animation) { + if (animation == null) throw new ArgumentNullException("animation cannot be null."); + Animations.Add(animation); + } + + /** @return May be null. */ + public Animation FindAnimation (String animationName) { + if (animationName == null) throw new ArgumentNullException("animationName cannot be null."); + for (int i = 0, n = Animations.Count; i < n; i++) { + Animation animation = Animations[i]; + if (animation.Name == animationName) return animation; + } + return null; + } + + // --- + + override public String ToString () { + return Name != null ? Name : base.ToString(); + } + } +} diff --git a/spine-csharp/src/SkeletonJson.cs b/spine-csharp/src/SkeletonJson.cs new file mode 100644 index 000000000..a2ca060ff --- /dev/null +++ b/spine-csharp/src/SkeletonJson.cs @@ -0,0 +1,256 @@ +using System; +using System.IO; +using System.Collections.Generic; + +namespace Spine { + public class SkeletonJson { + static public String TIMELINE_SCALE = "scale"; + static public String TIMELINE_ROTATE = "rotate"; + static public String TIMELINE_TRANSLATE = "translate"; + static public String TIMELINE_ATTACHMENT = "attachment"; + static public String TIMELINE_COLOR = "color"; + + static public String ATTACHMENT_REGION = "region"; + static public String ATTACHMENT_REGION_SEQUENCE = "regionSequence"; + + private AttachmentLoader attachmentLoader; + public float Scale { get; set; } + + public SkeletonJson (BaseAtlas atlas) { + this.attachmentLoader = new AtlasAttachmentLoader(atlas); + Scale = 1; + } + + public SkeletonJson (AttachmentLoader attachmentLoader) { + this.attachmentLoader = attachmentLoader; + Scale = 1; + } + + public SkeletonData readSkeletonData (String name, String json) { + if (json == null) throw new ArgumentNullException("json cannot be null."); + + SkeletonData skeletonData = new SkeletonData(); + skeletonData.Name = name; + + var root = Json.Deserialize(json) as Dictionary; + + // Bones. + foreach (Dictionary boneMap in (List)root["bones"]) { + BoneData parent = null; + if (boneMap.ContainsKey("parent")) { + parent = skeletonData.FindBone((String)boneMap["parent"]); + if (parent == null) throw new Exception("Parent bone not found: " + boneMap["parent"]); + } + BoneData boneData = new BoneData((String)boneMap["name"], parent); + boneData.Length = getFloat(boneMap, "length", 0) * Scale; + boneData.X = getFloat(boneMap, "x", 0) * Scale; + boneData.Y = getFloat(boneMap, "y", 0) * Scale; + boneData.Rotation = getFloat(boneMap, "rotation", 0); + boneData.ScaleX = getFloat(boneMap, "scaleX", 1); + boneData.ScaleY = getFloat(boneMap, "scaleY", 1); + skeletonData.AddBone(boneData); + } + + // Slots. + if (root.ContainsKey("slots")) { + var slots = (List)root["slots"]; + foreach (Dictionary slotMap in (List)slots) { + String slotName = (String)slotMap["name"]; + String boneName = (String)slotMap["bone"]; + BoneData boneData = skeletonData.FindBone(boneName); + if (boneData == null) throw new Exception("Slot bone not found: " + boneName); + SlotData slotData = new SlotData(slotName, boneData); + + if (slotMap.ContainsKey("color")) { + String color = (String)slotMap["color"]; + slotData.R = toColor(color, 0); + slotData.G = toColor(color, 1); + slotData.B = toColor(color, 2); + slotData.A = toColor(color, 3); + } + + slotData.AttachmentName = (String)slotMap["attachment"]; + + skeletonData.AddSlot(slotData); + } + } + + // Skins. + if (root.ContainsKey("skins")) { + Dictionary skinMap = (Dictionary)root["skins"]; + foreach (KeyValuePair entry in skinMap) { + Skin skin = new Skin(entry.Key); + foreach (KeyValuePair slotEntry in (Dictionary)entry.Value) { + int slotIndex = skeletonData.FindSlotIndex(slotEntry.Key); + foreach (KeyValuePair attachmentEntry in ((Dictionary)slotEntry.Value)) { + Attachment attachment = readAttachment(attachmentEntry.Key, (Dictionary)attachmentEntry.Value); + skin.AddAttachment(slotIndex, attachmentEntry.Key, attachment); + } + } + skeletonData.AddSkin(skin); + if (skin.Name == "default") skeletonData.DefaultSkin = skin; + } + } + + + // Animations. + if (root.ContainsKey("animations")) { + Dictionary animationMap = (Dictionary)root["animations"]; + foreach (KeyValuePair entry in animationMap) + readAnimation(entry.Key, (Dictionary)entry.Value, skeletonData); + } + + skeletonData.Bones.TrimExcess(); + skeletonData.Slots.TrimExcess(); + skeletonData.Skins.TrimExcess(); + skeletonData.Animations.TrimExcess(); + return skeletonData; + } + + private Attachment readAttachment (String name, Dictionary map) { + if (map.ContainsKey("name")) name = (String)map["name"]; + + AttachmentType type = AttachmentType.region; + if (map.ContainsKey("type")) type = (AttachmentType)Enum.Parse(typeof(AttachmentType), (String)map["type"], false); + Attachment attachment = attachmentLoader.NewAttachment(type, name); + + if (attachment is RegionAttachment) { + RegionAttachment regionAttachment = (RegionAttachment)attachment; + regionAttachment.X = getFloat(map, "x", 0) * Scale; + regionAttachment.Y = getFloat(map, "y", 0) * Scale; + regionAttachment.ScaleX = getFloat(map, "scaleX", 1); + regionAttachment.ScaleY = getFloat(map, "scaleY", 1); + regionAttachment.Rotation = getFloat(map, "rotation", 0); + regionAttachment.Width = getFloat(map, "width", 32) * Scale; + regionAttachment.Height = getFloat(map, "height", 32) * Scale; + regionAttachment.UpdateOffset(); + } + + return attachment; + } + + private float getFloat (Dictionary map, String name, float defaultValue) { + if (!map.ContainsKey(name)) return (float)defaultValue; + return (float)map[name]; + } + + public static float toColor (String hexString, int colorIndex) { + if (hexString.Length != 8) throw new ArgumentException("Color hexidecimal length must be 8, recieved: " + hexString); + return Convert.ToInt32(hexString.Substring(colorIndex * 2, 2), 16) / (float)255; + } + + private void readAnimation (String name, Dictionary map, SkeletonData skeletonData) { + var timelines = new List(); + float duration = 0; + + var bonesMap = (Dictionary)map["bones"]; + foreach (KeyValuePair entry in bonesMap) { + String boneName = entry.Key; + int boneIndex = skeletonData.FindBoneIndex(boneName); + if (boneIndex == -1) throw new Exception("Bone not found: " + boneName); + + Dictionary timelineMap = (Dictionary)entry.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + List values = (List)timelineEntry.Value; + String timelineName = (String)timelineEntry.Key; + if (timelineName.Equals(TIMELINE_ROTATE)) { + RotateTimeline timeline = new RotateTimeline(values.Count); + timeline.BoneIndex = boneIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + timeline.SetFrame(frameIndex, time, (float)valueMap["angle"]); + readCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.Frames[timeline.FrameCount * 2 - 2]); + + } else if (timelineName.Equals(TIMELINE_TRANSLATE) || timelineName.Equals(TIMELINE_SCALE)) { + TranslateTimeline timeline; + float timelineScale = 1; + if (timelineName.Equals(TIMELINE_SCALE)) + timeline = new ScaleTimeline(values.Count); + else { + timeline = new TranslateTimeline(values.Count); + timelineScale = Scale; + } + timeline.BoneIndex = boneIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + float x = valueMap.ContainsKey("x") ? (float)valueMap["x"] : 0; + float y = valueMap.ContainsKey("y") ? (float)valueMap["y"] : 0; + timeline.SetFrame(frameIndex, time, (float)x * timelineScale, (float)y * timelineScale); + readCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.Frames[timeline.FrameCount * 3 - 3]); + + } else + throw new Exception("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")"); + } + } + + if (map.ContainsKey("slots")) { + Dictionary slotsMap = (Dictionary)map["slots"]; + foreach (KeyValuePair entry in slotsMap) { + String slotName = entry.Key; + int slotIndex = skeletonData.FindSlotIndex(slotName); + Dictionary timelineMap = (Dictionary)entry.Value; + + foreach (KeyValuePair timelineEntry in timelineMap) { + List> values = (List>)timelineEntry.Value; + String timelineName = (String)timelineEntry.Key; + if (timelineName.Equals(TIMELINE_COLOR)) { + ColorTimeline timeline = new ColorTimeline(values.Count); + timeline.SlotIndex = slotIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + String c = (String)valueMap["color"]; + timeline.setFrame(frameIndex, time, toColor(c, 0), toColor(c, 1), toColor(c, 2), toColor(c, 3)); + readCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.Frames[timeline.FrameCount * 5 - 5]); + + } else if (timelineName.Equals(TIMELINE_ATTACHMENT)) { + AttachmentTimeline timeline = new AttachmentTimeline(values.Count); + timeline.SlotIndex = slotIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + timeline.setFrame(frameIndex++, time, (String)valueMap["name"]); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.Frames[timeline.FrameCount - 1]); + + } else + throw new Exception("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")"); + } + } + } + + timelines.TrimExcess(); + skeletonData.AddAnimation(new Animation(name, timelines, duration)); + } + + private void readCurve (CurveTimeline timeline, int frameIndex, Dictionary valueMap) { + if (!valueMap.ContainsKey("curve")) return; + Object curveObject = valueMap["curve"]; + if (curveObject.Equals("stepped")) + timeline.SetStepped(frameIndex); + else if (curveObject.GetType() == typeof(List)) { + List curve = (List)curveObject; + timeline.SetCurve(frameIndex, (float)curve[0], (float)curve[1], (float)curve[2], (float)curve[3]); + } + } + } +} diff --git a/spine-csharp/src/Skin.cs b/spine-csharp/src/Skin.cs new file mode 100644 index 000000000..cf2ccb66a --- /dev/null +++ b/spine-csharp/src/Skin.cs @@ -0,0 +1,54 @@ + +using System; +using System.Collections.Generic; + +namespace Spine { + /** Stores attachments by slot index and attachment name. */ + public class Skin { + public String Name { get; private set; } + private Dictionary, Attachment> attachments = new Dictionary, Attachment>(); + + public Skin (String name) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + Name = name; + } + + public void AddAttachment (int slotIndex, String name, Attachment attachment) { + if (attachment == null) throw new ArgumentNullException("attachment cannot be null."); + attachments.Add(Tuple.Create(slotIndex, name), attachment); + } + + /** @return May be null. */ + public Attachment GetAttachment (int slotIndex, String name) { + return attachments[Tuple.Create(slotIndex, name)]; + } + + public void FindNamesForSlot (int slotIndex, List names) { + if (names == null) throw new ArgumentNullException("names cannot be null."); + foreach (Tuple key in attachments.Keys) + if (key.Item1 == slotIndex) names.Add(key.Item2); + } + + public void FindAttachmentsForSlot (int slotIndex, List attachments) { + if (attachments == null) throw new ArgumentNullException("attachments cannot be null."); + foreach (KeyValuePair, Attachment> entry in this.attachments) + if (entry.Key.Item1 == slotIndex) attachments.Add(entry.Value); + } + + override public String ToString () { + return Name; + } + + /** Attach all attachments from this skin if the corresponding attachment from the old skin is currently attached. */ + internal void AttachAll (Skeleton skeleton, Skin oldSkin) { + foreach (KeyValuePair, Attachment> entry in oldSkin.attachments) { + int slotIndex = entry.Key.Item1; + Slot slot = skeleton.Slots[slotIndex]; + if (slot.Attachment == entry.Value) { + Attachment attachment = GetAttachment(slotIndex, entry.Key.Item2); + if (attachment != null) slot.Attachment = attachment; + } + } + } + } +} diff --git a/spine-csharp/src/Slot.cs b/spine-csharp/src/Slot.cs new file mode 100644 index 000000000..aa91e2ddf --- /dev/null +++ b/spine-csharp/src/Slot.cs @@ -0,0 +1,61 @@ +using System; + +namespace Spine { + public class Slot { + public SlotData Data { get; private set; } + public Bone Bone { get; private set; } + public Skeleton Skeleton { get; private set; } + public float R { get; set; } + public float G { get; set; } + public float B { get; set; } + public float A { get; set; } + + /** May be null. */ + private Attachment attachment; + public Attachment Attachment { + get { + return attachment; + } + set { + attachment = value; + attachmentTime = Skeleton.Time; + } + } + + private float attachmentTime; + public float AttachmentTime { + get { + return Skeleton.Time - attachmentTime; + } + set { + attachmentTime = Skeleton.Time - value; + } + } + + public Slot (SlotData data, Skeleton skeleton, Bone bone) { + if (data == null) throw new ArgumentNullException("data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + if (bone == null) throw new ArgumentNullException("bone cannot be null."); + Data = data; + Skeleton = skeleton; + Bone = bone; + SetToBindPose(); + } + + internal void SetToBindPose (int slotIndex) { + R = Data.R; + G = Data.G; + B = Data.B; + A = Data.A; + Attachment = Data.AttachmentName == null ? null : Skeleton.GetAttachment(slotIndex, Data.AttachmentName); + } + + public void SetToBindPose () { + SetToBindPose(Skeleton.Data.Slots.IndexOf(Data)); + } + + override public String ToString () { + return Data.Name; + } + } +} diff --git a/spine-csharp/src/SlotData.cs b/spine-csharp/src/SlotData.cs new file mode 100644 index 000000000..0866a895e --- /dev/null +++ b/spine-csharp/src/SlotData.cs @@ -0,0 +1,29 @@ +using System; + +namespace Spine { + public class SlotData { + public String Name { get; private set; } + public BoneData BoneData { get; private set; } + public float R { get; set; } + public float G { get; set; } + public float B { get; set; } + public float A { get; set; } + /** @param attachmentName May be null. */ + public String AttachmentName { get; set; } + + public SlotData (String name, BoneData boneData) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + if (boneData == null) throw new ArgumentNullException("boneData cannot be null."); + Name = name; + BoneData = boneData; + R = 1; + G = 1; + B = 1; + A = 1; + } + + override public String ToString () { + return Name; + } + } +} diff --git a/spine-xna/Properties/AssemblyInfo.cs b/spine-xna/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..8117bed42 --- /dev/null +++ b/spine-xna/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("spine-xna")] +[assembly: AssemblyProduct("spine-xna")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. Only Windows +// assemblies support COM. +[assembly: ComVisible(false)] + +// On Windows, the following GUID is for the ID of the typelib if this +// project is exposed to COM. On other platforms, it unique identifies the +// title storage container when deploying this assembly to the device. +[assembly: Guid("bce68f54-1e09-449a-90a2-b7ca28f491a5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] diff --git a/spine-xna/example/Game.ico b/spine-xna/example/Game.ico new file mode 100644 index 000000000..8cff41eab Binary files /dev/null and b/spine-xna/example/Game.ico differ diff --git a/spine-xna/example/GameThumbnail.png b/spine-xna/example/GameThumbnail.png new file mode 100644 index 000000000..462311aba Binary files /dev/null and b/spine-xna/example/GameThumbnail.png differ diff --git a/spine-xna/example/Properties/AssemblyInfo.cs b/spine-xna/example/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..f9b3c07df --- /dev/null +++ b/spine-xna/example/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("spine-xna-example")] +[assembly: AssemblyProduct("spine-xna-example")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. Only Windows +// assemblies support COM. +[assembly: ComVisible(false)] + +// On Windows, the following GUID is for the ID of the typelib if this +// project is exposed to COM. On other platforms, it unique identifies the +// title storage container when deploying this assembly to the device. +[assembly: Guid("078eb4ac-3a70-4ab4-b103-a048c6a15898")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] diff --git a/spine-xna/example/data/goblins.atlas b/spine-xna/example/data/goblins.atlas new file mode 100644 index 000000000..271742263 --- /dev/null +++ b/spine-xna/example/data/goblins.atlas @@ -0,0 +1,285 @@ + +goblins.png +format: RGBA8888 +filter: Linear,Linear +repeat: none +spear + rotate: false + xy: 2, 142 + size: 22, 368 + orig: 22, 368 + offset: 0, 0 + index: -1 +goblingirl/head + rotate: false + xy: 26, 429 + size: 103, 81 + orig: 103, 81 + offset: 0, 0 + index: -1 +goblin/head + rotate: false + xy: 26, 361 + size: 103, 66 + orig: 103, 66 + offset: 0, 0 + index: -1 +goblin/torso + rotate: false + xy: 131, 414 + size: 68, 96 + orig: 68, 96 + offset: 0, 0 + index: -1 +goblingirl/torso + rotate: false + xy: 26, 263 + size: 68, 96 + orig: 68, 96 + offset: 0, 0 + index: -1 +dagger + rotate: false + xy: 26, 153 + size: 26, 108 + orig: 26, 108 + offset: 0, 0 + index: -1 +goblin/right-lower-leg + rotate: false + xy: 201, 434 + size: 36, 76 + orig: 36, 76 + offset: 0, 0 + index: -1 +goblingirl/right-lower-leg + rotate: false + xy: 54, 185 + size: 36, 76 + orig: 36, 76 + offset: 0, 0 + index: -1 +goblin/left-upper-leg + rotate: false + xy: 96, 286 + size: 33, 73 + orig: 33, 73 + offset: 0, 0 + index: -1 +goblin/pelvis + rotate: false + xy: 131, 369 + size: 62, 43 + orig: 62, 43 + offset: 0, 0 + index: -1 +goblingirl/pelvis + rotate: false + xy: 131, 324 + size: 62, 43 + orig: 62, 43 + offset: 0, 0 + index: -1 +goblin/right-foot + rotate: false + xy: 131, 289 + size: 63, 33 + orig: 63, 33 + offset: 0, 0 + index: -1 +goblin/left-lower-leg + rotate: false + xy: 2, 70 + size: 33, 70 + orig: 33, 70 + offset: 0, 0 + index: -1 +goblin/right-upper-leg + rotate: false + xy: 2, 5 + size: 34, 63 + orig: 34, 63 + offset: 0, 0 + index: -1 +goblingirl/left-lower-leg + rotate: false + xy: 195, 342 + size: 33, 70 + orig: 33, 70 + offset: 0, 0 + index: -1 +goblingirl/left-upper-leg + rotate: false + xy: 37, 81 + size: 33, 70 + orig: 33, 70 + offset: 0, 0 + index: -1 +goblingirl/right-upper-leg + rotate: false + xy: 38, 16 + size: 34, 63 + orig: 34, 63 + offset: 0, 0 + index: -1 +goblin/eyes-closed + rotate: false + xy: 38, 2 + size: 34, 12 + orig: 34, 12 + offset: 0, 0 + index: -1 +goblin/undies + rotate: false + xy: 54, 154 + size: 36, 29 + orig: 36, 29 + offset: 0, 0 + index: -1 +goblin/right-arm + rotate: false + xy: 72, 102 + size: 23, 50 + orig: 23, 50 + offset: 0, 0 + index: -1 +goblin/left-foot + rotate: false + xy: 131, 256 + size: 65, 31 + orig: 65, 31 + offset: 0, 0 + index: -1 +goblingirl/right-arm + rotate: false + xy: 196, 290 + size: 28, 50 + orig: 28, 50 + offset: 0, 0 + index: -1 +goblingirl/left-shoulder + rotate: false + xy: 226, 294 + size: 28, 46 + orig: 28, 46 + offset: 0, 0 + index: -1 +goblin/left-arm + rotate: false + xy: 198, 253 + size: 37, 35 + orig: 37, 35 + offset: 0, 0 + index: -1 +goblingirl/left-foot + rotate: false + xy: 92, 223 + size: 65, 31 + orig: 65, 31 + offset: 0, 0 + index: -1 +goblingirl/right-foot + rotate: false + xy: 92, 188 + size: 63, 33 + orig: 63, 33 + offset: 0, 0 + index: -1 +goblin/undie-straps + rotate: false + xy: 92, 167 + size: 55, 19 + orig: 55, 19 + offset: 0, 0 + index: -1 +goblingirl/left-arm + rotate: false + xy: 159, 219 + size: 37, 35 + orig: 37, 35 + offset: 0, 0 + index: -1 +goblin/right-shoulder + rotate: false + xy: 97, 120 + size: 39, 45 + orig: 39, 45 + offset: 0, 0 + index: -1 +goblingirl/right-shoulder + rotate: false + xy: 198, 206 + size: 39, 45 + orig: 39, 45 + offset: 0, 0 + index: -1 +goblin/left-hand + rotate: false + xy: 157, 176 + size: 36, 41 + orig: 36, 41 + offset: 0, 0 + index: -1 +goblin/neck + rotate: false + xy: 195, 163 + size: 36, 41 + orig: 36, 41 + offset: 0, 0 + index: -1 +goblingirl/undie-straps + rotate: false + xy: 97, 99 + size: 55, 19 + orig: 55, 19 + offset: 0, 0 + index: -1 +goblingirl/neck + rotate: false + xy: 138, 120 + size: 35, 41 + orig: 35, 41 + offset: 0, 0 + index: -1 +goblingirl/left-hand + rotate: false + xy: 175, 121 + size: 35, 40 + orig: 35, 40 + offset: 0, 0 + index: -1 +goblin/left-shoulder + rotate: false + xy: 212, 117 + size: 29, 44 + orig: 29, 44 + offset: 0, 0 + index: -1 +goblingirl/eyes-closed + rotate: false + xy: 154, 97 + size: 37, 21 + orig: 37, 21 + offset: 0, 0 + index: -1 +goblin/right-hand + rotate: false + xy: 193, 78 + size: 36, 37 + orig: 36, 37 + offset: 0, 0 + index: -1 +goblingirl/right-hand + rotate: false + xy: 74, 39 + size: 36, 37 + orig: 36, 37 + offset: 0, 0 + index: -1 +goblingirl/undies + rotate: false + xy: 74, 8 + size: 36, 29 + orig: 36, 29 + offset: 0, 0 + index: -1 diff --git a/spine-xna/example/data/goblins.json b/spine-xna/example/data/goblins.json new file mode 100644 index 000000000..f1dcc96a0 --- /dev/null +++ b/spine-xna/example/data/goblins.json @@ -0,0 +1,499 @@ +{ +"bones": [ + { "name": "root" }, + { "name": "hip", "parent": "root", "x": 0.64, "y": 114.41 }, + { "name": "left upper leg", "parent": "hip", "length": 50.39, "x": 14.45, "y": 2.81, "rotation": -89.09 }, + { "name": "left lower leg", "parent": "left upper leg", "length": 49.89, "x": 56.34, "y": 0.98, "rotation": -16.65 }, + { "name": "left foot", "parent": "left lower leg", "length": 46.5, "x": 58.94, "y": -7.61, "rotation": 102.43 }, + { "name": "right upper leg", "parent": "hip", "length": 42.45, "x": -20.07, "y": -6.83, "rotation": -97.49 }, + { "name": "right lower leg", "parent": "right upper leg", "length": 58.52, "x": 42.99, "y": -0.61, "rotation": -14.34 }, + { "name": "right foot", "parent": "right lower leg", "length": 45.45, "x": 64.88, "y": 0.04, "rotation": 110.3 }, + { "name": "torso", "parent": "hip", "length": 85.82, "x": -6.42, "y": 1.97, "rotation": 93.92 }, + { "name": "neck", "parent": "torso", "length": 18.38, "x": 81.67, "y": -6.34, "rotation": -1.51 }, + { "name": "head", "parent": "neck", "length": 68.28, "x": 20.93, "y": 11.59, "rotation": -13.92 }, + { "name": "right shoulder", "parent": "torso", "length": 37.24, "x": 76.02, "y": 18.14, "rotation": 133.88 }, + { "name": "right arm", "parent": "right shoulder", "length": 36.74, "x": 37.6, "y": 0.31, "rotation": 36.32 }, + { "name": "right hand", "parent": "right arm", "length": 15.32, "x": 36.9, "y": 0.34, "rotation": 2.35 }, + { "name": "left shoulder", "parent": "torso", "length": 35.43, "x": 74.04, "y": -20.38, "rotation": -156.96 }, + { "name": "left arm", "parent": "left shoulder", "length": 35.62, "x": 37.85, "y": -2.34, "rotation": 28.16 }, + { "name": "left hand", "parent": "left arm", "length": 11.52, "x": 35.62, "y": 0.07, "rotation": 2.7 }, + { "name": "pelvis", "parent": "hip", "x": 1.41, "y": -6.57 } +], +"slots": [ + { "name": "left shoulder", "bone": "left shoulder", "attachment": "left shoulder" }, + { "name": "left arm", "bone": "left arm", "attachment": "left arm" }, + { "name": "left hand item", "bone": "left hand", "attachment": "spear" }, + { "name": "left hand", "bone": "left hand", "attachment": "left hand" }, + { "name": "left foot", "bone": "left foot", "attachment": "left foot" }, + { "name": "left lower leg", "bone": "left lower leg", "attachment": "left lower leg" }, + { "name": "left upper leg", "bone": "left upper leg", "attachment": "left upper leg" }, + { "name": "neck", "bone": "neck", "attachment": "neck" }, + { "name": "torso", "bone": "torso", "attachment": "torso" }, + { "name": "pelvis", "bone": "pelvis", "attachment": "pelvis" }, + { "name": "right foot", "bone": "right foot", "attachment": "right foot" }, + { "name": "right lower leg", "bone": "right lower leg", "attachment": "right lower leg" }, + { "name": "undie straps", "bone": "pelvis", "attachment": "undie straps" }, + { "name": "undies", "bone": "pelvis", "attachment": "undies" }, + { "name": "right upper leg", "bone": "right upper leg", "attachment": "right upper leg" }, + { "name": "head", "bone": "head", "attachment": "head" }, + { "name": "eyes", "bone": "head" }, + { "name": "right shoulder", "bone": "right shoulder", "attachment": "right shoulder" }, + { "name": "right arm", "bone": "right arm", "attachment": "right arm" }, + { "name": "right hand item", "bone": "right hand", "attachment": "dagger" }, + { "name": "right hand", "bone": "right hand", "attachment": "right hand" } +], +"skins": { + "default": { + "left hand item": { + "dagger": { "x": 7.88, "y": -23.45, "rotation": 10.47, "width": 26, "height": 108 }, + "spear": { "x": -4.55, "y": 39.2, "rotation": 13.04, "width": 22, "height": 368 } + }, + "right hand item": { + "dagger": { "x": 6.51, "y": -24.15, "rotation": -8.06, "width": 26, "height": 108 } + } + }, + "goblin": { + "neck": { + "neck": { "name": "goblin/neck", "x": 10.1, "y": 0.42, "rotation": -93.69, "width": 36, "height": 41 } + }, + "undies": { + "undies": { "name": "goblin/undies", "x": 6.3, "y": 0.12, "rotation": 0.91, "width": 36, "height": 29 } + }, + "right hand": { + "right hand": { "name": "goblin/right-hand", "x": 7.88, "y": 2.78, "rotation": 91.96, "width": 36, "height": 37 } + }, + "right arm": { + "right arm": { "name": "goblin/right-arm", "x": 16.44, "y": -1.04, "rotation": 94.32, "width": 23, "height": 50 } + }, + "head": { + "head": { "name": "goblin/head", "x": 25.73, "y": 2.33, "rotation": -92.29, "width": 103, "height": 66 } + }, + "left shoulder": { + "left shoulder": { "name": "goblin/left-shoulder", "x": 15.56, "y": -2.26, "rotation": 62.01, "width": 29, "height": 44 } + }, + "left arm": { + "left arm": { + "name": "goblin/left-arm", + "x": 16.7, + "y": -1.69, + "scaleX": 1.057, + "scaleY": 1.057, + "rotation": 33.84, + "width": 37, + "height": 35 + } + }, + "left hand": { + "left hand": { + "name": "goblin/left-hand", + "x": 3.47, + "y": 3.41, + "scaleX": 0.892, + "scaleY": 0.892, + "rotation": 31.14, + "width": 36, + "height": 41 + } + }, + "right lower leg": { + "right lower leg": { "name": "goblin/right-lower-leg", "x": 25.68, "y": -3.15, "rotation": 111.83, "width": 36, "height": 76 } + }, + "right upper leg": { + "right upper leg": { "name": "goblin/right-upper-leg", "x": 20.35, "y": 1.47, "rotation": 97.49, "width": 34, "height": 63 } + }, + "pelvis": { + "pelvis": { "name": "goblin/pelvis", "x": -5.61, "y": 0.76, "width": 62, "height": 43 } + }, + "left lower leg": { + "left lower leg": { "name": "goblin/left-lower-leg", "x": 23.58, "y": -2.06, "rotation": 105.75, "width": 33, "height": 70 } + }, + "left upper leg": { + "left upper leg": { "name": "goblin/left-upper-leg", "x": 29.68, "y": -3.87, "rotation": 89.09, "width": 33, "height": 73 } + }, + "torso": { + "torso": { "name": "goblin/torso", "x": 38.09, "y": -3.87, "rotation": -94.95, "width": 68, "height": 96 } + }, + "right shoulder": { + "right shoulder": { "name": "goblin/right-shoulder", "x": 15.68, "y": -1.03, "rotation": 130.65, "width": 39, "height": 45 } + }, + "right foot": { + "right foot": { "name": "goblin/right-foot", "x": 23.56, "y": 9.8, "rotation": 1.52, "width": 63, "height": 33 } + }, + "left foot": { + "left foot": { "name": "goblin/left-foot", "x": 24.85, "y": 8.74, "rotation": 3.32, "width": 65, "height": 31 } + }, + "undie straps": { + "undie straps": { "name": "goblin/undie-straps", "x": -3.87, "y": 13.1, "scaleX": 1.089, "width": 55, "height": 19 } + }, + "eyes": { + "eyes closed": { "name": "goblin/eyes-closed", "x": 32.21, "y": -21.27, "rotation": -88.92, "width": 34, "height": 12 } + } + }, + "goblingirl": { + "left upper leg": { + "left upper leg": { "name": "goblingirl/left-upper-leg", "x": 30.21, "y": -2.95, "rotation": 89.09, "width": 33, "height": 70 } + }, + "left lower leg": { + "left lower leg": { "name": "goblingirl/left-lower-leg", "x": 25.02, "y": -0.6, "rotation": 105.75, "width": 33, "height": 70 } + }, + "left foot": { + "left foot": { "name": "goblingirl/left-foot", "x": 25.17, "y": 7.92, "rotation": 3.32, "width": 65, "height": 31 } + }, + "right upper leg": { + "right upper leg": { "name": "goblingirl/right-upper-leg", "x": 19.69, "y": 2.13, "rotation": 97.49, "width": 34, "height": 63 } + }, + "right lower leg": { + "right lower leg": { "name": "goblingirl/right-lower-leg", "x": 26.15, "y": -3.27, "rotation": 111.83, "width": 36, "height": 76 } + }, + "right foot": { + "right foot": { "name": "goblingirl/right-foot", "x": 23.46, "y": 9.66, "rotation": 1.52, "width": 63, "height": 33 } + }, + "torso": { + "torso": { "name": "goblingirl/torso", "x": 36.28, "y": -5.14, "rotation": -95.74, "width": 68, "height": 96 } + }, + "left shoulder": { + "left shoulder": { "name": "goblingirl/left-shoulder", "x": 19.8, "y": -0.42, "rotation": 61.21, "width": 28, "height": 46 } + }, + "left arm": { + "left arm": { "name": "goblingirl/left-arm", "x": 19.64, "y": -2.42, "rotation": 33.05, "width": 37, "height": 35 } + }, + "left hand": { + "left hand": { + "name": "goblingirl/left-hand", + "x": 4.34, + "y": 2.39, + "scaleX": 0.896, + "scaleY": 0.896, + "rotation": 30.34, + "width": 35, + "height": 40 + } + }, + "neck": { + "neck": { "name": "goblingirl/neck", "x": 6.16, "y": -3.14, "rotation": -98.86, "width": 35, "height": 41 } + }, + "head": { + "head": { "name": "goblingirl/head", "x": 27.71, "y": -4.32, "rotation": -85.58, "width": 103, "height": 81 } + }, + "right shoulder": { + "right shoulder": { "name": "goblingirl/right-shoulder", "x": 14.46, "y": 0.45, "rotation": 129.85, "width": 39, "height": 45 } + }, + "right arm": { + "right arm": { "name": "goblingirl/right-arm", "x": 16.85, "y": -0.66, "rotation": 93.52, "width": 28, "height": 50 } + }, + "right hand": { + "right hand": { "name": "goblingirl/right-hand", "x": 7.21, "y": 3.43, "rotation": 91.16, "width": 36, "height": 37 } + }, + "pelvis": { + "pelvis": { "name": "goblingirl/pelvis", "x": -3.87, "y": 3.18, "width": 62, "height": 43 } + }, + "undie straps": { + "undie straps": { "name": "goblingirl/undie-straps", "x": -1.51, "y": 14.18, "width": 55, "height": 19 } + }, + "undies": { + "undies": { "name": "goblingirl/undies", "x": 5.4, "y": 1.7, "width": 36, "height": 29 } + }, + "eyes": { + "eyes closed": { "name": "goblingirl/eyes-closed", "x": 28, "y": -25.54, "rotation": -87.04, "width": 37, "height": 21 } + } + } +}, +"animations": { + "walk": { + "bones": { + "left upper leg": { + "rotate": [ + { "time": 0, "angle": -26.55 }, + { "time": 0.1333, "angle": -8.78 }, + { "time": 0.2333, "angle": 9.51 }, + { "time": 0.3666, "angle": 30.74 }, + { "time": 0.5, "angle": 25.33 }, + { "time": 0.6333, "angle": 26.11 }, + { "time": 0.7333, "angle": -7.7 }, + { "time": 0.8666, "angle": -21.19 }, + { "time": 1, "angle": -26.55 } + ], + "translate": [ + { "time": 0, "x": -1.32, "y": 1.7 }, + { "time": 0.3666, "x": -0.06, "y": 2.42 }, + { "time": 1, "x": -1.32, "y": 1.7 } + ] + }, + "right upper leg": { + "rotate": [ + { "time": 0, "angle": 42.45 }, + { "time": 0.1333, "angle": 52.1 }, + { "time": 0.2333, "angle": 8.53 }, + { "time": 0.5, "angle": -16.93 }, + { "time": 0.6333, "angle": 1.89 }, + { + "time": 0.7333, + "angle": 28.06, + "curve": [ 0.462, 0.11, 1, 1 ] + }, + { + "time": 0.8666, + "angle": 58.68, + "curve": [ 0.5, 0.02, 1, 1 ] + }, + { "time": 1, "angle": 42.45 } + ], + "translate": [ + { "time": 0, "x": 6.23, "y": 0 }, + { "time": 0.2333, "x": 2.14, "y": 2.4 }, + { "time": 0.5, "x": 2.44, "y": 4.8 }, + { "time": 1, "x": 6.23, "y": 0 } + ] + }, + "left lower leg": { + "rotate": [ + { "time": 0, "angle": -22.98 }, + { "time": 0.1333, "angle": -63.5 }, + { "time": 0.2333, "angle": -73.76 }, + { "time": 0.5, "angle": 5.11 }, + { "time": 0.6333, "angle": -28.29 }, + { "time": 0.7333, "angle": 4.08 }, + { "time": 0.8666, "angle": 3.53 }, + { "time": 1, "angle": -22.98 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0 }, + { "time": 0.2333, "x": 2.55, "y": -0.47 }, + { "time": 0.5, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1, "x": 0, "y": 0 } + ] + }, + "left foot": { + "rotate": [ + { "time": 0, "angle": -3.69 }, + { "time": 0.1333, "angle": -10.42 }, + { "time": 0.2333, "angle": -5.01 }, + { "time": 0.3666, "angle": 3.87 }, + { "time": 0.5, "angle": -3.87 }, + { "time": 0.6333, "angle": 2.78 }, + { "time": 0.7333, "angle": 1.68 }, + { "time": 0.8666, "angle": -8.54 }, + { "time": 1, "angle": -3.69 } + ] + }, + "right shoulder": { + "rotate": [ + { + "time": 0, + "angle": 5.29, + "curve": [ 0.264, 0, 0.75, 1 ] + }, + { "time": 0.6333, "angle": 6.65 }, + { "time": 1, "angle": 5.29 } + ] + }, + "right arm": { + "rotate": [ + { + "time": 0, + "angle": -4.02, + "curve": [ 0.267, 0, 0.804, 0.99 ] + }, + { + "time": 0.6333, + "angle": 19.78, + "curve": [ 0.307, 0, 0.787, 0.99 ] + }, + { "time": 1, "angle": -4.02 } + ] + }, + "right hand": { + "rotate": [ + { "time": 0, "angle": 8.98 }, + { "time": 0.6333, "angle": 0.51 }, + { "time": 1, "angle": 8.98 } + ] + }, + "left shoulder": { + "rotate": [ + { + "time": 0, + "angle": 6.25, + "curve": [ 0.339, 0, 0.683, 1 ] + }, + { + "time": 0.5, + "angle": -11.78, + "curve": [ 0.281, 0, 0.686, 0.99 ] + }, + { "time": 1, "angle": 6.25 } + ], + "translate": [ + { "time": 0, "x": 1.15, "y": 0.23 } + ] + }, + "left hand": { + "rotate": [ + { + "time": 0, + "angle": -21.23, + "curve": [ 0.295, 0, 0.755, 0.98 ] + }, + { + "time": 0.5, + "angle": -27.28, + "curve": [ 0.241, 0, 0.75, 0.97 ] + }, + { "time": 1, "angle": -21.23 } + ] + }, + "left arm": { + "rotate": [ + { + "time": 0, + "angle": 28.37, + "curve": [ 0.339, 0, 0.683, 1 ] + }, + { + "time": 0.5, + "angle": 60.09, + "curve": [ 0.281, 0, 0.686, 0.99 ] + }, + { "time": 1, "angle": 28.37 } + ] + }, + "torso": { + "rotate": [ + { "time": 0, "angle": -10.28 }, + { + "time": 0.1333, + "angle": -15.38, + "curve": [ 0.545, 0, 0.818, 1 ] + }, + { + "time": 0.3666, + "angle": -9.78, + "curve": [ 0.58, 0.17, 0.669, 0.99 ] + }, + { + "time": 0.6333, + "angle": -15.75, + "curve": [ 0.235, 0.01, 0.795, 1 ] + }, + { + "time": 0.8666, + "angle": -7.06, + "curve": [ 0.209, 0, 0.816, 0.98 ] + }, + { "time": 1, "angle": -10.28 } + ], + "translate": [ + { "time": 0, "x": -1.29, "y": 1.68 } + ] + }, + "right foot": { + "rotate": [ + { "time": 0, "angle": -5.25 }, + { "time": 0.2333, "angle": -1.91 }, + { "time": 0.3666, "angle": -6.45 }, + { "time": 0.5, "angle": -5.39 }, + { "time": 0.7333, "angle": -11.68 }, + { "time": 0.8666, "angle": 0.46 }, + { "time": 1, "angle": -5.25 } + ] + }, + "right lower leg": { + "rotate": [ + { + "time": 0, + "angle": -3.39, + "curve": [ 0.316, 0.01, 0.741, 0.98 ] + }, + { + "time": 0.1333, + "angle": -45.53, + "curve": [ 0.229, 0, 0.738, 0.97 ] + }, + { "time": 0.2333, "angle": -4.83 }, + { "time": 0.5, "angle": -19.53 }, + { "time": 0.6333, "angle": -64.8 }, + { + "time": 0.7333, + "angle": -82.56, + "curve": [ 0.557, 0.18, 1, 1 ] + }, + { "time": 1, "angle": -3.39 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.5, "x": 0, "y": 0 }, + { "time": 0.6333, "x": 2.18, "y": 0.21 }, + { "time": 1, "x": 0, "y": 0 } + ] + }, + "hip": { + "rotate": [ + { "time": 0, "angle": 0, "curve": "stepped" }, + { "time": 1, "angle": 0 } + ], + "translate": [ + { "time": 0, "x": 0, "y": -4.16 }, + { + "time": 0.1333, + "x": 0, + "y": -7.05, + "curve": [ 0.359, 0.47, 0.646, 0.74 ] + }, + { "time": 0.3666, "x": 0, "y": 6.78 }, + { "time": 0.5, "x": 0, "y": -6.13 }, + { + "time": 0.6333, + "x": 0, + "y": -7.05, + "curve": [ 0.359, 0.47, 0.646, 0.74 ] + }, + { "time": 0.8666, "x": 0, "y": 6.78 }, + { "time": 1, "x": 0, "y": -4.16 } + ] + }, + "neck": { + "rotate": [ + { "time": 0, "angle": 3.6 }, + { "time": 0.1333, "angle": 17.49 }, + { "time": 0.2333, "angle": 6.1 }, + { "time": 0.3666, "angle": 3.45 }, + { "time": 0.5, "angle": 5.17 }, + { "time": 0.6333, "angle": 18.36 }, + { "time": 0.7333, "angle": 6.09 }, + { "time": 0.8666, "angle": 2.28 }, + { "time": 1, "angle": 3.6 } + ] + }, + "head": { + "rotate": [ + { + "time": 0, + "angle": 3.6, + "curve": [ 0, 0, 0.704, 1.17 ] + }, + { "time": 0.1333, "angle": -0.2 }, + { "time": 0.2333, "angle": 6.1 }, + { "time": 0.3666, "angle": 3.45 }, + { + "time": 0.5, + "angle": 5.17, + "curve": [ 0, 0, 0.704, 1.61 ] + }, + { "time": 0.6666, "angle": 1.1 }, + { "time": 0.7333, "angle": 6.09 }, + { "time": 0.8666, "angle": 2.28 }, + { "time": 1, "angle": 3.6 } + ] + } + }, + "slots": { + "eyes": { + "attachment": [ + { "time": 0.7, "name": "eyes closed" }, + { "time": 0.8, "name": null } + ] + } + } + } +} +} \ No newline at end of file diff --git a/spine-xna/example/data/goblins.png b/spine-xna/example/data/goblins.png new file mode 100644 index 000000000..863b29467 Binary files /dev/null and b/spine-xna/example/data/goblins.png differ diff --git a/spine-xna/example/data/spineboy.atlas b/spine-xna/example/data/spineboy.atlas new file mode 100644 index 000000000..1f482c0a1 --- /dev/null +++ b/spine-xna/example/data/spineboy.atlas @@ -0,0 +1,166 @@ + +spineboy.png +format: RGBA8888 +filter: Linear,Linear +repeat: none +head + rotate: false + xy: 1, 122 + size: 121, 132 + orig: 121, 132 + offset: 0, 0 + index: -1 +torso + rotate: false + xy: 1, 28 + size: 68, 92 + orig: 68, 92 + offset: 0, 0 + index: -1 +left-pant-bottom + rotate: false + xy: 1, 4 + size: 44, 22 + orig: 44, 22 + offset: 0, 0 + index: -1 +right-pant-bottom + rotate: false + xy: 47, 8 + size: 46, 18 + orig: 46, 18 + offset: 0, 0 + index: -1 +right-upper-leg + rotate: false + xy: 71, 50 + size: 44, 70 + orig: 44, 70 + offset: 0, 0 + index: -1 +pelvis + rotate: false + xy: 95, 1 + size: 63, 47 + orig: 63, 47 + offset: 0, 0 + index: -1 +left-upper-leg + rotate: false + xy: 117, 53 + size: 33, 67 + orig: 33, 67 + offset: 0, 0 + index: -1 +right-foot + rotate: false + xy: 160, 224 + size: 67, 30 + orig: 67, 30 + offset: 0, 0 + index: -1 +left-shoulder + rotate: false + xy: 124, 201 + size: 34, 53 + orig: 34, 53 + offset: 0, 0 + index: -1 +left-ankle + rotate: false + xy: 229, 222 + size: 25, 32 + orig: 25, 32 + offset: 0, 0 + index: -1 +left-foot + rotate: false + xy: 160, 192 + size: 65, 30 + orig: 65, 30 + offset: 0, 0 + index: -1 +neck + rotate: false + xy: 124, 171 + size: 34, 28 + orig: 34, 28 + offset: 0, 0 + index: -1 +right-arm + rotate: false + xy: 124, 124 + size: 21, 45 + orig: 21, 45 + offset: 0, 0 + index: -1 +right-ankle + rotate: false + xy: 227, 190 + size: 25, 30 + orig: 25, 30 + offset: 0, 0 + index: -1 +left-hand + rotate: false + xy: 147, 131 + size: 35, 38 + orig: 35, 38 + offset: 0, 0 + index: -1 +left-arm + rotate: false + xy: 184, 161 + size: 35, 29 + orig: 35, 29 + offset: 0, 0 + index: -1 +eyes-closed + rotate: false + xy: 221, 161 + size: 34, 27 + orig: 34, 27 + offset: 0, 0 + index: -1 +right-lower-leg + rotate: false + xy: 152, 65 + size: 51, 64 + orig: 51, 64 + offset: 0, 0 + index: -1 +right-foot-idle + rotate: false + xy: 184, 131 + size: 53, 28 + orig: 53, 28 + offset: 0, 0 + index: -1 +left-lower-leg + rotate: false + xy: 205, 65 + size: 49, 64 + orig: 49, 64 + offset: 0, 0 + index: -1 +right-shoulder + rotate: false + xy: 160, 12 + size: 52, 51 + orig: 52, 51 + offset: 0, 0 + index: -1 +eyes + rotate: false + xy: 214, 36 + size: 34, 27 + orig: 34, 27 + offset: 0, 0 + index: -1 +right-hand + rotate: false + xy: 214, 2 + size: 32, 32 + orig: 32, 32 + offset: 0, 0 + index: -1 diff --git a/spine-xna/example/data/spineboy.json b/spine-xna/example/data/spineboy.json new file mode 100644 index 000000000..57b0f6cbe --- /dev/null +++ b/spine-xna/example/data/spineboy.json @@ -0,0 +1,787 @@ +{ +"bones": [ + { "name": "root" }, + { "name": "hip", "parent": "root", "x": 0.64, "y": 114.41 }, + { "name": "left upper leg", "parent": "hip", "length": 50.39, "x": 14.45, "y": 2.81, "rotation": -89.09 }, + { "name": "left lower leg", "parent": "left upper leg", "length": 56.45, "x": 51.78, "y": 3.46, "rotation": -16.65 }, + { "name": "left foot", "parent": "left lower leg", "length": 46.5, "x": 64.02, "y": -8.67, "rotation": 102.43 }, + { "name": "right upper leg", "parent": "hip", "length": 45.76, "x": -18.27, "rotation": -101.13 }, + { "name": "right lower leg", "parent": "right upper leg", "length": 58.52, "x": 50.21, "y": 0.6, "rotation": -10.7 }, + { "name": "right foot", "parent": "right lower leg", "length": 45.45, "x": 64.88, "y": 0.04, "rotation": 110.3 }, + { "name": "torso", "parent": "hip", "length": 85.82, "x": -6.42, "y": 1.97, "rotation": 94.95 }, + { "name": "neck", "parent": "torso", "length": 18.38, "x": 83.64, "y": -1.78, "rotation": 0.9 }, + { "name": "head", "parent": "neck", "length": 68.28, "x": 19.09, "y": 6.97, "rotation": -8.94 }, + { "name": "right shoulder", "parent": "torso", "length": 49.95, "x": 81.9, "y": 6.79, "rotation": 130.6 }, + { "name": "right arm", "parent": "right shoulder", "length": 36.74, "x": 49.95, "y": -0.12, "rotation": 40.12 }, + { "name": "right hand", "parent": "right arm", "length": 15.32, "x": 36.9, "y": 0.34, "rotation": 2.35 }, + { "name": "left shoulder", "parent": "torso", "length": 44.19, "x": 78.96, "y": -15.75, "rotation": -156.96 }, + { "name": "left arm", "parent": "left shoulder", "length": 35.62, "x": 44.19, "y": -0.01, "rotation": 28.16 }, + { "name": "left hand", "parent": "left arm", "length": 11.52, "x": 35.62, "y": 0.07, "rotation": 2.7 }, + { "name": "pelvis", "parent": "hip", "x": 1.41, "y": -6.57 } +], +"slots": [ + { "name": "left shoulder", "bone": "left shoulder", "attachment": "left-shoulder" }, + { "name": "left arm", "bone": "left arm", "attachment": "left-arm" }, + { "name": "left hand", "bone": "left hand", "attachment": "left-hand" }, + { "name": "left foot", "bone": "left foot", "attachment": "left-foot" }, + { "name": "left lower leg", "bone": "left lower leg", "attachment": "left-lower-leg" }, + { "name": "left upper leg", "bone": "left upper leg", "attachment": "left-upper-leg" }, + { "name": "pelvis", "bone": "pelvis", "attachment": "pelvis" }, + { "name": "right foot", "bone": "right foot", "attachment": "right-foot" }, + { "name": "right lower leg", "bone": "right lower leg", "attachment": "right-lower-leg" }, + { "name": "right upper leg", "bone": "right upper leg", "attachment": "right-upper-leg" }, + { "name": "torso", "bone": "torso", "attachment": "torso" }, + { "name": "neck", "bone": "neck", "attachment": "neck" }, + { "name": "head", "bone": "head", "attachment": "head" }, + { "name": "eyes", "bone": "head", "attachment": "eyes" }, + { "name": "right shoulder", "bone": "right shoulder", "attachment": "right-shoulder" }, + { "name": "right arm", "bone": "right arm", "attachment": "right-arm" }, + { "name": "right hand", "bone": "right hand", "attachment": "right-hand" } +], +"skins": { + "default": { + "left shoulder": { + "left-shoulder": { "x": 23.74, "y": 0.11, "rotation": 62.01, "width": 34, "height": 53 } + }, + "left arm": { + "left-arm": { "x": 15.11, "y": -0.44, "rotation": 33.84, "width": 35, "height": 29 } + }, + "left hand": { + "left-hand": { "x": 0.75, "y": 1.86, "rotation": 31.14, "width": 35, "height": 38 } + }, + "left foot": { + "left-foot": { "x": 24.35, "y": 8.88, "rotation": 3.32, "width": 65, "height": 30 } + }, + "left lower leg": { + "left-lower-leg": { "x": 24.55, "y": -1.92, "rotation": 105.75, "width": 49, "height": 64 } + }, + "left upper leg": { + "left-upper-leg": { "x": 26.12, "y": -1.85, "rotation": 89.09, "width": 33, "height": 67 } + }, + "pelvis": { + "pelvis": { "x": -4.83, "y": 10.62, "width": 63, "height": 47 } + }, + "right foot": { + "right-foot": { "x": 19.02, "y": 8.47, "rotation": 1.52, "width": 67, "height": 30 } + }, + "right lower leg": { + "right-lower-leg": { "x": 23.28, "y": -2.59, "rotation": 111.83, "width": 51, "height": 64 } + }, + "right upper leg": { + "right-upper-leg": { "x": 23.03, "y": 0.25, "rotation": 101.13, "width": 44, "height": 70 } + }, + "torso": { + "torso": { "x": 44.57, "y": -7.08, "rotation": -94.95, "width": 68, "height": 92 } + }, + "neck": { + "neck": { "x": 9.42, "y": -3.66, "rotation": -100.15, "width": 34, "height": 28 } + }, + "head": { + "head": { "x": 53.94, "y": -5.75, "rotation": -86.9, "width": 121, "height": 132 } + }, + "eyes": { + "eyes": { "x": 28.94, "y": -32.92, "rotation": -86.9, "width": 34, "height": 27 }, + "eyes-closed": { "x": 28.77, "y": -32.86, "rotation": -86.9, "width": 34, "height": 27 } + }, + "right shoulder": { + "right-shoulder": { "x": 25.86, "y": 0.03, "rotation": 134.44, "width": 52, "height": 51 } + }, + "right arm": { + "right-arm": { "x": 18.34, "y": -2.64, "rotation": 94.32, "width": 21, "height": 45 } + }, + "right hand": { + "right-hand": { "x": 6.82, "y": 1.25, "rotation": 91.96, "width": 32, "height": 32 } + } + } +}, +"animations": { + "walk": { + "bones": { + "left upper leg": { + "rotate": [ + { "time": 0, "angle": -26.55 }, + { "time": 0.1333, "angle": -8.78 }, + { "time": 0.2666, "angle": 9.51 }, + { "time": 0.4, "angle": 30.74 }, + { "time": 0.5333, "angle": 25.33 }, + { "time": 0.6666, "angle": 26.11 }, + { "time": 0.8, "angle": -7.7 }, + { "time": 0.9333, "angle": -21.19 }, + { "time": 1.0666, "angle": -26.55 } + ], + "translate": [ + { "time": 0, "x": -3, "y": -2.25 }, + { "time": 0.4, "x": -2.18, "y": -2.25 }, + { "time": 1.0666, "x": -3, "y": -2.25 } + ] + }, + "right upper leg": { + "rotate": [ + { "time": 0, "angle": 42.45 }, + { "time": 0.1333, "angle": 52.1 }, + { "time": 0.2666, "angle": 5.96 }, + { "time": 0.5333, "angle": -16.93 }, + { "time": 0.6666, "angle": 1.89 }, + { + "time": 0.8, + "angle": 28.06, + "curve": [ 0.462, 0.11, 1, 1 ] + }, + { + "time": 0.9333, + "angle": 58.68, + "curve": [ 0.5, 0.02, 1, 1 ] + }, + { "time": 1.0666, "angle": 42.45 } + ], + "translate": [ + { "time": 0, "x": 8.11, "y": -2.36 }, + { "time": 0.1333, "x": 10.03, "y": -2.56 }, + { "time": 0.4, "x": 2.76, "y": -2.97 }, + { "time": 0.5333, "x": 2.76, "y": -2.81 }, + { "time": 0.9333, "x": 8.67, "y": -2.54 }, + { "time": 1.0666, "x": 8.11, "y": -2.36 } + ] + }, + "left lower leg": { + "rotate": [ + { "time": 0, "angle": -10.21 }, + { "time": 0.1333, "angle": -55.64 }, + { "time": 0.2666, "angle": -68.12 }, + { "time": 0.5333, "angle": 5.11 }, + { "time": 0.6666, "angle": -28.29 }, + { "time": 0.8, "angle": 4.08 }, + { "time": 0.9333, "angle": 3.53 }, + { "time": 1.0666, "angle": -10.21 } + ] + }, + "left foot": { + "rotate": [ + { "time": 0, "angle": -3.69 }, + { "time": 0.1333, "angle": -10.42 }, + { "time": 0.2666, "angle": -17.14 }, + { "time": 0.4, "angle": -2.83 }, + { "time": 0.5333, "angle": -3.87 }, + { "time": 0.6666, "angle": 2.78 }, + { "time": 0.8, "angle": 1.68 }, + { "time": 0.9333, "angle": -8.54 }, + { "time": 1.0666, "angle": -3.69 } + ] + }, + "right shoulder": { + "rotate": [ + { + "time": 0, + "angle": 20.89, + "curve": [ 0.264, 0, 0.75, 1 ] + }, + { + "time": 0.1333, + "angle": 3.72, + "curve": [ 0.272, 0, 0.841, 1 ] + }, + { "time": 0.6666, "angle": -278.28 }, + { "time": 1.0666, "angle": 20.89 } + ], + "translate": [ + { "time": 0, "x": -7.84, "y": 7.19 }, + { "time": 0.1333, "x": -6.36, "y": 6.42 }, + { "time": 0.6666, "x": -11.07, "y": 5.25 }, + { "time": 1.0666, "x": -7.84, "y": 7.19 } + ] + }, + "right arm": { + "rotate": [ + { + "time": 0, + "angle": -4.02, + "curve": [ 0.267, 0, 0.804, 0.99 ] + }, + { + "time": 0.1333, + "angle": -13.99, + "curve": [ 0.341, 0, 1, 1 ] + }, + { + "time": 0.6666, + "angle": 36.54, + "curve": [ 0.307, 0, 0.787, 0.99 ] + }, + { "time": 1.0666, "angle": -4.02 } + ] + }, + "right hand": { + "rotate": [ + { "time": 0, "angle": 22.92 }, + { "time": 0.4, "angle": -8.97 }, + { "time": 0.6666, "angle": 0.51 }, + { "time": 1.0666, "angle": 22.92 } + ] + }, + "left shoulder": { + "rotate": [ + { "time": 0, "angle": -1.47 }, + { "time": 0.1333, "angle": 13.6 }, + { "time": 0.6666, "angle": 280.74 }, + { "time": 1.0666, "angle": -1.47 } + ], + "translate": [ + { "time": 0, "x": -1.76, "y": 0.56 }, + { "time": 0.6666, "x": -2.47, "y": 8.14 }, + { "time": 1.0666, "x": -1.76, "y": 0.56 } + ] + }, + "left hand": { + "rotate": [ + { + "time": 0, + "angle": 11.58, + "curve": [ 0.169, 0.37, 0.632, 1.55 ] + }, + { + "time": 0.1333, + "angle": 28.13, + "curve": [ 0.692, 0, 0.692, 0.99 ] + }, + { + "time": 0.6666, + "angle": -27.42, + "curve": [ 0.117, 0.41, 0.738, 1.76 ] + }, + { "time": 0.8, "angle": -36.32 }, + { "time": 1.0666, "angle": 11.58 } + ] + }, + "left arm": { + "rotate": [ + { "time": 0, "angle": -8.27 }, + { "time": 0.1333, "angle": 18.43 }, + { "time": 0.6666, "angle": 0.88 }, + { "time": 1.0666, "angle": -8.27 } + ] + }, + "torso": { + "rotate": [ + { "time": 0, "angle": -10.28 }, + { + "time": 0.1333, + "angle": -15.38, + "curve": [ 0.545, 0, 1, 1 ] + }, + { + "time": 0.4, + "angle": -9.78, + "curve": [ 0.58, 0.17, 1, 1 ] + }, + { "time": 0.6666, "angle": -15.75 }, + { "time": 0.9333, "angle": -7.06 }, + { "time": 1.0666, "angle": -10.28 } + ], + "translate": [ + { "time": 0, "x": -3.67, "y": 1.68 }, + { "time": 0.1333, "x": -3.67, "y": 0.68 }, + { "time": 0.4, "x": -3.67, "y": 1.97 }, + { "time": 0.6666, "x": -3.67, "y": -0.14 }, + { "time": 1.0666, "x": -3.67, "y": 1.68 } + ] + }, + "right foot": { + "rotate": [ + { "time": 0, "angle": -5.25 }, + { "time": 0.2666, "angle": -4.08 }, + { "time": 0.4, "angle": -6.45 }, + { "time": 0.5333, "angle": -5.39 }, + { "time": 0.8, "angle": -11.68 }, + { "time": 0.9333, "angle": 0.46 }, + { "time": 1.0666, "angle": -5.25 } + ] + }, + "right lower leg": { + "rotate": [ + { "time": 0, "angle": -3.39 }, + { "time": 0.1333, "angle": -45.53 }, + { "time": 0.2666, "angle": -2.59 }, + { "time": 0.5333, "angle": -19.53 }, + { "time": 0.6666, "angle": -64.8 }, + { + "time": 0.8, + "angle": -82.56, + "curve": [ 0.557, 0.18, 1, 1 ] + }, + { "time": 1.0666, "angle": -3.39 } + ] + }, + "hip": { + "rotate": [ + { "time": 0, "angle": 0, "curve": "stepped" }, + { "time": 1.0666, "angle": 0 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0 }, + { + "time": 0.1333, + "x": 0, + "y": -7.61, + "curve": [ 0.272, 0.86, 1, 1 ] + }, + { "time": 0.4, "x": 0, "y": 8.7 }, + { "time": 0.5333, "x": 0, "y": -0.41 }, + { + "time": 0.6666, + "x": 0, + "y": -7.05, + "curve": [ 0.235, 0.89, 1, 1 ] + }, + { "time": 0.8, "x": 0, "y": 2.92 }, + { "time": 0.9333, "x": 0, "y": 6.78 }, + { "time": 1.0666, "x": 0, "y": 0 } + ] + }, + "neck": { + "rotate": [ + { "time": 0, "angle": 3.6 }, + { "time": 0.1333, "angle": 17.49 }, + { "time": 0.2666, "angle": 6.1 }, + { "time": 0.4, "angle": 3.45 }, + { "time": 0.5333, "angle": 5.17 }, + { "time": 0.6666, "angle": 18.36 }, + { "time": 0.8, "angle": 6.09 }, + { "time": 0.9333, "angle": 2.28 }, + { "time": 1.0666, "angle": 3.6 } + ] + }, + "head": { + "rotate": [ + { + "time": 0, + "angle": 3.6, + "curve": [ 0, 0, 0.704, 1.61 ] + }, + { "time": 0.1666, "angle": -0.2 }, + { "time": 0.2666, "angle": 6.1 }, + { "time": 0.4, "angle": 3.45 }, + { + "time": 0.5333, + "angle": 5.17, + "curve": [ 0, 0, 0.704, 1.61 ] + }, + { "time": 0.7, "angle": 1.1 }, + { "time": 0.8, "angle": 6.09 }, + { "time": 0.9333, "angle": 2.28 }, + { "time": 1.0666, "angle": 3.6 } + ] + } + } + }, + "jump": { + "bones": { + "hip": { + "rotate": [ + { "time": 0, "angle": 0, "curve": "stepped" }, + { "time": 0.9333, "angle": 0, "curve": "stepped" }, + { "time": 1.3666, "angle": 0 } + ], + "translate": [ + { "time": 0, "x": -11.57, "y": -3 }, + { "time": 0.2333, "x": -16.2, "y": -19.43 }, + { + "time": 0.3333, + "x": 7.66, + "y": -8.48, + "curve": [ 0.057, 0.06, 0.712, 1 ] + }, + { "time": 0.3666, "x": 15.38, "y": 5.01 }, + { "time": 0.4666, "x": -7.84, "y": 57.22 }, + { + "time": 0.6, + "x": -10.81, + "y": 96.34, + "curve": [ 0.241, 0, 1, 1 ] + }, + { "time": 0.7333, "x": -7.01, "y": 54.7 }, + { "time": 0.8, "x": -10.58, "y": 32.2 }, + { "time": 0.9333, "x": -31.99, "y": 0.45 }, + { "time": 1.0666, "x": -12.48, "y": -29.47 }, + { "time": 1.3666, "x": -11.57, "y": -3 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "left upper leg": { + "rotate": [ + { "time": 0, "angle": 17.13 }, + { "time": 0.2333, "angle": 44.35 }, + { "time": 0.3333, "angle": 16.46 }, + { "time": 0.4, "angle": -9.88 }, + { "time": 0.4666, "angle": -11.42 }, + { "time": 0.5666, "angle": 23.46 }, + { "time": 0.7666, "angle": 71.82 }, + { "time": 0.9333, "angle": 65.53 }, + { "time": 1.0666, "angle": 51.01 }, + { "time": 1.3666, "angle": 17.13 } + ], + "translate": [ + { "time": 0, "x": -3, "y": -2.25, "curve": "stepped" }, + { "time": 0.9333, "x": -3, "y": -2.25, "curve": "stepped" }, + { "time": 1.3666, "x": -3, "y": -2.25 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "left lower leg": { + "rotate": [ + { "time": 0, "angle": -16.25 }, + { "time": 0.2333, "angle": -52.21 }, + { "time": 0.4, "angle": 15.04 }, + { "time": 0.4666, "angle": -8.95 }, + { "time": 0.5666, "angle": -39.53 }, + { "time": 0.7666, "angle": -27.27 }, + { "time": 0.9333, "angle": -3.52 }, + { "time": 1.0666, "angle": -61.92 }, + { "time": 1.3666, "angle": -16.25 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1.3666, "x": 0, "y": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "left foot": { + "rotate": [ + { "time": 0, "angle": 0.33 }, + { "time": 0.2333, "angle": 6.2 }, + { "time": 0.3333, "angle": 14.73 }, + { "time": 0.4, "angle": -15.54 }, + { "time": 0.4333, "angle": -21.2 }, + { "time": 0.5666, "angle": -7.55 }, + { "time": 0.7666, "angle": -0.67 }, + { "time": 0.9333, "angle": -0.58 }, + { "time": 1.0666, "angle": 14.64 }, + { "time": 1.3666, "angle": 0.33 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1.3666, "x": 0, "y": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "right upper leg": { + "rotate": [ + { "time": 0, "angle": 25.97 }, + { "time": 0.2333, "angle": 46.43 }, + { "time": 0.3333, "angle": 22.61 }, + { "time": 0.4, "angle": 2.13 }, + { + "time": 0.4666, + "angle": 0.04, + "curve": [ 0, 0, 0.637, 0.98 ] + }, + { "time": 0.6, "angle": 65.55 }, + { "time": 0.7666, "angle": 64.93 }, + { "time": 0.9333, "angle": 41.08 }, + { "time": 1.0666, "angle": 66.25 }, + { "time": 1.3666, "angle": 25.97 } + ], + "translate": [ + { "time": 0, "x": 5.74, "y": 0.61 }, + { "time": 0.2333, "x": 4.79, "y": 1.79 }, + { "time": 0.3333, "x": 6.05, "y": -4.55 }, + { "time": 0.9333, "x": 4.79, "y": 1.79, "curve": "stepped" }, + { "time": 1.0666, "x": 4.79, "y": 1.79 }, + { "time": 1.3666, "x": 5.74, "y": 0.61 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "right lower leg": { + "rotate": [ + { "time": 0, "angle": -27.46 }, + { "time": 0.2333, "angle": -64.03 }, + { "time": 0.4, "angle": -48.36 }, + { "time": 0.5666, "angle": -76.86 }, + { "time": 0.7666, "angle": -26.89 }, + { "time": 0.9, "angle": -18.97 }, + { "time": 0.9333, "angle": -14.18 }, + { "time": 1.0666, "angle": -80.45 }, + { "time": 1.3666, "angle": -27.46 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1.3666, "x": 0, "y": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "right foot": { + "rotate": [ + { "time": 0, "angle": 1.08 }, + { "time": 0.2333, "angle": 16.02 }, + { "time": 0.3, "angle": 12.94 }, + { "time": 0.3333, "angle": 15.16 }, + { "time": 0.4, "angle": -14.7 }, + { "time": 0.4333, "angle": -12.85 }, + { "time": 0.4666, "angle": -19.18 }, + { "time": 0.5666, "angle": -15.82 }, + { "time": 0.6, "angle": -3.59 }, + { "time": 0.7666, "angle": -3.56 }, + { "time": 0.9333, "angle": 1.86 }, + { "time": 1.0666, "angle": 16.02 }, + { "time": 1.3666, "angle": 1.08 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1.3666, "x": 0, "y": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "torso": { + "rotate": [ + { "time": 0, "angle": -13.35 }, + { "time": 0.2333, "angle": -48.95 }, + { "time": 0.4333, "angle": -35.77 }, + { "time": 0.6, "angle": -4.59 }, + { "time": 0.7666, "angle": 14.61 }, + { "time": 0.9333, "angle": 15.74 }, + { "time": 1.0666, "angle": -32.44 }, + { "time": 1.3666, "angle": -13.35 } + ], + "translate": [ + { "time": 0, "x": -3.67, "y": 1.68, "curve": "stepped" }, + { "time": 0.9333, "x": -3.67, "y": 1.68, "curve": "stepped" }, + { "time": 1.3666, "x": -3.67, "y": 1.68 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "neck": { + "rotate": [ + { "time": 0, "angle": 12.78 }, + { "time": 0.2333, "angle": 16.46 }, + { "time": 0.4, "angle": 26.49 }, + { "time": 0.6, "angle": 15.51 }, + { "time": 0.7666, "angle": 1.34 }, + { "time": 0.9333, "angle": 2.35 }, + { "time": 1.0666, "angle": 6.08 }, + { "time": 1.3, "angle": 21.23 }, + { "time": 1.3666, "angle": 12.78 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1.3666, "x": 0, "y": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "head": { + "rotate": [ + { "time": 0, "angle": 5.19 }, + { "time": 0.2333, "angle": 20.27 }, + { "time": 0.4, "angle": 15.27 }, + { "time": 0.6, "angle": -24.69 }, + { "time": 0.7666, "angle": -11.02 }, + { "time": 0.9333, "angle": -24.38 }, + { "time": 1.0666, "angle": 11.99 }, + { "time": 1.3, "angle": 4.86 }, + { "time": 1.3666, "angle": 5.19 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1.3666, "x": 0, "y": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "left shoulder": { + "rotate": [ + { + "time": 0, + "angle": 0.05, + "curve": [ 0, 0, 0.62, 1 ] + }, + { + "time": 0.2333, + "angle": 279.66, + "curve": [ 0.218, 0.67, 0.66, 0.99 ] + }, + { + "time": 0.5, + "angle": 62.27, + "curve": [ 0.462, 0, 0.764, 0.58 ] + }, + { "time": 0.9333, "angle": 28.91 }, + { "time": 1.0666, "angle": -8.62 }, + { "time": 1.1666, "angle": -18.43 }, + { "time": 1.3666, "angle": 0.05 } + ], + "translate": [ + { "time": 0, "x": -1.76, "y": 0.56, "curve": "stepped" }, + { "time": 0.9333, "x": -1.76, "y": 0.56, "curve": "stepped" }, + { "time": 1.3666, "x": -1.76, "y": 0.56 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "left hand": { + "rotate": [ + { "time": 0, "angle": 11.58, "curve": "stepped" }, + { "time": 0.9333, "angle": 11.58, "curve": "stepped" }, + { "time": 1.3666, "angle": 11.58 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1.3666, "x": 0, "y": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "left arm": { + "rotate": [ + { "time": 0, "angle": 0.51 }, + { "time": 0.4333, "angle": 12.82 }, + { "time": 0.6, "angle": 47.55 }, + { "time": 0.9333, "angle": 12.82 }, + { "time": 1.1666, "angle": -6.5 }, + { "time": 1.3666, "angle": 0.51 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1.3666, "x": 0, "y": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "right shoulder": { + "rotate": [ + { + "time": 0, + "angle": 43.82, + "curve": [ 0, 0, 0.62, 1 ] + }, + { + "time": 0.2333, + "angle": -8.74, + "curve": [ 0.304, 0.58, 0.709, 0.97 ] + }, + { + "time": 0.5333, + "angle": -208.02, + "curve": [ 0.462, 0, 0.764, 0.58 ] + }, + { "time": 0.9333, "angle": -246.72 }, + { "time": 1.0666, "angle": -307.13 }, + { "time": 1.1666, "angle": 37.15 }, + { "time": 1.3666, "angle": 43.82 } + ], + "translate": [ + { "time": 0, "x": -7.84, "y": 7.19, "curve": "stepped" }, + { "time": 0.9333, "x": -7.84, "y": 7.19, "curve": "stepped" }, + { "time": 1.3666, "x": -7.84, "y": 7.19 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "right arm": { + "rotate": [ + { "time": 0, "angle": -4.02 }, + { "time": 0.6, "angle": 17.5 }, + { "time": 0.9333, "angle": -4.02 }, + { "time": 1.1666, "angle": -16.72 }, + { "time": 1.3666, "angle": -4.02 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1.3666, "x": 0, "y": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "right hand": { + "rotate": [ + { "time": 0, "angle": 22.92, "curve": "stepped" }, + { "time": 0.9333, "angle": 22.92, "curve": "stepped" }, + { "time": 1.3666, "angle": 22.92 } + ], + "translate": [ + { "time": 0, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 0.9333, "x": 0, "y": 0, "curve": "stepped" }, + { "time": 1.3666, "x": 0, "y": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 0.9333, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + }, + "root": { + "rotate": [ + { "time": 0, "angle": 0 }, + { "time": 0.4333, "angle": -14.52 }, + { "time": 0.8, "angle": 9.86 }, + { "time": 1.3666, "angle": 0 } + ], + "scale": [ + { "time": 0, "x": 1, "y": 1, "curve": "stepped" }, + { "time": 1.3666, "x": 1, "y": 1 } + ] + } + } + } +} +} \ No newline at end of file diff --git a/spine-xna/example/data/spineboy.png b/spine-xna/example/data/spineboy.png new file mode 100644 index 000000000..b8b493dfd Binary files /dev/null and b/spine-xna/example/data/spineboy.png differ diff --git a/spine-xna/example/spine-xna-example.csproj b/spine-xna/example/spine-xna-example.csproj new file mode 100644 index 000000000..7d4e4684f --- /dev/null +++ b/spine-xna/example/spine-xna-example.csproj @@ -0,0 +1,168 @@ + + + + {29CC4385-294A-4885-A3E8-FD4825E0CFDD} + {6D335F3A-9D43-41b4-9D22-F6F17C4BE596};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Debug + x86 + WinExe + Properties + Spine + spine-xna-example + v4.0 + Client + v4.0 + Windows + HiDef + bf3b738e-f348-48d3-b35b-94bc118edb90 + Game + Game.ico + GameThumbnail.png + false + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + true + + + true + full + false + bin\x86\Debug + DEBUG;TRACE;WINDOWS + prompt + 4 + true + false + x86 + True + + + pdbonly + true + bin\x86\Release + TRACE;WINDOWS + prompt + 4 + true + false + x86 + True + + + Spine.ExampleProgram + + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + + + + + + + + + + + + {94144E22-2431-4A8F-AC04-DEC22F7EDD8F} + spine-csharp + + + {7F8F2327-C016-49C8-BB4D-F3F77971961E} + spine-xna + + + + + False + Microsoft .NET Framework 4 Client Profile %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + + False + Windows Installer 3.1 + true + + + False + Microsoft XNA Framework Redistributable 4.0 + true + + + + + + \ No newline at end of file diff --git a/spine-xna/example/src/ExampleGame.cs b/spine-xna/example/src/ExampleGame.cs new file mode 100644 index 000000000..59a13f6fd --- /dev/null +++ b/spine-xna/example/src/ExampleGame.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.GamerServices; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using Microsoft.Xna.Framework.Media; +using Spine; + +namespace Spine { + public class Example : Microsoft.Xna.Framework.Game { + GraphicsDeviceManager graphics; + SkeletonRenderer skeletonRenderer; + Skeleton skeleton; + Animation animation; + float time; + + public Example () { + graphics = new GraphicsDeviceManager(this); + graphics.IsFullScreen = false; + graphics.PreferredBackBufferWidth = 640; + graphics.PreferredBackBufferHeight = 480; + } + + protected override void Initialize () { + // TODO: Add your initialization logic here + + base.Initialize(); + } + + protected override void LoadContent () { + skeletonRenderer = new SkeletonRenderer(GraphicsDevice); + Atlas atlas = new Atlas(GraphicsDevice, "data/spineboy.atlas"); + SkeletonJson json = new SkeletonJson(atlas); + skeleton = new Skeleton(json.readSkeletonData("spineboy", File.ReadAllText("data/spineboy.json"))); + animation = skeleton.Data.FindAnimation("walk"); + + skeleton.RootBone.X = 320; + skeleton.RootBone.Y = 440; + skeleton.UpdateWorldTransform(); + } + + protected override void UnloadContent () { + // TODO: Unload any non ContentManager content here + } + + protected override void Update (GameTime gameTime) { + // Allows the game to exit + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) + this.Exit(); + + // TODO: Add your update logic here + + base.Update(gameTime); + } + + protected override void Draw (GameTime gameTime) { + GraphicsDevice.Clear(Color.Black); + + time += gameTime.ElapsedGameTime.Milliseconds / 1000f; + animation.Apply(skeleton, time, true); + skeleton.UpdateWorldTransform(); + skeletonRenderer.Draw(skeleton); + + base.Draw(gameTime); + } + } +} diff --git a/spine-xna/example/src/ExampleProgram.cs b/spine-xna/example/src/ExampleProgram.cs new file mode 100644 index 000000000..35c89a31f --- /dev/null +++ b/spine-xna/example/src/ExampleProgram.cs @@ -0,0 +1,13 @@ +using System; + +namespace Spine { +#if WINDOWS || XBOX + static class ExampleProgram { + static void Main (string[] args) { + using (Example game = new Example()) { + game.Run(); + } + } + } +#endif +} diff --git a/spine-xna/spine-xna.csproj b/spine-xna/spine-xna.csproj new file mode 100644 index 000000000..92851d9df --- /dev/null +++ b/spine-xna/spine-xna.csproj @@ -0,0 +1,116 @@ + + + + {7F8F2327-C016-49C8-BB4D-F3F77971961E} + {6D335F3A-9D43-41b4-9D22-F6F17C4BE596};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Debug + x86 + Library + Properties + Spine + spine-xna + v4.0 + Client + v4.0 + Windows + HiDef + f1fc4580-2d86-4a03-bd33-44b1703f36a1 + Library + + + true + full + false + bin\x86\Debug + DEBUG;TRACE;WINDOWS + prompt + 4 + true + false + x86 + false + + + pdbonly + true + bin\x86\Release + TRACE;WINDOWS + prompt + 4 + true + false + x86 + true + + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + 4.0 + False + + + 4.0 + False + + + False + + + + + + + + + + + {94144E22-2431-4A8F-AC04-DEC22F7EDD8F} + spine-csharp + + + + + + \ No newline at end of file diff --git a/spine-xna/spine-xna.sln b/spine-xna/spine-xna.sln new file mode 100644 index 000000000..fec423e85 --- /dev/null +++ b/spine-xna/spine-xna.sln @@ -0,0 +1,32 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual C# Express 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spine-xna", "spine-xna.csproj", "{7F8F2327-C016-49C8-BB4D-F3F77971961E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spine-xna-example", "example\spine-xna-example.csproj", "{29CC4385-294A-4885-A3E8-FD4825E0CFDD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "spine-csharp", "..\spine-csharp\spine-csharp.csproj", "{94144E22-2431-4A8F-AC04-DEC22F7EDD8F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|x86.ActiveCfg = Debug|x86 + {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Debug|x86.Build.0 = Debug|x86 + {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|x86.ActiveCfg = Release|x86 + {7F8F2327-C016-49C8-BB4D-F3F77971961E}.Release|x86.Build.0 = Release|x86 + {29CC4385-294A-4885-A3E8-FD4825E0CFDD}.Debug|x86.ActiveCfg = Debug|x86 + {29CC4385-294A-4885-A3E8-FD4825E0CFDD}.Debug|x86.Build.0 = Debug|x86 + {29CC4385-294A-4885-A3E8-FD4825E0CFDD}.Release|x86.ActiveCfg = Release|x86 + {29CC4385-294A-4885-A3E8-FD4825E0CFDD}.Release|x86.Build.0 = Release|x86 + {94144E22-2431-4A8F-AC04-DEC22F7EDD8F}.Debug|x86.ActiveCfg = Debug|x86 + {94144E22-2431-4A8F-AC04-DEC22F7EDD8F}.Debug|x86.Build.0 = Debug|x86 + {94144E22-2431-4A8F-AC04-DEC22F7EDD8F}.Release|x86.ActiveCfg = Release|x86 + {94144E22-2431-4A8F-AC04-DEC22F7EDD8F}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/spine-xna/src/Atlas.cs b/spine-xna/src/Atlas.cs new file mode 100644 index 000000000..9d67c5865 --- /dev/null +++ b/spine-xna/src/Atlas.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.GamerServices; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using Microsoft.Xna.Framework.Media; + +namespace Spine { + public class Atlas : BaseAtlas { + private GraphicsDevice device; + + public Atlas (GraphicsDevice device, String atlasFile) { + this.device = device; + using (StreamReader reader = new StreamReader(atlasFile)) { + load(reader, Path.GetDirectoryName(atlasFile)); + } + } + + override protected AtlasPage NewAtlasPage (String path) { + XnaAtlasPage page = new XnaAtlasPage(); + page.Texture = loadTexture(path); + return page; + } + + private Texture2D loadTexture (string path) { + Texture2D file; + using (Stream fileStream = new FileStream(path, FileMode.Open)) { + file = Texture2D.FromStream(device, fileStream); + } + + // Setup a render target to hold our final texture which will have premulitplied alpha values + RenderTarget2D result = new RenderTarget2D(device, file.Width, file.Height); + device.SetRenderTarget(result); + device.Clear(Color.Black); + + // Multiply each color by the source alpha, and write in just the color values into the final texture + BlendState blendColor = new BlendState(); + blendColor.ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue; + blendColor.AlphaDestinationBlend = Blend.Zero; + blendColor.ColorDestinationBlend = Blend.Zero; + blendColor.AlphaSourceBlend = Blend.SourceAlpha; + blendColor.ColorSourceBlend = Blend.SourceAlpha; + + SpriteBatch spriteBatch = new SpriteBatch(device); + spriteBatch.Begin(SpriteSortMode.Immediate, blendColor); + spriteBatch.Draw(file, file.Bounds, Color.White); + spriteBatch.End(); + + // Now copy over the alpha values from the PNG source texture to the final one, without multiplying them + BlendState blendAlpha = new BlendState(); + blendAlpha.ColorWriteChannels = ColorWriteChannels.Alpha; + blendAlpha.AlphaDestinationBlend = Blend.Zero; + blendAlpha.ColorDestinationBlend = Blend.Zero; + blendAlpha.AlphaSourceBlend = Blend.One; + blendAlpha.ColorSourceBlend = Blend.One; + + spriteBatch.Begin(SpriteSortMode.Immediate, blendAlpha); + spriteBatch.Draw(file, file.Bounds, Color.White); + spriteBatch.End(); + + // Release the GPU back to drawing to the screen + device.SetRenderTarget(null); + + return result as Texture2D; + } + } + + public class XnaAtlasPage : AtlasPage { + public Texture2D Texture { get; set; } + + override public int GetTextureWidth () { + return Texture.Width; + } + + override public int GetTextureHeight () { + return Texture.Height; + } + } +} diff --git a/spine-xna/src/SkeletonRenderer.cs b/spine-xna/src/SkeletonRenderer.cs new file mode 100644 index 000000000..f36e7f4fb --- /dev/null +++ b/spine-xna/src/SkeletonRenderer.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework; + +namespace Spine { + public class SkeletonRenderer { + GraphicsDevice device; + SpriteBatcher batcher; + BasicEffect effect; + RasterizerState rasterizerState; + + public SkeletonRenderer (GraphicsDevice device) { + this.device = device; + + batcher = new SpriteBatcher(); + + effect = new BasicEffect(device); + effect.World = Matrix.Identity; + effect.View = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 1.0f), Vector3.Zero, Vector3.Up); + effect.Projection = Matrix.CreateOrthographicOffCenter(0, device.Viewport.Width, device.Viewport.Height, 0, 1, 0); + effect.TextureEnabled = true; + effect.VertexColorEnabled = true; + + rasterizerState = new RasterizerState(); + rasterizerState.CullMode = CullMode.None; + + Bone.yDown = true; + } + + public void Draw (Skeleton skeleton) { + List drawOrder = skeleton.DrawOrder; + for (int i = 0, n = drawOrder.Count; i < n; i++) { + Slot slot = drawOrder[i]; + Attachment attachment = slot.Attachment; + if (attachment == null) continue; + if (attachment is RegionAttachment) { + RegionAttachment regionAttachment = (RegionAttachment)attachment; + + SpriteBatchItem item = batcher.CreateBatchItem(); + item.Texture = ((XnaAtlasPage)regionAttachment.Region.Page).Texture; + + byte r = (byte)(slot.R * 255); + byte g = (byte)(slot.G * 255); + byte b = (byte)(slot.B * 255); + byte a = (byte)(slot.A * 255); + item.vertexTL.Color.R = r; + item.vertexTL.Color.G = g; + item.vertexTL.Color.B = b; + item.vertexTL.Color.A = a; + item.vertexBL.Color.R = r; + item.vertexBL.Color.G = g; + item.vertexBL.Color.B = b; + item.vertexBL.Color.A = a; + item.vertexBR.Color.R = r; + item.vertexBR.Color.G = g; + item.vertexBR.Color.B = b; + item.vertexBR.Color.A = a; + item.vertexTR.Color.R = r; + item.vertexTR.Color.G = g; + item.vertexTR.Color.B = b; + item.vertexTR.Color.A = a; + + regionAttachment.UpdateVertices(slot.Bone); + float[] vertices = regionAttachment.Vertices; + item.vertexTL.Position.X = vertices[RegionAttachment.X1]; + item.vertexTL.Position.Y = vertices[RegionAttachment.Y1]; + item.vertexTL.Position.Z = 0; + item.vertexBL.Position.X = vertices[RegionAttachment.X2]; + item.vertexBL.Position.Y = vertices[RegionAttachment.Y2]; + item.vertexBL.Position.Z = 0; + item.vertexBR.Position.X = vertices[RegionAttachment.X3]; + item.vertexBR.Position.Y = vertices[RegionAttachment.Y3]; + item.vertexBR.Position.Z = 0; + item.vertexTR.Position.X = vertices[RegionAttachment.X4]; + item.vertexTR.Position.Y = vertices[RegionAttachment.Y4]; + item.vertexTR.Position.Z = 0; + + float[] uvs = regionAttachment.UVs; + item.vertexTL.TextureCoordinate.X = uvs[RegionAttachment.X1]; + item.vertexTL.TextureCoordinate.Y = uvs[RegionAttachment.Y1]; + item.vertexBL.TextureCoordinate.X = uvs[RegionAttachment.X2]; + item.vertexBL.TextureCoordinate.Y = uvs[RegionAttachment.Y2]; + item.vertexBR.TextureCoordinate.X = uvs[RegionAttachment.X3]; + item.vertexBR.TextureCoordinate.Y = uvs[RegionAttachment.Y3]; + item.vertexTR.TextureCoordinate.X = uvs[RegionAttachment.X4]; + item.vertexTR.TextureCoordinate.Y = uvs[RegionAttachment.Y4]; + } + } + + device.RasterizerState = rasterizerState; + device.BlendState = BlendState.AlphaBlend; + + foreach (EffectPass pass in effect.CurrentTechnique.Passes) { + pass.Apply(); + batcher.Draw(device); + } + } + } +} diff --git a/spine-xna/src/SpriteBatcher.cs b/spine-xna/src/SpriteBatcher.cs new file mode 100644 index 000000000..907dcb37c --- /dev/null +++ b/spine-xna/src/SpriteBatcher.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework; + +namespace Spine { + // #region License + // /* + // Microsoft Public License (Ms-PL) + // MonoGame - Copyright © 2009 The MonoGame Team + // + // All rights reserved. + // + // This license governs use of the accompanying software. If you use the software, you accept this license. If you do not + // accept the license, do not use the software. + // + // 1. Definitions + // The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under + // U.S. copyright law. + // + // A "contribution" is the original software, or any additions or changes to the software. + // A "contributor" is any person that distributes its contribution under this license. + // "Licensed patents" are a contributor's patent claims that read directly on its contribution. + // + // 2. Grant of Rights + // (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, + // each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. + // (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, + // each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + // + // 3. Conditions and Limitations + // (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. + // (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, + // your patent license from such contributor to the software ends automatically. + // (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution + // notices that are present in the software. + // (D) If you distribute any portion of the software in source code form, you may do so only under this license by including + // a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object + // code form, you may only do so under a license that complies with this license. + // (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees + // or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent + // permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular + // purpose and non-infringement. + // */ + // #endregion License + // + + /// + /// This class handles the queueing of batch items into the GPU by creating the triangle tesselations + /// that are used to draw the sprite textures. This class supports int.MaxValue number of sprites to be + /// batched and will process them into short.MaxValue groups (strided by 6 for the number of vertices + /// sent to the GPU). + /// + public class SpriteBatcher { + /* + * Note that this class is fundamental to high performance for SpriteBatch games. Please exercise + * caution when making changes to this class. + */ + + /// + /// Initialization size for the batch item list and queue. + /// + private const int InitialBatchSize = 256; + /// + /// The maximum number of batch items that can be processed per iteration + /// + private const int MaxBatchSize = short.MaxValue / 6; // 6 = 4 vertices unique and 2 shared, per quad + /// + /// Initialization size for the vertex array, in batch units. + /// + private const int InitialVertexArraySize = 256; + + /// + /// The list of batch items to process. + /// + private readonly List _batchItemList; + + /// + /// The available SpriteBatchItem queue so that we reuse these objects when we can. + /// + private readonly Queue _freeBatchItemQueue; + + /// + /// Vertex index array. The values in this array never change. + /// + private short[] _index; + + private VertexPositionColorTexture[] _vertexArray; + + public SpriteBatcher () { + _batchItemList = new List(InitialBatchSize); + _freeBatchItemQueue = new Queue(InitialBatchSize); + + EnsureArrayCapacity(InitialBatchSize); + } + + /// + /// Create an instance of SpriteBatchItem if there is none available in the free item queue. Otherwise, + /// a previously allocated SpriteBatchItem is reused. + /// + /// + public SpriteBatchItem CreateBatchItem () { + SpriteBatchItem item; + if (_freeBatchItemQueue.Count > 0) + item = _freeBatchItemQueue.Dequeue(); + else + item = new SpriteBatchItem(); + _batchItemList.Add(item); + return item; + } + + /// + /// Resize and recreate the missing indices for the index and vertex position color buffers. + /// + /// + private void EnsureArrayCapacity (int numBatchItems) { + int neededCapacity = 6 * numBatchItems; + if (_index != null && neededCapacity <= _index.Length) { + // Short circuit out of here because we have enough capacity. + return; + } + short[] newIndex = new short[6 * numBatchItems]; + int start = 0; + if (_index != null) { + _index.CopyTo(newIndex, 0); + start = _index.Length / 6; + } + for (var i = start; i < numBatchItems; i++) { + /* + * TL TR + * 0----1 0,1,2,3 = index offsets for vertex indices + * | /| TL,TR,BL,BR are vertex references in SpriteBatchItem. + * | / | + * | / | + * |/ | + * 2----3 + * BL BR + */ + // Triangle 1 + newIndex[i * 6 + 0] = (short)(i * 4); + newIndex[i * 6 + 1] = (short)(i * 4 + 1); + newIndex[i * 6 + 2] = (short)(i * 4 + 2); + // Triangle 2 + newIndex[i * 6 + 3] = (short)(i * 4 + 1); + newIndex[i * 6 + 4] = (short)(i * 4 + 3); + newIndex[i * 6 + 5] = (short)(i * 4 + 2); + } + _index = newIndex; + + _vertexArray = new VertexPositionColorTexture[4 * numBatchItems]; + } + + /// + /// Sorts the batch items and then groups batch drawing into maximal allowed batch sets that do not + /// overflow the 16 bit array indices for vertices. + /// + /// The type of depth sorting desired for the rendering. + public void Draw (GraphicsDevice device) { + // nothing to do + if (_batchItemList.Count == 0) + return; + + // Determine how many iterations through the drawing code we need to make + int batchIndex = 0; + int batchCount = _batchItemList.Count; + // Iterate through the batches, doing short.MaxValue sets of vertices only. + while (batchCount > 0) { + // setup the vertexArray array + var startIndex = 0; + var index = 0; + Texture2D tex = null; + + int numBatchesToProcess = batchCount; + if (numBatchesToProcess > MaxBatchSize) { + numBatchesToProcess = MaxBatchSize; + } + EnsureArrayCapacity(numBatchesToProcess); + // Draw the batches + for (int i = 0; i < numBatchesToProcess; i++, batchIndex++) { + SpriteBatchItem item = _batchItemList[batchIndex]; + // if the texture changed, we need to flush and bind the new texture + var shouldFlush = !ReferenceEquals(item.Texture, tex); + if (shouldFlush) { + FlushVertexArray(device, startIndex, index); + + tex = item.Texture; + startIndex = index = 0; + device.Textures[0] = tex; + } + + // store the SpriteBatchItem data in our vertexArray + _vertexArray[index++] = item.vertexTL; + _vertexArray[index++] = item.vertexTR; + _vertexArray[index++] = item.vertexBL; + _vertexArray[index++] = item.vertexBR; + + // Release the texture and return the item to the queue. + item.Texture = null; + _freeBatchItemQueue.Enqueue(item); + } + // flush the remaining vertexArray data + FlushVertexArray(device, startIndex, index); + // Update our batch count to continue the process of culling down + // large batches + batchCount -= numBatchesToProcess; + } + _batchItemList.Clear(); + } + + /// + /// Sends the triangle list to the graphics device. Here is where the actual drawing starts. + /// + /// Start index of vertices to draw. Not used except to compute the count of vertices to draw. + /// End index of vertices to draw. Not used except to compute the count of vertices to draw. + private void FlushVertexArray (GraphicsDevice device, int start, int end) { + if (start == end) + return; + + var vertexCount = end - start; + + device.DrawUserIndexedPrimitives( + PrimitiveType.TriangleList, + _vertexArray, + 0, + vertexCount, + _index, + 0, + (vertexCount / 4) * 2, + VertexPositionColorTexture.VertexDeclaration); + } + } + + public class SpriteBatchItem { + public Texture2D Texture; + public VertexPositionColorTexture vertexTL; + public VertexPositionColorTexture vertexTR; + public VertexPositionColorTexture vertexBL; + public VertexPositionColorTexture vertexBR; + } +}