• Editor
  • Runtime scaling of Spine figure in Libgdx (Actor)

I am trying to set up scaling of my Libgdx Actors which are also using Spine functionality. Each Actor has a Skeleton and a SkeletonRenderer to be drawn during runtime. Having done it like this, the Actor itself needs to have a position, a width and a height to be drawn. I thus need to do some scaling to get stuff straight for input detection and other detections. See attached image for an example of a simple bomb. What I am doing now is:

  1. Set a bounding box attachment on the bomb, called actor_bb. I want this bounding box to work as the area where input can be detected and thus also the position, width and height of the actor.

  2. I set the bounds of the Actor (the Bomb) using the vertices of the bounding box:

    /**Sets the bounds of the Actor based on the vertices of the actor_bb */
    private void setActorBounds(BoundingBoxAttachment actor_bb){
    	float[] vertices = actor_bb.getVertices();
    	float minX = Float.MAX_VALUE;
    	float maxX = Float.MIN_VALUE;
    	float minY = Float.MAX_VALUE;
    	float maxY = Float.MIN_VALUE;
    	
    	/*Iterates vertices to find the bounds. The vertices are ordered by x, y, x, y ...*/
    	for( int i = 0; i < vertices.length; i++ ){
    		
    if ( i % 2 == 0 ){ // i is even, coordinate is an x coordinate
    	if ( vertices[i] <= minX )
    		minX = vertices[i];
    	if ( vertices[i] >= maxX )
    		maxX = vertices[i];
    }
    else{ // i is odd, coordinate is a y coordinate
    	if ( vertices[i] <= minY )
    		minY = vertices[i];
    	if ( vertices[i] >= maxY )
    		maxY = vertices[i];
    }
    
    	}
    	setBounds(minX, minY, maxX - minX, maxY - minY);
    }
    1. I calculate and set a scaling factor based on this bounding box and a physical height (i.e. height in meters):

      /**Scales the ActorBaseClass in accordance with its physical height (in meters) and the height of the screen (in meters).*/
      @Override
      public void animatableScale() {
      	float heightInPixels = GameParameters.ViewportParameters.PPM * parameters.height;
      	float scalingFactor = heightInPixels / this.getHeight();
      	
      	this.setScale(scalingFactor);
      	rootBone.setScaleX(scalingFactor);
      	rootBone.setScaleY(scalingFactor);
      	
      	skeleton.updateWorldTransform();
      }

      This works to some extent and the scaling factor is based on the height of the bounding box for the Actor. However, the bounds of the Actor seems to start at the center point of the Bomb and stretches to the right of it, as if the bounds are moved "50 %" to the right. Maybe it has something to do with Actor coordinates versus Skeleton coordinates?

    Are there any better ways of doing this? It seems a bit silly to mix Actor and Spine stuff like this, but I cannot readily see how to do it otherwise. Please tear this approach apart end enlighten me on better ways to do this! Any comments are very appreciated 🙂

Related Discussions
...

And another strange thing to mention: I tried using the SkeletonRendererDebug to see what's going on and the coordinates are way off compared to what they should be. Also, the debug lines are not drawn either (they were at first but have gone missing). It is used like this:

@Override
public void draw(SpriteBatch batch, float parentAlpha) {
	skeletonRenderer.draw(batch, skeleton);
	skeletonRendererDebug.draw(skeleton);
}

Instead of starting at the proper position the Bomb starts off out of the screen to the left and down. I can pull it in to see it, however. It probably has something to do with the coordinates since everything else obeys my ground level, but the Bomb does not when using the SkeletonRendererDebug.

Just so you know about it 😉

SkeletonBounds already gives you min/max x/y (see getters) when you pass true to update, no need to compute it again.

When you draw inside an actor, the SpriteBatch has been adjusted to draw in the actor's parent coordinate system. You need to use getX() and getY() to draw at the actor position. Eg:

skeleton.setX(getX());
skeleton.setY(getY());
skeletonRenderer.draw(batch, skeleton);

If you want to use SkeletonRendererDebug, its ShapeRenderer needs to match the SpriteBatch:

ShapeRenderer renderer = skeletonRendererDebug.getShapeRenderer();
renderer.getProjectionMatrix().set(batch.getProjectionMatrix());
renderer.getTransformMatrix().set(batch.getTransformMatrix());
skeletonRendererDebug.draw(skeleton);

See if that helps. 🙂

The positioning of the Bomb is ok now, but there are no debug lines drawn. Maybe due to the stuff below, I don't know. So if I understand this correctly I should have a SkeletonBounds object for each Actor as well? Should this object be updated at every time step as for Skeleton and AnimationState?

When I try to do this though, the minX, maxX, minY and maxY are always Integer.MAX_VALUE, Integer.MIN_VALUE or NaN. Maybe there is a bug somewhere? It looks like the polygons Array is always filled with zeros and that they are used in the calculations in aabbCompute() instead of the bounding box vertices. Here is a print out when running aabbCompute() through update() every time step:

asdad: minX = NaN, maxX = NaN, minY = NaN, maxY = NaN
asdad: minX = NaN, maxX = NaN, minY = NaN, maxY = NaN
asdad: minX = 2.14748365E9, maxX = -2.14748365E9, minY = 2.14748365E9, maxY = -2.14748365E9
asdad: minX = 2.14748365E9, maxX = -2.14748365E9, minY = 2.14748365E9, maxY = -2.14748365E9

Anyway, could you tell if this approach seems ok to set the proper bounds for the Actor for input detection through listeners:

  1. I create the Skeleton and call setToSetupPose() on that.
  2. I create the SkeletonBounds and call update(skeleton, true) on that. Let's say I then have the Axis Aligned Bounding Box (i.e. minX, maxX, minY, maxY) ready to use from the SkeletonBounds object.
  3. I set the bounds of the Actor for input detection in the listeners like this:
    setOrigin(skeletonBounds.getMinX(), skeletonBounds.getMinY());
    setWidth(skeletonBounds.getMaxX() - skeletonBounds.getMinX());
    setHeight(skeletonBounds.getMaxY() - skeletonBounds.getMinY());
    You see, it is possible to pick up the Bomb, but only on it's right half and some distance to the right of it. I think it might have something to do with the Actor's origin or something. I am not really sure of this.

I hope this was clear 🙂 ... :sleepy:

I updated AnimationStateTest to show SkeletonBounds works. I don't know why it doesn't work for you. There shouldn't be any NaNs there, where do those come from?

1) You don't need to call setToSetupPose unless you change the skin. When a skeleton is created it has the setup pose.

2) SkeletonBounds uses the bone's world SRT to compute bounding box vertex positions. This means you need to make sure you have posed your skeleton (unless you want to use the setup pose) and called skeleton.updateWorldTransform before updating the SkeletonBounds.

3) An actor position is the lower left corner. The position you draw your skeleton is 0,0 in the Spine editor. If you have the center of your bomb on x=0 as it is in your screenshot, then you need to draw the bomb shifted to the right if you want the bomb drawn within the actor's bounds. Also note you can use skeletonBounds.getWidth/Height.

Thanks, Nate - I understand this a lot better now! 🙂

Nate wrote

There shouldn't be any NaNs there, where do those come from?

Well that's just because I am silly and try to calculate the bounds of skeletons without BoundingBoxAttachments, or the scale is infinity/NaN due to initial zero size.

Nate wrote

3) An actor position is the lower left corner. The position you draw your skeleton is 0,0 in the Spine editor. If you have the center of your bomb on x=0 as it is in your screenshot, then you need to draw the bomb shifted to the right if you want the bomb drawn within the actor's bounds.

Let's say I draw the Bomb starting at 0,0 instead of centered at it, will it not cause trouble when using flipX? Then I should rather, as you say, just render it shifted. But this is an awkward way of doing it.

The only reason I want to do it like this is because my listeners are currently set up to react based on the Actor bounds. Will it be better / faster to use the Spine bounds directly? If so - do you have any recommendations on how to implement these listeners when using Scene2d? Should I add a listener to the Stage (which is the InputProcessor) and go through all Actors to check if some are hit? Can I use the Scene2d functionality somehow?

Thanks for your efforts!

I think I have figured an ok way to do this; I add the following listener to my Stage:

public class SpineListenerTest extends InputListener{
	@Override
	public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {

	Stage actorStage = event.getStage();
	for ( int i = 0, n = actorStage.getActors().size; i < n; i++ ){
		
		Actor actor = actorStage.getActors().get(i);
		
		if ( actor instanceof Group )
			searchGroup((Group) actor, x, y);
		else if ( actor instanceof ActorBaseClass )
			searchActor((ActorBaseClass) actor, x, y);
		else
			continue;
	}
	return true;
}

/**Method to search a Group for ActorBaseClasses and find out which one
 * that is hit (if any). Recursive search through all Groups in the stage.*/
private void searchGroup(Group group, float x, float y){
	for ( int i = 0, n = group.getChildren().size; i < n; i++ ){
		Actor actor = group.getChildren().get(i);
		if (actor instanceof ActorBaseClass)
			searchActor((ActorBaseClass) actor, x, y);
		else if (actor instanceof Group)
			searchGroup((Group) actor, x, y);
		else
			continue;
	}
}

/**Method to search an ActorBaseClass to see if it is hit.*/
private void searchActor(ActorBaseClass actor, float x, float y){
	SkeletonBounds skeletonBounds = actor.getSkeletonBounds();
	skeletonBounds.update(actor.getSkeleton(), true);
	
	if ( skeletonBounds.aabbContainsPoint(x, y) ){
		BoundingBoxAttachment hit = skeletonBounds.containsPoint(x, y);
		Gdx.app.log(TAG, "Hit BB at: x = " + x + ", y = " + y);
		//TODO: Transfer event to another listener?
	}	
}

Does this look ok?

I am a bit unsure of how to transfer the event to the ActorBaseClass (my custom actor). Can I just fire the touchDown() in another listener and leave it like that? Doesn't seem to work quite right.

And as a side note; I have to set the Actor's bounds to (1,1) or whatever to make my skeleton visible. Does it matter which dummy size I set?

Usually you override hit to determine if the actor is hit(). Return null if it wasn't. No need to implement your own search through the actor hierarchy. This will also mean your actor will get a touch down as normal. Don't respond to a touch inside hit(), just return this or null. Respond to a touch by adding a listener.

Note that actor's bounds are only involved in hit detection because Actor#hit() uses the bounds. If you override hit() then you don't have to set the bounds. Then just draw your skeleton inside your actor at getX(), getY() and everything will work fine. The skeleton position will be the same as 0,0 in Spine. Leave your bomb centered at 0,0 as you had it and it will work fine with flip.

Well that was actually embarrassingly simple. I overrode hit() and it just works :$

Here, take this, it's well deserved


:beer:

Cheers! :beer: :clap: