• Runtimes
  • [Libgdx] Spine Animation stuck on first frame

Related Discussions
...

Hello Everyone,

I'm having a weird problem here, I followed the documentation and I'm still having problem with my character's animation, it stuck on the first frame of it's animation, even thought my app's lifecycle is working perfectly (I hope :giggle🙂.

  • Using in Gradle, spine-libgdx:3.5.51.1
  • Character uses skins and meshes - all are visible and working both in RunTime & Editor.
  • Animations work in the Editor.
  • Exporting using JSON, Atlas and PNG
  • Using Ashely
  • There are no crashes or Exceptions thrown.

CharacterBuilder.java
This class builds GameObject which I use to update and render Skeletons, SpineData is just a POJO that holds Spine information accessable for my RenderSystem, I checked and this build() function is called only once (for testing of course)

public GameObject build() throws CharacterBuilderException {

    /* Loading a SpineData */
    SpineData spineData = SpineDataLoader.getDefault().loadFromAssets(fileName);

    /* Don't create Characters without graphics */
    if (spineData == null) {
        throw new CharacterBuilderException("[!] CharacterBuilder couldn't load FileName: \"" + String.valueOf(fileName) + "\"");
    }

    /* In some cases load skin is mandatory or else nothing will be rendered */
    if (skinName != null) spineData.getSkeleton().setSkin(skinName);

    GameObject entity = new GameObject(x, y, z, spineData);

    /* Space for adding beyond basic Components */

    AnimationState state = spineData.getAnimationStateData();
    state.setAnimation(0, "Standing", false);
    state.addAnimation(0, "WalkingStart", false, 2);
    state.addAnimation(0, "Walking", true, 0);

    /* to test if it runs only once */
    System.out.println("CharacterBuilder.build()");

    return entity;
}

RenderSystem.java
This class is for rendering on screen, it gets updated by Ashley life cycle, this is the only place that draws anything on screen, please check the update() method.


import com.badlogic.ashley.core.*;
import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
import com.esotericsoftware.spine.AnimationState;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.SkeletonMeshRenderer;
import com.esotericsoftware.spine.SkeletonRendererDebug;
import com.yuststudio.meanspace.core.components.basic.PositionComponent;
import com.yuststudio.meanspace.core.components.basic.RenderableComponent;
import com.yuststudio.meanspace.core.model.SpineData;
import com.yuststudio.meanspace.core.mappers.Mapper;

import java.util.*;

public class RenderSystem extends EntitySystem implements EntityListener {
    /**
     * Simple Array List of z sorted Entities
     */
    private List<Entity> zOrderArray;
    private SkeletonMeshRenderer renderer;
    private SkeletonRendererDebug debugRenderer;
    private Family renderFamily;
    private PolygonSpriteBatch batch;

public RenderSystem(PolygonSpriteBatch batch, SkeletonMeshRenderer renderer, SkeletonRendererDebug debugRenderer) {
    this.batch = batch;
    this.renderer = renderer;
    this.debugRenderer = debugRenderer;
}

private int size;
private Entity entity;
private SpineData spineData;
private PositionComponent pos;
private RenderableComponent renderableComponent;

/[i].................................................Public.Methods.................................................[/i]/

/**
 * Updates Spine Skeleton's positions and draws everyone on screen
 *
 * @param deltaTime
 */
@Override
public void update(float deltaTime) {

    /* Sort only if Z was modified by someone */
    if (PositionComponent.s_Z_Modified) {
        sort();
        PositionComponent.s_Z_Modified = false;
    }

    /* Drawing using Z order */
    batch.begin();
    size = zOrderArray.size();
    for (int i = 0; i < size; i++) {
        entity = zOrderArray.get(i);

        pos = Mapper.position.get(entity);
        if (pos != null) {
            renderableComponent = Mapper.renderable.get(entity);

            spineData = renderableComponent.getSpineData();
            if (spineData != null) {
                Skeleton skeleton = spineData.getSkeleton();
                skeleton.setX(pos.getX());
                skeleton.setY(pos.getY());

                AnimationState state = spineData.getAnimationStateData();
                state.update(deltaTime);
                state.apply(skeleton);
                skeleton.updateWorldTransform();

                renderer.draw(batch, skeleton);
            }
        }
    }
    batch.end();
}

/[i]................................................Adders.Removers.................................................[/i]/

@Override
public void addedToEngine(Engine engine) {
    renderFamily = Family.all(PositionComponent.class, RenderableComponent.class).get();
    zOrderArray = new ArrayList<Entity>();
    engine.addEntityListener(renderFamily, this);
}

@Override
public void removedFromEngine(Engine engine) {
    engine.removeEntityListener(this);
}

/[i]................................................Entity.Listener.................................................[/i]/

@Override
public void entityAdded(Entity entity) {
    zOrderArray.add(entity);
    sort();
}

@Override
public void entityRemoved(Entity entity) {
    zOrderArray.remove(entity);
}

/[i]................................................Private.Methods.................................................[/i]/

private static final Comparator comparator = new Comparator<Entity>() {
    @Override
    public int compare(Entity o1, Entity o2) {
        return (int) Math.signum(
                Mapper.position.get(o1).getZ() - Mapper.position.get(o2).getZ()
        );
    }
};

private void sort() {
    Collections.sort(zOrderArray, comparator);
}
}

SpineData.java

public class SpineData implements Cloneable{
    private TextureAtlas atlas;
    private Skeleton skeleton;
    private AnimationState animationState;

@Override
protected Object clone() throws CloneNotSupportedException {
    return super.clone();
}

public SpineData(TextureAtlas atlas, Skeleton skeleton) {
    this.atlas = atlas;
    this.skeleton = skeleton;
}

public SpineData(TextureAtlas atlas, Skeleton skeleton, AnimationState animationState) {
    this.atlas = atlas;
    this.skeleton = skeleton;
    this.animationState = animationState;
}

public TextureAtlas getAtlas() {
    return atlas;
}

public Skeleton getSkeleton() {
    return skeleton;
}

public void setAnimationStateData(AnimationState animationState) {
    this.animationState = animationState;
}

public AnimationState getAnimationStateData() {
    return animationState;
}
}

Image removed due to the lack of support for HTTPS. | Show Anyway

It just stuck on the animation's first frame, if I change the deltaTime in the state.update(deltaTime); manually then it's stuck on a different frame, seems like state.getCurrent(0).getTrackTime() does increment but the animation doesn't happen anyway... I use InputProcessor to move this guy around and that works so the life cycle's functional.

I probably did something silly or missed something, wasted too much time on this, need some help from the pros, help will be most appreciated! :notme:

Thank you for your time hope we solve this quickly! :beer:

The code you shared is more complex than it really should be if you are hunting down a problem, but at a quick check nothing stands out as wrong. You should simplify the problem, modify one of the examples to use your skeleton, etc. Debug it further, check what time is being used to pose the animation. If that time doesn't increment, figure out why. FWIW, you might see the behavior you described if you were calling setAnimation every frame.

Just as a simple test, the first set-animation, make that into an add animation and see if that works. If that works than you're probably constantly resetting the first set animation and you need to change the way that works 🙂 If you constantly set the animation every update, then it gets stuck.

Source: my own project with these issues!
Your code is too complex for my own artist abilities, but that's what went wrong in my experience so it might be something like that! If not.. I hope someone else can help you

Thanks Nate and Nyenna for the quick reply,

Found a clue, it seems that my code works when I choose to animate "WalkingStart" in a loop, but won't work for "Walking" animation...
In the Editor both animations work! tried to export the character again just in case something's off but it didn't solve it 🙁

got any clue to why a working animation in the Editor won't work in Runtime ?

Update: it's the animation in the Editor, just created a new "Walking" animation exported to my project and it's stuck on first frame, it means I'm doing something wrong in the Editor perhaps I mess up the keys or something... :wonder:

Simplify your example.

Solved it!

The problem was with the speed of animating in my app, I didn't use

Gdx.graphics.getDeltaTime()

Instead I used my own DeltaTime to lock FPS, which was way too big and it screwed up animation :wonder:

To anyone with a weird animation problem - check that your drawing method receives the right delta or else animation's speed may be too fast and could look like it's in reverse!

Thanks for the help :nod: