Skip to content

SDL » SDL1 » Input

Ivan Perez edited this page Oct 5, 2015 · 17 revisions

Input handing in SDL works by polling events from an event queue. Events represent user actions on our focusable area, including mouse interactions (clicks, moving, etc.), key action (pressing, releasing, etc.).

Input events are obtained using the function Graphics.UI.SDL.pollEvent. When the queue is empty and there are no more events left the function returns the value NoEvent.

Events represent changes in the input devices, not the state of the input device itself. For instance, when the mouse is moved you will get a MouseMotion event, but if it isn't moved, no event will tell you the mouse position.

From event to state, and its meaning

To remember the last known mouse position, your game input subsystem should define its own Controller which holds the last known state of the input controller. The Controller does not need to be low-level or match the input device exactly, it can be abstract and adapted to your game. Know, however, that the same input event or controller state may have completely different interpretations during gameplay: for instance, a click on the screen may be interpreted as a menu activation at the beginning, selecting an option later on and firing a gun during the game. The controller will not be the last layer of abstraction between your input subsystem and the game logic, but it will be the first. The controller will also make it easier to abstract your game from SDL-specifics, which will be useful if you later consider using a different version of SDL or a completely different multimedia library.

Input events

Mouse events

SDL provides access to three kinds of mouse events: a mouse button being depressed, a mouse button being released, and the mouse being moved. When both things happen simultaneously (being moved and/or clicked), you will receive two events in no particular order.

Following the above recommendation, we are going to write a controller for a shooting game in which holding the mouse button down means firing continuously.

data Controller = Controller
 { gunFiring :: Bool
 , gunPos    :: (Int, Int)
 }

defaultController :: Controller
defaultController = Controller False (0,0)

-- SDL controller-updating function
updateController :: Controller -> IO Controller
updateController c = do
  ev <- SDL.pollEvent
  case ev of
    -- Mouse interaction
    SDL.MouseButtonUp _ _ SDL.ButtonLeft
      -> updateController (c { gunFiring = False })
    SDL.MouseButtonDown _ _ SDL.ButtonLeft
      -> updateController (c { gunFiring = True  })
    SDL.MouseMotion x y _ _
       -> updateController (c { gunPos = (fromIntegral x, fromIntegral y) })

    -- End of queue or any other event
    SDL.NoEvent -> return c
    _           -> updateController c  -- Discard other events

The function updateController we have written takes care of polling the status of the event queue as necessary and updating the know state of the input device. We can now use this function to write a program that will draw a blue circle where the mouse is, and turn it red when the mouse is being depressed.

import Graphics.UI.SDL            as SDL
import Graphics.UI.SDL.Primitives as SDL

main :: IO ()
main = do
  SDL.init [InitVideo, InitInput]
  SDL.setVideoMode width height 32 [SWSurface]
  SDL.setCaption "Input test" ""

  gameLoop defaultController

width  = 480
height = 320

gameLoop :: Controller -> IO ()
gameLoop c = do
  -- Sense
  c' <- updateController c

  -- Advance game state: Nothing to do here

  -- Render
  render c'

  -- Loop
  gameLoop c'

render :: Controller -> IO ()
render controller = do
  screen <- getVideoSurface

  -- 1) Green background
  let format = SDL.surfaceGetPixelFormat screen
  green <- SDL.mapRGB format 0 0xFF 0
  SDL.fillRect screen Nothing green

  -- 2) Gun
  let (x,y) = gunPos controller
      color = if gunFiring controller
                then Pixel 0xFF0000FF -- red  (alpha 255)
                else Pixel 0x0000FFFF -- blue (alpha 255)
  SDL.filledCircle screen (fromIntegral x) (fromIntegral y) 30 color

  SDL.flip screen

As you can see, our game loop calls updateController, but otherwise it is completely input-agnostic and knows nothing about SDL events. You could even use a completly different input mechanism and gameLoop would remain unchanged. This kind of modularity and separation of concerns is extremely important in software, and games are no exception. In real games, our input will most likely be configurable, which will mean that there will even be an extra layer between the Controller and SDL.

Keyboard events

Keyboard events are very similar to mouse button presses. Keyboard events carry a Keysym, which contains information about the key being depressed, the modifiers that depressed at the same time (Control, Shift, etc.) and the character that the key should produce.

To understand how they work, we are going to change the program above to use the keyboard to move the circle instead of using the mouse. To do that, we change only the function updateController, which will now update the position. Note that, because using keys we might move out of the screen, we make sure that the position is within screen boundaries. Note, however, that if the window is resized, your screen boundaries may change and the input subsystem should know. SDL notifies of window resizes using events, so it is trivial to adapt the code in this case, but it may not always be like this in all multimedia libraries.

-- SDL controller-updating function
  updateController :: Controller -> IO Controller
  updateController c = do
    ev <- SDL.pollEvent
    case ev of
      SDL.NoEvent -> return (withinScreenBoundaries c)

      -- Movement
      SDL.KeyDown (Keysym SDLK_LEFT  _ _)
        -> updateController (c { gunPos = ((-1, 0) ^+^ gunPos c)})
      SDL.KeyDown (Keysym SDLK_UP    _ _)
        -> updateController (c { gunPos = ((0, -1) ^+^ gunPos c)})
      SDL.KeyDown (Keysym SDLK_DOWN  _ _)
        -> updateController (c { gunPos = ((0,  1) ^+^ gunPos c)})
      SDL.KeyDown (Keysym SDLK_RIGHT _ _)
        -> updateController (c { gunPos = ((1,  0) ^+^ gunPos c)})

      -- Fire
      SDL.KeyDown (Keysym SDLK_SPACE _ _)
        -> updateController (c { gunFiring = True})
      SDL.KeyUp   (Keysym SDLK_SPACE _ _)
        -> updateController (c { gunFiring = False})

      -- Anything else
      _ -> updateController c  -- Discard any other event

  (^+^) :: Num a => (a, a) -> (a, a) -> (a, a)
  (^+^) (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

withinScreenBoundaries :: Controller -> Controller
withinScreenBoundaries c = c { gunPos = (x', y') }
 where x' = inRange (0, width)  (fst (gunPos c))
       y' = inRange (0, height) (snd (gunPos c))

inRange :: Ord a => (a,a) -> a -> a
inRange (mn, mx) v
 | v < mn    = mn
 | v > mx    = mx
 | otherwise = v

Note that, to simplify our code, we have created three auxiliary functions: one to keep the controller position within screen boundaries, one to add vectors, and one to return the closest number within a subrange.

If you try the code above, you will see that the program now indeed moves one pixel every time a key is depressed, but it does not keep moving when the key is held down. It is very slow, barely noticeable. To address this problem, the controller needs to know when each button is depressed, and then we need to our game logic updating function to transform the controller state into a game state:

data Controller = Controller
 { gunFiring :: Bool
 , gunLeft   :: Bool
 , gunRight  :: Bool
 , gunUp     :: Bool
 , gunDown   :: Bool
 }

defaultController :: Controller
defaultController = Controller False False False False False

Our controller sensing function is now a bit simpler, although more verbose:

-- SDL controller-updating function
updateController :: Controller -> IO Controller
updateController c = do
  ev <- SDL.pollEvent
  case ev of
    SDL.NoEvent -> return c
    -- Movement
    SDL.KeyDown (SDL.Keysym SDLK_LEFT  _ _) -> updateController (c { gunLeft  = True  })
    SDL.KeyUp   (SDL.Keysym SDLK_LEFT  _ _) -> updateController (c { gunLeft  = False })
    SDL.KeyDown (SDL.Keysym SDLK_UP    _ _) -> updateController (c { gunUp    = True  })
    SDL.KeyUp   (SDL.Keysym SDLK_UP    _ _) -> updateController (c { gunUp    = False })
    SDL.KeyDown (SDL.Keysym SDLK_DOWN  _ _) -> updateController (c { gunDown  = True  })
    SDL.KeyUp   (SDL.Keysym SDLK_DOWN  _ _) -> updateController (c { gunDown  = False })
    SDL.KeyDown (SDL.Keysym SDLK_RIGHT _ _) -> updateController (c { gunRight = True  })
    SDL.KeyUp   (SDL.Keysym SDLK_RIGHT _ _) -> updateController (c { gunRight = False })

    -- Fire
    SDL.KeyDown (SDL.Keysym SDLK_SPACE _ _) -> updateController (c { gunFiring = True})
    SDL.KeyUp   (SDL.Keysym SDLK_SPACE _ _) -> updateController (c { gunFiring = False})

    -- Anything else
    _ ->
      updateController c  -- Discard any other event

Our game will now hold the position of the gun and whether it is being fired in an internal game state:

data GameState = GameState
  { gsGamePos  :: (Int, Int)
  , gsGameFire :: Bool
  }

The game state now needs to be updated from the previous state, and the state of the controller. We turn every controller button into a vector with the displacement to be applied, and then we add all those vectors to the previous position. We use a slightly modified version of withinScreenBoundaries to ensure that our player is always in the screen.

updateGameLogic :: Controller -> GameState -> GameState
updateGameLogic c gs = gs'
 where gs' = gs { gsGameFire = gunFire c
                , gsGamePos  = withinScreenBoundaries (vtotal ^+^ gsGamePos gs)
                }

       -- Displacement caused by input controller state
       vtotal = vl ^+^ vr ^+^ vu ^+^ vd

       -- Displacement caused by controller in each direction
       vl = if gunLeft  c then (-1, 0) else (0, 0)
       vr = if gunRight c then (1,  0) else (0, 0)
       vu = if gunUp    c then (0, -1) else (0, 0)
       vd = if gunDown  c then (0,  1) else (0, 0)

-- Write this for the game state instead
withinScreenBoundaries :: (Int, Int) -> (Int, Int)
withinScreenBoundaries (x, y) = (x', y')
 where x' = inRange (0, width)  x
       y' = inRange (0, height) y

To make this work you would also need to adapt gameLoop, which would now receive the initial game state when called from main, and pass the new game state at every iteration:

main :: IO ()
main = do
  SDL.init [InitVideo, InitInput]
  SDL.setVideoMode width height 32 [SWSurface]
  SDL.setCaption "Input test" ""

  gameLoop emptyController (GameState (0,0) False)

gameLoop :: Controller -> GameState ->  IO ()
gameLoop c gs = do
  -- Sense
  c' <- updateController c

  -- Advance game state
  let gs' = updateGameLogic c gs

  -- Render
  render gs'

  -- Loop
  gameLoop c' gs'

render :: GameState -> IO ()
render gs = do
  screen <- getVideoSurface

  -- 1) Green background
  let format = SDL.surfaceGetPixelFormat screen
  green <- SDL.mapRGB format 0 0xFF 0
  SDL.fillRect screen Nothing green

  -- 2) Gun
  let (x,y) = gsGamePos gs
      color = if gsGameFire gs
                 then Pixel 0xFF0000FF -- red  (alpha 255)
                 else Pixel 0x0000FFFF -- blue (alpha 255)
  SDL.filledCircle screen (fromIntegral x) (fromIntegral y) 30 color

  SDL.flip screen

Note that, in the rendering function, all we have done is to gather the data from the game state instead of using the controller.

Homework

To be completed

Clone this wiki locally