How I built a game in Haskell - pure functional style
I'd like to share with you my experience of building a game in Haskell.
I'll describe my experience, share some key ideas, and try to dispel some myths about Haskell.
main = putStrLn "Hello, world!"
Separation between effectful code (mutation, IO, etc) and uneffectful code
Referential transparency
=
(equals sign) means both sides are interchangeableAnd we can have them by writing a game in Haskell using pure functional style.
I chose to build a custom engine on top of the sdl2 bindings as they are quite thorough and stable and I didn't want to get stuck.
But this means that I need to decide on the architecture myself.
Many engines (such as Flash and Love2d) use events and callbacks.
This design makes the program inherently effectful.
A function that returns void
is all about effects!
The Game loop handles effects
Let's focus on a few components:
data SceneF a
= SceneF
{ state :: a
, update :: Input -> a -> Result ([Request], (Command, a))
, render :: Renderer -> a -> IO ()
}
Scene
s of different data{-# LANGUAGE ExistentialQuantification #-}
-- Definition
data Scene = forall s. Scene (SceneF s)
-- Creation
mkScene :: SceneF s -> Scene
mkScene scene = Scene scene
-- Usage: render the top scene on the stack
renderer :: Renderer -> Stack Scene -> IO ()
renderer sdlRenderer scenes =
case head scenes of
Scene SceneF{render, state} ->
render sdlRenderer state
Scene
does not know (nor care) about s
SceneF
encodes the fact the render
's s
and state's s
are the sameFirst class (composable) getters and setters.
Can be used to:
view (hitbox . size . x) character
set direction newDir character
over position (flip addPoint move) character
We can talk about types that has the same components polymorphicaly:
isTouchingCircleCircle
:: (HasSize a Size, HasPos a IPoint)
=> (HasSize b Size, HasPos b IPoint)
=> a -> b -> Maybe (a, b)
You can pass a lens to a function:
circleDistance circle rect = Point (dist x) (dist y)
where
dist axis =
abs
( circle ^. pos . axis
- (rect ^. hitbox . pos . axis)
- (rect ^. hitbox . size . axis) `div` 2
)
data Command
= Wait Actions Int
| Spawn [Enemy]
| LoadScene Scene
| LoadTextBox Actions TB.TextBox
| PlayMusic BS.ByteString
| StopMusic
| Shake
| ...
Can you write games in GC languages? Does Haskell have unacceptable pause times?
➜ haskell-play git:(master) stack exec app -- +RTS -sstderr
...
Tot time (elapsed) Avg pause Max pause
Gen 0 0.082s 0.087s 0.0001s 0.0007s
Gen 1 0.001s 0.001s 0.0002s 0.0003s
INIT time 0.000s ( 0.002s elapsed)
MUT time 7.862s ( 92.500s elapsed)
GC time 0.083s ( 0.087s elapsed)
EXIT time 0.000s ( 0.005s elapsed)
Total time 7.945s ( 92.594s elapsed)
%GC time 1.0% (0.1% elapsed)
Alloc rate 81,352,956 bytes per MUT second
Productivity 99.0% of total user, 99.9% of total elapsed
You can also write a game in Haskell in purely functional style.
If you want to play the game, you can find it at: