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.
As a first step, we’ll construct a test program that initializes SDL, creates a window, waits for a keypress, shutsdown SDL, then quits.
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:
Right after the line in main that calls SDL.setCaption, add this code:
> 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.
List all of the art files that will be used.
> "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.
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.
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.
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 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.
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 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 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 = 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:
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:
Edit: If you want a direct download of the code, here it is: Test.lhs





Awesome writeup. One minor comment: would it be possible to put the .lhs file online somewhere? That would help a lot with getting up to speed quickly.
April 13, 2010 @ 7:09 am
I added a link to the bottom of the page for my version of the source file.
Thanks for the feedback.
April 13, 2010 @ 7:21 am
I just reinstalled my OS with Ubuntu Lucid Lynx (10.4) Beta 2 and figured I’d post the minimum steps to get this project building.
sudo apt-get install build-essential
sudo apt-get install ghc6 ghc6-doc ghc6-prof cabal-install
sudo apt-get install libsdl1.2-dev libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev
cabal update
cabal install SDL
cabal install SDL-image
April 15, 2010 @ 10:13 am
Nice writeup. Some minor comments:
rather than relying on Blue being the first value in Enum, you can write
> terrainTypes = enumFromTo minBound maxBound
or even better
> terrainTypes = [minBound..maxBound]
As an instance of Enum and Bounded, it is natural to declare TerrainType as instance of Random as well:
> instance R.Random TerrainType where
> randomR (a,b) = onFst toEnum . R.randomR (fromEnum a, fromEnum b)
> where onFst f (a,b) = (f a,b)
> random = R.randomR (minBound,maxBound)
With this instance declaration, the definition of makeRandomMap/getRandomTerrain can rewritten as
> makeRandomMap :: Int -> Int -> IO (TerrainMap)
> makeRandomMap w h = do
> tiles return $ DMap.fromAscList (zip [(x,y) | x <-[1..w], y<-[1..h]] tiles)
May 1, 2010 @ 8:32 am
Last definition got garbled. It should be:
makeRandomMap :: Int -> Int -> IO (TerrainMap)
makeRandomMap w h = do
tiles <- CM.replicateM (w*h) randomIO
return $ DMap.fromAscList (zip [(x,y) | x <-[1..w], y<-[1..h]] tiles)
May 1, 2010 @ 8:35 am
Oh wow, that would have made it easier! I never liked how that bit looked.
Thanks for the feedback.
May 3, 2010 @ 2:20 pm
This:
> print “done”
> where
> eventLoop = SDL.waitEventBlocking >>= checkEvent
Should be this:
> print “done”
> where
> eventLoop = SDL.waitEventBlocking >>= checkEvent
Shouldn’t it? I’m getting parse errors otherwise and it was really throwing me off.
May 9, 2010 @ 1:34 pm
Hello, I am trying to build a game engine on Haskell which uses SDL as a media library. Could you give me some advice about what I should look into? Currently, do you know how plausible my idea is? Hopefully I would like to use 3d graphics :D
Thank you!
June 18, 2010 @ 12:16 am
Well Juan, SDL does have bindings for OpenGL. I’ve actually abandoned my SDL 2d game project and am about to start using SDL with OpenGL to do 2d. There should be some posts coming up in the next month from me that will cover this.
But to answer your question: I don’t know. I’m learning this all as I go.
Good luck!
June 26, 2010 @ 10:22 am