Game SpaceStride

For those using a RSS reader, subscribe here: rss.xml

Haskell, the language loved for its types, laziness, and declarative nature. I was already familiar with the language; A couple of years ago, it seemed like a nice language to try out project euler problems on. So if I were to make a game using it, the focus would be on learning common practices and convenient ways to structure your code.

And so a game was born. Would it be 3D? No, ofcourse not. 2D? Well that sounds more like it. What about making some graphics? Why not. Pair programming? Well THAT sounds really nice.

The entire project took only 35 hours ish, with 20 hours pair programming. So it was a gamejam. We had a lot of fun making it, and you can checkout the game here: Space Stride A lot of the architecture on top of MVC is from this documentation on design patterns in Elm.

To give an example how it translated to SpaceStride, just look at src/Model.hs. It has the following all-describing GameState:

16: data GameState = Playing            { _playingGame :: PlayingState }
17:                | Paused             { _pausedGame  :: PlayingState }
18:                | PlayerDead         { _deadGame    :: PlayingState
19:                                     , _animationFrameCount :: Int
20:                                     }
21:                | GameOverTypeName   { _score :: Int
22:                                     , _playerName :: String
23:                                     }
24:                | GameOverShowScores { _score :: Int
25:                                     , _playerName :: String
26:                                     , _highscoreBoard :: HighScoreBoard
27:                                     }

This already gives you tons of information about the game, and gives you a clear idea of the data that gets passed around. And it gets even better. These records can be enhanced with Lenses and Prisms. I absolutely fell in love with how declarative the code can get with this small addition.

To illustrate this, lets have a peek at src/Controller.hs. All you need to know is in the top-level functions.

For Playing, it looks like this:

20: step :: Float -> GameState -> IO GameState
21: step secs (Playing pstate)
22:   | delta > secsPerUpdate
23:   = do randomNumber <- randomIO :: IO Int
24:        return $ (Playing $ pstate
25:          & seed %~ const randomNumber
26:          & movePlayer delta
27:          & moveEnemies delta
28:          & pruneOffScreenEnemies
29:          & scrollBackground delta
30:          & attemptEnemySpawn
31:          & elapsedTime %~ const 0
32:         )& collisionCheck

And it reads just as you would describe it: If we are playing and it is time for an update, then we go to the next playing state. This state starts with the old one adds a new seed, which is constantly a new randomNumber, moves the player, moves the enemies, …

This simplicity also bubbles down to the event handlers, where at no cost keybindings can choose to be dependent on the current GameState variant:

59: inputKey :: Event -> GameState -> GameState
60: inputKey (EventKey (Char 'p') Down _ _) (Playing pstate) = Paused pstate
61: inputKey (EventKey (Char 'p') Down _ _) (Paused  pstate) = Playing pstate
62: inputKey (EventKey (Char 'q') Down _ _) (Playing pstate) = GameOverTypeName (getScore pstate) ""
63: inputKey (EventKey (Char 'q') Down _ _) (Paused  pstate) = GameOverTypeName (getScore pstate) ""

And likewise supports updating in the same clear way:

64: inputKey (EventKey (MouseButton LeftButton) ks _ __)  (Playing pstate) = Playing $ pstate
65:   & (player . moveDirection %~ updatePlayerDirection 'a' ks)
66: inputKey (EventKey (MouseButton RightButton) ks _ __) (Playing pstate) = Playing $ pstate
67:   & (player . moveDirection %~ updatePlayerDirection 'd' ks)
68: inputKey (EventKey (Char c) ks _ _)     (Playing pstate) = Playing $ pstate
69:   & (player . moveDirection %~ updatePlayerDirection c ks)
70: inputKey _ gstate = gstate