Using direct checks against Input
for certain keys that have been pressed or if a certain button on a gamepad was used and then doing an action as a result of that event can work just fine.
But what if we wanted to define our own input types that can have keys, gamepad buttons, and such bound to that one type so that when a player uses that action we can react without having to do all of the verbose checks.
InputMapController
The InputMapController is a class that handles key, game buttons, and game axis inputs and converts them into a single custom input signal type. The controller is also an InputProcessor.
Using the controller is easy and a one time setup. All we have to do is define our own InputType
that we want to use as the signal and then add use it to add bindings to the controller.
Create an InputMapController
For this example, we are going to create bindings that allows the user to run around using either the WASD, the arrow keys, or a gamepad along with adding a simple jump binding.
The first thing we need to do is create our own input type. For this we are just going to create an enum with a few values. A value for each action we want to define. We can also add values for defining certain axis and vectors (2 axis combined).
enum class GameInput {
MOVE_LEFT,
MOVE_RIGHT,
MOVE_UP,
MOVE_DOWN,
HORIZONTAL, // an axis
VERTICAL, // an axis
MOVEMENT, // movement vector
JUMP
}
Next is to create the InputMapController
and then adding the bindings. We must also be sure to add the instance of the controller to the list of input processors using Context.input
.
val controller = InputMapController<GameInput>(input).also {
input.addInputProcessor(it)
}
Create a Binding
Now that we have our controller setup we can finally create a binding. We can use the addBinding()
method on the controller by passing in the input type value and passing in a few list of bindings from keyboard and gamepad.
// the 'A' and 'left arrow' keys and the 'x-axis of the left stick' with trigger the 'MOVE_LEFT' input type
controller.addBinding(GameInput.MOVE_LEFT, listOf(Key.A, Key.ARROW_LEFT), axes = listOf(GameAxis.LX))
// now lets define the rest of the movement
controller.addBinding(GameInput.MOVE_RIGHT, listOf(Key.D, Key.ARROW_RIGHT), axes = listOf(GameAxis.LX))
controller.addBinding(GameInput.MOVE_UP, listOf(Key.W, Key.ARROW_UP), axes = listOf(GameAxis.LY))
controller.addBinding(GameInput.MOVE_DOWN, listOf(Key.S, Key.ARROW_DOWN), axes = listOf(GameAxis.LY))
// define jumping with a key and a gamepad button
controller.addBinding(GameInput.JUMP, listOf(Key.SPACE), buttons = listOf(GameButton.XBOX_A))
Use a Binding
We can now use the binding, similarly to how we would check if a key was used normally, by checking if the input signal is “pressed” or “down”.
// check if JUMP is pressed so we can make our hero jump
if (controller.pressed(GameInput.JUMP)) {
hero.y -= 25f
}
We can use the following methods:
down()
: checks to see if the input type is currently “down” for any inputs. This doesn’t trigger for aGameAxis
.pressed()
: checks to see if the input type was just “pressed” for any inputs. This doesn’t trigger for aGameAxis
.released()
: checks to see if the input type was just “released” for any inputs. This doesn’t trigger for aGameAxis
.strength()
: returns the strength / pressure of the input type. AKey
or aGameButton
will return either a-1
,0
, or a1
. AGameAxis
will return anything between-1
to1
.dist()
: takes the absolute value ofstrength()
Create an Axis
If we are using inputs that share the same axis (horizontally or vertically) we could combine these inputs into a single axis which we then can query. For our movement code, we have the HORIZONTAL
and VERTICAL
axes defined in our GameInput
enum. We must define the input types that determine the postive and negative directions of our axis. For example, for our horizontal axis, the postive side would be to the right and the negative side to the left which matches the coordinate system.
// creates an axis based off the RIGHT and LEFT input types
controller.addAxis(GameInput.HORIZONTAL, GameInput.MOVE_RIGHT, GameInput.MOVE_LEFT)
// creates an axis based off the DOWN and UP input types
controller.addAxis(GameInput.VERTICAL, GameInput.MOVE_DOWN, GameInput.MOVE_UP)
Using an Axis
We can query the strength of an axis which will returns a float which a value between -1
and 1
.
val horizStrength = controller.axis(GameInput.HORIZONTAL)
val vertStrength = controller.axis(GameInput.VERTICAL)
// using the axes to move our hero
hero.x += horizStrength * speed
hero.y += vertStrength * speed
Create a Vector
We can take it another step further we can take two axes that are related and combine them to create a vector. This vector is just the calculation of the strength of each invidiual axis combined and returns the result as a Vec2f
. We need to define the postive and negative directions for each axis.
controller.addVector(
GameInput.MOVEMENT,
GameInput.MOVE_RIGHT,
GameInput.MOVE_DOWN,
GameInput.MOVE_LEFT,
GameInput.MOVE_UP
)
Using a Vector
We can query the strength of the vector which returns a Vec2f
with the xy
having a value between -1
and 1
.
val dir = controller.vector(GameInput.MOVEMENT)
hero.x += dir.x * speed
hero.y += dir.y * speed
All together now
If you have been following the above, you should have something that looks like this:
enum class GameInput {
MOVE_LEFT,
MOVE_RIGHT,
MOVE_UP,
MOVE_DOWN,
HORIZONTAL, // an axis
VERTICAL, // an axis
MOVEMENT, // movement vector
JUMP
}
val controller = InputMapController<GameInput>(input).also {
input.addInputProcessor(it)
}
init {
controller.addBinding(GameInput.MOVE_LEFT, listOf(Key.A, Key.ARROW_LEFT), axes = listOf(GameAxis.LX))
controller.addBinding(GameInput.MOVE_RIGHT, listOf(Key.D, Key.ARROW_RIGHT), axes = listOf(GameAxis.LX))
controller.addBinding(GameInput.MOVE_UP, listOf(Key.W, Key.ARROW_UP), axes = listOf(GameAxis.LY))
controller.addBinding(GameInput.MOVE_DOWN, listOf(Key.S, Key.ARROW_DOWN), axes = listOf(GameAxis.LY))
controller.addBinding(GameInput.JUMP, listOf(Key.SPACE), listOf(GameButton.XBOX_A))
controller.addVector(
GameInput.MOVEMENT,
GameInput.MOVE_RIGHT,
GameInput.MOVE_DOWN,
GameInput.MOVE_LEFT,
GameInput.MOVE_UP
)
}
override suspend fun Context.start() {
onUpdate { dt ->
val dir = controller.vector(GameInput.MOVEMENT)
// ...
}
}