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 (0,0)(0,0) 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.