• Runtimes
  • [spine-libGDX] use SkeletonActor in Group

In my game development , I need to combined a SkeletonActor and a Label into a group as a mixed actor. I'll change the group's position and rotation. The animationState can follow this translate, but the skeleton cannot follow the translate, the skeleton's position only (0,0). I know the skeleton only follow the skeletonActor's local position in the group.
I want to know how to make skeletonActor 's skeleton and animationstate as the group transform or a best practice for this.Thanks for any suggestion.
I use [spine-editor] 4.2.20 and [spine-libgdx-runtime] 4.2.0.

`
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Pool;
import com.esotericsoftware.spine.utils.SkeletonActor;

public class ProjectileActor extends Group {

private float damage = ProjectileConstraints.BASE_DAMAGE;
private final SkeletonActor skeletonActor;
private final Label label = new Label("22", Assets.getInstance().getSmallLabelStyle());


public ProjectileActor(SkeletonActor skeletonActor) {
    this.skeletonActor = skeletonActor;
    label.setPosition(getX(), getY() + 100, Align.center);
    this.addActor(skeletonActor);
    this.addActor(label);

}

public float getDamage() {
    return damage * MathUtils.random(ProjectileConstraints.DAMAGE_LOW_LIMIT, ProjectileConstraints.DAMAGE_HIGH_LIMIT);
}


public void setDamage(float damage) {
    this.damage = damage;
}


public SkeletonActor getSkeletonActor() {
    return skeletonActor;
}


static public class ProjectileConstraints {
    public static final float DAMAGE_LOW_LIMIT = .5f;
    public static final float DAMAGE_HIGH_LIMIT = 1.2f;
    public static final float BASE_DAMAGE = 2f;
}

}
`

Related Discussions
...

To ensure that the SkeletonActor's skeleton and animation state follow the transformations of the Group, you need to update the skeleton's position and rotation based on the group's transformations. Here's how you can achieve this:

  1. Update Skeleton Position and Rotation: Override the act method in your ProjectileActor class to update the skeleton's position and rotation based on the group's transformations.

  2. Apply Transformations: Ensure that the skeleton's world transform is updated after setting its position and rotation.

Here's an example of how you can modify your ProjectileActor class:

import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.utils.Align;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.utils.SkeletonActor;

public class ProjectileActor extends Group {

    private float damage = ProjectileConstraints.BASE_DAMAGE;
    private final SkeletonActor skeletonActor;
    private final Label label = new Label("22", Assets.getInstance().getSmallLabelStyle());

    public ProjectileActor(SkeletonActor skeletonActor) {
        this.skeletonActor = skeletonActor;
        label.setPosition(getX(), getY() + 100, Align.center);
        this.addActor(skeletonActor);
        this.addActor(label);
    }

    @Override
    public void act(float delta) {
        super.act(delta);
        // Update skeleton position and rotation based on the group's transformations
        Skeleton skeleton = skeletonActor.getSkeleton();
        skeleton.setPosition(getX(), getY());
        skeleton.getRootBone().setRotation(getRotation());
        skeleton.updateWorldTransform();
    }

    public float getDamage() {
        return damage * MathUtils.random(ProjectileConstraints.DAMAGE_LOW_LIMIT, ProjectileConstraints.DAMAGE_HIGH_LIMIT);
    }

    public void setDamage(float damage) {
        this.damage = damage;
    }

    public SkeletonActor getSkeletonActor() {
        return skeletonActor;
    }

    static public class ProjectileConstraints {
        public static final float DAMAGE_LOW_LIMIT = .5f;
        public static final float DAMAGE_HIGH_LIMIT = 1.2f;
        public static final float BASE_DAMAGE = 2f;
    }
}

In this example, the act method is overridden to update the skeleton's position and rotation based on the group's current position and rotation. The updateWorldTransform method is called to ensure the skeleton's world transform is updated correctly.

This approach ensures that the SkeletonActor's skeleton and animation state follow the transformations of the Group.

Yes, thank you, I tried this . But another problem occurs. It looks like I'll make a SkeletonGroup which extends Group but implemented as SkeletonActor to solve the problem? or any else suggetions?


public class ProjectileActor extends Group  {

    private float damage = ProjectileConstraints.BASE_DAMAGE;
    private final SkeletonActor skeletonActor;
    private final Label label = new Label("22", Assets.getInstance().getSmallLabelStyle());


    public ProjectileActor(SkeletonActor skeletonActor) {
        this.skeletonActor = skeletonActor;
        label.setPosition(getX(), getY() + 100, Align.center);
        this.addActor(skeletonActor);
        this.addActor(label);
    }

    @Override
    public void act(float delta) {
        super.act(delta);
        Skeleton skeleton = skeletonActor.getSkeleton();
        skeleton.setPosition(getX(), getY());
        skeleton.getRootBone().setPosition(getX(),getY());
        skeleton.updateWorldTransform(Skeleton.Physics.update);

    }

    public float getDamage() {
        return damage * MathUtils.random(ProjectileConstraints.DAMAGE_LOW_LIMIT, ProjectileConstraints.DAMAGE_HIGH_LIMIT);
    }


    public void setDamage(float damage) {
        this.damage = damage;
    }


    public SkeletonActor getSkeletonActor() {
        return skeletonActor;
    }


    static public class ProjectileConstraints {
        public static final float DAMAGE_LOW_LIMIT = .5f;
        public static final float DAMAGE_HIGH_LIMIT = 1.2f;
        public static final float BASE_DAMAGE = 2f;
    }
}

Because In SkeletonActor's draw method. the skeleton's position follow the actor's position. but in group, the skeletonActor's position in group always (0,0), It is explain why this happens. If make a class like a group implements as skeletonActor, this problem can be solved. But , is this the best practice? I don't know.

public void draw (Batch batch, float parentAlpha) {
		int blendSrc = batch.getBlendSrcFunc(), blendDst = batch.getBlendDstFunc();
		int blendSrcAlpha = batch.getBlendSrcFuncAlpha(), blendDstAlpha = batch.getBlendDstFuncAlpha();

		Color color = skeleton.getColor();
		float oldAlpha = color.a;
		skeleton.getColor().a *= parentAlpha;

		skeleton.setPosition(getX(), getY());
		updateWorldTransform();
		renderer.draw(batch, skeleton);

		if (resetBlendFunction) batch.setBlendFunctionSeparate(blendSrc, blendDst, blendSrcAlpha, blendDstAlpha);

		color.a = oldAlpha;
	}

I solved the problem, it seems that Group is not suitable for this, I can build a new Actor, follow the SkeletonActor to this Actor, and find its own coordinates.

The animationState can follow this translate, but the skeleton cannot follow the translate
This doesn't make sense. AnimationState doesn't have a position.

In general, you probably don't want to use a Stage for your entire game. It makes sense for your UI, but I would draw the game world without scene2d.

I don't understand the problem you are having. A SkeletonActor can be placed in a group and will translate with the parent group. I've added ActorTest:
EsotericSoftware/spine-runtimesblob/4.2/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ActorTest.java

Here I've modified it to show a parent group that is translated, rotated, and scaled, and I've used ShapeRenderer to make the positions of the group, label, and SkeletonActor:

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle;
import com.badlogic.gdx.utils.viewport.ScreenViewport;

import com.esotericsoftware.spine.Skeleton.Physics;
import com.esotericsoftware.spine.utils.SkeletonActor;
import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;

public class ActorTest extends ApplicationAdapter {
	Stage stage;
	TwoColorPolygonBatch batch;
	ShapeRenderer shapes;
	SkeletonRenderer renderer;

	TextureAtlas atlas;
	Skeleton skeleton;
	AnimationState state;
	SkeletonActor skeletonActor;
	Group group;
	Label label;
	Vector2 position = new Vector2();

	public void create () {
		batch = new TwoColorPolygonBatch();
		shapes = new ShapeRenderer();

		stage = new Stage(new ScreenViewport(), batch);
		Gdx.input.setInputProcessor(stage);

		renderer = new SkeletonRenderer();
		renderer.setPremultipliedAlpha(true);

		atlas = new TextureAtlas(Gdx.files.internal("spineboy/spineboy-pma.atlas"));
		SkeletonJson json = new SkeletonJson(atlas);
		json.setScale(0.6f);
		SkeletonData skeletonData = json.readSkeletonData(Gdx.files.internal("spineboy/spineboy-pro.json"));

		skeleton = new Skeleton(skeletonData);

		AnimationStateData stateData = new AnimationStateData(skeletonData);
		stateData.setMix("run", "jump", 0.2f);
		stateData.setMix("jump", "run", 0.2f);

		state = new AnimationState(stateData);
		state.setTimeScale(0.5f);
		state.setAnimation(0, "run", true);
		state.addAnimation(0, "jump", false, 2);
		state.addAnimation(0, "run", true, 0);

		skeletonActor = new SkeletonActor(renderer, skeleton, state);

		LabelStyle style = new LabelStyle();
		style.font = new BitmapFont();
		label = new Label("test", style);

		group = new Group();
		group.addActor(skeletonActor);
		group.addActor(label);
		stage.addActor(group);

		group.setScale(1.3f, 1);
		group.setRotation(30);
		group.setPosition(120, 20);
		label.setPosition(30, 30);
		skeletonActor.setPosition(150, 10);
	}

	public void render () {
		float delta = Gdx.graphics.getDeltaTime();
		state.update(delta);

		state.apply(skeleton);
		skeleton.update(delta);
		skeleton.updateWorldTransform(Physics.update);

		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

		stage.draw();

		shapes.begin(ShapeType.Line);

		shapes.setColor(Color.RED);
		group.localToStageCoordinates(position.set(0, 0));
		shapes.x(position, 10);

		shapes.setColor(Color.GREEN);
		skeletonActor.localToStageCoordinates(position.set(0, 0));
		shapes.x(position, 10);

		shapes.setColor(Color.YELLOW);
		label.localToStageCoordinates(position.set(0, 0));
		shapes.x(position, 10);

		shapes.end();
	}

	public void resize (int width, int height) {
		stage.getViewport().update(width, height, true);
	}

	public void dispose () {
		atlas.dispose();
	}

	public static void main (String[] args) throws Exception {
		new Lwjgl3Application(new ActorTest());
	}
}

Note if you don't use rotate or scale you should use group.setTransform(false);. See:
https://libgdx.com/wiki/graphics/2d/scene2d/scene2d#group-transform

Thank you for your response. I think I previously misunderstood the use of Group. As you mentioned, Stage is used for the UI, and I will use Ashley to draw the game world. Now, everything is working well.

Group is an actor that has children. The children are transformed relative to their parent group, so groups creates a hierarchy. The stage has a root group and all actors in the stage are in that group or child groups.

Glad it's working like you want!