com.bladecoder.engine.spine.SpineRenderer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of blade-engine-spine-plugin Show documentation
Show all versions of blade-engine-spine-plugin Show documentation
Classic point and click adventure game engine - Spine plugin
/*******************************************************************************
* Copyright 2014 Rafael Garcia Moreno.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.bladecoder.engine.spine;
import java.util.HashMap;
import java.util.Map.Entry;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonValue;
import com.bladecoder.engine.actions.ActionCallback;
import com.bladecoder.engine.anim.AnimationDesc;
import com.bladecoder.engine.anim.SpineAnimationDesc;
import com.bladecoder.engine.anim.Tween;
import com.bladecoder.engine.assets.EngineAssetManager;
import com.bladecoder.engine.model.AnimationRenderer;
import com.bladecoder.engine.model.InteractiveActor;
import com.bladecoder.engine.model.SpriteActor;
import com.bladecoder.engine.model.World;
import com.bladecoder.engine.serialization.ActionCallbackSerializer;
import com.bladecoder.engine.serialization.BladeJson;
import com.bladecoder.engine.serialization.BladeJson.Mode;
import com.bladecoder.engine.spine.SkeletonDataLoader.SkeletonDataLoaderParameter;
import com.bladecoder.engine.util.EngineLogger;
import com.bladecoder.engine.util.RectangleRenderer;
import com.esotericsoftware.spine.Animation;
import com.esotericsoftware.spine.AnimationState;
import com.esotericsoftware.spine.AnimationState.AnimationStateAdapter;
import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
import com.esotericsoftware.spine.AnimationState.TrackEntry;
import com.esotericsoftware.spine.AnimationStateData;
import com.esotericsoftware.spine.Event;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.SkeletonBounds;
import com.esotericsoftware.spine.SkeletonData;
import com.esotericsoftware.spine.SkeletonRenderer;
import com.esotericsoftware.spine.Skin;
public class SpineRenderer extends AnimationRenderer {
private final static int PLAY_ANIMATION_EVENT = 0;
private final static int PLAY_SOUND_EVENT = 1;
private final static int RUN_VERB_EVENT = 2;
private final static int LOOP_EVENT = 3;
private ActionCallback animationCb = null;
private int currentCount;
private Tween.Type currentAnimationType;
private SkeletonRenderer renderer;
private SkeletonBounds bounds;
private float width = super.getWidth(), height = super.getHeight();
private float lastAnimationTime = 0;
private boolean complete = false;
private boolean eventsEnabled = true;
private int loopCount = 0;
private String secondaryAnimation;
private String skin;
private World world;
class SkeletonCacheEntry extends CacheEntry {
Skeleton skeleton;
AnimationState animation;
String atlas;
}
public SpineRenderer() {
}
public void enableEvents(boolean v) {
eventsEnabled = v;
}
private AnimationStateListener animationListener = new AnimationStateAdapter() {
@Override
public void complete(TrackEntry entry) {
if (complete || (entry != null && entry.getTrackIndex() != 0))
return;
loopCount++;
if ((currentAnimationType == Tween.Type.REPEAT || currentAnimationType == Tween.Type.REVERSE_REPEAT)
&& (currentCount == Tween.INFINITY || currentCount >= loopCount)) {
return;
}
complete = true;
computeBbox();
if (animationCb != null) {
ActionCallback tmpcb = animationCb;
animationCb = null;
tmpcb.resume();
}
}
@Override
public void event(TrackEntry entry, Event event) {
if (!eventsEnabled || currentAnimationType == Tween.Type.REVERSE)
return;
String actorId = event.getData().getName();
EngineLogger.debug("Spine event " + event.getInt() + ":" + actorId + "." + event.getString());
InteractiveActor actor = (InteractiveActor) world.getCurrentScene().getActor(actorId, true);
switch (event.getInt()) {
case PLAY_ANIMATION_EVENT:
if (actor == null) {
EngineLogger.debug("Actor in Spine animation event not found in scene: " + actorId);
return;
}
((SpriteActor) actor).startAnimation(event.getString(), null);
break;
case PLAY_SOUND_EVENT:
// Backwards compatibility
String sid = event.getString();
if (world.getSounds().get(sid) == null && actor != null)
sid = actor.getId() + "_" + sid;
world.getCurrentScene().getSoundManager().playSound(sid);
break;
case RUN_VERB_EVENT:
if (actor != null)
actor.runVerb(event.getString());
else
world.getCurrentScene().runVerb(event.getString());
break;
case LOOP_EVENT:
// used for looping from a starting frame
break;
default:
EngineLogger.error("Spine event not recognized.");
}
}
};
@Override
public String[] getInternalAnimations(AnimationDesc anim) {
try {
retrieveSource(anim.source, ((SpineAnimationDesc) anim).atlas);
} catch (GdxRuntimeException e) {
sourceCache.remove(anim.source);
Array dependencies = EngineAssetManager.getInstance().getDependencies(getFileName(anim.source));
if (dependencies.size > 0)
dependencies.removeIndex(dependencies.size - 1);
return new String[0];
}
Array animations = ((SkeletonCacheEntry) sourceCache.get(anim.source)).skeleton.getData()
.getAnimations();
String[] result = new String[animations.size];
for (int i = 0; i < animations.size; i++) {
Animation a = animations.get(i);
result[i] = a.getName();
}
return result;
}
@Override
public void update(float delta) {
if (complete) {
// keep updating secondary animation
// WARNING: It doesn't work with REVERSE ANIMATION
if (secondaryAnimation != null && currentSource != null
&& (!((SkeletonCacheEntry) currentSource).animation.getTracks().get(1).isComplete()
|| ((SkeletonCacheEntry) currentSource).animation.getTracks().get(1).getLoop()))
updateAnimation(delta);
return;
}
if (currentSource != null && ((SkeletonCacheEntry) currentSource).skeleton != null) {
float d = delta;
if (currentAnimationType == Tween.Type.REVERSE) {
d = -delta;
if (lastAnimationTime < 0) {
lastAnimationTime = 0;
loopCount = 0;
animationListener.complete(null);
return;
}
}
lastAnimationTime += d;
if (lastAnimationTime >= 0)
updateAnimation(d);
}
}
private void updateAnimation(float time) {
SkeletonCacheEntry cs = (SkeletonCacheEntry) currentSource;
cs.animation.update(time);
cs.animation.apply(cs.skeleton);
cs.skeleton.updateWorldTransform();
}
private static final Matrix4 tmp = new Matrix4();
@Override
public void draw(SpriteBatch batch, float x, float y, float scaleX, float scaleY, float rotation, Color tint) {
SkeletonCacheEntry cs = (SkeletonCacheEntry) currentSource;
if (cs != null && cs.skeleton != null) {
Matrix4 tm = batch.getTransformMatrix();
tmp.set(tm);
float originX = cs.skeleton.getRootBone().getX();
float originY = cs.skeleton.getRootBone().getY();
tm.translate(x, y, 0).rotate(0, 0, 1, rotation).scale(scaleX, scaleY, 1).translate(originX, originY, 0);
// cs.skeleton.setX(x / scale);
// cs.skeleton.setY(y / scale);
batch.setTransformMatrix(tm);
if (tint != null)
cs.skeleton.setColor(tint);
renderer.draw(batch, cs.skeleton);
if (tint != null)
batch.setColor(Color.WHITE);
batch.setTransformMatrix(tmp);
} else {
float dx = getAlignDx(getWidth(), orgAlign);
float dy = getAlignDy(getHeight(), orgAlign);
RectangleRenderer.draw(batch, x + dx * scaleX, y + dy * scaleY, getWidth() * scaleX, getHeight() * scaleY,
Color.RED);
}
}
@Override
public float getWidth() {
return width;
}
@Override
public float getHeight() {
return height;
}
public String getSkin() {
return skin;
}
public void setSkin(String skin) {
// set the skin if the current source is loaded
if (currentSource != null && currentSource.refCounter > 0) {
SkeletonCacheEntry sce = (SkeletonCacheEntry) currentSource;
if (skin != null) {
sce.skeleton.setSkin(skin);
} else {
sce.skeleton.setSkin((Skin) null);
}
}
this.skin = skin;
}
@Override
public void startAnimation(String id, Tween.Type repeatType, int count, ActionCallback cb, String direction) {
StringBuilder sb = new StringBuilder(id);
// if dir==null gets the current animation direction
if (direction == null) {
int idx = getCurrentAnimationId().indexOf('.');
if (idx != -1) {
String dir = getCurrentAnimationId().substring(idx);
sb.append(dir);
}
} else {
sb.append('.');
sb.append(direction);
}
String anim = sb.toString();
if (getAnimation(anim) == null) {
anim = id;
}
startAnimation(anim, repeatType, count, null);
}
@Override
public void startAnimation(String id, Tween.Type repeatType, int count, ActionCallback cb, Vector2 p0, Vector2 pf) {
startAnimation(id, repeatType, count, cb, getDirectionString(p0, pf, getDirs(id, fanims)));
}
@Override
public void startAnimation(String id, Tween.Type repeatType, int count, ActionCallback cb) {
SpineAnimationDesc fa = (SpineAnimationDesc) getAnimation(id);
if (fa == null) {
EngineLogger.error("AnimationDesc not found: " + id);
return;
}
if (currentAnimation != null && currentAnimation.disposeWhenPlayed)
disposeSource(currentAnimation.source);
currentAnimation = fa;
currentSource = sourceCache.get(fa.source);
animationCb = cb;
// If the source is not loaded. Load it.
if (currentSource == null || currentSource.refCounter < 1) {
loadSource(fa.source, fa.atlas);
EngineAssetManager.getInstance().finishLoading();
retrieveSource(fa.source, fa.atlas);
currentSource = sourceCache.get(fa.source);
if (currentSource == null) {
EngineLogger.error("Could not load AnimationDesc: " + id);
currentAnimation = null;
return;
}
}
if (repeatType == Tween.Type.SPRITE_DEFINED) {
currentAnimationType = currentAnimation.animationType;
currentCount = currentAnimation.count;
} else {
currentCount = count;
currentAnimationType = repeatType;
}
if (currentAnimationType == Tween.Type.REVERSE) {
// get animation duration
Array animations = ((SkeletonCacheEntry) currentSource).skeleton.getData().getAnimations();
for (Animation a : animations) {
if (a.getName().equals(currentAnimation.id)) {
lastAnimationTime = a.getDuration() / currentAnimation.duration - 0.01f;
break;
}
}
} else {
lastAnimationTime = 0f;
}
complete = false;
loopCount = 0;
setCurrentAnimation();
}
public void setSecondaryAnimation(String animation) {
secondaryAnimation = animation;
SkeletonCacheEntry cs = (SkeletonCacheEntry) currentSource;
try {
if (animation == null) {
cs.animation.setEmptyAnimation(1, 0);
// cs.animation.clearTrack(1);
} else {
SpineAnimationDesc fa = (SpineAnimationDesc) fanims.get(animation);
if (fa == null) {
EngineLogger.error("SpineRenderer:setCurrentFA Animation not found: " + animation);
return;
}
cs.animation.setAnimation(1, secondaryAnimation, fa.animationType == Tween.Type.REPEAT);
}
updateAnimation(0);
} catch (Exception e) {
EngineLogger.error("SpineRenderer:setCurrentFA " + e.getMessage());
}
}
private void setCurrentAnimation() {
try {
SkeletonCacheEntry cs = (SkeletonCacheEntry) currentSource;
cs.skeleton.setToSetupPose();
cs.skeleton.setFlipX(flipX);
cs.animation.setTimeScale(currentAnimation.duration);
cs.animation.clearTracks();
cs.animation.setAnimation(0, currentAnimation.id, currentAnimationType == Tween.Type.REPEAT);
if (secondaryAnimation != null)
setSecondaryAnimation(secondaryAnimation);
updateAnimation(lastAnimationTime);
computeBbox();
} catch (Exception e) {
EngineLogger.error("SpineRenderer:setCurrentFA " + e.getMessage());
}
}
@Override
public void computeBbox() {
float minX, minY, maxX, maxY;
if (bbox != null && (bbox.getVertices() == null || bbox.getVertices().length != 8)) {
bbox.setVertices(new float[8]);
}
SkeletonCacheEntry cs = (SkeletonCacheEntry) currentSource;
if (cs == null || cs.skeleton == null) {
if (bbox != null) {
float[] verts = bbox.getVertices();
verts[0] = -getWidth() / 2;
verts[1] = 0f;
verts[2] = -getWidth() / 2;
verts[3] = getHeight();
verts[4] = getWidth() / 2;
verts[5] = getHeight();
verts[6] = getWidth() / 2;
verts[7] = 0f;
bbox.dirty();
}
return;
}
cs.skeleton.setPosition(0, 0);
cs.skeleton.updateWorldTransform();
bounds.update(cs.skeleton, true);
if (bounds.getWidth() > 0 && bounds.getHeight() > 0) {
width = bounds.getWidth();
height = bounds.getHeight();
minX = bounds.getMinX();
minY = bounds.getMinY();
maxX = bounds.getMaxX();
maxY = bounds.getMaxY();
} else {
Vector2 offset = new Vector2();
Vector2 size = new Vector2();
cs.skeleton.getBounds(offset, size, new FloatArray());
width = size.x;
height = size.y;
minX = offset.x;
minY = offset.y;
maxX = offset.x + width;
maxY = offset.y + height;
}
if (bbox != null) {
float[] verts = bbox.getVertices();
verts[0] = minX;
verts[1] = minY;
verts[2] = minX;
verts[3] = maxY;
verts[4] = maxX;
verts[5] = maxY;
verts[6] = maxX;
verts[7] = minY;
bbox.dirty();
}
}
private String getFileName(String source) {
return EngineAssetManager.SPINE_DIR + source + EngineAssetManager.SPINE_EXT;
}
private void loadSource(String source, String atlas) {
EngineLogger.debug("Loading: " + source);
SkeletonCacheEntry entry = (SkeletonCacheEntry) sourceCache.get(source);
if (entry == null) {
entry = new SkeletonCacheEntry();
entry.atlas = atlas == null ? source : atlas;
sourceCache.put(source, entry);
}
if (entry.refCounter == 0) {
if (EngineAssetManager.getInstance().getLoader(SkeletonData.class) == null) {
EngineAssetManager.getInstance().setLoader(SkeletonData.class,
new SkeletonDataLoader(EngineAssetManager.getInstance().getFileHandleResolver()));
}
SkeletonDataLoaderParameter parameter = new SkeletonDataLoaderParameter(
EngineAssetManager.ATLASES_DIR + entry.atlas + EngineAssetManager.ATLAS_EXT,
EngineAssetManager.getInstance().getScale());
EngineAssetManager.getInstance().load(getFileName(source), SkeletonData.class, parameter);
}
entry.refCounter++;
}
private void retrieveSource(String source, String atlas) {
EngineLogger.debug("Retrieving: " + source);
SkeletonCacheEntry entry = (SkeletonCacheEntry) sourceCache.get(source);
if (entry == null || entry.refCounter < 1) {
loadSource(source, atlas);
EngineAssetManager.getInstance().finishLoading();
entry = (SkeletonCacheEntry) sourceCache.get(source);
}
if (entry.skeleton == null) {
SkeletonData skeletonData = EngineAssetManager.getInstance().get(getFileName(source), SkeletonData.class);
entry.skeleton = new Skeleton(skeletonData);
AnimationStateData stateData = new AnimationStateData(skeletonData); // Defines
// mixing
// between
// animations.
stateData.setDefaultMix(0f);
entry.animation = new AnimationState(stateData);
entry.animation.addListener(animationListener);
}
}
private void disposeSource(String source) {
EngineLogger.debug("Disposing: " + source);
SkeletonCacheEntry entry = (SkeletonCacheEntry) sourceCache.get(source);
if (entry.refCounter == 1) {
EngineAssetManager.getInstance()
.unload(EngineAssetManager.SPINE_DIR + source + EngineAssetManager.SPINE_EXT);
entry.animation = null;
entry.skeleton = null;
}
entry.refCounter--;
}
@Override
public void loadAssets() {
for (AnimationDesc fa : fanims.values()) {
if (fa.preload)
loadSource(fa.source, ((SpineAnimationDesc) fa).atlas);
}
if (currentAnimation != null && !currentAnimation.preload) {
loadSource(currentAnimation.source, ((SpineAnimationDesc) currentAnimation).atlas);
} else if (currentAnimation == null && initAnimation != null) {
AnimationDesc fa = fanims.get(initAnimation);
if (fa != null && !fa.preload)
loadSource(fa.source, ((SpineAnimationDesc) fa).atlas);
}
}
@Override
public void retrieveAssets() {
renderer = new SkeletonRenderer();
renderer.setPremultipliedAlpha(false);
bounds = new SkeletonBounds();
for (String key : sourceCache.keySet()) {
if (sourceCache.get(key).refCounter > 0)
retrieveSource(key, ((SkeletonCacheEntry) sourceCache.get(key)).atlas);
}
if (currentAnimation != null) {
SkeletonCacheEntry entry = (SkeletonCacheEntry) sourceCache.get(currentAnimation.source);
currentSource = entry;
// Stop events to avoid event trigger
boolean prevEnableEvents = eventsEnabled;
eventsEnabled = false;
setCurrentAnimation();
eventsEnabled = prevEnableEvents;
} else if (initAnimation != null) {
startAnimation(initAnimation, Tween.Type.SPRITE_DEFINED, 1, null);
}
setSkin(skin);
computeBbox();
}
@Override
public void dispose() {
for (Entry entry : sourceCache.entrySet()) {
if (entry.getValue().refCounter > 0) {
String filename = EngineAssetManager.SPINE_DIR + entry.getKey() + EngineAssetManager.SPINE_EXT;
if (EngineAssetManager.getInstance().isLoaded(filename))
EngineAssetManager.getInstance().unload(filename);
}
}
sourceCache.clear();
currentSource = null;
renderer = null;
bounds = null;
}
@Override
public void write(Json json) {
super.write(json);
BladeJson bjson = (BladeJson) json;
if (bjson.getMode() == Mode.MODEL) {
} else {
if (animationCb != null)
json.writeValue("cb", ActionCallbackSerializer.find(bjson.getWorld(), animationCb));
json.writeValue("currentCount", currentCount);
if (currentAnimation != null)
json.writeValue("currentAnimationType", currentAnimationType);
json.writeValue("lastAnimationTime", lastAnimationTime);
json.writeValue("complete", complete);
json.writeValue("loopCount", loopCount);
json.writeValue("secondaryAnimation", secondaryAnimation);
}
json.writeValue("skin", skin);
}
@SuppressWarnings("unchecked")
@Override
public void read(Json json, JsonValue jsonData) {
super.read(json, jsonData);
BladeJson bjson = (BladeJson) json;
if (bjson.getMode() == Mode.MODEL) {
fanims = json.readValue("fanims", HashMap.class, SpineAnimationDesc.class, jsonData);
world = bjson.getWorld();
} else {
animationCb = ActionCallbackSerializer.find(((BladeJson) json).getWorld(),
json.readValue("cb", String.class, jsonData));
currentCount = json.readValue("currentCount", Integer.class, jsonData);
if (currentAnimation != null)
currentAnimationType = json.readValue("currentAnimationType", Tween.Type.class, jsonData);
lastAnimationTime = json.readValue("lastAnimationTime", Float.class, jsonData);
complete = json.readValue("complete", Boolean.class, jsonData);
loopCount = json.readValue("loopCount", int.class, loopCount, jsonData);
secondaryAnimation = json.readValue("secondaryAnimation", String.class, (String) null, jsonData);
}
skin = json.readValue("skin", String.class, skin, jsonData);
}
}