Documentation of the haskell code for Defect Process, a 2d hack n' slash game on Steam. The full source code is available on GitHub.
[Brief Overview] |
[Cross-platform Notes] |
[Additional Links]
gameMain :: World -> IO World
gameMain world = do
world' <- updateWorld world
gameMain world'
Since this is a purely functional language the way this works in general is that we use modified copies of data, rather than updating the data in-place.[1]
src/AppEnv/Types.hs type for the full definition.
updatePlayer :: Player -> IO Player
updatePlayer player = do
player' <- ... useful things ...
return player'
This is fine until updatePlayer needs to change anything else in addition to Player, e.g. spawn a particle effect or pull a switch in the level. Adding individual things to the return type gets tedious quickly, e.g. updatePlayer :: Player -> IO (Player, [Particle], Level).
updatePlayer :: World -> IO World
updatePlayer world =
let
player = _player world
particles = _particles world
level = _level world
in do
player' <- ... useful things ...
particles' <- ... useful things ...
level' <- ... useful things ...
return $ world
{ _player = player'
, _particles = particles'
, _level = level'
}
This also works but now looking at the updatePlayer type it's no longer clear what it changes. Any field of World could be modified so it's much harder to reason about what's happening.
update functions into two parts, think/update:
thinkPlayer :: Player -> [Message]
thinkPlayer player =
[ ... message to move the player ...
, ... message to make a particle effect ...
, ... message to flip a level switch ...
]
updatePlayer :: [Message] -> Player -> IO Player
updatePlayer messages player = do
player' <- ... update player according to messages ...
return player'
which is used as follows:
gameMain :: World -> IO World
gameMain world =
let
player = _player world
particles = _particles world
level = _level world
playerMessages = thinkPlayer player
particlesMessages = thinkParticles particles
levelMessages = thinkLevel level
allMessages = playerMessages ++ particleMessages ++ levelMessages
in do
player' <- updatePlayer allMessages player
particles' <- updateParticles allMessages particles
level' <- updateLevel allMessages level
return $ world
{ _player = player'
, _particles = particles'
, _level = level'
}
This allows different game entities to communicate with each other while keeping their own update functions clean.[2] The full implementation is more complicated but the core idea is as described above, see src/Msg/ and src/World/Main.hs for the relevant code.
src/Window/. It's kind of a basic 2d engine, not that much interesting to talk about.
--nonmoving-gc garbage collector, which avoids stop-the-world behavior that could cause stutters. That flag isn't critical however, the default --copying-gc behavior is also fine in testing (< 1 ms pauses).