Bläddra i källkod

Event timeline.

NathanSweet 12 år sedan
förälder
incheckning
354d3b75d6

+ 1 - 1
spine-libgdx/.settings/org.eclipse.jdt.core.prefs

@@ -85,7 +85,7 @@ org.eclipse.jdt.core.compiler.problem.unusedImport=warning
 org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
 org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
 org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
-org.eclipse.jdt.core.compiler.problem.unusedParameter=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
 org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=disabled
 org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
 org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled

+ 70 - 13
spine-libgdx/src/com/esotericsoftware/spine/Animation.java

@@ -55,27 +55,45 @@ public class Animation {
 		this.duration = duration;
 	}
 
-	/** Poses the skeleton at the specified time for this animation. */
+	/** @deprecated */
 	public void apply (Skeleton skeleton, float time, boolean loop) {
+		apply(skeleton, Float.MAX_VALUE, time, loop, null);
+	}
+
+	/** Poses the skeleton at the specified time for this animation.
+	 * @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, boolean loop, Array<Event> events) {
 		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
 
-		if (loop && duration != 0) time %= duration;
+		if (loop && duration != 0) {
+			lastTime %= duration;
+			time %= duration;
+		}
 
 		Array<Timeline> timelines = this.timelines;
 		for (int i = 0, n = timelines.size; i < n; i++)
-			timelines.get(i).apply(skeleton, time, 1);
+			timelines.get(i).apply(skeleton, lastTime, time, 1, events);
 	}
 
-	/** 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. */
+	/** @deprecated */
 	public void mix (Skeleton skeleton, float time, boolean loop, float alpha) {
+		mix(skeleton, Float.MAX_VALUE, time, loop, null, alpha);
+	}
+
+	/** 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.
+	 * @param events Any triggered events are added. May be null if lastTime is known to not cause any events to trigger. */
+	public void mix (Skeleton skeleton, float lastTime, float time, boolean loop, Array<Event> events, float alpha) {
 		if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
 
-		if (loop && duration != 0) time %= duration;
+		if (loop && duration != 0) {
+			lastTime %= duration;
+			time %= duration;
+		}
 
 		Array<Timeline> timelines = this.timelines;
 		for (int i = 0, n = timelines.size; i < n; i++)
-			timelines.get(i).apply(skeleton, time, alpha);
+			timelines.get(i).apply(skeleton, lastTime, time, alpha, events);
 	}
 
 	public String getName () {
@@ -110,7 +128,7 @@ public class Animation {
 
 	static public interface Timeline {
 		/** Sets the value(s) for the specified time. */
-		public void apply (Skeleton skeleton, float time, float alpha);
+		public void apply (Skeleton skeleton, float lastTime, float time, float alpha, Array<Event> events);
 	}
 
 	/** Base class for frames that use an interpolation bezier curve. */
@@ -235,7 +253,7 @@ public class Animation {
 			frames[frameIndex + 1] = angle;
 		}
 
-		public void apply (Skeleton skeleton, float time, float alpha) {
+		public void apply (Skeleton skeleton, float lastTime, float time, float alpha, Array<Event> events) {
 			float[] frames = this.frames;
 			if (time < frames[0]) return; // Time is before first frame.
 
@@ -305,7 +323,7 @@ public class Animation {
 			frames[frameIndex + 2] = y;
 		}
 
-		public void apply (Skeleton skeleton, float time, float alpha) {
+		public void apply (Skeleton skeleton, float lastTime, float time, float alpha, Array<Event> events) {
 			float[] frames = this.frames;
 			if (time < frames[0]) return; // Time is before first frame.
 
@@ -335,7 +353,7 @@ public class Animation {
 			super(frameCount);
 		}
 
-		public void apply (Skeleton skeleton, float time, float alpha) {
+		public void apply (Skeleton skeleton, float lastTime, float time, float alpha, Array<Event> events) {
 			float[] frames = this.frames;
 			if (time < frames[0]) return; // Time is before first frame.
 
@@ -398,7 +416,7 @@ public class Animation {
 			frames[frameIndex + 4] = a;
 		}
 
-		public void apply (Skeleton skeleton, float time, float alpha) {
+		public void apply (Skeleton skeleton, float lastTime, float time, float alpha, Array<Event> events) {
 			float[] frames = this.frames;
 			if (time < frames[0]) return; // Time is before first frame.
 
@@ -471,7 +489,7 @@ public class Animation {
 			attachmentNames[frameIndex] = attachmentName;
 		}
 
-		public void apply (Skeleton skeleton, float time, float alpha) {
+		public void apply (Skeleton skeleton, float lastTime, float time, float alpha, Array<Event> events) {
 			float[] frames = this.frames;
 			if (time < frames[0]) return; // Time is before first frame.
 
@@ -486,4 +504,43 @@ public class Animation {
 				attachmentName == null ? null : skeleton.getAttachment(slotIndex, attachmentName));
 		}
 	}
+
+	static public class EventTimeline implements Timeline {
+		private final float[] frames; // time, ...
+		private final Event[] events;
+
+		public EventTimeline (int frameCount) {
+			frames = new float[frameCount];
+			events = new Event[frameCount];
+		}
+
+		public int getFrameCount () {
+			return frames.length;
+		}
+
+		public float[] getFrames () {
+			return frames;
+		}
+
+		/** Sets the time of the specified keyframe. */
+		public void setFrame (int frameIndex, float time, Event event) {
+			frames[frameIndex] = time;
+			events[frameIndex] = event;
+		}
+
+		public void apply (Skeleton skeleton, float lastTime, float time, float alpha, Array<Event> firedEvents) {
+			float[] frames = this.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.
+
+			int frameIndex = binarySearch(frames, lastTime, 1);
+			float frame = frames[frameIndex];
+			while (frameIndex > 0 && frame == frames[frameIndex - 1])
+				frameIndex--; // Fire multiple events with the same frame.
+			for (; frameIndex < frameCount && time > frames[frameIndex]; frameIndex++)
+				firedEvents.add(events[frameIndex]);
+		}
+	}
 }

+ 71 - 12
spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java

@@ -32,10 +32,12 @@ import com.badlogic.gdx.utils.Pools;
 public class AnimationState {
 	private final AnimationStateData data;
 	private Animation current, previous;
-	private float currentTime, previousTime;
+	private float currentTime, currentLastTime, previousTime;
 	private boolean currentLoop, previousLoop;
 	private float mixTime, mixDuration;
-	private Array<QueueEntry> queue = new Array();
+	private final Array<QueueEntry> queue = new Array();
+	private final Array<Event> events = new Array();
+	private final Array<AnimationStateListener> listeners = new Array();
 
 	public AnimationState (AnimationStateData data) {
 		if (data == null) throw new IllegalArgumentException("data cannot be null.");
@@ -43,10 +45,20 @@ public class AnimationState {
 	}
 
 	public void update (float delta) {
+		currentLastTime = currentTime;
 		currentTime += delta;
 		previousTime += delta;
 		mixTime += delta;
 
+		if (current != null) {
+			float duration = current.getDuration();
+			if (currentLoop ? (currentLastTime % duration > currentTime % duration)
+				: (currentLastTime < duration && currentTime >= duration)) {
+				for (int i = 0, n = listeners.size; i < n; i++)
+					listeners.get(i).complete((int)(currentTime / duration));
+			}
+		}
+
 		if (queue.size > 0) {
 			QueueEntry entry = queue.first();
 			if (currentTime >= entry.delay) {
@@ -59,16 +71,27 @@ public class AnimationState {
 
 	public void apply (Skeleton skeleton) {
 		if (current == null) return;
+
+		Array<Event> events = this.events;
+		events.clear();
+
 		if (previous != null) {
-			previous.apply(skeleton, previousTime, previousLoop);
+			previous.apply(skeleton, Float.MAX_VALUE, previousTime, previousLoop, null);
 			float alpha = mixTime / mixDuration;
 			if (alpha >= 1) {
 				alpha = 1;
 				previous = null;
 			}
-			current.mix(skeleton, currentTime, currentLoop, alpha);
+			current.mix(skeleton, currentLastTime, currentTime, currentLoop, events, alpha);
 		} else
-			current.apply(skeleton, currentTime, currentLoop);
+			current.apply(skeleton, currentLastTime, currentTime, currentLoop, events);
+
+		int listenerCount = listeners.size;
+		for (int i = 0, n = events.size; i < n; i++) {
+			Event event = events.get(i);
+			for (int ii = 0; ii < listenerCount; ii++)
+				listeners.get(ii).event(event);
+		}
 	}
 
 	public void clearAnimation () {
@@ -84,18 +107,26 @@ public class AnimationState {
 
 	private void setAnimationInternal (Animation animation, boolean loop) {
 		previous = null;
-		if (animation != null && current != null) {
-			mixDuration = data.getMix(current, animation);
-			if (mixDuration > 0) {
-				mixTime = 0;
-				previous = current;
-				previousTime = currentTime;
-				previousLoop = currentLoop;
+		if (current != null) {
+			for (int i = 0, n = listeners.size; i < n; i++)
+				listeners.get(i).end();
+
+			if (animation != null) {
+				mixDuration = data.getMix(current, animation);
+				if (mixDuration > 0) {
+					mixTime = 0;
+					previous = current;
+					previousTime = currentTime;
+					previousLoop = currentLoop;
+				}
 			}
 		}
 		current = animation;
 		currentLoop = loop;
 		currentTime = 0;
+
+		for (int i = 0, n = listeners.size; i < n; i++)
+			listeners.get(i).start();
 	}
 
 	/** @see #setAnimation(Animation, boolean) */
@@ -168,6 +199,15 @@ public class AnimationState {
 		return current == null || currentTime >= current.getDuration();
 	}
 
+	public void addListener (AnimationStateListener listener) {
+		if (listener == null) throw new IllegalArgumentException("listener cannot be null.");
+		listeners.add(listener);
+	}
+
+	public void removeListener (AnimationStateListener listener) {
+		listeners.removeValue(listener, true);
+	}
+
 	public AnimationStateData getData () {
 		return data;
 	}
@@ -181,4 +221,23 @@ public class AnimationState {
 		boolean loop;
 		float delay;
 	}
+
+	static public abstract class AnimationStateListener {
+		/** Invoked when the current animation triggers an event. */
+		public void event (Event event) {
+		}
+
+		/** Invoked when the current animation has completed.
+		 * @param loopCount The number of times the animation reached the end. */
+		public void complete (int loopCount) {
+		}
+
+		/** Invoked just after the current animation is set. */
+		public void start () {
+		}
+
+		/** Invoked just before the current animation is replaced. */
+		public void end () {
+		}
+	}
 }

+ 45 - 0
spine-libgdx/src/com/esotericsoftware/spine/Event.java

@@ -0,0 +1,45 @@
+
+package com.esotericsoftware.spine;
+
+public class Event {
+	final private EventData data;
+	private int intValue;
+	private float floatValue;
+	private String stringValue;
+
+	public Event (EventData data) {
+		this.data = data;
+	}
+
+	public int getInt () {
+		return intValue;
+	}
+
+	public void setInt (int intValue) {
+		this.intValue = intValue;
+	}
+
+	public float getFloat () {
+		return floatValue;
+	}
+
+	public void setFloat (float floatValue) {
+		this.floatValue = floatValue;
+	}
+
+	public String getString () {
+		return stringValue;
+	}
+
+	public void setString (String stringValue) {
+		this.stringValue = stringValue;
+	}
+
+	public EventData getData () {
+		return data;
+	}
+
+	public String toString () {
+		return data.name;
+	}
+}

+ 46 - 0
spine-libgdx/src/com/esotericsoftware/spine/EventData.java

@@ -0,0 +1,46 @@
+
+package com.esotericsoftware.spine;
+
+public class EventData {
+	final String name;
+	private int intValue;
+	private float floatValue;
+	private String stringValue;
+
+	public EventData (String name) {
+		if (name == null) throw new IllegalArgumentException("name cannot be null.");
+		this.name = name;
+	}
+
+	public int getInt () {
+		return intValue;
+	}
+
+	public void setInt (int intValue) {
+		this.intValue = intValue;
+	}
+
+	public float getFloat () {
+		return floatValue;
+	}
+
+	public void setFloat (float floatValue) {
+		this.floatValue = floatValue;
+	}
+
+	public String getString () {
+		return stringValue;
+	}
+
+	public void setString (String stringValue) {
+		this.stringValue = stringValue;
+	}
+
+	public String getName () {
+		return name;
+	}
+
+	public String toString () {
+		return name;
+	}
+}

+ 1 - 0
spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java

@@ -55,6 +55,7 @@ public class SkeletonBinary {
 	static public final int TIMELINE_TRANSLATE = 2;
 	static public final int TIMELINE_ATTACHMENT = 3;
 	static public final int TIMELINE_COLOR = 4;
+	static public final int TIMELINE_EVENT = 5;
 
 	static public final int CURVE_LINEAR = 0;
 	static public final int CURVE_STEPPED = 1;

+ 20 - 0
spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java

@@ -33,6 +33,7 @@ public class SkeletonData {
 	final Array<SlotData> slots = new Array(); // Setup pose draw order.
 	final Array<Skin> skins = new Array();
 	Skin defaultSkin;
+	final Array<EventData> eventDatas = new Array();
 	final Array<Animation> animations = new Array();
 
 	public void clear () {
@@ -135,6 +136,25 @@ public class SkeletonData {
 		return skins;
 	}
 
+	// --- Events.
+
+	public void addEvent (EventData eventData) {
+		if (eventData == null) throw new IllegalArgumentException("eventData cannot be null.");
+		eventDatas.add(eventData);
+	}
+
+	/** @return May be null. */
+	public EventData findEvent (String eventDataName) {
+		if (eventDataName == null) throw new IllegalArgumentException("eventDataName cannot be null.");
+		for (EventData eventData : eventDatas)
+			if (eventData.name.equals(eventDataName)) return eventData;
+		return null;
+	}
+
+	public Array<EventData> getEvents () {
+		return eventDatas;
+	}
+
 	// --- Animations.
 
 	public void addAnimation (Animation animation) {

+ 32 - 5
spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java

@@ -28,6 +28,7 @@ package com.esotericsoftware.spine;
 import com.esotericsoftware.spine.Animation.AttachmentTimeline;
 import com.esotericsoftware.spine.Animation.ColorTimeline;
 import com.esotericsoftware.spine.Animation.CurveTimeline;
+import com.esotericsoftware.spine.Animation.EventTimeline;
 import com.esotericsoftware.spine.Animation.RotateTimeline;
 import com.esotericsoftware.spine.Animation.ScaleTimeline;
 import com.esotericsoftware.spine.Animation.Timeline;
@@ -135,6 +136,15 @@ public class SkeletonJson {
 			if (skin.name.equals("default")) skeletonData.setDefaultSkin(skin);
 		}
 
+		// Events.
+		for (JsonValue eventMap = root.getChild("events"); eventMap != null; eventMap = eventMap.next()) {
+			EventData eventData = new EventData(eventMap.name());
+			eventData.setInt(eventMap.getInt("int", 0));
+			eventData.setFloat(eventMap.getFloat("float", 0f));
+			eventData.setString(eventMap.getString("string", null));
+			skeletonData.addEvent(eventData);
+		}
+
 		// Animations.
 		for (JsonValue animationMap = root.getChild("animations"); animationMap != null; animationMap = animationMap.next())
 			readAnimation(animationMap.name(), animationMap, skeletonData);
@@ -188,7 +198,7 @@ public class SkeletonJson {
 			for (JsonValue timelineMap = boneMap.child(); timelineMap != null; timelineMap = timelineMap.next()) {
 				String timelineName = timelineMap.name();
 				if (timelineName.equals(TIMELINE_ROTATE)) {
-					RotateTimeline timeline = new RotateTimeline(timelineMap.size());
+					RotateTimeline timeline = new RotateTimeline(timelineMap.size);
 					timeline.setBoneIndex(boneIndex);
 
 					int frameIndex = 0;
@@ -205,9 +215,9 @@ public class SkeletonJson {
 					TranslateTimeline timeline;
 					float timelineScale = 1;
 					if (timelineName.equals(TIMELINE_SCALE))
-						timeline = new ScaleTimeline(timelineMap.size());
+						timeline = new ScaleTimeline(timelineMap.size);
 					else {
-						timeline = new TranslateTimeline(timelineMap.size());
+						timeline = new TranslateTimeline(timelineMap.size);
 						timelineScale = scale;
 					}
 					timeline.setBoneIndex(boneIndex);
@@ -234,7 +244,7 @@ public class SkeletonJson {
 			for (JsonValue timelineMap = slotMap.child(); timelineMap != null; timelineMap = timelineMap.next()) {
 				String timelineName = timelineMap.name();
 				if (timelineName.equals(TIMELINE_COLOR)) {
-					ColorTimeline timeline = new ColorTimeline(timelineMap.size());
+					ColorTimeline timeline = new ColorTimeline(timelineMap.size);
 					timeline.setSlotIndex(slotIndex);
 
 					int frameIndex = 0;
@@ -249,7 +259,7 @@ public class SkeletonJson {
 					duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() * 5 - 5]);
 
 				} else if (timelineName.equals(TIMELINE_ATTACHMENT)) {
-					AttachmentTimeline timeline = new AttachmentTimeline(timelineMap.size());
+					AttachmentTimeline timeline = new AttachmentTimeline(timelineMap.size);
 					timeline.setSlotIndex(slotIndex);
 
 					int frameIndex = 0;
@@ -265,6 +275,23 @@ public class SkeletonJson {
 			}
 		}
 
+		JsonValue eventsMap = map.get("events");
+		if (eventsMap != null) {
+			EventTimeline timeline = new EventTimeline(eventsMap.size);
+			int frameIndex = 0;
+			for (JsonValue eventMap = eventsMap.child; eventMap != null; eventMap = eventMap.next()) {
+				EventData eventData = skeletonData.findEvent(eventMap.getString("name"));
+				if (eventData == null) throw new SerializationException("Event not found: " + eventMap.getString("name"));
+				Event event = new Event(eventData);
+				event.setInt(eventMap.getInt("int", eventData.getInt()));
+				event.setFloat(eventMap.getFloat("float", eventData.getFloat()));
+				event.setString(eventMap.getString("string", eventData.getString()));
+				timeline.setFrame(frameIndex++, eventMap.getFloat("time"), event);
+			}
+			timelines.add(timeline);
+			duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]);
+		}
+
 		timelines.shrink();
 		skeletonData.addAnimation(new Animation(name, timelines, duration));
 	}

+ 22 - 3
spine-libgdx/test/com/esotericsoftware/spine/AnimationStateTest.java

@@ -25,6 +25,8 @@
 
 package com.esotericsoftware.spine;
 
+import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
+
 import com.badlogic.gdx.ApplicationAdapter;
 import com.badlogic.gdx.Gdx;
 import com.badlogic.gdx.InputAdapter;
@@ -58,6 +60,23 @@ public class AnimationStateTest extends ApplicationAdapter {
 		stateData.setMix("jump", "jump", 0.2f);
 
 		state = new AnimationState(stateData);
+		state.addListener(new AnimationStateListener() {
+			public void event (Event event) {
+				System.out.println("Event: " + event.getData().getName());
+			}
+
+			public void complete (int loopCount) {
+				System.out.println("Complete: " + state.getAnimation() + ", " + loopCount);
+			}
+
+			public void start () {
+				System.out.println("Start: " + state.getAnimation());
+			}
+
+			public void end () {
+				System.out.println("End: " + state.getAnimation());
+			}
+		});
 		state.setAnimation("walk", true);
 
 		skeleton = new Skeleton(skeletonData);
@@ -87,9 +106,9 @@ public class AnimationStateTest extends ApplicationAdapter {
 		state.apply(skeleton);
 		if (state.getAnimation().getName().equals("walk")) {
 			// After one second, change the current animation. Mixing is done by AnimationState for you.
-			if (state.getTime() > 2) state.setAnimation("jump", false);
-		} else {
-			if (state.getTime() > 1) state.setAnimation("walk", true);
+// if (state.getTime() > 2) state.setAnimation("jump", false);
+// } else {
+// if (state.getTime() > 1) state.setAnimation("walk", true);
 		}
 		skeleton.updateWorldTransform();
 

+ 8 - 1
spine-libgdx/test/com/esotericsoftware/spine/SkeletonTest.java

@@ -39,6 +39,7 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch;
 import com.badlogic.gdx.graphics.g2d.TextureAtlas;
 import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
 import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
+import com.badlogic.gdx.utils.Array;
 
 public class SkeletonTest extends ApplicationAdapter {
 	SpriteBatch batch;
@@ -49,6 +50,7 @@ public class SkeletonTest extends ApplicationAdapter {
 	SkeletonData skeletonData;
 	Skeleton skeleton;
 	Animation animation;
+	Array<Event> events = new Array();
 
 	public void create () {
 		batch = new SpriteBatch();
@@ -81,6 +83,7 @@ public class SkeletonTest extends ApplicationAdapter {
 			// binary.setScale(2);
 			skeletonData = binary.readSkeletonData(Gdx.files.internal(name + ".skel"));
 		}
+		System.out.println(skeletonData.getEvents().size);
 		animation = skeletonData.findAnimation("walk");
 
 		skeleton = new Skeleton(skeletonData);
@@ -106,6 +109,7 @@ public class SkeletonTest extends ApplicationAdapter {
 	}
 
 	public void render () {
+		float lastTime = time;
 		time += Gdx.graphics.getDeltaTime();
 
 		float x = skeleton.getX() + 160 * Gdx.graphics.getDeltaTime() * (skeleton.getFlipX() ? -1 : 1);
@@ -115,7 +119,10 @@ public class SkeletonTest extends ApplicationAdapter {
 
 		Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
 
-		animation.apply(skeleton, time, true);
+		events.clear();
+		animation.apply(skeleton, lastTime, time, true, events);
+		if (events.size > 0) System.out.println(events);
+
 		skeleton.updateWorldTransform();
 		skeleton.update(Gdx.graphics.getDeltaTime());
 

+ 10 - 1
spine-libgdx/test/goblins.json

@@ -198,6 +198,10 @@
 		}
 	}
 },
+"events": {
+	"test1": { "int": 1, "float": 2, "string": "three" },
+	"test2": { "int": 123, "float": 456, "string": "789" }
+},
 "animations": {
 	"walk": {
 		"bones": {
@@ -493,7 +497,12 @@
 					{ "time": 0.8, "name": null }
 				]
 			}
-		}
+		},
+		"events": [
+			{ "time": 0.4, "name": "test1", "int": 0, "float": 0, "string": "" },
+			{ "time": 0.4, "name": "test2", "int": 0, "float": 0, "string": "" },
+			{ "time": 0.8, "name": "test1", "int": 12, "float": 0, "string": "" }			
+		]
 	}
 }
 }

+ 13 - 1
spine-libgdx/test/spineboy.json

@@ -94,8 +94,15 @@
 		}
 	}
 },
+"events": {
+	"test1": { "int": 1, "float": 2, "string": "three" },
+	"test2": { "int": 123, "float": 456, "string": "789" }
+},
 "animations": {
 	"walk": {
+		"events": [
+			{ "time": 0, "name": "test1", int: 123, float: 12.3, string: "meow" },
+		],
 		"bones": {
 			"left upper leg": {
 				"rotate": [
@@ -781,7 +788,12 @@
 					{ "time": 1.3666, "x": 1, "y": 1 }
 				]
 			}
-		}
+		},
+		"events": [
+			{ "time": 0.4, "name": "test1", "int": 0, "float": 0, "string": "" },
+			{ "time": 0.4, "name": "test2", "int": 0, "float": 0, "string": "" },
+			{ "time": 0.8, "name": "test1", "int": 12, "float": 0, "string": "" },
+		]
 	}
 }
 }