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.
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 {
[]uint8
keyboardState }
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{
: sdl.GetKeyboardState(),
keyboardState}
}
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() {
.PumpEvents()
sdl}
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 (
= iota + sdl.SCANCODE_A
KeyboardA
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 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 {
[]uint8
keyboardState uint32
mouseState int32
mouseX int32
mouseY }
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() {
.PumpEvents()
sdl.mouseX, s.mouseY, s.mouseState = sdl.GetMouseState()
s}
Mouse button state is enumerated as follows:
type MouseButton int
const (
= iota
MouseLeft MouseButton
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]{
: float32(s.mouseX),
X: float32(h - s.mouseY),
Y}
}
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.
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 {
*sprite.Atlas
Atlas .Shader
Shader shaders*platform.InputState // NEW
Input }
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.
:= time.Now()
timer for {
:= time.Since(timer)
dt = time.Now()
timer .Input.GetState() // NEW
Game.Update(dt)
level.Render(renderer)
level.Render()
renderer}
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) {
.SetTranslation(k.GetTranslation().Add(math.Vector3[float32]{0,1,0}))
k}
// ...
.animation.Update(dt, k)
k}
More succinctly, we can update all directions by utilizing the
KeyValue
method.
func (k *Knight) Update(dt time.Duration) {
:= float32(dt.Seconds())
fdt
// keyboard movement
:= math.Vector2[float32]{
moveVector : Game.Input.KeyValue(platform.KeyboardD) - Game.Input.KeyValue(platform.KeyboardA),
X: Game.Input.KeyValue(platform.KeyboardW) - Game.Input.KeyValue(platform.KeyboardR),
Y}
.SetTranslation(k.GetTranslation().Add(moveVector.Scale(50 * fdt).AddZ(0)))
k
.animation.Update(dt, k)
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.
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 {
.animation.SetClip(k.runClip)
k} else {
.animation.SetClip(k.idleClip)
k}
Notice that we don’t check the vector’s length against zero to
avoid situations such as
moveVector.Length()=0.0000001
.
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() {
.SetRotation(math.Vector3[float32]{0, 3.14, 0})
k}
func (k *Knight) faceRight() {
.SetRotation(math.Vector3[float32]{0, 0, 0})
k}
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 {
.faceLeft()
k} else if moveVector.X > 0 {
.faceRight()
k}
If we had up/down facing sprites we would use a similar method to switch the two.
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.
= Game.Input.MousePosition().Sub(k.GetTranslation().XY()).Normalize()
moveVector .SetTranslation(k.GetTranslation().Add(moveVector.Scale(50 * fdt).AddZ(0))) k
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) {
= Game.Input.MousePosition().Sub(k.GetTranslation().XY()).Normalize()
moveVector .SetTranslation(k.GetTranslation().Add(moveVector.Scale(50 * fdt).AddZ(0)))
k}
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.
:= math.Vector2[float32]{}
moveVector if Game.Input.MouseButtonDown(platform.MouseLeft) {
.destination = Game.Input.MousePosition()
k.hasDestination = true
k}
if k.hasDestination {
= k.destination.Sub(k.GetTranslation().XY()).Normalize()
moveVector .SetTranslation(k.GetTranslation().Add(moveVector.Scale(50 * fdt).AddZ(0)))
k}
if k.GetTranslation().Sub(k.destination.AddZ(0)).Length() < math.EpsilonLax {
.hasDestination = false
k}
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.
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.Duration
ShowTimer time.GameObjectCommon
game}
func NewClickEffect() *ClickEffect {
:= ClickEffect{}
c , err := sprite.RgbaFromFile("data/pointer.png")
spriteImg(err)
panicOnError, _ := Game.Atlas.AddImage(spriteImg)
spriteId.sprite, _ = sprite.NewSprite(spriteId, Game.Atlas, &Game.Shader, 0)
c.sprite.SetScale(c.sprite.GetScale().Scale(2))
creturn &c
}
func (ce *ClickEffect) Update(dt time.Duration) {
if ce.ShowTimer > 0 {
.sprite.SetPosition(ce.GetTranslation())
ce.ShowTimer -= dt
ce}
}
func (ce *ClickEffect) Render(r *sprite.Renderer) {
if ce.ShowTimer > 0 {
.QueueRender(&ce.sprite)
r}
}
In knights constructor, we create a ClickEffect and add it as a child. We keep a reference to it on knight for easy access.
.clickEffect = NewClickEffect()
knight.AddChild(knight.clickEffect) knight
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) {
.destination = Game.Input.MousePosition()
k.hasDestination = true
k.clickEffect.ShowTimer = time.Second / 4
k.clickEffect.SetTranslation(k.destination.AddZ(0))
k}
//...
}
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.