Tagged: haskell

Another glance at LambdaHack — a year later

A while back, when I first started to get serious about writing a game in Haskell, I decided to take a look at some existing code and try to figure out how they did things. At the time I had an interest in roguelikes so LambdaHack was a natural choice for investigaion. (part 1 and part 2)

I learned a lot from that project, though the way I wrote up my posts was a little bizarre. After making some decent progress on my turn-based strategy game, I stalled out after getting frustrated with Haskell and my difficulty in managing game state. Oddly enough, after a year, I’m getting the urge to give Haskell another shot so I figured I’d catch up on what’s changed with LambdaHack and take another run through the code now that I have some more experience.

Installation

Firstly, the project now has documentation on how to play and the design of the project. Writing them up in markdown is a great idea and github displays it nicely.

I’m also sad to report that there were still some installation issues for me. I’m running a new install of Fedora 15 with Haskell Platform 2011.2.0.0 and I could not cabal install LambdaHack my way through a default install. I don’t believe this to be LambdaHack’s problem as it appears to be some issue with cairo-0.12.0 and it’s dependencies. Cabal is a pretty awesome tool, but one problem with having a wide and deep dependency tree is that one hitch in the install process ruins it all. In my previous posts I installed LambdaHack using the -fcurses flag in cabal. This installed successfully, but ended up corrupting the terminal when I ran the game. The README now mentions using the following to install for terminals (which did work for me, though I also used –global):

cabal install -fvty LambdaHack

Interesting Bits

With the installation done, I want to cover some things that a new Haskell programmer [like me] might find interesting that I didn’t cover in earlier posts. My hope is that this post helps other people new to Haskell navigate the code to LambdaHack and make it easier to learn from the project.

Startup and Display Specific Code

As noted in part 2, the definition of main in LambdaHack.ls immediately calls into whatever module got included into the build (controlled via LambdaHack.cabal). For the steps above, this is Display\Vty.hs. Each of the sub-systems in the Display folder end up implementing a startup function that takes a parameter of type Session. For vty, this is simply a Graphics.Vty.Vty type synonym.

type Session = V.Vty

Gtk.hs and Curses.hs each implement a record.

-- Gtk.hs
data Session =
  Session {
    schan :: Chan String,
    stags :: Map AttrKey TextTag,
    sview :: TextView }
-- Curses.hs
data Session =
  Session
    { win :: Window,
      styles :: Map (Maybe AttrColor, Maybe AttrColor) C.CursesStyle }

All of these are called Session, so that way cabal can choose which implementation to include. Naturally, each of the display implementations have a startup function defined (to be called straight away from main) with the same type:

startup :: (Session -> IO ()) -> IO ()

The Game Loop

Once some boilerplate stuff is done in the startup code of LambdaHack.hs, ultimately a call is made to handlerToIO.

handlerToIO :: Session -> State -> Message -> Action () -> IO ()

This can be found in Action.hs and exists to run the Action monad defined in that file. As a new Haskeller, also having been away for a while, the code in here is a bit scary. Fortunately, the details of this are not important to getting a high level view of the game loop.

Lets approach this from a practical standpoint by looking at Turn.hs. The start and generate functions from LambdaHack.ls eventually call handlerToIO feeding it a function called handle defined in Turn.hs.

handle :: Action ()

This function simply returns an Action monad. It does some updating to the state, updates monsters and the player and then ultimately calls nextMove which then calls handle. That’s effectively it. The functions in Turn.hs do some book keeping by updating time, perceptions, monsters and running the player’s commands, but that it.

In this loop, handlePlayer may get called if the player can make a turn. This function will eventually evoke playerCommand which is where all of the input handling takes place. The playerCommand function handles some special run processing, but then calls handleKey (defined in KeyBindings.hs) looks the action up in the standard key bindings (defined back in Turn.hs) that were passed in and then returns the Action associated with that key (defined in Command.hs). As for the Actions themselves, they’re defined in Actions.hs which is where you should go to see the game logic.

What’s Next

This barely scratches the surface of interesting stuff about LambdaHack like the level generation stuff in Dungeon.hs.

Simple math and JSON in Haskell – (SDL game series part 4)

(This is part 4 in a series of blog posts.)

There’s been a lot of changes in the project since part 3. The project has been officially renamed to Exit Strategy and the source code is hosted on github. The game mechanics have been prototyped with wet-erase hex mat and seem to work okay. I’ve managed to successfully build the project in Windows. The basic functionality of the map editor is now complete.

Since the source code of the project is now too unwieldy to post into a single entry, I’ll stick to covering things that I think are useful. Any code posted is released under GPLv3 like the project itself and should correspond to the 0.0.3 version as tagged on github.

Simple math in Haskell

Someone new to Haskell might get a bit upset when they do something like this:

timothy@angry:~/projects/exitstrategy$ ghci
GHCi, version 6.12.1: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> let test = 64
Prelude> test
64
Prelude> test / 4

<interactive>:1:0:
    No instance for (Fractional Integer)
      arising from a use of `/' at <interactive>:1:0-7
    Possible fix: add an instance declaration for (Fractional Integer)
    In the expression: test / 4
    In the definition of `it': it = test / 4
Prelude>

This leaves a bad impression. How can it not not be able to divide 64 by 4 without error?! When I seen that, I started questioning whether or not Haskell was the language for me.

However, as with a lot of things in Haskell, the reason for this boils down to types. And the types are set up so that you have to be specific with the type of math you’re trying to perform. While a little more painful to learn up front, it really does root out a lot of errors that might have slipped by in other languages.

Here are two useful links about basic math operations: a gentle introduction to Haskell: numbers, and converting numbers.

Lets look at a real example. This is part of the calcCoordinate function from Editor.lhs. It takes the height and width of the graphic tiles and a mouse cursor point and returns a game map location. Basically, it takes the mouse cursor position and finds the corresponding hex-grid map location. The algorithm is complicated by the shape of the hexagon as opposed to simple square grids.

> calcCoordinate :: Int -> Int -> Point -> Point
> calcCoordinate tileW tileH (mx, my) = (flatX + diffX + 1, flatY + diffY + 1)
>     where
>         halfW =  quot tileW  2
>         scaleH = round $ toRational tileH * 3 / 4
>         flatY = quot my scaleH
>         offsetX = if even flatY
>                   then halfW
>                   else 0
>         modifiedMx = mx - offsetX
>         flatX = quot modifiedMx tileW
[...]

You can see that halfW is defined with the quot function. This performs integer division and returns and integer result. Already we can repeat the initial experiment using quot instead of (/).

timothy@angry:~/projects/exitstrategy$ ghci
GHCi, version 6.12.1: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> let test = 64
Prelude> test
64
Prelude> quot test 4
16
Prelude> test `quot` 4
16
Prelude>

In scaleH, toRational is used to convert tileH to a rational number before doing the multiplication and division. By doing this, division can be be performed without the missing Fractional Integer instance error.

Another useful function is fromIntegral. An example how I used it is in the checkEvent function from Editor.lhs. In this function, the SDL.MouseMotion event is handled. The event is given four parameters: two Word16 objects and then two Int16 objects.

[...]
>     SDL.MouseMotion x y xr yr -> do
>         if elem SDL.ButtonRight $ uiMouseButtonsDown ui
>         then eventLoop $ rightMouseBMHandler ui
>                                      (fromIntegral xr)
>                                      (fromIntegral yr)
[...]
> rightMouseBMHandler :: UIState -> Int -> Int -> UIState
[...]

Passing the Int16 parameters xr and yr to rightMuseBMHandler unchanged — without the call to fromIntegral — will result in a type error:

timothy@angry:~/projects/exitstrategy$ cabal build
Preprocessing executables for ExitStrategy-0.0.3...
Building ExitStrategy-0.0.3...
[6 of 6] Compiling Main             ( Editor.lhs, dist/build/exitstrategy-editor/exitstrategy-editor-tmp/Main.o )

Editor.lhs:346:58:
    Couldn't match expected type `Int'
           against inferred type `GHC.Int.Int16'
    In the second argument of `rightMouseBMHandler', namely `xr'
    In the second argument of `($)', namely
        `rightMouseBMHandler ui xr yr'
    In the expression: eventLoop $ rightMouseBMHandler ui xr yr

The use of fromIntegral solves this problem.

Using JSON to serialize data

Incorporating the Text.JSON module is fairly easy. As shown in Magnus’s blog, the boilerplate code can be generated automatically if all that needs to be done is encoding the data type to JSON.

My data types and JSON instances to read and write the game map are very straight forward. The code is lifted from GameMap.lhs.

> import Text.JSON
> import qualified Data.Map as DMap

[...]

> data GameMap = GameMap
>     {
>          gmFileVersion :: Int,
>          gmHeight :: Int,
>          gmWidth :: Int,
>          gmTileSetName :: String,
>          gmLocations :: DMap.Map Point GameMapLoc
>     } deriving (Eq, Show)

> data GameMapLoc = GameMapLoc
>     {
>          gmlTileName :: String
>     } deriving (Eq, Show)

> instance JSON GameMap where
>     showJSON gm = makeObj
>         [ ("version", showJSON $ gmFileVersion gm)
>         , ("height", showJSON $ gmHeight gm)
>         , ("width", showJSON $ gmWidth gm)
>         , ("tileSetName", showJSON $ gmTileSetName gm)
>         , ("locations", showJSON $ gmLocations gm)
>         ]
>
>     readJSON (JSObject obj) = do
>         let objA = fromJSObject obj
>         v <- lookupM "version" objA >>= readJSON
>         h <- lookupM "height" objA >>= readJSON
>         w <- lookupM "width" objA >>= readJSON
>         tsn <- lookupM "tileSetName" objA >>= readJSON
>         locs <- lookupM "locations" objA >>= readJSON
>         return $ GameMap v h w tsn locs

> instance JSON GameMapLoc where
>     showJSON gml = makeObj
>         [ ("tileName", showJSON $ gmlTileName gml)
>         ]
>
>     readJSON (JSObject obj) = do
>         let objA = fromJSObject obj
>         tn <- lookupM "tileName" objA >>= readJSON
>         return $ GameMapLoc tn

Unfortunately, it seems as if deriving from Data.Data and Data.Typeable is not enough for Text.JSON to automatically work.

To read and write these files, I wrote a few helper functions. They use System.FilePath and System.Directory for path manipulation.

> import System.FilePath as FP
> import System.Directory as Dir

[...]

> writeMapToFile :: String -> GameMap -> IO ()
> writeMapToFile fn gm = do
>     audDir <- Dir.getAppUserDataDirectory appName
>     let dirp = FP.combine audDir "maps"
>     let fp = FP.combine dirp $ FP.replaceExtension fn "map"
>     createDirectoryIfMissing True dirp
>     writeFile fp $ encode gm
>     return ()

> readMapFromFile :: String -> IO (Maybe GameMap)
> readMapFromFile fn = do
>     audDir <- Dir.getAppUserDataDirectory appName
>     let dirp = FP.combine audDir  "maps"
>     let fp = FP.combine dirp $ FP.replaceExtension fn "map"
>     exists <- doesFileExist fp
>     if exists
>         then do
>             f <- readFile fp
>             let json = decode f :: Result GameMap
>             case json of
>                 Ok gm -> return $ Just gm
>                 _ -> return Nothing
>         else
>             return Nothing

There’s very little error handling going on there. Several possibilites exist for the function to fail out. Other than that, you can see how easy it is to serialize.

A Haskell adventure in Windows

I’ve been blogging about my recent project using Haskell – a strategy game using SDL. I use GNU/Linux as my primary operating system, Ubuntu 10.4 to be exact, but I wanted to make sure my project can run in Windows too because that’s what my wife uses. This turned out to be much harder than I first anticipated, considering the general ease in which you can code Haskell in linux.

For this post, I’m using a fresh installation of Windows 7 x64 running inside VirtualBox 3.1.6 OSE. Your mileage with other versions of Windows may vary. Installing Haskell and manually installing FFI bindings, as well as installing git tools to clone the source code repository and building it, will all be covered here. This is mostly here to preserve my notes for myself, though maybe someone else on the Internet will find it useful.

The first thing I do is install some basic free software that I find very useful: the great Firefox web browser, a nice programming text editor called Notepad++, a better command line called Console2 and a good archive utility called 7-zip. These packages provide the needed functionality to accomplish everything below, though are not strictly required if you have other software to handle the job.

Installing Haskell, the SDL, SDL-image, SDL-ttf FFI bindings

Installing the Haskell Platform for Windows is easy. You can download the latest version here. I used the 2010.1.0.0 version. Accepting the defaults while installing will be fine. This gives you the GHC compiler and GHCi interpreter and adds the binaries to your PATH environment variable.

The platform only installs commonly used packages and SDL and associated libraries don’t make the cut. Installing the libraries on Windows must be done by hand with edits made to the build files.

Download that SDL development package for MinGW32, SDL-devel-1.2.14-mingw32.tar.gz; then the SDL_image development package, SDL_image-devel-1.2.10-VC.zip; then the SDL_ttf development package, SDL_ttf-devel-2.0.9-VC8.zip .I unzipped all of them to the C:\ folder as shown in this screen shot:

Unzipped SDL and SDL_image folders .

This will provide the c headers and libraries that we’ll need for the Haskell FFI bindings. The first binding we’ll install will be SDL. Go to the SDL page on hackage, and download the SDL-0.5.9.tar.gz archive at the bottom. Unzip it somewhere that’s convenient for you, but the exact location isn’t important.

Inside the SDL-0.5.9 folder there is a WIN32 file that describes the process of building the Haskell bindings on Windows. The following is an abbreviated version:

1. Open the SDL-0.5.9/SDL.cabal file in your favorite editor.

2. Change the line that reads “Extra-Libraries: SDL” to “Extra-Libraries: SDL.dll SDLmain”

3. Below the line you just edited, add the following two lines (corrected from original version):

  Include-Dirs: C:\SDL-1.2.14\include\SDL
  Extra-Lib-Dirs: C:\SDL-1.2.14\lib

4. Open up a command line window – either cmd.exe or use Console2. I run this as administrator because I’ll be using this command line to install the Haskell packages which, by default, end up installing to C:\Program Files (x64).

5. Run the following command:

runghc Setup.lhs configure

You’ll see this error message about the use of a configure script but ignore it:

Error in configuring SDL

6. Run the next two commands. The last one will require admin rights to write to the C:\Program Files (x86)\Haskell folder.

runghc Setup.lhs build
runghc Setup.lhs install

After building the main SDL binding, it is time to build the SDL-image binding. A lot of the steps will be similar to what we just did for SDL. Go to the SDL-image page on hackage and download the SDL-image-0.5.2.tar.gz file. Unzip it somewhere convenient; I put it on my desktop.

1. Open up the SDL-image-0.5.2\SDL-image.cabal file in your favorite editor. Add the following line to the end of the file.

Include-Dirs: C:\SDL_image-1.2.10\include

2. Open up the SDL-image-0.5.2\Graphics\UI\SDL\Image\Version.hsc file and underneath #include “SDL_image.h” add:

#include "SDL.h"
#ifdef main
#undef main
#endif

This fix was originally found here.

3. Run the following commands ignoring the complaints about the configure script:

runghc Setup.lhs configure
runghc Setup.lhs build
runghc Setup.lhs install

Proof that this too can be installed is here:

The last binding necessary is SDL_ttf. The process should be familiar to you now. Go to the SDL-ttf page on hackage and download the SDL-ttf-0.5.5.tar.gz file. Unzip it somewhere.

1. Open up the SDL-ttf-0.5.5\SDL-ttf.cabal file in your favorite editor. Add the following line to the end of the file.

Include-Dirs: C:\SDL_ttf-2.0.9\include

2. Open up the SDL-ttf-0.5.5\Graphics\UI\SDL\TTF\Version.hsc file and underneath #include “SDL_ttf.h” add:

#include "SDL.h"
#ifdef main
#undef main
#endif

3. Run the following commands ignoring the complaints about the configure script:

runghc Setup.lhs configure
runghc Setup.lhs build
runghc Setup.lhs install

I’ll spare the boring screen shot of a terminal.

Installing git and cloning the source code

Per the Pro Git book, I use msysgit to provide the basic git functionality. Do not follow the “InstallMSysGit” wiki page because it will lead you down the wrong path. Just download the full installer for git (Git-1.7.0.2-preview20100309.exe) and install it. I use all of the default settings in the installer except the one regarding the PATH variable – I choose “Run Git from the Windows Command Prompt.”

To get a good difference view, I install the KDiff3 package, which integrates well with the next tool I install. Download the KDiff3 installer and install it.

Next up is the GitExtentions package. This is a nicer GUI for dealing with git. Grab the complete installer here. I install it will all default settings. When it first runs, it will scan your system and try to find the software it wants to use. If you have set up git and kdiff3 as above, it should automatically find them. To complete the GitExtensions setup, switch to the Global settings tab in the Settings window and enter a user name and email. Switch back to the Checklist tab and click the “Save and rescan” button, then the “Ok”button to finish the setup.

Click the “Clone repository” button and use http://github.com/tbogdala/ExitStrategy.git as the repository to clone (link). I set the destination to C:\Projects as that is my standard folder. Click “Clone” to download a copy of the source files.

For more information on git, check out the Pro Git book. The Seeker’s Quill has a good post on using git. There’s also a blog series on lostechies that I found helpful.

Building the game

Now that we have the prerequisite software installed and a copy of the source code for the project we’re all set to go …

Well, not quite. If you perform a cabal configure and build on the project right now you get massive linker errors pointing to SDL-image and SDL-ttf. Undefined reference to ‘IMG_Load_RW’, undefined reference to ‘IMG_Load’, etc …

1. Open up the exitstrategy.cabal file and add these two lines to the end:

  Extra-Lib-Dirs:    C:\SDL_image-1.2.10\lib, C:\SDL_ttf-2.0.9\lib
  Extra-Libraries:   SDL_image, SDL_ttf

2. Go to the C:\SDL_image\lib folder and make a copy of SDL_image.dll and call it libSDL_image.dll.a (lib*.a)

3. Go to the C:\SDL_ttf-2.0.9\lib folder and make a copy of SDL_ttf.dll and call it libSDL_ttf.dll.a, similar to what we did in step 2.

After that, the project will build.

One last step is to make a copy of the following files and place them in C:\Projects\ExitStrategy\dist\build\exitstrategy-editor:

  • C:\SDL-1.2.14\bin\SDL.dll
  • C:\SDL_image-1.2.10\lib\jpeg.dll
  • C:\SDL_image-1.2.10\lib\libpng12-0.dll
  • C:\SDL_image-1.2.10\lib\libtiff-3.dll
  • C:\SDL_image-1.2.10\lib\SDL_image.dll
  • C:\SDL_image-1.2.10\lib\zlib1.dll
  • C:\SDL_ttf-2.0.9\lib\libfreetype-6.dll
  • C:\SDL_ttf-2.0.9\lib\SDL_ttf.dll

Finally, from the C:\Projects\ExitStrategy folder, run “dist\build\exitstrategy-editor\exitstrategy-editor.exe” in the command line. Success!

For now, double clicking on the exitstrategy-editor.exe file won’t work because it won’t find the art/64×74 folder where all of the art is (unless you also copy that folder to the build directory with the dlls). That will be fixed in a future version.

Version History

Apr 26, 2010: Updates: Project name changed to Exit Strategy. Updated the steps to reference the project with the new name. The steps should work with version v0.0.3 as tagged in gitthub.

Apr 19, 2010: Updated to include SDL_ttf for the master git branch. The new steps should fix the problems caused by adding the SDL_ttf dependency, but I have not run through all of the steps from scratch again to verify this. Leave a comment if something doesn’t work.

Apr 17, 2010: Originally published.

Profiling in Haskell (SDL game series part 3)

In this post, I’ll show how I profiled the SDL project. The code shown is a continuation of the efforts in part one and part two. The starting point will be this version of the project: strat-0.0.1.tar.gz.

With version 0.0.1 from part two, I noticed that whenever I did a right mouse button drag to scroll the map, the redraw would always lag behind my mouse movement. Since I’m new to Haskell, I don’t yet have an instinctual feel for what code actually costs me at run time. I figured now is as good of a time as any to take a peek. I’m not interested in getting it as fast as possible, but I feel uncomfortable proceeding without at least checking out some numbers.

First, I would like to reference this blog post by Nathan Sanders. That page, and the profiling chapter in Real World Haskell, got me up and running. For the official version, check out GHC’s user guide on profiling.

The first step in profiling is rebuilding everything with profiling support if it has not been done already. You can execute cabal commands to do this on a package-by-package basis:

cabal install SDL -p --reinstall

The ‘-p’ flag tells cabal to build the library with profiling support.

Doing this for everything on my system quickly got annoying. In the end, I deleted my ~/.ghc and ~/.cabal folder and started from scratch. After the initial ‘cabal update’, edit the ~/.cabal/config file and set library-profiling to True.

Once the dependencies were reinstalled with profiling enabled, I executed a new cabal configure command to enable profiling for the SDL game:

cabal clean
cabal configure --enable-executable-profiling
cabal build

When GHC generates code that can be profiled the binaries come out thicker. Unstripped, my non-profile version of strat is 2.4 MB. The profiled version is 4.0 MB.

To generate a profiling report you must pass a command line flag to the run-time system of the executable. Here’s the command to run from the source directory of the project:

./dist/build/strat/strat +RTS -p

This will start up the SDL game. Move the mouse around a bit and exit. Once the program stops, you’ll notice a file called strat.prof. Here’s what was generated for me with the current settings:

    Tue Apr 13 19:27 2010 Time and Allocation Profiling Report  (Final)

       strat +RTS -p -RTS

    total time  =        2.28 secs   (114 ticks @ 20 ms)
    total alloc = 772,177,168 bytes  (excludes profiling overheads)

COST CENTRE                    MODULE               %time %alloc

MAIN                           MAIN                 100.0  100.0


                                                                                               individual    inherited
COST CENTRE              MODULE                                               no.    entries  %time %alloc   %time %alloc

MAIN                     MAIN                                                   1           0 100.0  100.0   100.0  100.0
 CAF                     Main                                                 348          13   0.0    0.0     0.0    0.0
 CAF                     GHC.Float                                            325           7   0.0    0.0     0.0    0.0
 CAF                     GHC.IO.Handle.FD                                     286           2   0.0    0.0     0.0    0.0
 CAF                     System.Posix.Internals                               285           1   0.0    0.0     0.0    0.0
 CAF                     GHC.Conc                                             269           1   0.0    0.0     0.0    0.0
 CAF                     GHC.IO.Handle.Internals                              260           1   0.0    0.0     0.0    0.0
 CAF                     GHC.IO.Encoding.Iconv                                254           5   0.0    0.0     0.0    0.0
 CAF                     UIState                                              251           1   0.0    0.0     0.0    0.0
 CAF                     Graphics.UI.SDL.General                              227           1   0.0    0.0     0.0    0.0
 CAF                     Graphics.UI.SDL.Events                               223           1   0.0    0.0     0.0    0.0
 CAF                     Graphics.UI.SDL.Video                                215           3   0.0    0.0     0.0    0.0
 CAF                     System.Random                                        210           1   0.0    0.0     0.0    0.0
 CAF                     Data.Time.Clock.POSIX                                203           2   0.0    0.0     0.0    0.0
 CAF                     Data.Time.Clock.CTimeval                             202           1   0.0    0.0     0.0    0.0

Not very helpful is it? It basically only profiled main and some CAFs (Constant Applicative Function – fancy name for functions without arguments).

What we need to do is pass some more build flags to tell GHC to profile more than just that.

cabal build --ghc-options="-caf-all -auto-all -fforce-recomp"

The ‘-caf-all’ option tells GHC to profile all top-level CAFs separately instead of as a group (the default behavior, as shown above). The ‘-auto-all’ option tells GHC to profile all top-level functions. Lastly, the ‘-fforce-recomp’ option forces the recompilation of the files, thereby making sure we have the profiling code we want. With the new executable, try this:

./dist/build/strat/strat +RTS -p

Which generates this report:

    Tue Apr 13 19:39 2010 Time and Allocation Profiling Report  (Final)

       strat +RTS -p -RTS

    total time  =        3.42 secs   (171 ticks @ 20 ms)
    total alloc = 1,303,852,488 bytes  (excludes profiling overheads)

COST CENTRE                    MODULE               %time %alloc

drawTile                       Main                  72.5   35.8
redrawScreen                   Main                  22.2   57.9
getHexmapOffset                Main                   2.3    4.8
tileInViewPort                 Main                   1.8    0.0
makeRandomMap                  Main                   0.6    1.3


                                                                                               individual    inherited
COST CENTRE              MODULE                                               no.    entries  %time %alloc   %time %alloc

MAIN                     MAIN                                                   1           0   0.0    0.0   100.0  100.0
 main                    Main                                                 432         383   0.6    0.0   100.0  100.0
  vpY                    UIState                                              465         320   0.0    0.0     0.0    0.0
  vpX                    UIState                                              464         320   0.0    0.0     0.0    0.0
  uiViewPort             UIState                                              463         320   0.0    0.0     0.0    0.0
  uiMouseButtonsDown     UIState                                              462           3   0.0    0.0     0.0    0.0
  redrawScreen           Main                                                 448         393  22.2   57.9    98.8   98.5
   drawTile              Main                                                 449     3930000  72.5   35.8    76.6   40.6
    ==_aNo               UIState                                              458       75680   0.0    0.0     0.0    0.0
    tileInViewPort       Main                                                 452     3930000   1.8    0.0     1.8    0.0
    getHexmapOffset      Main                                                 450           0   2.3    4.8     2.3    4.8
  makeRandomMap          Main                                                 445         102   0.6    1.3     0.6    1.5
   getRandomTerrain      Main                                                 446         100   0.0    0.3     0.0    0.3
    toEnum_aOi           UIState                                              461         155   0.0    0.0     0.0    0.0
  loadArt                Main                                                 436           1   0.0    0.0     0.0    0.0
 CAF:artFilePaths        Main                                                 426           1   0.0    0.0     0.0    0.0
  artFilePaths           Main                                                 437           1   0.0    0.0     0.0    0.0
 CAF:main18              Main                                                 425           1   0.0    0.0     0.0    0.0
  artFilePaths           Main                                                 439           0   0.0    0.0     0.0    0.0
 CAF:main19              Main                                                 424           1   0.0    0.0     0.0    0.0
  artFilePaths           Main                                                 441           0   0.0    0.0     0.0    0.0
 CAF:main20              Main                                                 423           1   0.0    0.0     0.0    0.0
  artFilePaths           Main                                                 443           0   0.0    0.0     0.0    0.0
 CAF:tileWidth_r3OL      Main                                                 422           1   0.0    0.0     0.0    0.0
  tileWidth              Main                                                 451           1   0.0    0.0     0.0    0.0
 CAF:tileHeight_r3ON     Main                                                 421           1   0.0    0.0     0.0    0.0
  tileHeight             Main                                                 454           1   0.0    0.0     0.0    0.0
 CAF:windowWidth         Main                                                 420           1   0.0    0.0     0.0    0.0
  windowWidth            Main                                                 453           1   0.0    0.0     0.0    0.0
 CAF:windowHeight        Main                                                 419           1   0.0    0.0     0.0    0.0
  windowHeight           Main                                                 455           1   0.0    0.0     0.0    0.0
 CAF:mapColumns          Main                                                 418           1   0.0    0.0     0.0    0.0
  mapColumns             Main                                                 447           1   0.0    0.0     0.0    0.0
 CAF:main24              Main                                                 417           1   0.0    0.0     0.0    0.0
  artFilePaths           Main                                                 438           0   0.0    0.0     0.0    0.0
 CAF:main23              Main                                                 416           1   0.0    0.0     0.0    0.0
  artFilePaths           Main                                                 440           0   0.0    0.0     0.0    0.0
 CAF:main22              Main                                                 415           1   0.0    0.0     0.0    0.0
  artFilePaths           Main                                                 442           0   0.0    0.0     0.0    0.0
 CAF:main21              Main                                                 414           1   0.0    0.0     0.0    0.0
  artFilePaths           Main                                                 444           0   0.0    0.0     0.0    0.0
 CAF:main27              Main                                                 413           1   0.0    0.0     0.0    0.0
  main                   Main                                                 434           0   0.0    0.0     0.0    0.0
 CAF:main26              Main                                                 412           1   0.0    0.0     0.0    0.0
  main                   Main                                                 435           0   0.0    0.0     0.0    0.0
 CAF:main4               Main                                                 411           1   0.0    0.0     0.0    0.0
  main                   Main                                                 466           0   0.0    0.0     0.0    0.0
 CAF:main29              Main                                                 410           1   0.0    0.0     0.0    0.0
 CAF                     GHC.Float                                            387           7   0.0    0.0     0.0    0.0
 CAF                     GHC.IO.Handle.FD                                     348           2   0.0    0.0     0.0    0.0
 CAF                     System.Posix.Internals                               347           1   0.0    0.0     0.0    0.0
 CAF                     GHC.Conc                                             331           1   0.0    0.0     0.0    0.0
 CAF                     GHC.IO.Handle.Internals                              322           1   0.0    0.0     0.0    0.0
 CAF                     GHC.IO.Encoding.Iconv                                316           5   0.0    0.0     0.0    0.0
 CAF:terrainMaxBound     UIState                                              307           1   0.0    0.0     0.0    0.0
  terrainMaxBound        UIState                                              459           1   0.0    0.0     0.0    0.0
   fromEnum_aOn          UIState                                              460           1   0.0    0.0     0.0    0.0
 CAF:terrainTypes        UIState                                              306           1   0.0    0.0     0.0    0.0
  terrainTypes           UIState                                              456           1   0.0    0.0     0.0    0.0
   enumFrom_aOx          UIState                                              457           1   0.0    0.0     0.0    0.0
 CAF                     Graphics.UI.SDL.General                              266           1   0.0    0.0     0.0    0.0
 CAF                     Graphics.UI.SDL.Events                               262           1   0.0    0.0     0.0    0.0
 CAF                     Graphics.UI.SDL.Video                                254           3   0.0    0.0     0.0    0.0
 CAF                     System.Random                                        249           1   0.0    0.0     0.0    0.0
 CAF                     Data.Time.Clock.POSIX                                242           2   0.0    0.0     0.0    0.0
 CAF                     Data.Time.Clock.CTimeval                             241           1   0.0    0.0     0.0    0.0

Much better! Near the top of the report there is a short list of the most expensive functions. We can see that drawTile takes about 72% of execution time and redrawScreen takes about 22%. The next few functions after that barely cost anything by comparison.

If we wanted to see some memory profiling data on the garbage collector we can execute the binary with these options:

./dist/build/strat/strat +RTS -sstderr

Which outputs this to stderr:

dist/build/strat/strat +RTS -sstderr
done
   2,722,681,960 bytes allocated in the heap
     644,419,088 bytes copied during GC
       4,732,608 bytes maximum residency (78 sample(s))
         149,912 bytes maximum slop
              14 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  5106 collections,     0 parallel,  0.50s,  0.52s elapsed
  Generation 1:    78 collections,     0 parallel,  0.42s,  0.42s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    4.59s  (  6.27s elapsed)
  GC    time    0.92s  (  0.94s elapsed)
  RP    time    0.00s  (  0.00s elapsed)
  PROF  time    0.00s  (  0.00s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    5.51s  (  7.21s elapsed)

  %GC time      16.7%  (13.1% elapsed)

  Alloc rate    593,646,382 bytes per MUT second

  Productivity  83.3% of total user, 63.6% of total elapsed

There are ways to profile the heap as well. For this program, the results from that were not enlightening.

So what to do now? It does not appear that we are having major GC or memory errors. Lets change the two most expensive functions and manually place ‘call centers’ around most expressions. This way we can see the cost of almost everything in the functions.

> drawTile :: UIState -> Point -> IO ()
> drawTile (UIState vp _ mainSurf terrainSurfs tm) (x,y) = do
>      let sr = {-# SCC "dT-sr" #-} Just (SDL.Rect 0 0 tileWidth tileHeight)
>          (tX, tY) = {-# SCC "dT-gP2V" #-} gamePoint2View vp  $ {-# SCC "dT-gHO" #-} getHexmapOffset tileWidth tileHeight x y
>          dr = {-# SCC "dT-dr" #-} Just $ SDL.Rect tX tY 0 0
>          tt = DM.fromJust $ {-# SCC "dT-tmLookup" #-} DMap.lookup (x,y) tm
>          terrainSurf = DM.fromJust $ {-# SCC "dT-tsLookup" #-} lookup tt terrainSurfs
>      if {-# SCC "dT-tIVP" #-} tileInViewPort vp tileWidth tileHeight (tX,tY)
>          then do
>                {-# SCC "dT-blit" #-} SDL.blitSurface terrainSurf sr mainSurf dr
>                return ()
>          else
>               return ()
> redrawScreen ::  UIState -> IO ()
> redrawScreen ui@(UIState vp _ mainSurf terrainSurfs terrainMap) = do
>     {-# SCC "rS-fillRect" #-} SDL.fillRect mainSurf Nothing (SDL.Pixel 0)
>     {-# SCC "rS-mDrawTile" #-} mapM_ (drawTile ui) $ {-# SCC "rS-tmKeys" #-} DMap.keys terrainMap
>     {-# SCC "rS-flip" #-} SDL.flip mainSurf
>     return ()

The “{-# SCC “LABEL” #-} set a cost center for the expression that follows it. With this, rebuild the project and run the binary with profiling enabled.

cabal build --ghc-options="-caf-all -auto-all"
dist/build/strat/strat +RTS -p

This will generate a report that looks like this (only part of the report is shown):

    Tue Apr 13 21:05 2010 Time and Allocation Profiling Report  (Final)

       strat +RTS -p -RTS

    total time  =       12.72 secs   (636 ticks @ 20 ms)
    total alloc = 4,299,436,864 bytes  (excludes profiling overheads)

COST CENTRE                    MODULE               %time %alloc

dT-blit                        Main                  58.2    0.8
rS-tmKeys                      Main                  10.8   44.9
dT-gHO                         Main                   7.2   24.7
gamePoint2View                 Main                   6.1   24.7
rS-mDrawTile                   Main                   4.1    0.0
dT-gP2V                        Main                   3.9    0.0
drawTile                       Main                   3.0    0.2
getHexmapOffset                Main                   1.7    3.7
rS-fillRect                    Main                   1.6    0.0
tileInViewPort                 Main                   1.1    0.0


                                                       individual    inherited
COST CENTRE              MODULE       no.    entries  %time %alloc   %time %alloc

MAIN                     MAIN           1           0   0.0    0.0   100.0  100.0
 main                    Main         452         987   0.5    0.0   100.0  100.0
  vpY                    UIState      513         443   0.0    0.0     0.0    0.0
  vpX                    UIState      512         443   0.0    0.0     0.0    0.0
  uiViewPort             UIState      511         443   0.0    0.0     0.0    0.0
  uiMouseButtonsDown     UIState      510           6   0.0    0.0     0.0    0.0
  redrawScreen           Main         472        1006   0.0    0.0    99.1   99.5
   rS-flip               Main         509        1006   0.0    0.0     0.0    0.0
   rS-mDrawTile          Main         474    10062012   4.1    0.0    97.5   99.5
    drawTile             Main         476    10060000   3.0    0.2    82.5   54.6
     dT-tmLookup         Main         505      109188   0.2    0.1     0.2    0.1
     dT-tsLookup         Main         501      109188   0.2    0.0     0.3    0.0
      ==_aO1             UIState      504      385235   0.2    0.0     0.2    0.0
     dT-blit             Main         488      109188  58.2    0.8    58.2    0.8
     dT-dr               Main         487      109188   0.0    0.2     0.0    0.2
     dT-tIVP             Main         482    10060000   0.8    0.0     1.9    0.0
      tileInViewPort     Main         483    10060000   1.1    0.0     1.1    0.0
     dT-gP2V             Main         477    10060000   3.9    0.0    19.0   53.2
      gamePoint2View     Main         479    10060000   6.1   24.7     6.1   24.7
      dT-gHO             Main         478    10060000   7.2   24.7     9.0   28.5
       getHexmapOffset   Main         480           0   1.7    3.7     1.7    3.7
    rS-tmKeys            Main         475        1006  10.8   44.9    10.8   44.9
   rS-fillRect           Main         473        1006   1.6    0.0     1.6    0.0
  makeRandomMap          Main         469         102   0.3    0.4     0.5    0.5
   getRandomTerrain      Main         470         100   0.2    0.1     0.2    0.1
    toEnum_aOV           UIState      508         294   0.0    0.0     0.0    0.0
  loadArt                Main         456           1   0.0    0.0     0.0    0.0

Now we have a decent idea what expressions are really costing us in execution.

The call to SDL.blitSurface is by far the most costly. This is natural as this is the function doing all the actual rendering IO work behind the scenes.

What caught me off guard was the hit taken by calling Data.Map.keys. Almost half of my memory allocations are coming from this call.

Lets add a list comprehension to generate the coordinates just once, since that’s all the terrainMap’s keys were anyway.

> mapCoordinates :: [Point]
> mapCoordinates = [(x,y) | x <- [1..mapRows]
>                         , y <- [1..mapColumns]]

> redrawScreen ::  UIState -> IO ()
> redrawScreen ui@(UIState vp _ mainSurf terrainSurfs terrainMap) = do
>     {-# SCC "rS-fillRect" #-} SDL.fillRect mainSurf Nothing (SDL.Pixel 0)
>     {-# SCC "rS-mDrawTile" #-} mapM_ (drawTile ui) $ {-# SCC "rS-mapCoords" #-} mapCoordinates
>     {-# SCC "rS-flip" #-} SDL.flip mainSurf
>     return ()

A recompile and another run with ‘+RTS -p’ yeilds a profile report with these as the top call centers:

        Tue Apr 13 21:55 2010 Time and Allocation Profiling Report

           strat +RTS -p -RTS

    total time  =        8.10 secs   (405 ticks @ 20 ms)
    total alloc = 1,868,126,656 bytes  (excludes profiling overheads)

COST CENTRE                    MODULE               %time %alloc

dT-blit                        Main                  64.7    1.4
dT-gHO                         Main                   9.1   44.8
drawTile                       Main                   4.9    0.4
gamePoint2View                 Main                   4.2   44.8
dT-gP2V                        Main                   3.7    0.0
rS-mDrawTile                   Main                   3.5    0.0
rS-fillRect                    Main                   3.0    0.0
getHexmapOffset                Main                   2.5    6.8
dT-tIVP                        Main                   2.2    0.0

A run with ‘+RTS -sstderr’ gives this output:

dist/build/strat/strat +RTS -sstderr
   2,082,792,480 bytes allocated in the heap
      33,971,320 bytes copied during GC
       4,853,488 bytes maximum residency (6 sample(s))
          95,640 bytes maximum slop
              13 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  3955 collections,     0 parallel,  0.07s,  0.06s elapsed
  Generation 1:     6 collections,     0 parallel,  0.02s,  0.03s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    5.05s  (  6.54s elapsed)
  GC    time    0.09s  (  0.09s elapsed)
  RP    time    0.00s  (  0.00s elapsed)
  PROF  time    0.00s  (  0.00s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    5.14s  (  6.63s elapsed)

  %GC time       1.8%  (1.3% elapsed)

  Alloc rate    412,461,270 bytes per MUT second

  Productivity  98.2% of total user, 76.2% of total elapsed

That change alone eliminated a ton of GC copies and drastically reduced the amount of time spent in the GC. The program felt snappier as well.

However, the big payoff comes from realizing that I need to cut down on my blits. Our main’s inner function eventLoop looks like this:

>      eventLoop ui = do
>          redrawScreen ui
>          e <- SDL.waitEvent
>          checkEvent ui e

This means that for every event sent by SDL the screen is redrawn. Mouse movement generates lots of events! Recall how fast they printed out to console when we were writing tracing output at the beginning of part two?

A better solution is to process all of the events in the queue before redrawing the screen. After the screen is redrawn, we can block on waiting for an event. Replace the code above with this:

>      eventLoop ui = do
>          e <- SDL.pollEvent
>          checkEvent ui e
>      checkEvent ui (SDL.NoEvent) = do
>          redrawScreen ui
>          e <- SDL.waitEvent
>          checkEvent ui e

It adds another pattern match for checkEvent that will run when no events are available. This made an enormous difference in the scroll speed.

With that change, I’m happy with the performance of the game for now.

Here’s a preview of the new programmer art that will be used in the next post:

SDL and Game State (SDL game series part 2)

This post will continue exploring Haskell and SDL by handling mouse movement events and using them to scroll the view port. This series, however, will have a goal of building a simple turn-based strategy game – hopefully with multi-player and AI.

This first section introduces mouse events and the information contained in them.

It assumes the code from my first SDL tutorial post is available. You can download a version of it here – Test.lhs.

Getting Mouse Moves

Lets start by changing the local eventLoop function inside main to print debug data out to console every time it gets a mouse event.

Change the checkEvent definitions to these:

>      checkEvent (SDL.KeyUp _) = return ()
>      checkEvent (SDL.MouseMotion x y xr yr ) =
>          putStrLn ("MouseMotion X:" ++ show x ++ "  Y:" ++ show y ++
>                    "  RX:" ++ show xr ++ "  RY:" ++ show yr)
>                    >> eventLoop
>      checkEvent (SDL.MouseButtonDown x y b) =
>          putStrLn ("MouseBDown  X:" ++ show x ++ "  Y:" ++ show y ++ "  B:" ++ show b)
>                   >> eventLoop
>      checkEvent (SDL.MouseButtonUp x y b) =
>          putStrLn ("MouseBUp    X:" ++ show x ++ "  Y:" ++ show y ++ "  B:" ++ show b)
>                   >> eventLoop
>      checkEvent _         = eventLoop

To recap: The event loop continues until a SDL.KeyUp event is received. New handlers were defiend for SDL.MouseMotion, SDL.MouseButtonDown and SDL.MouseButtonUp. Each of the new handlers prints diagnostic information to the console. Here’s some example output:

MouseMotion X:151  Y:284  RX:13  RY:-2
MouseMotion X:160  Y:284  RX:9  RY:0
MouseMotion X:167  Y:282  RX:7  RY:-2
MouseMotion X:171  Y:284  RX:4  RY:2
MouseMotion X:171  Y:283  RX:0  RY:-1
MouseMotion X:171  Y:282  RX:0  RY:-1
MouseMotion X:170  Y:281  RX:-1  RY:-1
MouseMotion X:169  Y:280  RX:-1  RY:-1
MouseMotion X:169  Y:279  RX:0  RY:-1
MouseMotion X:169  Y:278  RX:0  RY:-1
MouseMotion X:169  Y:277  RX:0  RY:-1
MouseMotion X:170  Y:277  RX:1  RY:0
MouseMotion X:170  Y:276  RX:0  RY:-1
MouseBDown  X:170  Y:276  B:ButtonLeft
MouseBDown  X:170  Y:276  B:ButtonRight
MouseBUp    X:170  Y:276  B:ButtonMiddle
MouseBDown  X:170  Y:276  B:ButtonWheelDown
MouseBDown  X:170  Y:276  B:ButtonWheelUp

Adding State and Event Handling

After that brief addition to the previous code, lets start from scratch and build a program that displays a hexagon tile map and responds to mouse button events.

With the expansion of the scope of this project, lets start dividing the code into modules and use the cabal system to do the build system for us. The more I use Haskell, the more I appreciate the tools. Cabal is no exception to this. It’s very helpful and easy to use.

Lets start off with a new file called UIState.lhs. We’ll define the module and list our imports.

> module UIState where

> import qualified Data.Map as DMap
> import qualified Graphics.UI.SDL as SDL

After this, we’ll pull some definitions from the previous file. For this stage of the project we’ll combine the user interface code with the data for the map.

> data TerrainType = TTh_Blue | TTh_Green | TTh_White | TTh_Brown
>      deriving (Bounded, Eq, Enum, Ord, Show)

> terrainMaxBound :: Int
> terrainMaxBound = fromEnum (maxBound :: TerrainType)

> terrainTypes :: [TerrainType]
> terrainTypes = enumFrom TTh_Blue

> type Point = (Int, Int)
> type TerrainSurfaces = [(TerrainType, SDL.Surface)]
> type TerrainMap = DMap.Map Point TerrainType

Next lets define a new data type to represent the user view port. This will keep track of the user’s view on the map while responding to mouse button events.

> data ViewPort = ViewPort {
>                          vpX :: Int,
>                          vpY :: Int,
>                          vpWidth :: Int,
>                          vpHeight :: Int
>                          }

Using record syntax will automatically create what would be accessor functions in other programming languages. Because of this, I use a prefix associated with the data type (e.g. vpX).

The next data type will define the container type for all things related to the user interface state.

> data UIState = UIState {
>                        uiViewPort :: ViewPort,
>                        uiMouseButtonsDown :: [SDL.MouseButton],
>                        uiMainSurface :: SDL.Surface,
>                        uiTerrainSurfaces :: TerrainSurfaces,
>                        uiTerrainMap :: TerrainMap
>                        }

Using this type we’ll keep track of the user’s view port, what mouse buttons are pressed, SDL surface resources and, for now, the terrain map. Mouse buttons are tracked because getting data from SDL.getMouseState appears to be unreliable with regards to the MouseButtons.

That is all were are going to put into UIState.lhs for now.

Lets continue on to Strat.lhs which is our file that contains the Main module.

The first step in Strat.lhs will be to import the necessary modules and define some constants. Note that we import the UIState module we defined earlier unqualified.

> 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

> import UIState

> artFilePaths = [ "art/64x74_blue.png",
>                  "art/64x74_green.png",
>                  "art/64x74_white.png",
>                  "art/64x74_brown.png" ]
> tileWidth = 64
> tileHeight = 74
> windowWidth = 640
> windowHeight = 480
> mapRows = 100
> mapColumns = 100

Whoa! 100 x 100? Yes, this map will be significantly larger than the 9×8 map we defined earlier. We’ll use it’s size for profiling, but that will be later on. We also codify our art resources as 64 x 74 pixel tiles and a screen resolution of 640 x 480.

Lets pull in the map generation code from the previous post. getRandomTerrain returns a random row of tiles and makeRandomMap compaines these rows. foldM is used here because makeRow returns an IO type, which causes makeRandomMap return an IO type.

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

> 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

The loadArt function will also be pulled in without changes.

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

Lets skip ahead and define the main function so we can see where we are going with all of this. The main function can be broken down to these steps:

  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. Setup a default UIState and attach all of the data to it.
  6. Enter event loop which monitors mouse movement.
  7. Pressing a key exits the event loop.
  8. Free the SDL Surfaces and quit.
> main :: IO ()
> main = do
>      SDL.init [SDL.InitEverything]
>      SDL.setVideoMode windowWidth windowHeight 32 []    
>      SDL.setCaption "Video Test!" "video test"
>
>      mainSurf <- SDL.getVideoSurface
>      tileSurfs <- loadArt artFilePaths
>      randomMap <- makeRandomMap mapColumns mapRows
>
>      let initialUI = UIState (ViewPort 0 0 windowWidth windowHeight) [] mainSurf tileSurfs randomMap
>      eventLoop initialUI
>
>      mapM_ freeSurf tileSurfs
>      SDL.quit
>      putStrLn "done"
>  where
>      freeSurf (_ , s) = SDL.freeSurface s
>      eventLoop ui = do
>          redrawScreen ui
>          e <- SDL.waitEvent
>          checkEvent ui e
>      checkEvent ui (SDL.KeyUp _) = return ()
>      checkEvent ui (SDL.MouseMotion _ _ xr yr ) = do
>          if elem SDL.ButtonRight $ uiMouseButtonsDown ui
>              then eventLoop ui'
>              else eventLoop ui
>        where
>          ui' = ui { uiViewPort = updatedVP }
>          updatedVP = vp { vpX = x', vpY = y' }
>          vp = uiViewPort ui
>          x' = (vpX vp) + fromIntegral xr
>          y' = (vpY vp) + fromIntegral yr
>      checkEvent ui (SDL.MouseButtonDown _ _ b) = do
>          let mbs = uiMouseButtonsDown ui
>          eventLoop $ ui { uiMouseButtonsDown = mbs ++ [b] }
>      checkEvent ui (SDL.MouseButtonUp _ _ b) = do
>          let mbs = uiMouseButtonsDown ui
>          let mbs' = filter (\i -> if i == b then False else True) mbs
>          eventLoop $ ui { uiMouseButtonsDown = mbs' }
>      checkEvent ui _      = eventLoop ui

Let’s take a look at this.

You’ll see a lot of code from the previous post regarding the initializiation of SDL libraries and the loading of art assets. However, the event handling system was changed.

The meat of the function is the creation of the UIState value and the invocation of the local eventLoop function. checkEvent has been significantly extended to track and react to mouse events, where, in the previous version, it used to only print out diagnostic material.

The default checkEvent case as well as the pattern for SDL.KeyUp have stayed the same, minus the additional state parameter. Handlers have been added for SDL.MouseButtonDown and SDL.MouseButtonUp events to track button state.

The handler for SDL.MouseMotion checks to see if the right mouse button is down. If it is, the UIState is modified by changing the view port based on the relative (x,y) coordinate in the event. This creates the effect of panning our view of the map.

Lets take a peek at the new redrawScreen function.

> redrawScreen ::  UIState -> IO ()
> redrawScreen ui@(UIState vp _ mainSurf terrainSurfs terrainMap) = do
>     SDL.fillRect mainSurf Nothing (SDL.Pixel 0)
>     mapM_ (drawTile ui) $ DMap.keys terrainMap
>     SDL.flip mainSurf
>     return ()

This was basically the old code mixed into a function that will zero out the screen then draw all of the tiles of the map.

> drawTile :: UIState -> Point -> IO ()
> drawTile (UIState vp _ mainSurf terrainSurfs tm) (x,y) = do
>      let sr = Just (SDL.Rect 0 0 tileWidth tileHeight)
>          (tX, tY) = gamePoint2View vp  $ getHexmapOffset tileWidth tileHeight x y
>          dr = Just $ SDL.Rect tX tY 0 0
>          tt = DM.fromJust $ DMap.lookup (x,y) tm
>          terrainSurf = DM.fromJust $ lookup tt terrainSurfs
>      if tileInViewPort vp tileWidth tileHeight (tX,tY)
>          then do
>                SDL.blitSurface terrainSurf sr mainSurf dr
>                return ()
>          else
>               return ()

> getHexmapOffset :: Int -> Int -> Int -> Int -> Point
> getHexmapOffset tileW tileH x y =
>      (adjX , adjY)
>   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)

Draw tile now does a little more work. It will correct the coordinates of the tile relative to the UIState viewport (using the gamePoint2View method to do the translation) and also test to see if the tile is in view (using tileInViewPort).

> tileInViewPort :: ViewPort -> Int -> Int -> Point -> Bool
> tileInViewPort (ViewPort _ _ vpW vpH) tileW tileH (pX , pY) =
>     let pX' = pX + tileW
>         pY' = pY + tileH
>     in
>         if ((pX' < 0) || (pX > vpW) || (pY' < 0) || pY > vpH)
>             then False else True

> gamePoint2View :: ViewPort -> Point -> Point
> gamePoint2View (ViewPort vpx vpy _ _) (gx , gy) =
>     ((gx + vpx) , (gy + vpy))

The gamePoint2View translates a point on the map to fit into the view point the user has. This is the motion that was tracked in checkEvent.

After the translation, drawTile checks tileInViewPort to see if part of the tile is visible. If the tile is not visible, it is not drawn.

This completes the listing for Strat.lhs. Lets create a strat.cabal file. I edited a few personal details out.

Name:                strat
Version:             0.0.1
Stability:           experimental
Synopsis:            Turn based strategy game using SDL
Description:         Turn based strategy game using SDL
License:             GPL-3
License-file:        LICENSE
Author:              AuthorName
Maintainer:          AuthorEmail
Build-Type:          Simple
Cabal-Version:       >=1.4
Category:            Game
Homepage:            ProjectHomepage

Data-Files:          art/64x74_blue.png, art/64x74_green.png, art/64x74_brown.png,
                     art/64x74_white.png

Executable strat
  Main-is:           Strat.lhs
  Other-modules:     UIState
  Build-Depends:     base >= 4, containers, random, SDL, SDL-image

Having all three of these files (strat.cabal, Strat.lhs and UIState.lhs) next to each other with the art subdirectory containing the graphics from the first post, you can then execute the following commands to build, then run the program.

cabal configure
cabal build
./dist/build/strat/strat

And if you wanted to, you could run this command to build a source file distribution:

cabal sdist

Here is my copy of the code that was generated by cabal’s sdist: strat.0.0.1.tar.gz

Here’s a video of the mouse motion captured with XVidCap:

Note: I intend to write the first version of the game without any use of monads, if at all possible, other than the basics like IO, Maybe, and []. You can already see where a monadic pattern is showing up with UIState and eventLoop. I’m avoiding it on purpose and will convert things over to monads at the end of the series.