Making a game in Haskell

July 14, 2020

Making a game in Haskell

During my vacation I’ve been working on making a game in Haskell called HexTech. It has been really fun and inspiring and I’ve learned a lot of new things. The game is a sort of turn-based strategy game on a hexagonal grid. It is mostly a hobby project and it has taken more time than I wanted, but I have been making steady progress. I’m using several tutorials as a guide: this guide for working with hexagonal grids and this one which really gave me a kick start. A lot of my source is based on a rewrite of that game code.

Concepts and libraries that I have used and needed to learn include: monad transformers, lenses, and SDL bindings. They lead to very elegant code and makes things like managing state and updating values easy in a purely functional context. Later I might want to rewrite the program to use a FRP (Functional Reactive Programming) design with a library like NetWire. I’m going to give some examples of the concepts.

Monad Transformers

Monads are sort of expressions that can be composed and are used to encode side effects in Haskell. I’m not going to describe them in detail here.

The state of the program is represented by this record type:

import Control.Lens

-- State of the program
data Model = Model
  { vGame :: Game
  , vScene :: SceneType
  , vNextScene :: SceneType
  , vTitle :: TitleVars
  , vPlay :: PlayVars
  , vInput :: Input
  , vCamera :: Camera
  , vSettings :: Settings
  } deriving (Show, Eq)
makeClassy_ ''Model

The makeClassy_ uses Template Haskell to generate lenses for all the fields including a typeclass HasModel s.

The program itself is wrapped inside this all-around newtype monad:

newtype HexTech a = HexTech (ReaderT Config (StateT Model IO) a)
  deriving (Functor, Applicative, Monad, MonadReader Config, MonadState Model, MonadIO, MonadThrow, MonadCatch)

runHexTech :: Config -> Model -> HexTech a -> IO a
runHexTech config v (HexTech m) = evalStateT (runReaderT m config) v

Config represents read only resources and values like sprites, font and music. This massive monad can do everything necessary in the program and with the runHexTech function we can initialize the state and reader monads. Elsewhere in the code I mostly use an unresolved monad and list the necessary constraints instead.

Challenges with this approach

This approach has some drawbacks. Most notably we loose some type safety since most of the programs logic is encapsulated in the newtype monad HexTech. Almost all of the primary functions of this program are working on this monad which means essentially that anything, even IO, is allowed anywhere. This means that we are basically back to an imperative style program. It is possible to restrict this a bit by not explicitly declaring functions as returning this monad, but only listing the minimal type class constraints. This usually also includes MonadIO because of the SDL functions. It can be enforced somewhat by restricitng the available constraints in upstream functions so that smaller functions can not have any more constraints than their parents.

For example take a look at the signature to the main loop (which is very based on the DinoRush example):

mainLoop
  :: ( MonadReader Config m
     , MonadState State.Model m
     , Audio m
     , Logger m
     , Clock m
     , CameraControl m
     , Renderer m
     , SDLRenderer m
     , MonadIO m
     , HasInput m
     , SceneManager m
     )
  => m ()
mainLoop = do
  Input.updateInput
  input <- Input.getInput
  clearScreen
  scene <- gets vScene
  step scene
  drawScreen
  delayMilliseconds frameDeltaMilliseconds
  nextScene <- gets vNextScene
  stepScene scene nextScene
  let quit = nextScene == Scene'Quit || Input.iQuit input
  unless quit mainLoop
 where
  playScene = Play.playScene
  step scene = do
    case scene of
      Scene'Title    -> Title.titleStep
      Scene'Play     -> State.stepScene playScene
      Scene'Pause    -> pauseStep
      Scene'GameOver -> return ()
      Scene'Quit     -> return ()

  stepScene scene nextScene = do
    when (nextScene /= scene) $ do
      case nextScene of
        Scene'Title -> titleTransition
        Scene'Play  -> case scene of
          Scene'Title -> State.sceneTransition playScene
          Scene'Pause -> pauseToPlay
          _           -> return ()
        Scene'Pause -> case scene of
          Scene'Play -> playToPause
          _          -> return ()
        Scene'GameOver -> return ()
        Scene'Quit     -> return ()
      modify (\v -> v { vScene = nextScene })

My ambition from this point is to reduce the complexity of this mainLoop by adding a generic abstraction to Scene and reduce the number of type classes. Also using more qualified imports would make the code more navigateable.

Lenses

Lenses provides purely functional first-class setters and getters which can be configured to work on any structure.

SDL bindings

Simple DirectMedia Layer (SDL) is a cross platform C library with access to graphics hardware via OpenGL and Direct3D. It is therefore well suited for making games although you are responsible for combining the primitive components to more high level graphics and audio logic.

More to come…