This post will explain some basics involved in using SDL in Haskell. Most of the function links will go to the documentation for the SDL package on hackage, but SDL’s own API documentation should be consulted as well. Further, it is assumed that you manage all ancillary tasks such as installing required libraries in cabal and in your system, setting up a cabal file or using ghc on a command line, etc…

The four png files used can be downloaded here: 64x74_blue.png, 64x74_white.png, 64x74_brown.png, and 64x74_green.png.

SDL Basics

First thing to do is import the SDL module. I like to qualify my imports to make it clear where things come from.

> import Graphics.UI.SDL as SDL

As a first step, we’ll construct a test program that initializes SDL, creates a window, waits for a keypress, shutsdown SDL, then quits.

> main :: IO ()
> main = do
>      SDL.init [SDL.InitEverything]
>      SDL.setVideoMode 640 480 32 []
>      SDL.setCaption "Video Test!" "video test"
>      eventLoop
>      SDL.quit
>      print "done"
>  where
>      eventLoop = SDL.waitEventBlocking >>= checkEvent
>      checkEvent (KeyUp _) = return ()
>      checkEvent _         = eventLoop

The API is very readable. The SDL init function starts up various SDL subsystems (controlled by the InitFlag passed). It’s easiest to just initialize everything and be done with it, but you could always create an array of InitFlag values.

The SDL setVideoMode function sets up the graphics window with width, height, pixel depth and an array of SurfaceFlags respectively. This test uses a hardcoded 640×480 32bpp setting.

The local definition of eventLoop calls waitEventBlocking which calls the SDL WaitEvent function and waits for an event. Once an event is received it is passed to the checkEvent function that uses pattern matching to find a KeyUp event.

Once a KeyUp happens, controll is passed back to the main do sequence and quit is called to shutdown SDL.

Next, lets load a graphic from a png file and display it in the window. Loading png files is easily done with the SDL-image library. Import it at the top of your source code:

> import Graphics.UI.SDL.Image as SDLi

Right after the line in main that calls SDL.setCaption, add this code:

>      mainSurf <- getVideoSurface
>      tileSurf <- SDLi.load "art/64x74_blue.png"
>      let r = Just (Rect 0 0 64 74)
>      SDL.blitSurface tileSurf r mainSurf Nothing
>      SDL.flip mainSurf

The first new line calls getVideoSurface to return the main window’s SDL Surface. This will be used when we blit the graphic and then display our changes in the last two lines.

SDL-image’s load function takes only a file path. The partial path used in the example above is relative to the execution location of the program. On linux, this means that the art directory that contains the 64x74_blue.png file is a sub-directory of the current directory when I run the program.

There are ways to reference this with Cabal instead, but I’m keeping it simple for now. While testing if the program fails with the error “SDL message: Couldn’t open art/64x74_blue.png)” then it wasn’t able to find the file.

SDL’s blitSurface takes a source Surface, a Rect (width and height), destination Surface and a Rect (x,y location). In the example, the 64x74_blue.png’s graphic that has a 64 pixel width and 74 pixel height is copied to the main window at location 0,0. The flip function is called to make the changes visible on the main screen.

A Random Map

The rest of this post will be a literate Haskell program that generates a random two dimensional map using hex grid style graphics.

Again, the png files listed at the start of the post is assumed to be in an ‘art’ subfolder from the directory in which you compile. Also, this depends on the following packages from cabal: SDL, SDL-image, base, containers, and random.

Lets start off with the qualified imports. I prefer to qualify everything.

> import qualified Data.Maybe as DM
> import qualified Data.Map as DMap
> import qualified System.Random as R
> import qualified Control.Monad as CM
> import qualified Graphics.UI.SDL as SDL
> import qualified Graphics.UI.SDL.Image as SDLi

List all of the art files that will be used.

> artFilePaths = [ "art/64x74_blue.png",
>                  "art/64x74_green.png",
>                  "art/64x74_white.png",
>                  "art/64x74_brown.png" ]

Lets define the data structures we will use and some helper types.

The TerrainType enumeration is used as the key for TerrainSurfaces associative list which gives easy access to the SDL Surface. The TerrainMap has the TerrainType as a value indexed by a 2d point.

> data TerrainType = TTh_Blue | TTh_Green | TTh_White | TTh_Brown
>      deriving (Bounded, Eq, Enum, Ord, Show)
>
> type Point = (Int, Int)
> type TerrainSurfaces = [(TerrainType, SDL.Surface)]
> type TerrainMap = DMap.Map Point TerrainType

Simple definitions to get the max index for the TerrainType enum, and a list of all enumeration values. It does rely on TTh_Blue being the first enumerated value defined for TerrainType.

> terrainMaxBound :: Int
> terrainMaxBound = fromEnum (maxBound :: TerrainType)
>
> terrainTypes :: [TerrainType]
> terrainTypes = enumFrom TTh_Blue

Generates a variable length list of random TerrainType values. This will be used in the next function to generate the map. This is an IO action due to the use of randomRIO. The numbers generated are within a range of values that can be converted to TerrainType. replicateM executes the monad the specified umber of times, returning the values in a list.

> getRandomTerrain :: Int -> IO [TerrainType]
> getRandomTerrain l = do
>     randomNumbers <- CM.replicateM l $ R.randomRIO (0,terrainMaxBound)
>     return $ map toEnum randomNumbers

Creates the random 2d map – a map of (Int,Int) to TerrainType. This is an IO action due to the getRandomTerrain. foldM is used to get the return type to be IO (TerrainMap).

> makeRandomMap :: Int -> Int -> IO (TerrainMap)
> makeRandomMap w h = do
>    CM.foldM (\m y -> makeRow w y m) DMap.empty [1..h]
>   where
>      makeRow :: Int -> Int -> TerrainMap -> IO (TerrainMap)
>      makeRow w y tileMap = do
>          rt <- getRandomTerrain w
>          let tp = zip [1..w] rt
>          return $ foldr (\(x,t) m -> DMap.insert (x,y) t m)  tileMap tp

In an IO action, load all of the artwork used in the map.

> loadArt :: [String] -> IO TerrainSurfaces
> loadArt paths = do
>      tileSurfs <- mapM SDLi.load paths
>      return $ zip terrainTypes tileSurfs

This method draws the TerrainType associated Surface onto another surface (the main screen in this sample).

Note that it does not update the destination surface, which will neeed to be flipped before the effects of this function can be seen.

> drawTile :: SDL.Surface -> TerrainSurfaces -> TerrainMap -> Point -> IO ()
> drawTile mainSurf terrainSurfs tm (x,y) = do
>      let sr = Just (SDL.Rect 0 0 64 74)
>      let dr = Just $ getHexmapOffset 64 74 x y
>      let tt = DM.fromJust $ DMap.lookup (x,y) tm
>      let terrainSurf = DM.fromJust $ lookup tt terrainSurfs
>      SDL.blitSurface terrainSurf sr mainSurf dr
>      return ()

This does some trickery with numbers to get the tiles to display in the well known hex grid format. Requires shifting the X value by half a tile for even rows, and a linear scale of 1/4 a tile height subtracted from what would otherwise be the y offset. The div function automatically rounds down, which is the effect we need.

> getHexmapOffset :: Int -> Int -> Int -> Int -> SDL.Rect
> getHexmapOffset tileW tileH x y =
>      SDL.Rect adjX adjY 0 0
>   where
>      baseAdjX = (tileW * (x-1))
>      baseAdjY = (tileH * (y-1))
>      quarterH = tileH `div` 4
>      halfW = tileW `div` 2
>      adjX = if odd y
>                then baseAdjX + halfW
>                else baseAdjX
>      adjY = baseAdjY - ((y-1) * quarterH)

The main worker beast for the program.

1. Initializes SDL
2. Creates a window and sets its caption
3. Setup the SDL Surfaces for the png file artwork
4. Generate a random map
5. Draw the map to screen and update the display.
6. Wait for a keypress, then tear everything down.
7. Free the SDL Surfaces and quit.

> main :: IO ()
> main = do
>      SDL.init [SDL.InitEverything]
>      SDL.setVideoMode 640 480 32 []
>      SDL.setCaption "Video Test!" "video test"
>
>      mainSurf <- SDL.getVideoSurface
>      tileSurfs <- loadArt artFilePaths
>
>      randomMap <- makeRandomMap 9 8
>
>      mapM_ (drawTile mainSurf tileSurfs randomMap) $ DMap.keys randomMap
>      SDL.flip mainSurf
>
>      eventLoop
>      mapM_ freeSurf tileSurfs
>      SDL.quit
>      print "done"
>  where
>      freeSurf (_ , s) = SDL.freeSurface s
>      eventLoop = SDL.waitEventBlocking >>= checkEvent
>      checkEvent (SDL.KeyUp _) = return ()
>      checkEvent _         = eventLoop

The Glorious Result

That section’s code can be copied out to a file, saved, then compiled with ghc assuming you have the required libraries and cabal packages installed. [see the top of this post]

If you save the code in a file called Test.lhs, you can use this command in your shell to compile it under linux:

ghc -o Test -package SDL -package SDL-image -package containers -package random  Test.lhs

If you have the art subdirectory with the four png files, you can then run the executable! Here’s an example of what you should see:

A 9x8 random hex grid

Edit: If you want a direct download of the code, here it is: Test.lhs