Refactoring.

API clean up, better JSON parsing, better atlas, easier to extend.
This commit is contained in:
NathanSweet 2013-04-13 19:17:29 +02:00
parent 2b49c59907
commit 3b1cf6579c
11 changed files with 205 additions and 231 deletions

View File

@ -60,12 +60,12 @@
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="src\Animation.cs" />
<Compile Include="src\Atlas.cs" />
<Compile Include="src\Attachments\AttachmentLoader.cs" />
<Compile Include="src\Attachments\AtlasAttachmentLoader.cs" />
<Compile Include="src\Attachments\Attachment.cs" />
<Compile Include="src\Attachments\AttachmentType.cs" />
<Compile Include="src\Attachments\RegionAttachment.cs" />
<Compile Include="src\BaseAtlas.cs" />
<Compile Include="src\Bone.cs" />
<Compile Include="src\BoneData.cs" />
<Compile Include="src\Json.cs" />

View File

@ -22,101 +22,133 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
namespace Spine {
abstract public class BaseAtlas {
List<AtlasPage> pages = new List<AtlasPage>();
List<AtlasRegion> regions = new List<AtlasRegion>();
public class Atlas {
public Format Format;
public TextureFilter MinFilter;
public TextureFilter MagFilter;
public TextureWrap UWrap;
public TextureWrap VWrap;
public int TextureWidth;
public int TextureHeight;
public List<AtlasRegion> Regions;
public Object Texture;
abstract protected AtlasPage NewAtlasPage (String path);
public Atlas (String path, Object texture, int textureWidth, int textureHeight) {
using (Stream input = new FileStream(path, FileMode.Open, FileAccess.Read)) {
try {
initialize(input, texture, textureWidth, textureHeight);
} catch (Exception ex) {
throw new Exception("Error reading atlas file: " + path, ex);
}
}
}
public void load (StreamReader reader, String imagesDir) {
public Atlas (Stream input, Object texture, int textureWidth, int textureHeight) {
initialize(input, texture, textureWidth, textureHeight);
}
private void initialize (Stream input, Object texture, int textureWidth, int textureHeight) {
TextureWidth = textureWidth;
TextureHeight = textureHeight;
Texture = texture;
Regions = new List<AtlasRegion>();
float invTexWidth = 1f / textureWidth;
float invTexHeight = 1f / textureHeight;
String[] tuple = new String[4];
AtlasPage page = null;
StreamReader reader = new StreamReader(input);
// Skip to first page entry.
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));
break;
}
reader.ReadLine(); // Skip first page name.
page.Format = (Format)Enum.Parse(typeof(Format), readValue(reader), false);
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]);
readTuple(reader, tuple);
MinFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[0]);
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;
String direction = readValue(reader);
UWrap = TextureWrap.ClampToEdge;
VWrap = TextureWrap.ClampToEdge;
if (direction == "x")
UWrap = TextureWrap.Repeat;
else if (direction == "y")
VWrap = TextureWrap.Repeat;
else if (direction == "xy")
UWrap = VWrap = TextureWrap.Repeat;
pages.Add(page);
while (true) {
String line = reader.ReadLine();
if (line == null || line.Trim().Length == 0) break;
} else {
AtlasRegion region = new AtlasRegion();
region.Name = line;
region.Page = page;
AtlasRegion region = new AtlasRegion();
region.Atlas = this;
region.Name = line;
region.Rotate = Boolean.Parse(readValue(reader));
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 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]);
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);
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]),
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]),
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);
}
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);
}
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);
}
while (true) {
String line = reader.ReadLine();
if (line == null)
break;
if (line.Trim().Length != 0) throw new Exception("An atlas with multiple images is not supported.");
}
}
static String readValue (StreamReader reader) {
String line = reader.ReadLine();
int colon = line.IndexOf(':');
if (colon == -1) throw new Exception("Invalid line: " + line);
if (colon == -1)
throw new Exception("Invalid line: " + line);
return line.Substring(colon + 1).Trim();
}
@ -124,12 +156,14 @@ namespace Spine {
static int readTuple (StreamReader reader, String[] tuple) {
String line = reader.ReadLine();
int colon = line.IndexOf(':');
if (colon == -1) throw new Exception("Invalid line: " + line);
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);
if (i == 0)
throw new Exception("Invalid line: " + line);
break;
}
tuple[i] = line.Substring(lastMatch, comma - lastMatch).Trim();
@ -143,8 +177,9 @@ namespace Spine {
* 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];
for (int i = 0, n = Regions.Count; i < n; i++)
if (Regions[i].Name == name)
return Regions[i];
return null;
}
}
@ -175,19 +210,8 @@ namespace Spine {
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 Atlas Atlas;
public float U, V;
public float U2, V2;
public int Width, Height;

View File

@ -27,9 +27,9 @@ using System;
namespace Spine {
public class AtlasAttachmentLoader : AttachmentLoader {
private BaseAtlas atlas;
private Atlas atlas;
public AtlasAttachmentLoader (BaseAtlas atlas) {
public AtlasAttachmentLoader (Atlas atlas) {
if (atlas == null) throw new ArgumentNullException("atlas cannot be null.");
this.atlas = atlas;
}

View File

@ -96,16 +96,18 @@ namespace Spine {
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;
if (region != null) {
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;

View File

@ -81,12 +81,10 @@ namespace Spine
/// </summary>
/// <param name="json">A JSON string.</param>
/// <returns>An List&lt;object&gt;, a Dictionary&lt;string, object&gt;, a float, an integer,a string, null, true, or false</returns>
public static object Deserialize(string json) {
// save the string for debug information
public static object Deserialize (TextReader json) {
if (json == null) {
return null;
}
}
return Parser.Parse(json);
}
@ -109,14 +107,14 @@ namespace Spine
NULL
};
StringReader json;
TextReader json;
Parser(string jsonString) {
json = new StringReader(jsonString);
Parser(TextReader reader) {
json = reader;
}
public static object Parse(string jsonString) {
using (var instance = new Parser(jsonString)) {
public static object Parse (TextReader reader) {
using (var instance = new Parser(reader)) {
return instance.ParseValue();
}
}
@ -288,14 +286,6 @@ namespace Spine
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, NumberStyles.Float, CultureInfo.InvariantCulture, out parsedFloat);
return parsedFloat;

View File

@ -41,7 +41,7 @@ namespace Spine {
private AttachmentLoader attachmentLoader;
public float Scale { get; set; }
public SkeletonJson (BaseAtlas atlas) {
public SkeletonJson (Atlas atlas) {
this.attachmentLoader = new AtlasAttachmentLoader(atlas);
Scale = 1;
}
@ -51,14 +51,21 @@ namespace Spine {
Scale = 1;
}
public SkeletonData readSkeletonData (String name, String json) {
if (json == null)
public SkeletonData ReadSkeletonData (String path) {
using (Stream input = new FileStream(path, FileMode.Open, FileAccess.Read)) {
SkeletonData skeletonData = ReadSkeletonData(input);
skeletonData.Name = Path.GetFileNameWithoutExtension(path);
return skeletonData;
}
}
public SkeletonData ReadSkeletonData (Stream input) {
if (input == null)
throw new ArgumentNullException("json cannot be null.");
SkeletonData skeletonData = new SkeletonData();
skeletonData.Name = name;
var root = Json.Deserialize(json) as Dictionary<String, Object>;
var root = Json.Deserialize(new StreamReader(input)) as Dictionary<String, Object>;
// Bones.
foreach (Dictionary<String, Object> boneMap in (List<Object>)root["bones"]) {

View File

@ -58,11 +58,13 @@ namespace Spine {
protected override void LoadContent () {
skeletonRenderer = new SkeletonRenderer(GraphicsDevice);
Atlas atlas = new Atlas(GraphicsDevice, "data/goblins.atlas");
Texture2D texture = Util.LoadTexture(GraphicsDevice, "data/goblins.png");
Atlas atlas = new Atlas("data/goblins.atlas", texture, texture.Width, texture.Height);
SkeletonJson json = new SkeletonJson(atlas);
skeleton = new Skeleton(json.readSkeletonData("goblins", File.ReadAllText("data/goblins.json")));
skeleton = new Skeleton(json.ReadSkeletonData("data/goblins.json"));
skeleton.SetSkin("goblingirl");
skeleton.SetSlotsToBindPose();
skeleton.SetSlotsToBindPose(); // Without this the skin attachments won't be attached. See SetSkin.
animation = skeleton.Data.FindAnimation("walk");
skeleton.RootBone.X = 320;

View File

@ -93,7 +93,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="src\Atlas.cs" />
<Compile Include="src\Util.cs" />
<Compile Include="src\SkeletonRenderer.cs" />
<Compile Include="src\SpriteBatcher.cs" />
</ItemGroup>

View File

@ -1,109 +0,0 @@
/*******************************************************************************
* Copyright (c) 2013, Esoteric Software
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
using System;
using System.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;
}
}
}

View File

@ -34,6 +34,7 @@ namespace Spine {
SpriteBatcher batcher;
BasicEffect effect;
RasterizerState rasterizerState;
public BlendState BlendState { get; set; }
public SkeletonRenderer (GraphicsDevice device) {
this.device = device;
@ -49,12 +50,14 @@ namespace Spine {
rasterizerState = new RasterizerState();
rasterizerState.CullMode = CullMode.None;
BlendState = BlendState.AlphaBlend;
Bone.yDown = true;
}
public void Begin () {
device.RasterizerState = rasterizerState;
device.BlendState = BlendState.AlphaBlend;
device.BlendState = BlendState;
effect.Projection = Matrix.CreateOrthographicOffCenter(0, device.Viewport.Width, device.Viewport.Height, 0, 1, 0);
}
@ -71,13 +74,11 @@ namespace Spine {
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;
item.Texture = (Texture2D)regionAttachment.Region.Atlas.Texture;
byte r = (byte)(slot.R * 255);
byte g = (byte)(slot.G * 255);

57
spine-xna/src/Util.cs Normal file
View File

@ -0,0 +1,57 @@
using System;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Spine {
static public class Util {
static public Texture2D LoadTexture (GraphicsDevice device, String path) {
using (Stream input = new FileStream(path, FileMode.Open, FileAccess.Read)) {
try {
return Util.LoadTexture(device, input);
} catch (Exception ex) {
throw new Exception("Error reading texture file: " + path, ex);
}
}
}
static public Texture2D LoadTexture (GraphicsDevice device, Stream input) {
Texture2D file = Texture2D.FromStream(device, input);
// 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;
}
}
}