/******************************************************************************* * 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.Polygon; import com.badlogic.gdx.math.Rectangle; 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<String> dependencies = EngineAssetManager.getInstance().getDependencies(getFileName(anim.source)); if (dependencies.size > 0) dependencies.removeIndex(dependencies.size - 1); return new String[0]; } Array<Animation> 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; EngineLogger.debug("Setting Spine skin: " + skin); if (skin != null) { SkeletonData skeletonData = sce.skeleton.getData(); if (skin.indexOf(',') == -1 || skeletonData.findSkin(skin) != null) { sce.skeleton.setSkin(skin); } else { // we can combine several skins separated by ',' String[] skins = skin.split(","); Skin combinedSkin = new Skin(skin); for (String sk : skins) { // Get the source skins. Skin singleSkin = skeletonData.findSkin(sk.trim()); combinedSkin.addSkin(singleSkin); } // Set and apply the Skin to the skeleton. sce.skeleton.setSkin(combinedSkin); } } else { sce.skeleton.setSkin((Skin) null); } // sce.skeleton.setSlotsToSetupPose(); } 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<Animation> 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()); } } public Skeleton getCurrentSkeleton() { SkeletonCacheEntry cs = (SkeletonCacheEntry) currentSource; return cs.skeleton; } public AnimationState getCurrentAnimationState() { SkeletonCacheEntry cs = (SkeletonCacheEntry) currentSource; return cs.animation; } private void setCurrentAnimation() { try { SkeletonCacheEntry cs = (SkeletonCacheEntry) currentSource; if (skin != null && (cs.skeleton.getSkin() == null || !skin.equals(cs.skeleton.getSkin().getName()))) { setSkin(skin); } cs.skeleton.setToSetupPose(); cs.skeleton.setScaleX(flipX ? -1 : 1); 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 = new Polygon(new float[8]); 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) { 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) { // if there is only one bbox, get the polygon, else get the rectangle bbox // (union of all bboxes). if (bounds.getPolygons().size == 1) { FloatArray p = bounds.getPolygons().get(0); bbox.setVertices(p.toArray()); bbox.dirty(); Rectangle boundingRectangle = bbox.getBoundingRectangle(); width = boundingRectangle.getWidth(); height = boundingRectangle.getHeight(); return; } else { 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; } 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); } } @Override public void dispose() { for (Entry<String, CacheEntry> 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(), bjson.getScene(), 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(), ((BladeJson) json).getScene(), 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); } }