Search code examples
javalibgdxbox2d

LibGDX camera.project() is not working properly


I use Box2D to manage my game world. I want to display text above the body. I plan to do this by setting the Label appropriately. The problem is that the body is in the game world and the Label is in the Stage (UI). So I tried to use the camera.project() method to convert world coordinates to screen coordinates. Unfortunately, for some reason I couldn't do it - the Label is displayed in a different place than it should be - it is shifted down and to the left relative to the target position. When resizing the window, this position also changes, but also incorrectly. I will add that otherwise I use the camera.unproject() method in the same way and I have no problems here - everything works fine. I don't know why it's different the other way around.

this.camera = new OrthographicCamera(ScreenManager.WIDTH * SCALE, ScreenManager.HEIGHT * SCALE);
this.viewport = new FitViewport(ScreenManager.WIDTH, ScreenManager.HEIGHT);
this.stage = new Stage(viewport, batch);
this.world = new World(Vector2.Zero, true);

// ...

Vector3 worldPosition = new Vector3(x, y, 0);
Vector3 screenPosition = camera.project(worldPosition,
    viewport.getScreenX(), viewport.getScreenY(),
    viewport.getScreenWidth(), viewport.getScreenHeight());

label.setPosition(screenPosition.x, screenPosition.y);

And my resize method:

@Override
public void resize(int width, int height) {
    viewport.update(width, height, true);
}

Of course I call act() and draw() for Stage in the render method. What am I doing wrong?


Solution

  • In order to translate from World-coordinates to Stage-coordinates you need to first project the World position onto screen-space, and then unproject it back into Stage-space.

    Vector2 worldPosition = body.getPosition();
    Vector3 screenPosition = worldCamera.project(new Vector3(worldPosition.x, worldPosition.y, 1.0f));
    Vector3 stagePosition = stage.getCamera().unproject(screenPosition);
    
    label.setPosition(stagePosition.x, stage.getHeight() - stagePosition.y)); // stage.getHeight() - because UP is flipped.
    

    That process will allow you to track a Box2D Body and set a Labels position to match it:

    Label tracking Box2D body

    Full source for the example above is included below, it uses the default font from the libGDX tests (font and the font image).

    package com.bornander.sandbox;
    
    import com.badlogic.gdx.ApplicationAdapter;
    import com.badlogic.gdx.Gdx;
    import com.badlogic.gdx.graphics.Color;
    import com.badlogic.gdx.graphics.OrthographicCamera;
    import com.badlogic.gdx.graphics.g2d.BitmapFont;
    import com.badlogic.gdx.math.Vector2;
    import com.badlogic.gdx.math.Vector3;
    import com.badlogic.gdx.physics.box2d.*;
    import com.badlogic.gdx.scenes.scene2d.Stage;
    import com.badlogic.gdx.scenes.scene2d.ui.Label;
    import com.badlogic.gdx.utils.ScreenUtils;
    import com.badlogic.gdx.utils.viewport.ScreenViewport;
    
    public class MyGdxSandbox extends ApplicationAdapter {
        World world;
        OrthographicCamera worldCamera;
        Box2DDebugRenderer box2DDebugRenderer;
    
        Body ballBody;
        Stage stage;
        Label label;
        
        @Override
        public void create () {
            world = new World(new Vector2(0.0f, -10.0f), false);
            float aspectRatio = (float)Gdx.graphics.getWidth() / Gdx.graphics.getHeight();
            float worldCameraViewportWidth = 100.0f;
            worldCamera = new OrthographicCamera(worldCameraViewportWidth, worldCameraViewportWidth / aspectRatio);
            worldCamera.position.set(worldCamera.viewportWidth / 2.0f, worldCamera.viewportHeight / 2.0f, 1.0f);
            box2DDebugRenderer = new Box2DDebugRenderer();
    
            CircleShape ballShape = new CircleShape();
            ballShape.setRadius(4.0f);
    
            FixtureDef ballFixtureDef = new FixtureDef();
            ballFixtureDef.shape = ballShape;
            ballFixtureDef.friction = 0.2f;
            ballFixtureDef.density = 1.0f;
            ballFixtureDef.restitution = 0.65f;
    
            BodyDef ballBodyDef = new BodyDef();
            ballBodyDef.type = BodyDef.BodyType.DynamicBody;
    
            ballBody = world.createBody(ballBodyDef);
            ballBody.createFixture(ballFixtureDef);
            ballBody.setTransform(25, 50, 0);
            ballBody.applyLinearImpulse(200, 0, ballBody.getPosition().x, ballBody.getPosition().y, true);
    
            PolygonShape groundShape = new PolygonShape();
            groundShape.setAsBox(25, 2);
    
            FixtureDef groundFixtureDef = new FixtureDef();
            groundFixtureDef.shape = groundShape;
            groundFixtureDef.friction = 0.2f;
            groundFixtureDef.density = 1.0f;
            groundFixtureDef.restitution = 0.2f;
    
            BodyDef groundBodyDef = new BodyDef();
            groundBodyDef.type = BodyDef.BodyType.StaticBody;
    
            Body ground = world.createBody(groundBodyDef);
            ground.createFixture(groundFixtureDef);
            ground.setTransform(50, 20, 0);
    
    
            stage = new Stage(new ScreenViewport());
            stage.setDebugAll(true);
            label = new Label("TEXT", new Label.LabelStyle(new BitmapFont(Gdx.files.internal("default.fnt")), Color.WHITE));
            label.setPosition(10, 10);
            stage.addActor(label);
        }
    
        @Override
        public void render () {
            ScreenUtils.clear(0, 0, 0, 1);
            float delta = Gdx.graphics.getDeltaTime();
            worldCamera.update();
            world.step(delta, 8, 8);
            Vector2 ballPositionWorld = ballBody.getPosition();
            Vector3 ballScreenPosition = worldCamera.project(new Vector3(ballPositionWorld.x, ballPositionWorld.y, 1.0f));
            Vector3 ballStagePosition = stage.getCamera().unproject(ballScreenPosition);
            label.setText(String.format("(%.3f, %.3f)", ballPositionWorld.x, ballPositionWorld.y));
            label.pack();
            label.setPosition(ballStagePosition.x, stage.getHeight() - ballStagePosition.y);
            box2DDebugRenderer.render(world, worldCamera.combined);
            stage.act();
            stage.draw();
        }
    }