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:
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);
}
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
);
}
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
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:
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();
}
}