Event timeline.

This commit is contained in:
NathanSweet 2013-08-08 22:57:57 +02:00
parent a2041fa9bb
commit 354d3b75d6
12 changed files with 339 additions and 37 deletions

View File

@ -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

View File

@ -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);
}
/** @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<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]);
}
}
}

View File

@ -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 () {
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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) {

View File

@ -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));
}

View File

@ -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();

View File

@ -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());

View File

@ -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": "" }
]
}
}
}

View File

@ -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": "" },
]
}
}
}