Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Game entity modeling with netwire

Tags:

haskell

frp

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?

like image 928
Venge Avatar asked Feb 03 '13 00:02

Venge


3 Answers

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)
like image 101
ertes Avatar answered Nov 12 '22 07:11

ertes


There is a very simple and general solution to this. The key idea is that you never merge sources of different types. Instead, you only merge sources of the same type. The trick that makes this work is that you wrap the output of all your diverse sources in an algebraic data type.

I'm not really familiar with netwire, so if you don't mind I will use pipes as the example. What we want is a merge function that takes a list of sources and combines them into a single source that merges their outputs concurrently, finishing when they all complete. The key type signature is:

merge
 :: (Proxy p)
 => [() -> Producer ProxyFast a IO r] -> () -> Producer p a IO ()

That just says that it takes a list of Producers of values of type a, and combines them into a single Producer of values of type a. Here's the implementation of merge, if you are curious and you want to follow along:

import Control.Concurrent
import Control.Concurrent.Chan
import Control.Monad
import Control.Proxy

fromNChan :: (Proxy p) => Int -> Chan (Maybe a) -> () -> Producer p a IO ()
fromNChan n0 chan () = runIdentityP $ loop n0 where
    loop 0 = return ()
    loop n = do
        ma <- lift $ readChan chan
        case ma of
            Nothing -> loop (n - 1)
            Just a  -> do
                respond a
                loop n

toChan :: (Proxy p) => Chan ma -> () -> Consumer p ma IO r
toChan chan () = runIdentityP $ forever $ do
    ma <- request ()
    lift $ writeChan chan ma

merge
 :: (Proxy p)
 => [() -> Producer ProxyFast a IO r] -> () -> Producer p a IO ()
merge producers () = runIdentityP $ do
    chan <- lift newChan
    lift $ forM_ producers $ \producer -> do
        let producer' () = do
                (producer >-> mapD Just) ()
                respond Nothing
        forkIO $ runProxy $ producer' >-> toChan chan
    fromNChan (length producers) chan ()

Now, let's imagine that we have two sources of input. The first one generates the integers from 1 to 10 in one second intervals:

throttle :: (Proxy p) => Int -> () -> Pipe p a a IO r
throttle microseconds () = runIdentityP $ forever $ do
    a <- request ()
    respond a
    lift $ threadDelay microseconds

source1 :: (Proxy p) => () -> Producer p Int IO ()
source1 = enumFromS 1 10 >-> throttle 1000000

The second source reads three Strings from user input:

source2 :: (Proxy p) => () -> Producer p String IO ()
source2 = getLineS >-> takeB_ 3

We want to combine these two sources, but their output types don't match, so we define an algebraic data type to unify their outputs into a single type:

data Merge = UserInput String | AutoInt Int deriving Show

Now we can combine them into a single list of identically typed producers by wrapping their outputs in our algebraic data type:

producers :: (Proxy p) => [() -> Producer p Merge IO ()]
producers =
    [ source1 >-> mapD UserInput
    , source2 >-> mapD AutoInt
    ]

And we can test it out really quickly:

>>> runProxy $ merge producers >-> printD
AutoInt 1
Test<Enter>
UserInput "Test"
AutoInt 2
AutoInt 3
AutoInt 4
AutoInt 5
Apple<Enter>
UserInput "Apple"
AutoInt 6
AutoInt 7
AutoInt 8
AutoInt 9
AutoInt 10
Banana<Enter>
UserInput "Banana"
>>>

Now you have a combined source. You can then write your game engine to just read from that source, pattern match on the input and then behave appropriately:

engine :: (Proxy p) => () -> Consumer p Merge IO ()
engine () = runIdentityP loop where
    loop = do
        m <- request ()
        case m of
            AutoInt   n   -> do
                lift $ putStrLn $ "Generate unit wave #" ++ show n
                loop
            UserInput str -> case str of
                "quit" -> return ()
                _      -> loop

Let's try it:

>>> runProxy $ merge producers >-> engine
Generate unit wave #1
Generate unit wave #2
Generate unit wave #3
Test<Enter>
Generate unit wave #4
quit<Enter>
>>>

I imagine the same trick will work for netwire.

like image 24
Gabriella Gonzalez Avatar answered Nov 12 '22 08:11

Gabriella Gonzalez


Elm has a library for Automatons which I believe is similar to what you are doing.

You could use a typeclass for each type of state you want something to have access to. Then implement each of those classes for the entire state of your game (Assuming you have 1 big fat object holding everything).

-- bfgo = Big fat game object
class HasUserInput bfgo where
    mouseState :: bfgo -> MouseState
    keyState   :: bfgo -> KeyState

class HasPositionState bfgo where
    positionState :: bfgo -> [Position] -- Use your data structure

Then when you create the functions for using the data, you simply specify the typeclasses those functions will be using.

{-#LANGUAGE RankNTypes #-}

data Player i = Player 
    {playerRun :: (HasUserInput i) => (i -> Player i)}

data Projectile i = Projectile
    {projectileRun :: (HasPositionState i) => (i -> Projectile i)}
like image 2
Zachary Kamerling Avatar answered Nov 12 '22 08:11

Zachary Kamerling