Kotlin Game Development: Controller Input

Controllers are devices that can register players’ actions and we want our players to be able to affect the course of a game, otherwise it’s just a fancy movie. In more technical terms it means that every game has a state and the game controllers are devices that can be used to alter that state.

This is the third 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 (you are here)
  4. Part 4 - Game Loop
  5. Part 5 - Game Factory
  6. Part 6 - Main Menu
  7. Part 7 - Model
  8. Part 8 - Game Scene
  9. Part 9 - Finalizing The Game

There are lots of different input devices: typical console games tend to rely on gamepads, flight or racing simulators can support their own unique controllers but we’re writing a PC game so we’ll stick with the keyboard: the most popular input device used to interact with personal computers.

It makes sense to also support the computer mouse and trackpad but we don’t really need those devices to control a snake so we can do it later. It will also make the code a bit more simple and it’s always better not to add any unnecessary complexity.

It’s Coding Time

Let’s add a new class and call it Input:

 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
package com.bubelov.snake.engine

import java.awt.event.KeyEvent
import java.awt.event.KeyListener

class Input : KeyListener {
    private val events = mutableListOf<Event>()

    override fun keyTyped(e: KeyEvent) {
        // We're not interested in this kind of events
    }

    override fun keyPressed(e: KeyEvent) {
        synchronized(this) {
            events += Event.KeyPressed(e)
        }
    }

    override fun keyReleased(e: KeyEvent) {
        synchronized(this) {
            events += Event.KeyReleased(e)
        }
    }

    fun consumeEvents(): List<Event> {
        synchronized(this) {
            val consumedEvents = events.toList()
            events.clear()
            return consumedEvents
        }
    }

    sealed class Event {
        data class KeyPressed(val data: KeyEvent) : Event()
        data class KeyReleased(val data: KeyEvent) : Event()
    }
}

How Does It Work?

Let’s take a closer look at our Input class. It implements the KeyListener interface which allows it to be notified of any keyboard events that happen inside the game window.

There are 3 methods we have to listen to in order to implement the KeyListener interface:

  • keyTyped - we don’t need those events so we will ignore them

  • keyPressed - this method is being called each time our player presses a button on a keyboard. We want to know about that because we need to save this event for later processing

  • keyReleased - the player released a button, it can be valuable so we need to save it too. Sometimes we want to do something while a certain button is pressed so we need to know when to start as well as when to stop such actions (think of a gas pedal or something like that)

Note that we just store those events for later use so we expect some other component to actually react to them. I’ve called it event consumption because once the external component reads the available events - they are gone. It’s more straightforward because it helps us to avoid processing the same event twice because it will never return twice but it also assumes that we have a single consumer, otherwise such a model wouldn’t work in a predictable way.

Why Synchronize?

Probably you’ve noticed that all of the code that touches the events collection is placed inside the synchronized blocks. The reason is that our game loop is using it’s own thread but our game window is a part of the Swing library which uses a different thread so we have to be extra cautious about that. Concurrent modification of a collection is a thing we would like to avoid since it will crash our game from time to time or introduce a lot of weird and hard to trace side effects.

So what exactly are the synchronized blocks used for? They guarantee that the code inside them will never be used by more than one thread at the same time. We can also use this keyword for several methods inside a class and that would guarantee that only one thread can access any of those methods at any given moment. This of course leads to some performance drawbacks but it will guarantee that our class will work properly in a concurrent environment and we can actually use more advanced synchronization techniques but we won’t be using them in this tutorial to keep the code as simple as possible.

Conclusion

Now we have the Scene and the Input classes done. In the next piece we’ll create the key element of our mini engine - the Game class. It will utilize and tie together the code we created earlier.