diff --git a/spine-libgdx/.settings/org.eclipse.jdt.core.prefs b/spine-libgdx/.settings/org.eclipse.jdt.core.prefs index 7f8a35b9c..e6944cba0 100644 --- a/spine-libgdx/.settings/org.eclipse.jdt.core.prefs +++ b/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 diff --git a/spine-libgdx/src/com/esotericsoftware/spine/Animation.java b/spine-libgdx/src/com/esotericsoftware/spine/Animation.java index e5f65a689..a352de77e 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/Animation.java +++ b/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 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 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); + } + + /** @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. */ - public void mix (Skeleton skeleton, float time, boolean loop, float alpha) { + * @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 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 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 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 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 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 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 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 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 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]); + } + } } diff --git a/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java b/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java index d363bc8f6..cda2fcd3e 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/AnimationState.java +++ b/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 queue = new Array(); + private final Array queue = new Array(); + private final Array events = new Array(); + private final Array 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 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 () { + } + } } diff --git a/spine-libgdx/src/com/esotericsoftware/spine/Event.java b/spine-libgdx/src/com/esotericsoftware/spine/Event.java new file mode 100644 index 000000000..6318e9375 --- /dev/null +++ b/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; + } +} diff --git a/spine-libgdx/src/com/esotericsoftware/spine/EventData.java b/spine-libgdx/src/com/esotericsoftware/spine/EventData.java new file mode 100644 index 000000000..386f5eb45 --- /dev/null +++ b/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; + } +} diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java index 971e9b416..cd8ebdae4 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java +++ b/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; diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java index b8f4b5f96..a7803a832 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java +++ b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonData.java @@ -33,6 +33,7 @@ public class SkeletonData { final Array slots = new Array(); // Setup pose draw order. final Array skins = new Array(); Skin defaultSkin; + final Array eventDatas = new Array(); final Array 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 getEvents () { + return eventDatas; + } + // --- Animations. public void addAnimation (Animation animation) { diff --git a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java b/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java index dd8358b5f..11c80d260 100644 --- a/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java +++ b/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)); } diff --git a/spine-libgdx/test/com/esotericsoftware/spine/AnimationStateTest.java b/spine-libgdx/test/com/esotericsoftware/spine/AnimationStateTest.java index c13f34752..e2f9ecf4e 100644 --- a/spine-libgdx/test/com/esotericsoftware/spine/AnimationStateTest.java +++ b/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(); diff --git a/spine-libgdx/test/com/esotericsoftware/spine/SkeletonTest.java b/spine-libgdx/test/com/esotericsoftware/spine/SkeletonTest.java index 6de073b34..e33fd1586 100644 --- a/spine-libgdx/test/com/esotericsoftware/spine/SkeletonTest.java +++ b/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 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()); diff --git a/spine-libgdx/test/goblins.json b/spine-libgdx/test/goblins.json index 25f174191..35838d58c 100644 --- a/spine-libgdx/test/goblins.json +++ b/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": "" } + ] } } } \ No newline at end of file diff --git a/spine-libgdx/test/spineboy.json b/spine-libgdx/test/spineboy.json index 57b0f6cbe..a301b4271 100644 --- a/spine-libgdx/test/spineboy.json +++ b/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": "" }, + ] } } } \ No newline at end of file