Search code examples
javalibgdx

Draw rotated ninepatches with LibGDX and Box2d


I'm using Android Studio with Java and LibGDX.

I need to draw the rotated ninepatches correctly, based on their position in the Box2d world.

The Spritebatch.draw method does not handle ninepatches so I have to use NinePatch.draw method.

Box2d uses the center as the origin to apply body rotation.

The problem is that NinePatch.draw rotates the object around the body's lower left corner instead of around the center (as SpriteBatch.draw does), resulting in an incorrect drawing position. This difference also produces an incorrect rotation for bodies with a RevoluteJoint and a motor.

Note that I place the bricks in the world considering their center as the origin, so in the draw method I'll do body.getPosition().x - widthInWorld / 2.

game.getBatch() returns a SpriteBatch set with setProjectionMatrix(viewport.getCamera().combined); and the viewport is set as new FitViewport(WORLD_WIDTH, WORLD_HEIGHT);

The brick has a ninepatch png of 130w x 34h pixels (so 128x32 without the patch borders) and its world size is 4 x 1 (as you can see in the screenshots).

These are the rotation angles of the bricks: brick's rotation angle

Actual working solution

I found this working solution but it causes Android Studio to warn "Possible flush inside a loop" (well said because batch.setTransformMatrix does a flush internally. More info in this SO question).

Matrix4 tempMatrix = new Matrix4();
Matrix4 originalMatrix = new Matrix4();

DelayedRemovalArray<Brick> bricks = ...;

bricks.begin();
for(Brick brick : bricks) {
    brick.draw(game.getBatch());
}
bricks.end();

Brick.draw method:

NinePatchDrawable ninePatchDrawable = ...;

public void draw(SpriteBatch batch) {
    originalMatrix.set(batch.getTransformMatrix());

    tempMatrix.set(originalMatrix);
    
    tempMatrix
        .translate(widthInWorld/2, heightInWorld/2, 0)
        .rotate(0, 0, angle, 10)
        .translate(-widthInWorld/2, -heightInWorld/2, 0);
    batch.setTransformMatrix(tempMatrix);

    ninePatchDrawable.draw(
        batch,
        0,
        0,
        0,
        0,
        widthInWorld,
        heightInWorld,
        1f,
        1f,
        0
    );

    batch.setTransformMatrix(originalMatrix); 
}

working with Matrix4

Without Matrix4


public void draw(SpriteBatch batch) {
    ninePatchDrawable.draw(
        batch,
        body.getPosition().x - widthInWorld / 2,
        body.getPosition().y - heightInWorld / 2,
        0,
        0,
        widthInWorld,
        heightInWorld,
        1,
        1,
        body.getAngle()  * MathUtils.radiansToDegrees
    );
}

Without Matrix4


Since batch flush affects performance, is there a better way to achieve this?

Ps. I've already searched and read other SO's answers and web regarding similar issues but haven't found any better solution.

Thanks a lot


Solution

  • Adjust the origin when drawing the rotated NinePatchDrawable by half the size of the NinePatchDrawable.

        public void render(SpriteBatch batch) {
            position.set(body.getPosition());
            float angle = body.getAngle() * MathUtils.radiansToDegrees;
            npd.draw(batch,
                    position.x - 0.5f * size.x,
                    position.y - 0.5f * size.y,
                    0.5f * size.x,
                    0.5f * size.y,
                    size.x,
                    size.y,
                    1.0f,
                    1.0f,
                    angle);
        }
    

    That will make sure the rotation is about the center of the NinePatchDrawable whilst still matching the rotation and position of the Box2D Body.

    In the example below I am fading out the NinePatchDrawable to show what the Box2DDebugRenderer considers the position to be:

    NinePatchDrawable centered rotation

    Full source code for the animation above:

        public class SomeCoolGameAboutRotatedBricks extends Game {
    
    
            public static class MyEntity {
                private final Vector2 position = new Vector2();
                private final Vector2 size = new Vector2();
                private final NinePatchDrawable npd;
                private final Body body;
    
                public MyEntity(float px, float py, float w, float h, NinePatchDrawable npd, World world) {
                    this.size.set(w, h);
                    this.npd = npd;
    
                    var shape = new PolygonShape();
                    shape.setAsBox(w / 2.0f, h / 2.0f);
    
                    var fixtureDef = new FixtureDef();
                    fixtureDef.shape = shape;
    
                    var bodyDef = new BodyDef();
                    bodyDef.type = BodyDef.BodyType.KinematicBody;
                    bodyDef.position.set(px, py);
    
                    body = world.createBody(bodyDef);
                    body.createFixture(fixtureDef);
    
                    shape.dispose();
                }
    
                public void spin(float rotation) {
                    body.setAngularVelocity(rotation);
                }
    
                public void render(SpriteBatch batch) {
                    position.set(body.getPosition());
                    float angle = body.getAngle() * MathUtils.radiansToDegrees;
                    npd.draw(batch,
                            position.x - 0.5f * size.x,
                            position.y - 0.5f * size.y,
                            0.5f * size.x,
                            0.5f * size.y,
                            size.x,
                            size.y,
                            1.0f,
                            1.0f,
                            angle);
                }
            }
    
            private World world;
            private Box2DDebugRenderer box2DDebugRenderer;
            private SpriteBatch batch;
            private OrthographicCamera camera;
    
            private Array<MyEntity> entities = new Array<>();
    
            @Override
            public void create() {
    
                world = new World(new Vector2(0, 0), false);
                box2DDebugRenderer = new Box2DDebugRenderer();
    
                // Camera setup with fixed world size, aspect-ratio of the window and positioned so that (0, 0) is center screen
                var WORLD_WIDTH = 1000.0f;
                var WORLD_HEIGHT = WORLD_WIDTH * Gdx.graphics.getHeight() / Gdx.graphics.getWidth();
                camera = new OrthographicCamera(WORLD_WIDTH, WORLD_HEIGHT);
                camera.position.set(0, 0, 1);
                camera.update();
    
                batch = new SpriteBatch();
    
    
                var npd = /* Load this somehow */  Assets.instance.ui.buttonBackground;
    
                entities.add(new MyEntity(-(WORLD_WIDTH * 0.25f), WORLD_HEIGHT * 0.25f, 100, 25.0f, npd, world));
                entities.add(new MyEntity(-(WORLD_WIDTH * 0.00f), WORLD_HEIGHT * 0.25f, 100, 25.0f, npd, world));
                entities.add(new MyEntity((WORLD_WIDTH * 0.25f), WORLD_HEIGHT * 0.25f, 100, 25.0f, npd, world));
    
                entities.add(new MyEntity(-(WORLD_WIDTH * 0.25f), WORLD_HEIGHT * 0.00f, 100, 25.0f, npd, world));
                entities.add(new MyEntity(-(WORLD_WIDTH * 0.00f), WORLD_HEIGHT * 0.00f, 100, 25.0f, npd, world));
                entities.add(new MyEntity((WORLD_WIDTH * 0.25f), WORLD_HEIGHT * 0.00f, 100, 25.0f, npd, world));
    
                entities.add(new MyEntity(-(WORLD_WIDTH * 0.25f), -WORLD_HEIGHT * 0.25f, 100, 25.0f, npd, world));
                entities.add(new MyEntity(-(WORLD_WIDTH * 0.00f), -WORLD_HEIGHT * 0.25f, 100, 25.0f, npd, world));
                entities.add(new MyEntity((WORLD_WIDTH * 0.25f), -WORLD_HEIGHT * 0.25f, 100, 25.0f, npd, world));
    
    
                for (var entity : entities)
                    entity.spin(MathUtils.random(-4, 4));
            }
    
            float alphaTimer = 0.0f;
    
            @Override
            public void render() {
                alphaTimer += Gdx.graphics.getDeltaTime();
                float alpha = 0.5f + MathUtils.sin(alphaTimer * 2);
                ScreenUtils.clear(Color.BLACK);
                world.step(Gdx.graphics.getDeltaTime(), 8, 8);
                box2DDebugRenderer.render(world, camera.combined);
    
                batch.setProjectionMatrix(camera.combined);
                batch.setColor(1, 1, 1, alpha);
                batch.begin();
    
                for (var entity : entities)
                    entity.render(batch);
    
                batch.end();
            }
        }