Skip to content

Application

Simon edited this page Apr 30, 2019 · 14 revisions

Setup is done? Initial LibGDX game window is starting up? Great job! :)

Now let's focus on the fun part. First we will look into the app extension from LibKTX.

The starting point of the tutorial will be the plain-kotlin branch that is simply containing the Kotlin code for the LibGDX Extending the Simple Game tutorial.

In this part we will improve the Game and Screen classes by using the KTX counterparts. In addition we will improve the code a little bit with some Kotlin beest practices.


To use the KTX app extension we first need to update our project's build.gradle file by adding a ktxVersion variable and a new dependency to our core project.

allprojects {
    apply plugin: "idea"

    version = '1.0'
    ext {
        appName = "A simple libktx game"
        gdxVersion = '1.9.9'
        roboVMVersion = '2.3.6'
        box2DLightsVersion = '1.4'
        ashleyVersion = '1.7.3'
        aiVersion = '1.8.1'
+        ktxVersion = '1.9.9-b1'
    }

    repositories {
        //...
    }
}
// ...
project(":core") {
    apply plugin: "kotlin"

    dependencies {
        api "com.badlogicgames.gdx:gdx:$gdxVersion"
        api "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
+        api "io.github.libktx:ktx-app:$ktxVersion"
    }
}

Re-sync your gradle project to be able to use the new KTX app extensions.


As mentioned in the LibGDX Extending the Simple Game wikipedia page it is a good practice to use the Game and Screen class. I think everyone who used these classes before knows that you will need to write your own ScreenCache or something similar to avoid creating new screens all the time and to remember the old status of your screen when switching through them.
The second things is that you always had to manually take care of disposing screens and usually you anyway want to do that when your Game's dispose() method is called.

Luckily for us LibKTX already takes care of that with its KtxGame implementation. It will have an internal ScreenCache and it will automatically dispose all screens when dispose is called.

Therefore our main game class will extend KtxGame. KtxGame has two optional parameters:

class Game : KtxGame<KtxScreen>()
  1. firstScreen: This will define the initial screen when starting up your game. Since our first screen will require some additional information we cannot pass it immediatly and therefore we don't specify it.
    In this case it will use the emptyScreen() implementation from LibKTX which is a KtxScreen implementation which overrides all Screen methods with an empty body

  2. clearScreen: default is true which means that at the beginning of the render method KtxGame will automatically clear the screen before calling the screen's render method. In my opinion this is a desired behavior for almost every game and it is nice that LibKTX takes care of that for us automatically.

    inline fun clearScreen(red: Float, green: Float, blue: Float, alpha: Float = 1f) {
        Gdx.gl.glClearColor(red, green, blue, alpha)
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
    }

Besides KtxGame there is also KtxScreen and our screens (MainMenuScreen and GameScreen) will implement KtxScreen instead of the normal LibGDX Screen interface.
This is also a convenient way to only implement those methods that you need since there is no ScreenAdapter in LibGDX available.

You can add screens to your game via addScreen and change to a specific screen via setScreen.
Note that you cannot add the same screen multiple times to the game. If you want to do that you first need to call removeScreen before adding the screen again.

override fun create() {
    // ...
    addScreen(MainMenuScreen(this))
    setScreen<MainMenuScreen>()
    super.create()
}

Let's check-out some of the code changes including some general coding conventions and Kotlin best practices. This reduces the lines of code needed quite a bit and is easier to read and maintain.
The final code can be checked out with the 01-app branch.

  • We are now using KtxGame instead of Game and by adding the MainMenuScreen via addScreen and setting it via setScreen, our game will start with the MainMenu.
    Also, a call to super.dispose() will automatically call the dispose method of all screens added via addScreen.
    In addition we changed lateinit var batch and font to val by lazy because a batch should not get reassigned and with lazy initialization these resources are really only allocated the first time they are used, which is a nicer solution in my opinion.

    class Game : KtxGame<KtxScreen>() {
        val batch by lazy { SpriteBatch() }
        // use LibGDX's default Arial font
        val font by lazy { BitmapFont() }
    
        override fun create() {
            addScreen(MainMenuScreen(this))
            setScreen<MainMenuScreen>()
            super.create()
        }
    
        override fun dispose() {
            batch.dispose()
            font.dispose()
            super.dispose()
        }
    }
  • We got rid of the init block since the camera can be directly assigned. Using apply also allows us to directly call setToOrtho within a single line.
    Since we are implementing KtxScreen we do not need to override hide, show, pause, resume, resize and dispose anymore.
    With Kotlin we can also use the property access syntax instead of calling the setter methods. E.g. game.batch.setProjectionMatrix(camera.combined) can be simplified to game.batch.projectionMatrix = camera.combined.
    We no longer need to call Gdx.gl.glClearColor and Gdx.gl.glClear because this is already done within KtxGame.
    Finally, we are adding and setting our GameScreen and we remove and dispose our MainMenuScreen since it will no longer be needed.

    class MainMenuScreen(val game: Game) : KtxScreen {
        private var camera = OrthographicCamera().apply { setToOrtho(false, 800f, 480f) }
    
        override fun render(delta: Float) {
            camera.update()
            game.batch.projectionMatrix = camera.combined
    
            game.batch.begin()
            game.font.draw(game.batch, "Welcome to Drop!!! ", 100f, 150f)
            game.font.draw(game.batch, "Tap anywhere to begin!", 100f, 100f)
            game.batch.end()
    
            if (Gdx.input.isTouched) {
                game.addScreen(GameScreen(game))
                game.setScreen<GameScreen>()
                game.removeScreen<MainMenuScreen>()
                dispose()
            }
        }
    }
  • As a last step we also simplify our GameScreen. Similar to the MainMenuScreen we can do all the assignments of our private variables directly and by moving the spawnRainDrop() call from the init block to the show method let's us get rid of the init block. We again use the property access syntax, override only the necessary methods and don't call the glClear methods.
    Next we optimize our spawnRainDrop method by using the Rectangle constructor with four parameters and directly adding it to our raindrops array.
    Thanks to Kotlin we can also avoid the string concatenation of "Drops Collected: " + dropsGathered and change it to a string template "Drops Collected: $dropsGathered".
    There is also an easier way to keep the bucket's x value within its boundaries. We use the MathUtils.clamp(bucket.x, 0f, 800f - 64f) method for it.
    As a last step we change Gdx.graphics.getDeltaTime() to delta which is passed to the Screen's render method and represents the same value.

    class GameScreen(val game: Game) : KtxScreen {
        // load the images for the droplet & bucket, 64x64 pixels each
        private var dropImage = Texture(Gdx.files.internal("images/drop.png"))
        private var bucketImage = Texture(Gdx.files.internal("images/bucket.png"))
        // load the drop sound effect and the rain background music
        private var dropSound = Gdx.audio.newSound(Gdx.files.internal("sounds/drop.wav"))
        private var rainMusic = Gdx.audio.newMusic(Gdx.files.internal("music/rain.mp3")).apply { isLooping = true }
        // The camera ensures we can render using our target resolution of 800x480
        //    pixels no matter what the screen resolution is.
        private var camera = OrthographicCamera().apply { setToOrtho(false, 800f, 480f) }
        // create a Rectangle to logically represent the bucket
        // center the bucket horizontally
        // bottom left bucket corner is 20px above
        private var bucket = Rectangle(800f / 2f - 64f / 2f, 20f, 64f, 64f)
        // create the touchPos to store mouse click position
        private var touchPos = Vector3()
        // create the raindrops array and spawn the first raindrop
        private var raindrops = Array<Rectangle>() // gdx, not Kotlin Array
        private var lastDropTime: Long = 0L
        private var dropsGathered: Int = 0
    
        private fun spawnRaindrop() {
            raindrops.add(Rectangle(MathUtils.random(0f, 800f - 64f), 480f, 64f, 64f))
            lastDropTime = TimeUtils.nanoTime()
        }
    
        override fun render(delta: Float) {
            // generally good practice to update the camera's matrices once per frame
            camera.update()
    
            // tell the SpriteBatch to render in the coordinate system specified by the camera.
            game.batch.projectionMatrix = camera.combined
    
            // begin a new batch and draw the bucket and all drops
            game.batch.begin()
            game.font.draw(game.batch, "Drops Collected: $dropsGathered", 0f, 480f)
            game.batch.draw(bucketImage, bucket.x, bucket.y, bucket.width, bucket.height)
            for (raindrop in raindrops) {
                game.batch.draw(dropImage, raindrop.x, raindrop.y)
            }
            game.batch.end()
    
            // process user input
            if (Gdx.input.isTouched) {
                touchPos.set(Gdx.input.x.toFloat(), Gdx.input.y.toFloat(), 0f)
                camera.unproject(touchPos)
                bucket.x = touchPos.x - 64f / 2f
            }
            if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) {
                bucket.x -= 200 * delta
            }
            if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) {
                bucket.x += 200 * delta
            }
    
            // make sure the bucket stays within the screen bounds
            bucket.x = MathUtils.clamp(bucket.x, 0f, 800f - 64f)
    
            // check if we need to create a new raindrop
            if (TimeUtils.nanoTime() - lastDropTime > 1_000_000_000L) {
                spawnRaindrop()
            }
    
            // move the raindrops, remove any that are beneath the bottom edge of the
            //    screen or that hit the bucket.  In the latter case, play back a sound
            //    effect also
            val iter = raindrops.iterator()
            while (iter.hasNext()) {
                val raindrop = iter.next()
                raindrop.y -= 200 * delta
                if (raindrop.y + 64 < 0)
                    iter.remove()
    
                if (raindrop.overlaps(bucket)) {
                    dropsGathered++
                    dropSound.play()
                    iter.remove()
                }
            }
        }
    
        override fun show() {
            // start the playback of the background music when the screen is shown
            rainMusic.play()
            spawnRaindrop()
        }
    
        override fun dispose() {
            dropImage.dispose()
            bucketImage.dispose()
            dropSound.dispose()
            rainMusic.dispose()
        }
    }

This ends the basic LibKTX application section. We did not cover all the code which is in this class as this will be done in their specific sections.
For example we did not talk about the inject() part of the LoadingScreen parameters. That will be mentioned later on in the inject section.

Clone this wiki locally