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