Search code examples
haskellsdlmonads

How to draw randomly-placed rectangles in Haskell's SDL2 bindings?


I'm trying to draw randomly-positioned rectangles in SDL2 using Haskell. The IO monad is really driving me insane and I can't get it to work. I have the boilerplate SDL2 code ready to go and a custom datatype for the RectangleObject, and all I want to do is randomly generate the rectangles in the main function, then draw them inside the appLoop function.

I get the error:

BadRandomRectangles.hs:17:61-85: error:
    • Couldn't match expected type: [RectangleObject]
                  with actual type: IO [RectangleObject]
    • In the fourth argument of ‘GameState’, namely
        ‘(createRandomRectangles 10)’
      In the expression:
        GameState
          (V2 10 10) (V2 40 40) (V2 1 1) (createRandomRectangles 10)
      In an equation for ‘gameState’:
          gameState
            = GameState
                (V2 10 10) (V2 40 40) (V2 1 1) (createRandomRectangles 10)
   |
17 |   let gameState = GameState (V2 10 10) (V2 40 40) (V2 1 1) (createRandomRectangles 10) in
   |                                                             ^^^^^^^^^^^^^^^^^^^^^^^^^

My code is:

{-# LANGUAGE OverloadedStrings #-}

import SDL
import System.Random
import Control.Monad (unless, replicateM)
import Foreign.C.Types (CInt)

data RectangleObject = RectangleObject { rectPosn :: V2 CInt, rectDim :: V2 CInt };

data GameState = GameState { posn :: V2 CInt, dim :: V2 CInt, vel :: V2 CInt, objects :: [RectangleObject] };

main :: IO ()
main = do
  initializeAll
  window <- createWindow "Rectangles" (defaultWindow { windowInitialSize = V2 640 480 })
  renderer <- createRenderer window (-1) defaultRenderer
  let gameState = GameState (V2 10 10) (V2 40 40) (V2 1 1) (createRandomRectangles 10) in
    appLoop renderer gameState
  destroyRenderer renderer
  destroyWindow window
  quit

appLoop :: Renderer -> GameState -> IO ()
appLoop renderer gameState = do
  events <- pollEvents

  clear renderer
  rendererDrawColor renderer $= V4 0 0 0 255

  -- Get the RectangleObjects from the game state and draw them
  let objs = objects gameState
  mapM_ (\(RectangleObject posn dim) -> fillRect renderer (Just $ Rectangle (P $ posn) dim)) objs

  -- Fill a rectangle of size 1280x720 with black color
  fillRect renderer (Just $ Rectangle (P $ V2 0 0) (V2 1280 720))
  rendererDrawColor renderer $= V4 255 0 0 255

  fillRect renderer (Just $ Rectangle (P $ posn gameState) (dim gameState))
  present renderer
  SDL.delay 16
  unless qPressed (appLoop renderer (updateGameState vGameState))

updateGameState :: GameState -> GameState
updateGameState (GameState posn dim vel objs) =
  GameState (posn + vel) dim vel objs

createRandomRectangles :: Int -> IO [RectangleObject]
createRandomRectangles n = replicateM n generateRandomRectangle

generateRandomRectangle :: IO RectangleObject
generateRandomRectangle = do
  x <- randomRIO (1, 10)
  y <- randomRIO (1, 10)
  w <- randomRIO (1, 10)
  h <- randomRIO (1, 10)
  return $ RectangleObject (V2 x y) (V2 w h)

Solution

  • Your main problem is that, in this code:

    let gameState = GameState (V2 10 10) (V2 40 40) (V2 1 1) (createRandomRectangles 10) in
      appLoop renderer gameState
    

    the expression createRandomRectangles 10 is of type IO [RectangleObject], but the fourth field in your GameState object is a pure [RectangleObject].

    Rewrite this as:

    rects <- createRandomRectangles 10
    let gameState = GameState (V2 10 10) (V2 40 40) (V2 1 1) rects in
      appLoop renderer gameState
    

    and your main function should type check.

    There were still a couple of undefined variables in your code (qPressed and vGameState) and some logic errors in your renderer (e.g., you were drawing the random rectangles and then erasing them by drawing over them with a black rectangle). Also, the random rectangles were so small, they just showed up as little blips in the top left corner.

    After making a couple adjustments, the following code seemed to work and even looked "game-like". Good luck!

    {-# LANGUAGE OverloadedStrings #-}
    
    import SDL
    import System.Random
    import Control.Monad (unless, replicateM)
    import Foreign.C.Types (CInt)
    
    data RectangleObject = RectangleObject { rectPosn :: V2 CInt, rectDim :: V2 CInt };
    
    data GameState = GameState { posn :: V2 CInt, dim :: V2 CInt, vel :: V2 CInt, objects :: [RectangleObject] };
    
    main :: IO ()
    main = do
      initializeAll
      window <- createWindow "Rectangles" (defaultWindow { windowInitialSize = V2 640 480 })
      renderer <- createRenderer window (-1) defaultRenderer
      rects <- createRandomRectangles 10
      let gameState = GameState (V2 10 10) (V2 40 40) (V2 1 1) rects in
        appLoop renderer gameState
      destroyRenderer renderer
      destroyWindow window
      quit
    
    appLoop :: Renderer -> GameState -> IO ()
    appLoop renderer gameState = do
      events <- pollEvents
    
      clear renderer
    
      -- Fill a rectangle of size 1280x720 with black color
      rendererDrawColor renderer $= V4 0 0 0 255
      fillRect renderer (Just $ Rectangle (P $ V2 0 0) (V2 1280 720))
    
      -- Get the RectangleObjects from the game state and draw them in green
      let objs = objects gameState
      rendererDrawColor renderer $= V4 255 255 0 255
      mapM_ (\(RectangleObject posn dim) -> fillRect renderer (Just $ Rectangle (P $ posn) dim)) objs
    
      -- Draw the main game rectangle in red
      rendererDrawColor renderer $= V4 255 0 0 255
      fillRect renderer (Just $ Rectangle (P $ posn gameState) (dim gameState))
    
      present renderer
      SDL.delay 16
    
      -- unless qPressed (appLoop renderer (updateGameState vGameState))
      appLoop renderer (updateGameState gameState)
    
    updateGameState :: GameState -> GameState
    updateGameState (GameState posn dim vel objs) =
      GameState (posn + vel) dim vel objs
    
    createRandomRectangles :: Int -> IO [RectangleObject]
    createRandomRectangles n = replicateM n generateRandomRectangle
    
    generateRandomRectangle :: IO RectangleObject
    generateRandomRectangle = do
      let scale = 500
      x <- randomRIO (1, scale)
      y <- randomRIO (1, scale)
      w <- randomRIO (1, scale)
      h <- randomRIO (1, scale)
      return $ RectangleObject (V2 x y) (V2 w h)