Input
In this tutorial we will see how to read inputs from the keyboard and mouse. We will update our Knight vs Trolls game-in-progress to make our knight move with both the keyboard and mouse. This tutorial does not cover game controllers. They will be covered in a later tutorial.
Keyboard
Our keyboard input processing code resolves around the
InputState
struct which stores an array with the state of
every keyboard button. Each entry in the array corresponds to one
keyboard key and the mapping is given by SDL. Each entry can be zero
(not pressed) or one (pressed).
type InputState struct {
keyboardState []uint8
}
InputState is initialized in NewInputState
with the
sdl.GetKeyboardState()
function. GetKeyboardState returns a
pointer to SDL’s internal state. When that state is update by SDL,
keyboardState
will also reflect the changes.
func NewInputState() *InputState {
return &InputState{
keyboardState: sdl.GetKeyboardState(),
}
}
The keyboard state must be updated frequently with the
GetState
method. It simply calls
sdl.PumpEvents()
which updates SDL’s internal state. We are
calling a function that updates some hidden state that we have a access
to through a pointer. It’s a bit spaghetti but thankfully it is hidden
from the end user. A good strategy is to call GetState
on
every frame of our game loop.
func (s *InputState) GetState() {
sdl.PumpEvents()
}
We check if a button is pressed using the KeyDown
method. It accepts a KeyboardKey
which directly maps into
the keyboardState
array.
func (s *InputState) KeyDown(key KeyboardKey) bool {
return s.keyboardState[key] > 0
}
KeyboardKey
values are int enumerations given by SDL. We
remap these into our own naming scheme:
type KeyboardKey int
const (
KeyboardA = iota + sdl.SCANCODE_A
KeyboardB
KeyboardC
KeyboardD
KeyboardE
KeyboardF
...
To check if the ‘e’ button is pressed we would use
stateManager.KeyDown(KeyboardE)
. We also provide a similar
KeyValue
method that returns 1/0 instead of true/false.
This is convenient when the input directly affects some value such a
transform.
func (s *InputState) KeyValue(key KeyboardKey) float32 {
return float32(s.keyboardState[key])
}
Mouse
Mouse input handling is similar to the keyboard. We need to store the state of the mouse buttons and the position of the cursor.
type InputState struct {
keyboardState []uint8
mouseState uint32
mouseX int32
mouseY int32
}
SDL stores the state of all mouse buttons in a single variable using
bit masking (such are the efficiencies of C code!). The state and mouse
position is updated using sdl.GetMouseState
which we add to
the GetState
method.
func (s *InputState) GetState() {
sdl.PumpEvents()
s.mouseX, s.mouseY, s.mouseState = sdl.GetMouseState()
}
Mouse button state is enumerated as follows:
type MouseButton int
const (
MouseLeft MouseButton = iota
MouseMiddle
MouseRight
MouseButtonDown
MouseButtonUp
)
To get the state of a specific mouse button we use
MouseButtonDown
. It checks the corresponding bit in the
mouseState
variable and returns true if it is set.
func (s *InputState) MouseButtonDown(button MouseButton) bool {
return (1<<button)&s.mouseState != 0
}
To get the mouse position we use:
func (s *InputState) MousePosition() math.Vector2[float32] {
_, h := window.GetSize()
return math.Vector2[float32]{
X: float32(s.mouseX),
Y: float32(h - s.mouseY),
}
}
MousePosition
reverses the Y coordinate that increases
from top to bottom in SDL land but is bottom-to-top in the rest of our
code.
Moving Knight
Let’s add movement to our knight using the keyboard. We will use last
tutorial’s code as a starting point. Before we code knight’s movement we
must include input processing to our game. In our main file we will add
a InputState
to our global game struct.
var Game struct {
Atlas *sprite.Atlas
Shader shaders.Shader
Input *platform.InputState // NEW
}
And we will initialize it somewhere in main with
Game.Input = platform.NewInputState()
. In our game loop we
will call GetState
to update the keyboard and mouse state
on each frame.
timer := time.Now()
for {
dt := time.Since(timer)
timer = time.Now()
Game.Input.GetState() // NEW
level.Update(dt)
level.Render(renderer)
renderer.Render()
}
We are now ready to code knight’s movement. In the update method we can conditionally change the knight’s transform based on keyboard presses. For example, we move the knight up when the W key is pressed:
func (k *Knight) Update(dt time.Duration) {
if Game.Input.KeyDown(platform.KeyboardW) {
k.SetTranslation(k.GetTranslation().Add(math.Vector3[float32]{0,1,0}))
}
// ...
k.animation.Update(dt, k)
}
More succinctly, we can update all directions by utilizing the
KeyValue
method.
func (k *Knight) Update(dt time.Duration) {
fdt := float32(dt.Seconds())
// keyboard movement
moveVector := math.Vector2[float32]{
X: Game.Input.KeyValue(platform.KeyboardD) - Game.Input.KeyValue(platform.KeyboardA),
Y: Game.Input.KeyValue(platform.KeyboardW) - Game.Input.KeyValue(platform.KeyboardR),
}
k.SetTranslation(k.GetTranslation().Add(moveVector.Scale(50 * fdt).AddZ(0)))
k.animation.Update(dt, k)
}
This creates a move vector that has values of -1 to 1 on X and Y and
this vector drives the knight’s transform. We multiply the move vector
by 50 times the loop time dt
. This ensures that the knight
moves 50 pixels per second and is not affected by fluctuations in game
speed. See the game loop tutorial for
how this works.
Switching Animation Clips
In the animation component tutorial we gave our knight an idle animation and a run animation. By default the knight plays the idle clip. We would like to change this so it switches to the run clip when the knight is moving but changes back to idle when the knight stops.
We can use the moveVector
to achieve this. If the move
vector is
our knight is at rest either because the player is not pressing any keys
or they are pressing both opposites at the same time. In this case the
length of moveVector
will be zero. If the knight is moving
the move vector will have some length. Using this we can switch the
animation clip as needed.
if moveVector.Length() > math.EpsilonLax {
k.animation.SetClip(k.runClip)
} else {
k.animation.SetClip(k.idleClip)
}
Notice that we don’t check the vector’s length against zero to avoid
situations such as moveVector.Length()=0.0000001
.
Switching Sides
A problem with our current implementation is that the knight always faces right even when moving to the left. Although moon walking is cool, our knight can’t attack enemies behind his back so we have to fix this. Fortunately the fix is easy. We introduce two functions to turn the knight left and right. They simply rotate the knight 180 degrees (Pi radians) on the Y axis.
func (k *Knight) faceLeft() {
k.SetRotation(math.Vector3[float32]{0, 3.14, 0})
}
func (k *Knight) faceRight() {
k.SetRotation(math.Vector3[float32]{0, 0, 0})
}
Then, in our update we check where the knight is facing using the
sign of moveVector.X
and call the appropriate face
method:
if moveVector.X < 0 {
k.faceLeft()
} else if moveVector.X > 0 {
k.faceRight()
}
If we had up/down facing sprites we would use a similar method to switch the two.
Mouse Movement
We can make the knight follow the mouse position by setting the moveVector to be the vector that starts at knight and ends at the cursor. We then translate using that vector in the same way as with the keyboard.
moveVector = Game.Input.MousePosition().Sub(k.GetTranslation().XY()).Normalize()
k.SetTranslation(k.GetTranslation().Add(moveVector.Scale(50 * fdt).AddZ(0)))
We normalize the move vector so that the movement is constant. We could instead use the length of the vector to have the knight move faster the further they are from the cursor. As is, the knight always moves to the cursor. More realistically we should move when a mouse button is pressed. The following only moves the knight if the left mouse button is held:
if Game.Input.MouseButtonDown(platform.MouseLeft) {
moveVector = Game.Input.MousePosition().Sub(k.GetTranslation().XY()).Normalize()
k.SetTranslation(k.GetTranslation().Add(moveVector.Scale(50 * fdt).AddZ(0)))
}
We can also make it so that the user only clicks once and the knight travels there. To do that we need to record the destination in a variable. We then move the knight to the destination until we arrive.
moveVector := math.Vector2[float32]{}
if Game.Input.MouseButtonDown(platform.MouseLeft) {
k.destination = Game.Input.MousePosition()
k.hasDestination = true
}
if k.hasDestination {
moveVector = k.destination.Sub(k.GetTranslation().XY()).Normalize()
k.SetTranslation(k.GetTranslation().Add(moveVector.Scale(50 * fdt).AddZ(0)))
}
if k.GetTranslation().Sub(k.destination.AddZ(0)).Length() < math.EpsilonLax {
k.hasDestination = false
}
We use a boolean, hasDestination
, to start and stop the
movement. We set hasDestination
to true when we click with
the mouse. While it is true the knight’s transform is updated to move
closer to destination. When we get very close to the destination we set
hasDestination
to false which stops the movement.
Click Effect
Let’s show a quick effect when our player clicks somewhere to move
the knight. Our click effect is a simple game object that shows a
sprite. It has a single exposed variable ShowTimer
that
other game objects can write. When set to some duration, the effect is
shown for the duration. The effect’s location is set by changing the
game object’s transform.
type ClickEffect struct {
sprite sprite.Sprite
ShowTimer time.Duration
game.GameObjectCommon
}
func NewClickEffect() *ClickEffect {
c := ClickEffect{}
spriteImg, err := sprite.RgbaFromFile("data/pointer.png")
panicOnError(err)
spriteId, _ := Game.Atlas.AddImage(spriteImg)
c.sprite, _ = sprite.NewSprite(spriteId, Game.Atlas, &Game.Shader, 0)
c.sprite.SetScale(c.sprite.GetScale().Scale(2))
return &c
}
func (ce *ClickEffect) Update(dt time.Duration) {
if ce.ShowTimer > 0 {
ce.sprite.SetPosition(ce.GetTranslation())
ce.ShowTimer -= dt
}
}
func (ce *ClickEffect) Render(r *sprite.Renderer) {
if ce.ShowTimer > 0 {
r.QueueRender(&ce.sprite)
}
}
In knights constructor, we create a ClickEffect and add it as a child. We keep a reference to it on knight for easy access.
knight.clickEffect = NewClickEffect()
knight.AddChild(knight.clickEffect)
In Knight’s update method, whenever we click the mouse button we also
enable the effect by setting its ShowTimer
. We also set the
effect’s transform to where we clicked.
func (k *Knight) Update(dt time.Duration) {
//...
if Game.Input.MouseButtonDown(platform.MouseLeft) {
k.destination = Game.Input.MousePosition()
k.hasDestination = true
k.clickEffect.ShowTimer = time.Second / 4
k.clickEffect.SetTranslation(k.destination.AddZ(0))
}
//...
}
This will make the click effect appear for a quarter of a second at the position where we clicked. We could have also made click effect play an animation by adding an animation component to it. Also, instead of having the effect play for a fixed duration we could have it toggle on and off. It could be set on when the player clicks and off when the knight reaches their destination. These exercises are left for the reader.