Search code examples
javakotlinlibgdxlibktx

Custom actor for BitmapFont (libgdx)


I've spent several frustrating hours trying to implement (what I thought would be) a simple FontActor class.

The idea is to just draw text at a specific position using a provided BitmapFont. That much, I've managed to accomplish. However, I'm struggling to compute my actor's width/height based on the rendered text.

(Using a FitViewport for testing)

open class FontActor<T : BitmapFont>(val font: T, var text: CharSequence = "") : GameActor() {
    val layout = Pools.obtain(GlyphLayout::class.java)!!

    companion object {
        val identity4 = Matrix4().idt()
        val distanceFieldShader: ShaderProgram = DistanceFieldFont.createDistanceFieldShader()
    }

    override fun draw(batch: Batch?, parentAlpha: Float) {
        if (batch == null) return
        batch.end()

        // grab ui camera and backup current projection
        val uiCamera = Game.context.inject<OrthographicCamera>()
        val prevTransform = batch.transformMatrix
        val prevProjection = batch.projectionMatrix
        batch.transformMatrix = identity4
        batch.projectionMatrix = uiCamera.combined
        if (font is DistanceFieldFont) batch.shader = distanceFieldShader

        // the actor has pos = x,y in local coords, but we need UI coords
        // start by getting group -> stage coords (world)
        val coords = Vector3(localToStageCoordinates(Vector2(0f, 0f)), 0f)

        // world coordinate destination -> screen coords
        stage.viewport.project(coords)

        // screen coords -> font camera world coords
        uiCamera.unproject(coords,
                stage.viewport.screenX.toFloat(),
                stage.viewport.screenY.toFloat(),
                stage.viewport.screenWidth.toFloat(),
                stage.viewport.screenHeight.toFloat())

        // adjust position by cap height so that bottom left of text aligns with x, y
        coords.y = uiCamera.viewportHeight - coords.y + font.capHeight

        /// TODO: use BitmapFontCache to prevent this call on every frame and allow for offline bounds calculation
        batch.begin()
        layout.setText(font, text)
        font.draw(batch, layout, coords.x, coords.y)
        batch.end()

        // viewport screen coordinates -> world coordinates
        setSize((layout.width / stage.viewport.screenWidth) * stage.width,
                (layout.height / stage.viewport.screenHeight) * stage.height)

        // restore camera
        if (font is DistanceFieldFont) batch.shader = null
        batch.projectionMatrix = prevProjection
        batch.transformMatrix = prevTransform
        batch.begin()
    }
}

And in my parent Screen class implementation, I rescale my fonts on every window resize so that they don't become "smooshed" or stretched:

override fun resize(width: Int, height: Int) {
    stage.viewport.update(width, height)
    context.inject<OrthographicCamera>().setToOrtho(false, width.toFloat(), height.toFloat())

    // rescale fonts
    scaleX = width.toFloat() / Config.screenWidth
    scaleY = height.toFloat() / Config.screenHeight
    val scale = minOf(scaleX, scaleY)
    gdxArrayOf<BitmapFont>().apply {
        Game.assets.getAll(BitmapFont::class.java, this)
        forEach { it.data.setScale(scale) }
    }
    gdxArrayOf<DistanceFieldFont>().apply {
        Game.assets.getAll(DistanceFieldFont::class.java, this)
        forEach { it.data.setScale(scale) }
    }
}

This works and looks great until you resize your window. After a resize, the fonts look fine and automatically adjust with the relative size of the window, but the FontActor has the wrong size, because my call to setSize is wrong.

Initial window:

before horizontal stretch

After making window horizontally larger:

after horizontal stretch

For example, if I then scale my window horizontally (which has no effect on the world size, because I'm using a FitViewport), the font looks correct, just as intended. However, the layout.width value coming back from the draw() changes, even though the text size hasn't changed on-screen. After investigation, I realized this is due to my use of setScale, but simply dividing the width by the x-scaling factor doesn't correct the error. And again, if I remove my setScale calls, the numbers make sense, but the font is now squished!

Another strategy I tried was converting the width/height into screen coordinates, then using the relevant project/unproject methods to get the width and height in world coordinates. This suffers from the same issue shown in the images.

How can I fix my math?

Or, is there a smarter/easier way to implement all of this? (No, I don't want Label, I just want a text actor.)


Solution

  • One problem was my scaling code. The fix was to change the camera update as follows:

    context.inject<OrthographicCamera>().setToOrtho(false, stage.viewport.screenWidth.toFloat(), stage.viewport.screenHeight.toFloat())
    

    Which causes my text camera to match the world viewport camera. I was using the entire screen for my calculations, hence the stretching.

    My scaleX/Y calculations were wrong for the same reason. After correcting both of those miscalculations, I have a nicely scaling FontActor with correct bounds in world coordinates.