Search code examples
androidkotlinlibgdx

Libgdx stage add actor low FPS


I'm using ApplicationAdapter. When I have the code below in create() method, my fps is always about 14.

for (i in 0..1000) {
                stage.addActor(Food(
                        Food.TEXTURE_PURPLE!!,
                        100f,
                        (random.nextInt(MAP_SIZE)).toFloat(),
                        (random.nextInt(MAP_SIZE)).toFloat()))

                stage.addActor(Food(
                        Food.TEXTURE_GREEN!!,
                        100f,
                        (random.nextInt(MAP_SIZE)).toFloat(),
                        (random.nextInt(MAP_SIZE)).toFloat()))
}

But when I have this in create() method, my fps is always 60. Why?

    for (i in 0..1000) {
                stage.addActor(Food(
                        Food.TEXTURE_PURPLE!!,
                        100f,
                        (random.nextInt(MAP_SIZE)).toFloat(),
                        (random.nextInt(MAP_SIZE)).toFloat()))
            }

   for (i in 0..1000) {
              stage.addActor(Food(
                        Food.TEXTURE_GREEN!!,
                        100f,
                        (random.nextInt(MAP_SIZE)).toFloat(),
                        (random.nextInt(MAP_SIZE)).toFloat()))
            }

Solution

  • The problem is texture swapping. The way SpriteBatch works is you submit items to draw into its queue using one of its draw functions. Once you add something that can't be drawn with everything else in the queue, it flushes the queue by making an OpenGL draw call, and then it starts a new queue.

    Two different textures cannot be drawn in the same OpenGL draw call, so when your Food items are not sorted by texture, each one of them is triggering another flush. Flushes are an expensive operation an you want to avoid having more than a few dozen.

    The solution to this is to use a TextureAtlas, which combines multiple images into a single Texture, accessed via TextureRegion. You can submit TextureRegions to SpriteBatch.draw(...). Many items with different TextureRegions can be drawn in a single batch queue so long as the TextureRegions share the same Texture.

    On a side note...I strongly advise against using static references to Textures. The most commonly asked libgdx question on here is "why are my textures black" and it's always because they were using static texture references without understanding exactly how to dispose of the textures and reload properly. Static references outlive your game when on Android, which can cause big memory leaks, and bizarre bugs if you do any lazy loading of anything, which is very easy to accidentally do when using static references.

    A cleaner design is to have one Assets class (maybe one for each stage if there is a lot of unique stuff per stage in your game) that has an AssetManager for loading all your stuff. A single assets instance can be passed to all your game object constructors. For example:

    class Assets: Disposable {
        private val assetManager = AssetManager()
    
        lateinit val foodGreenTexture: Texture
        lateinit val foodRedTexture: Texture
        lateinit val foodBlueTexture: Texture
        //...
    
        init {
            assetManager.load("green.png", Texture::class.java)
            assetManager.load("red.png", Texture::class.java)
            //...
            assetManager.finishLoading()
            foodGreenTexture = assetManager.get("green.png")
            //...
        }
    
        override fun dispose (){
            assetManager.dispose()
        }
    }
    

    Don't forget to call dispose on your Assets class in the same object that initializes it (your game or screen).

    I'll also suggest looking into using the CoveTools library which has a helper called AssignmentAssetManager that cuts out a bunch of the above boilerplate. The same class would look like this. Note how the init block will be super short, no matter how many different assets you are loading:

    class Assets: Disposable {
        private val assetManager = AssignmentAssetManager()
    
        @Asset("green.png") val foodGreenTexture: Texture = null
        @Asset("red.png") val foodRedTexture: Texture = null
        @Asset("blue.png") val foodBlueTexture: Texture = null
        //...
    
        init {
            assetManager.loadAssetFields(this)
            assetManager.finishLoading()
        }
    
        override fun dispose (){
            assetManager.dispose()
        }
    }
    

    And a second side note... I strongly advise against using the Sprite class, especially when using Actors. Use the TextureRegion class instead. Sprite is a highly specialized class that extends TextureRegion but includes extra data like position, size, rotation, etc. This extra data is redundant if you are using Actor, so it's just error prone and a waste of CPU and memory to have to copy the data back and forth.