Browse Source

Event timeline for spine-csharp.

NathanSweet 12 năm trước cách đây
mục cha
commit
d2d6919afb

+ 2 - 0
spine-csharp/spine-csharp_xna.csproj

@@ -105,6 +105,8 @@
     <Compile Include="src\Attachments\RegionAttachment.cs" />
     <Compile Include="src\Bone.cs" />
     <Compile Include="src\BoneData.cs" />
+    <Compile Include="src\Event.cs" />
+    <Compile Include="src\EventData.cs" />
     <Compile Include="src\Json.cs" />
     <Compile Include="src\Skeleton.cs" />
     <Compile Include="src\SkeletonBounds.cs" />

+ 85 - 14
spine-csharp/src/Animation.cs

@@ -48,27 +48,47 @@ namespace Spine {
 			Duration = duration;
 		}
 
-		/** Poses the skeleton at the specified time for this animation. */
+		/** @deprecated */
 		public void Apply (Skeleton skeleton, float time, bool loop) {
+			Apply(skeleton, time, time, loop, null);
+		}
+
+		/** Poses the skeleton at the specified time for this animation.
+		 * @param lastTime The last time the animation was applied. Can be equal to time if events shouldn't be fired.
+		 * @param events Any triggered events are added. May be null if lastTime is known to not cause any events to trigger. */
+		public void Apply (Skeleton skeleton, float lastTime, float time, bool loop, List<Event> events) {
 			if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null.");
 
-			if (loop && Duration != 0) time %= Duration;
+			if (loop && Duration != 0) {
+				time %= Duration;
+				lastTime %= Duration;
+			}
 
 			List<Timeline> timelines = Timelines;
 			for (int i = 0, n = timelines.Count; i < n; i++)
-				timelines[i].Apply(skeleton, time, 1);
+				timelines[i].Apply(skeleton, lastTime, time, 1, events);
+		}
+
+		/** @deprecated */
+		public void Mix (Skeleton skeleton, float time, bool loop, float alpha) {
+			Mix(skeleton, time, time, loop, null, alpha);
 		}
 
 		/** Poses the skeleton at the specified time for this animation mixed with the current pose.
+		 * @param lastTime The last time the animation was applied. Can be equal to time if events shouldn't be fired.
+		 * @param events Any triggered events are added. May be null if lastTime is known to not cause any events to trigger.
 		 * @param alpha The amount of this animation that affects the current pose. */
-		public void Mix (Skeleton skeleton, float time, bool loop, float alpha) {
+		public void Mix (Skeleton skeleton, float lastTime, float time, bool loop, List<Event> events, float alpha) {
 			if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null.");
 
-			if (loop && Duration != 0) time %= Duration;
+			if (loop && Duration != 0) {
+				time %= Duration;
+				lastTime %= Duration;
+			}
 
 			List<Timeline> timelines = Timelines;
 			for (int i = 0, n = timelines.Count; i < n; i++)
-				timelines[i].Apply(skeleton, time, alpha);
+				timelines[i].Apply(skeleton, lastTime, time, alpha, events);
 		}
 
 		/** @param target After the first and before the last entry. */
@@ -96,7 +116,7 @@ namespace Spine {
 
 	public interface Timeline {
 		/** Sets the value(s) for the specified time. */
-		void Apply (Skeleton skeleton, float time, float alpha);
+		void Apply (Skeleton skeleton, float lastTime, float time, float alpha, List<Event> firedEvents);
 	}
 
 	/** Base class for frames that use an interpolation bezier curve. */
@@ -116,7 +136,7 @@ namespace Spine {
 			curves = new float[(frameCount - 1) * 6];
 		}
 
-		abstract public void Apply (Skeleton skeleton, float time, float alpha);
+		abstract public void Apply (Skeleton skeleton, float lastTime, float time, float alpha, List<Event> firedEvents);
 
 		public void SetLinear (int frameIndex) {
 			curves[frameIndex * 6] = LINEAR;
@@ -202,7 +222,7 @@ namespace Spine {
 			Frames[frameIndex + 1] = angle;
 		}
 
-		override public void Apply (Skeleton skeleton, float time, float alpha) {
+		override public void Apply (Skeleton skeleton, float lastTime, float time, float alpha, List<Event> firedEvents) {
 			float[] frames = Frames;
 			if (time < frames[0]) return; // Time is before first frame.
 
@@ -262,7 +282,7 @@ namespace Spine {
 			Frames[frameIndex + 2] = y;
 		}
 
-		override public void Apply (Skeleton skeleton, float time, float alpha) {
+		override public void Apply (Skeleton skeleton, float lastTime, float time, float alpha, List<Event> firedEvents) {
 			float[] frames = Frames;
 			if (time < frames[0]) return; // Time is before first frame.
 
@@ -292,7 +312,7 @@ namespace Spine {
 			: base(frameCount) {
 		}
 
-		override public void Apply (Skeleton skeleton, float time, float alpha) {
+		override public void Apply (Skeleton skeleton, float lastTime, float time, float alpha, List<Event> firedEvents) {
 			float[] frames = Frames;
 			if (time < frames[0]) return; // Time is before first frame.
 
@@ -341,7 +361,7 @@ namespace Spine {
 			Frames[frameIndex + 4] = a;
 		}
 
-		override public void Apply (Skeleton skeleton, float time, float alpha) {
+		override public void Apply (Skeleton skeleton, float lastTime, float time, float alpha, List<Event> firedEvents) {
 			float[] frames = Frames;
 			if (time < frames[0]) return; // Time is before first frame.
 
@@ -405,7 +425,7 @@ namespace Spine {
 			AttachmentNames[frameIndex] = attachmentName;
 		}
 
-		public void Apply (Skeleton skeleton, float time, float alpha) {
+		public void Apply (Skeleton skeleton, float lastTime, float time, float alpha, List<Event> firedEvents) {
 			float[] frames = Frames;
 			if (time < frames[0]) return; // Time is before first frame.
 
@@ -421,6 +441,57 @@ namespace Spine {
 		}
 	}
 
+	public class EventTimeline : Timeline {
+		public float[] Frames { get; private set; } // time, ...
+		public Event[] Events { get; private set; }
+		public int FrameCount {
+			get {
+				return Frames.Length;
+			}
+		}
+
+		public EventTimeline (int frameCount) {
+			Frames = new float[frameCount];
+			Events = new Event[frameCount];
+		}
+
+		/** Sets the time and value of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, Event e) {
+			Frames[frameIndex] = time;
+			Events[frameIndex] = e;
+		}
+
+		public void Apply (Skeleton skeleton, float lastTime, float time, float alpha, List<Event> firedEvents) {
+			float[] frames = Frames;
+			if (time < frames[0]) return; // Time is before first frame.
+
+			int frameCount = frames.Length;
+			if (lastTime >= frames[frameCount - 1]) return; // Last time is after last frame.
+
+			if (lastTime > time) {
+				// Fire events after last time for looped animations.
+				Apply(skeleton, lastTime, int.MaxValue, alpha, firedEvents);
+				lastTime = 0;
+			}
+
+			int frameIndex;
+			if (frameCount == 1)
+				frameIndex = 0;
+			else {
+				frameIndex = Animation.binarySearch(frames, lastTime, 1);
+				float frame = frames[frameIndex];
+				while (frameIndex > 0) {
+					float lastFrame = frames[frameIndex - 1];
+					// Fire multiple events with the same frame and events that occurred at lastTime.
+					if (lastFrame != frame && lastFrame != lastTime) break;
+					frameIndex--;
+				}
+			}
+			for (; frameIndex < frameCount && time > frames[frameIndex]; frameIndex++)
+				firedEvents.Add(Events[frameIndex]);
+		}
+	}
+
 	public class DrawOrderTimeline : Timeline {
 		public float[] Frames { get; private set; } // time, ...
 		public int[][] DrawOrders { get; private set; }
@@ -441,7 +512,7 @@ namespace Spine {
 			DrawOrders[frameIndex] = drawOrder;
 		}
 
-		public void Apply (Skeleton skeleton, float time, float alpha) {
+		public void Apply (Skeleton skeleton, float lastTime, float time, float alpha, List<Event> firedEvents) {
 			float[] frames = Frames;
 			if (time < frames[0]) return; // Time is before first frame.
 

+ 141 - 50
spine-csharp/src/AnimationState.cs

@@ -38,28 +38,55 @@ namespace Spine {
 	public class AnimationState {
 		public AnimationStateData Data { get; private set; }
 		public Animation Animation { get; private set; }
-		public float Time { get; set; }
+
+		private float time;
+		public float Time {
+			get { return time; }
+			set {
+				time = value;
+				currentLastTime = value - 0.00001f;
+			}
+		}
+
+		private float currentLastTime;
 		public bool Loop { get; set; }
 		private Animation previous;
 		private float previousTime;
 		private bool previousLoop;
+		private QueueEntry currentQueueEntry;
 		private float mixTime, mixDuration;
+		private List<Event> events = new List<Event>();
 		private List<QueueEntry> queue = new List<QueueEntry>();
 
+		public event EventHandler Start;
+		public event EventHandler End;
+		public event EventHandler<EventTriggeredArgs> Event;
+		public event EventHandler<CompleteArgs> Complete;
+
 		public AnimationState (AnimationStateData data) {
 			if (data == null) throw new ArgumentNullException("data cannot be null.");
 			Data = data;
 		}
 
 		public void Update (float delta) {
-			Time += delta;
+			time += delta;
 			previousTime += delta;
 			mixTime += delta;
 
+			if (Animation != null) {
+				float duration = Animation.Duration;
+				if (Loop ? (currentLastTime % duration > time % duration)
+					: (currentLastTime < duration && time >= duration)) {
+					int count = (int)(time / duration);
+					if (currentQueueEntry != null) currentQueueEntry.OnComplete(this, count);
+					if (Complete != null) Complete(this, new CompleteArgs(count));
+				}
+			}
+
 			if (queue.Count > 0) {
 				QueueEntry entry = queue[0];
-				if (Time >= entry.delay) {
-					SetAnimationInternal(entry.animation, entry.loop);
+				if (time >= entry.delay) {
+					SetAnimationInternal(entry.animation, entry.loop, entry);
 					queue.RemoveAt(0);
 				}
 			}
@@ -67,35 +94,94 @@ namespace Spine {
 
 		public void Apply (Skeleton skeleton) {
 			if (Animation == null) return;
+
+			List<Event> events = this.events;
+			events.Clear();
+
 			if (previous != null) {
-				previous.Apply(skeleton, previousTime, previousLoop);
+				previous.Apply(skeleton, int.MaxValue, previousTime, previousLoop, null);
 				float alpha = mixTime / mixDuration;
 				if (alpha >= 1) {
 					alpha = 1;
 					previous = null;
 				}
-				Animation.Mix(skeleton, Time, Loop, alpha);
+				Animation.Mix(skeleton, currentLastTime, time, Loop, events, alpha);
 			} else
-				Animation.Apply(skeleton, Time, Loop);
+				Animation.Apply(skeleton, currentLastTime, time, Loop, events);
+
+			if (Event != null || currentQueueEntry != null) {
+				foreach (Event e in events) {
+					if (currentQueueEntry != null) currentQueueEntry.OnEvent(this, e);
+					if (Event != null) Event(this, new EventTriggeredArgs(e));
+				}
+			}
+
+			currentLastTime = time;
+		}
+
+		public void ClearAnimation () {
+			previous = null;
+			Animation = null;
+			queue.Clear();
+		}
+
+		private void SetAnimationInternal (Animation animation, bool loop, QueueEntry entry) {
+			previous = null;
+			if (Animation != null) {
+				if (currentQueueEntry != null) currentQueueEntry.OnEnd(this);
+				if (End != null) End(this, EventArgs.Empty);
+
+				if (animation != null) {
+					mixDuration = Data.GetMix(Animation, animation);
+					if (mixDuration > 0) {
+						mixTime = 0;
+						previous = Animation;
+						previousTime = time;
+						previousLoop = Loop;
+					}
+				}
+			}
+			Animation = animation;
+			Loop = loop;
+			time = 0;
+			currentLastTime = 0;
+			currentQueueEntry = entry;
+
+			if (currentQueueEntry != null) currentQueueEntry.OnStart(this);
+			if (Start != null) Start(this, EventArgs.Empty);
+		}
+
+		public void SetAnimation (String animationName, bool loop) {
+			Animation animation = Data.SkeletonData.FindAnimation(animationName);
+			if (animation == null) throw new ArgumentException("Animation not found: " + animationName);
+			SetAnimation(animation, loop);
+		}
+
+		/** Set the current animation. Any queued animations are cleared and the current animation time is set to 0.
+		 * @param animation May be null.
+		 * @param listener May be null. */
+		public void SetAnimation (Animation animation, bool loop) {
+			queue.Clear();
+			SetAnimationInternal(animation, loop, null);
 		}
 
-		public void AddAnimation (String animationName, bool loop) {
-			AddAnimation(animationName, loop, 0);
+		public QueueEntry AddAnimation (String animationName, bool loop) {
+			return AddAnimation(animationName, loop, 0);
 		}
 
-		public void AddAnimation (String animationName, bool loop, float delay) {
+		public QueueEntry AddAnimation (String animationName, bool loop, float delay) {
 			Animation animation = Data.SkeletonData.FindAnimation(animationName);
 			if (animation == null) throw new ArgumentException("Animation not found: " + animationName);
-			AddAnimation(animation, loop, delay);
+			return AddAnimation(animation, loop, delay);
 		}
 
-		public void AddAnimation (Animation animation, bool loop) {
-			AddAnimation(animation, loop, 0);
+		public QueueEntry AddAnimation (Animation animation, bool loop) {
+			return AddAnimation(animation, loop, 0);
 		}
 
 		/** Adds an animation to be played delay seconds after the current or last queued animation.
 		 * @param delay May be <= 0 to use duration of previous animation minus any mix duration plus the negative delay. */
-		public void AddAnimation (Animation animation, bool loop, float delay) {
+		public QueueEntry AddAnimation (Animation animation, bool loop, float delay) {
 			QueueEntry entry = new QueueEntry();
 			entry.animation = animation;
 			entry.loop = loop;
@@ -110,54 +196,59 @@ namespace Spine {
 			entry.delay = delay;
 
 			queue.Add(entry);
+			return entry;
 		}
 
-		private void SetAnimationInternal (Animation animation, bool loop) {
-			previous = null;
-			if (animation != null && Animation != null) {
-				mixDuration = Data.GetMix(Animation, animation);
-				if (mixDuration > 0) {
-					mixTime = 0;
-					previous = Animation;
-					previousTime = Time;
-					previousLoop = Loop;
-				}
-			}
-			Animation = animation;
-			Loop = loop;
-			Time = 0;
+		/** Returns true if no animation is set or if the current time is greater than the animation duration, regardless of looping. */
+		public bool IsComplete () {
+			return Animation == null || time >= Animation.Duration;
 		}
 
-		public void SetAnimation (String animationName, bool loop) {
-			Animation animation = Data.SkeletonData.FindAnimation(animationName);
-			if (animation == null) throw new ArgumentException("Animation not found: " + animationName);
-			SetAnimation(animation, loop);
+		override public String ToString () {
+			return (Animation != null && Animation.Name != null) ? Animation.Name : base.ToString();
 		}
+	}
 
-		public void SetAnimation (Animation animation, bool loop) {
-			queue.Clear();
-			SetAnimationInternal(animation, loop);
+	public class EventTriggeredArgs : EventArgs {
+		public Event Event { get; private set; }
+
+		public EventTriggeredArgs (Event e) {
+			Event = e;
 		}
+	}
 
-		public void ClearAnimation () {
-			previous = null;
-			Animation = null;
-			queue.Clear();
+	public class CompleteArgs : EventArgs {
+		public int LoopCount { get; private set; }
+
+		public CompleteArgs (int loopCount) {
+			LoopCount = loopCount;
 		}
+	}
 
-		/** Returns true if no animation is set or if the current time is greater than the animation duration, regardless of looping. */
-		public bool IsComplete () {
-			return Animation == null || Time >= Animation.Duration;
+	public class QueueEntry {
+		internal Spine.Animation animation;
+		internal bool loop;
+		internal float delay;
+
+		public event EventHandler Start;
+		public event EventHandler End;
+		public event EventHandler<EventTriggeredArgs> Event;
+		public event EventHandler<CompleteArgs> Complete;
+
+		internal void OnStart (AnimationState state) {
+			if (Start != null) Start(state, EventArgs.Empty);
 		}
 
-		override public String ToString () {
-			return (Animation != null && Animation.Name != null) ? Animation.Name : base.ToString();
+		internal void OnEnd (AnimationState state) {
+			if (End != null) End(state, EventArgs.Empty);
 		}
-	}
-}
 
-class QueueEntry {
-	public Spine.Animation animation;
-	public bool loop;
-	public float delay;
+		internal void OnEvent (AnimationState state, Event e) {
+			if (Event != null) Event(state, new EventTriggeredArgs(e));
+		}
+
+		internal void OnComplete (AnimationState state, int loopCount) {
+			if (Complete != null) Complete(state, new CompleteArgs(loopCount));
+		}
+	}
 }

+ 51 - 0
spine-csharp/src/Event.cs

@@ -0,0 +1,51 @@
+/******************************************************************************
+ * Spine Runtime Software License - Version 1.0
+ * 
+ * Copyright (c) 2013, Esoteric Software
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms in whole or in part, with
+ * or without modification, are permitted provided that the following conditions
+ * are met:
+ * 
+ * 1. A Spine Single User License or Spine Professional License must be
+ *    purchased from Esoteric Software and the license must remain valid:
+ *    http://esotericsoftware.com/
+ * 2. Redistributions of source code must retain this license, which is the
+ *    above copyright notice, this declaration of conditions and the following
+ *    disclaimer.
+ * 3. Redistributions in binary form must reproduce this license, which is the
+ *    above copyright notice, this declaration 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;
+
+namespace Spine {
+	public class Event {
+		public EventData Data { get; private set; }
+		public int Int { get; set; }
+		public float Float { get; set; }
+		public String String { get; set; }
+
+		public Event (EventData data) {
+			Data = data;
+		}
+
+		override public String ToString () {
+			return Data.Name;
+		}
+	}
+}

+ 52 - 0
spine-csharp/src/EventData.cs

@@ -0,0 +1,52 @@
+/******************************************************************************
+ * Spine Runtime Software License - Version 1.0
+ * 
+ * Copyright (c) 2013, Esoteric Software
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms in whole or in part, with
+ * or without modification, are permitted provided that the following conditions
+ * are met:
+ * 
+ * 1. A Spine Single User License or Spine Professional License must be
+ *    purchased from Esoteric Software and the license must remain valid:
+ *    http://esotericsoftware.com/
+ * 2. Redistributions of source code must retain this license, which is the
+ *    above copyright notice, this declaration of conditions and the following
+ *    disclaimer.
+ * 3. Redistributions in binary form must reproduce this license, which is the
+ *    above copyright notice, this declaration 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;
+
+namespace Spine {
+	public class EventData {
+		public String Name { get; private set; }
+		public int Int { get; set; }
+		public float Float { get; set; }
+		public String String { get; set; }
+
+		public EventData (String name) {
+			if (name == null) throw new ArgumentNullException("name cannot be null.");
+			Name = name;
+		}
+
+		override public String ToString () {
+			return Name;
+		}
+	}
+}

+ 4 - 2
spine-csharp/src/SkeletonBounds.cs

@@ -133,8 +133,10 @@ namespace Spine {
 				} else
 					polygon = new Polygon();
 				polygons.Add(polygon);
-				polygon.Count = boundingBox.Vertices.Length;
-				if (polygon.Vertices.Length < polygon.Count) polygon.Vertices = new float[polygon.Count];
+
+				int count = boundingBox.Vertices.Length;
+				polygon.Count = count;
+				if (polygon.Vertices.Length < count) polygon.Vertices = new float[count];
 				boundingBox.ComputeWorldVertices(x, y, slot.Bone, polygon.Vertices);
 			}
 		}

+ 17 - 0
spine-csharp/src/SkeletonData.cs

@@ -42,12 +42,14 @@ namespace Spine {
 		public List<Skin> Skins { get; private set; }
 		/** May be null. */
 		public Skin DefaultSkin;
+		public List<EventData> Events { get; private set; }
 		public List<Animation> Animations { get; private set; }
 
 		public SkeletonData () {
 			Bones = new List<BoneData>();
 			Slots = new List<SlotData>();
 			Skins = new List<Skin>();
+			Events = new List<EventData>();
 			Animations = new List<Animation>();
 		}
 
@@ -117,6 +119,21 @@ namespace Spine {
 			return null;
 		}
 
+		// --- Events.
+
+		public void AddEvent (EventData eventData) {
+			if (eventData == null) throw new ArgumentNullException("eventData cannot be null.");
+			Events.Add(eventData);
+		}
+
+		/** @return May be null. */
+		public EventData findEvent (String eventDataName) {
+			if (eventDataName == null) throw new ArgumentNullException("eventDataName cannot be null.");
+			foreach (EventData eventData in Events)
+				if (eventData.Name == eventDataName) return eventData;
+			return null;
+		}
+
 		// --- Animations.
 
 		public void AddAnimation (Animation animation) {

+ 47 - 12
spine-csharp/src/SkeletonJson.cs

@@ -118,8 +118,7 @@ namespace Spine {
 
 			// Slots.
 			if (root.ContainsKey("slots")) {
-				var slots = (List<Object>)root["slots"];
-				foreach (Dictionary<String, Object> slotMap in slots) {
+				foreach (Dictionary<String, Object> slotMap in (List<Object>)root["slots"]) {
 					String slotName = (String)slotMap["name"];
 					String boneName = (String)slotMap["bone"];
 					BoneData boneData = skeletonData.FindBone(boneName);
@@ -147,8 +146,7 @@ namespace Spine {
 
 			// Skins.
 			if (root.ContainsKey("skins")) {
-				var skinMap = (Dictionary<String, Object>)root["skins"];
-				foreach (KeyValuePair<String, Object> entry in skinMap) {
+				foreach (KeyValuePair<String, Object> entry in (Dictionary<String, Object>)root["skins"]) {
 					Skin skin = new Skin(entry.Key);
 					foreach (KeyValuePair<String, Object> slotEntry in (Dictionary<String, Object>)entry.Value) {
 						int slotIndex = skeletonData.FindSlotIndex(slotEntry.Key);
@@ -163,11 +161,21 @@ namespace Spine {
 				}
 			}
 
+			// Events.
+			if (root.ContainsKey("events")) {
+				foreach (KeyValuePair<String, Object> entry in (Dictionary<String, Object>)root["events"]) {
+					var entryMap = (Dictionary<String, Object>)entry.Value;
+					EventData eventData = new EventData(entry.Key);
+					eventData.Int = GetInt(entryMap, "int", 0);
+					eventData.Float = GetFloat(entryMap, "float", 0);
+					eventData.String = GetString(entryMap, "string", null);
+					skeletonData.AddEvent(eventData);
+				}
+			}
 
 			// Animations.
 			if (root.ContainsKey("animations")) {
-				var animationMap = (Dictionary<String, Object>)root["animations"];
-				foreach (KeyValuePair<String, Object> entry in animationMap)
+				foreach (KeyValuePair<String, Object> entry in (Dictionary<String, Object>)root["animations"])
 					ReadAnimation(entry.Key, (Dictionary<String, Object>)entry.Value, skeletonData);
 			}
 
@@ -213,16 +221,28 @@ namespace Spine {
 
 		private float GetFloat (Dictionary<String, Object> map, String name, float defaultValue) {
 			if (!map.ContainsKey(name))
-				return (float)defaultValue;
+				return defaultValue;
 			return (float)map[name];
 		}
 
+		private int GetInt (Dictionary<String, Object> map, String name, int defaultValue) {
+			if (!map.ContainsKey(name))
+				return defaultValue;
+			return (int)map[name];
+		}
+
 		private bool GetBoolean (Dictionary<String, Object> map, String name, bool defaultValue) {
 			if (!map.ContainsKey(name))
-				return (bool)defaultValue;
+				return defaultValue;
 			return (bool)map[name];
 		}
 
+		private String GetString (Dictionary<String, Object> map, String name, String defaultValue) {
+			if (!map.ContainsKey(name))
+				return defaultValue;
+			return (String)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);
@@ -234,8 +254,7 @@ namespace Spine {
 			float duration = 0;
 
 			if (map.ContainsKey("bones")) {
-				var bonesMap = (Dictionary<String, Object>)map["bones"];
-				foreach (KeyValuePair<String, Object> entry in bonesMap) {
+				foreach (KeyValuePair<String, Object> entry in (Dictionary<String, Object>)map["bones"]) {
 					String boneName = entry.Key;
 					int boneIndex = skeletonData.FindBoneIndex(boneName);
 					if (boneIndex == -1)
@@ -289,8 +308,7 @@ namespace Spine {
 			}
 
 			if (map.ContainsKey("slots")) {
-				var slotsMap = (Dictionary<String, Object>)map["slots"];
-				foreach (KeyValuePair<String, Object> entry in slotsMap) {
+				foreach (KeyValuePair<String, Object> entry in (Dictionary<String, Object>)map["slots"]) {
 					String slotName = entry.Key;
 					int slotIndex = skeletonData.FindSlotIndex(slotName);
 					var timelineMap = (Dictionary<String, Object>)entry.Value;
@@ -331,6 +349,23 @@ namespace Spine {
 				}
 			}
 
+			if (map.ContainsKey("events")) {
+				var eventsMap = (List<Object>)map["events"];
+				EventTimeline timeline = new EventTimeline(eventsMap.Count);
+				int frameIndex = 0;
+				foreach (Dictionary<String, Object> eventMap in eventsMap) {
+					EventData eventData = skeletonData.findEvent((String)eventMap["name"]);
+					if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]);
+					Event e = new Event(eventData);
+					e.Int = GetInt(eventMap, "int", eventData.Int);
+					e.Float = GetFloat(eventMap, "float", eventData.Float);
+					e.String = GetString(eventMap, "string", eventData.String);
+					timeline.setFrame(frameIndex++, (float)eventMap["time"], e);
+				}
+				timelines.Add(timeline);
+				duration = Math.Max(duration, timeline.Frames[timeline.FrameCount - 1]);
+			}
+
 			if (map.ContainsKey("draworder")) {
 				var values = (List<Object>)map["draworder"];
 				DrawOrderTimeline timeline = new DrawOrderTimeline(values.Count);

+ 21 - 0
spine-xna/example/src/ExampleGame.cs

@@ -86,6 +86,11 @@ namespace Spine {
 			}
 
 			state = new AnimationState(stateData);
+			state.Start += new EventHandler(Start);
+			state.End += new EventHandler(End);
+			state.Complete += new EventHandler<CompleteArgs>(Complete);
+			state.Event += new EventHandler<EventTriggeredArgs>(Event);
+
 			if (true) {
 				state.SetAnimation("drawOrder", true);
 			} else {
@@ -140,5 +145,21 @@ namespace Spine {
 
 			base.Draw(gameTime);
 		}
+
+		public void Start (object sender, EventArgs e) {
+			Console.WriteLine("start");
+		}
+
+		public void End (object sender, EventArgs e) {
+			Console.WriteLine("end");
+		}
+
+		public void Complete (object sender, CompleteArgs e) {
+			Console.WriteLine("complete " + e.LoopCount);
+		}
+
+		public void Event (object sender, EventTriggeredArgs e) {
+			Console.WriteLine("event " + e.Event);
+		}
 	}
 }