Search code examples
haskellfrp

Game entity modeling with netwire


I'm going to be writing a real-time game in Haskell using netwire and OpenGL. The basic idea is that each object will be represented by a wire, which will get some amount of data as input and output its state, and then I'll hook it all up into one big wire that gets the state of the GUI as input and outputs the world state, which I can then pass onto a renderer as well as some 'global' logic like collision detection.

One thing I'm not sure about is: how do I want to type the wires? Not all entities have the same input; the player is the only entity that can access the state of the key input, seeking missiles need the position of their target, etc.

  • One idea would be to have an ObjectInput type that gets passed to everything, but that seems bad to me since I could accidentally introduce dependencies I don't want.
  • On the other hand, I don't know if having a SeekerWire, a PlayerWire, an EnemyWire, etc., would be a good idea since they're almost 'identical' and so I'd have to duplicate functionality across them.

What should I do?


Solution

  • The inhibition monoid e is the type for inhibition exceptions. It's not something the wire produces, but takes about the same role as the e in Either e a. In other words, if you combine wires by <|>, then the output types must be equal.

    Let's say your GUI events are passed to the wire through input and you have a continuous key-down event. One way to model this is the most straightforward:

    keyDown :: (Monad m, Monoid e) => Key -> Wire e m GameState ()
    

    This wire takes the current game state as input and produces a () if the key is held down. While the key is not pressed, it simply inhibits. Most applications don't really care about why a wire inhibits, so most wires inhibit with mempty.

    A much more convenient way to express this event is by using a reader monad:

    keyDown :: (Monoid e) => Key -> Wire e (Reader GameState) a a
    

    What's really useful about this variant is that now you don't have to pass the game state as input. Instead this wire just acts like the identity wire when the even happens and inhibits when it doesn't:

    quitScreen . keyDown Escape <|> mainGame
    

    The idea is that when the escape key is pressed, then the event wire keyDown Escape vanishes temporarily, because it acts like the identity wire. So the whole wire acts like quitScreen assuming that it doesn't inhibit itself. Once the key is released, the event wire inhibits, so the composition with quitScreen inhibits, too. Thus the whole wire acts like mainGame.

    If you want to limit the game state a wire can see, you can easily write a wire combinator for that:

    trans :: (forall a. m' a -> m a) -> Wire e m' a b -> Wire e m a b
    

    This allows you to apply withReaderT:

    trans (withReaderT fullGameStateToPartialGameState)