Kotlin Game Development: Main Menu

Most of the games have a main menu because we might not always know what the user wants to do when he launches a game. He might want to start a new game, load the saved game data or modify the game settings but we will have a very simple menu in the Snake game which will have only one option: start a new game.

This is the sixth part of the “Kotlin Game Development” series and it’s better if you read it in chronological order:

  1. Part 1 - Introduction
  2. Part 2 - Creating a Scene
  3. Part 3 - Controller Input
  4. Part 4 - Game Loop
  5. Part 5 - Game Factory
  6. Part 6 - Main Menu (you are here)
  7. Part 7 - Model
  8. Part 8 - Game Scene
  9. Part 9 - Finalizing The Game

The main menu is just a Scene so it already has the ability to draw on the screen and it can also handle user input. Let’s see the whole code first:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.bubelov.snake.scene

import com.bubelov.snake.engine.Game
import com.bubelov.snake.engine.Input
import com.bubelov.snake.engine.Scene
import java.awt.Color
import java.awt.Font
import java.awt.Graphics2D
import java.awt.event.KeyEvent

class MainMenuScene(game: Game) : Scene(game) {
    private val primaryFont = Font("Default", Font.BOLD, 30)
    private val secondaryFont = Font("Default", Font.PLAIN, 20)

    override fun update(nanosecondsPassed: Long) {
        game.input.consumeEvents().forEach {
            if (it is Input.Event.KeyPressed && it.data.keyCode == KeyEvent.VK_ENTER) {
                game.scene = GameScene(game)
            }
        }
    }

    override fun draw(graphics: Graphics2D) {
        graphics.apply {
            color = Color.black
            fillRect(0, 0, game.screenSize.width, game.screenSize.height)

            font = primaryFont
            color = Color.white
            val name = "Snake"

            drawString(
                name,
                game.screenSize.width / 2 - fontMetrics.stringWidth(name) / 2,
                game.screenSize.height / 2 - 50
            )

            font = secondaryFont
            color = Color.gray
            val message = "Press Enter to continue"

            drawString(
                message,
                game.screenSize.width / 2 - fontMetrics.stringWidth(message) / 2,
                game.screenSize.height / 2 + 50
            )
        }
    }
}

Properties

Let’s examine all the properties that are declared in this class:

1
2
private val primaryFont = Font("Default", Font.BOLD, 30)
private val secondaryFont = Font("Default", Font.PLAIN, 20)

Both of those properties have the same type: Font. We could have declared them in the draw method because it’s the only method that uses them but we have to keep in mind that the draw method is usually called multiple times per second so it’s extremely wasteful to initialize complex objects each time this method is called.

Update

1
2
3
4
5
6
7
override fun update(nanosecondsPassed: Long) {
    game.input.consumeEvents().forEach {
        if (it is Input.Event.KeyPressed && it.data.keyCode == KeyEvent.VK_ENTER) {
            // game.scene = GameScene(game) TODO Implement later
        }
    }
}

This method is quite straightforward because the only thing we have to do is to scan through all of the keys that were pressed and check if any of them is the enter key. In case the enter key was pressed we should switch to the next screen. You can create an empty scene and call it GameScene to avoid compilation errors.

Draw

Let’s take a look at the draw method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
override fun draw(graphics: Graphics2D) {
    graphics.apply {
        color = Color.black
        fillRect(0, 0, game.screenSize.width, game.screenSize.height)

        font = primaryFont
        color = Color.white
        val name = "Snake"

        drawString(
            name,
            game.screenSize.width / 2 - fontMetrics.stringWidth(name) / 2,
            game.screenSize.height / 2 - 50
        )

        font = secondaryFont
        color = Color.gray
        val message = "Press Enter to continue"

        drawString(
            message,
            game.screenSize.width / 2 - fontMetrics.stringWidth(message) / 2,
            game.screenSize.height / 2 + 50
        )
    }
}

The first line sets the color to Color.black which means whatever we’re going to draw next, it will have the black color. It does not apply to the images of course but it will affect the color of the geometrical primitives as well as fonts.

The second line just fills the screen with the previously specified color.

The next two blocks are more involved but they just repeat the same pattern twice: - set font - set color - draw string

We use different fonts for the game title and for the hint message but we just repeat the same steps, the only difference is in the values.

Testing

Let’s check this scene to see if it works. Now we can assign it to our Game instance in the Main.kt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.bubelov.snake

import com.bubelov.snake.engine.GameFactory
import com.bubelov.snake.scene.MainMenuScene
import java.awt.Dimension

fun main(args: Array<String>) {
    val screenSize = Dimension(660, 660)
    val game = GameFactory.create(screenSize)
    game.scene = MainMenuScene(game)
    game.play()
}

Now let’s launch our game. You should see the following screen:

Main menu

Conclusion

Now we know the basics of creating the scenes and we also have the game menu. In the next piece we’re going to define the game model.