使用netwire进行游戏实体建模

时间:2013-02-03 00:17:59

标签: haskell frp

我将使用netwire和OpenGL在Haskell中编写实时游戏。基本的想法是每个对象将由一个导线表示,它将获得一些数据作为输入并输出其状态,然后我将它全部挂钩到一个大的线路,将GUI的状态作为输入并输出世界状态,然后我可以将其传递给渲染器以及碰撞检测等一些“全局”逻辑。

我不确定的一件事是:我如何键入电线?并非所有实体都有相同的输入;玩家是唯一可以访问键输入状态的实体,寻找需要目标位置的导弹等。

  • 一个想法是将一个ObjectInput类型传递给所有东西,但这对我来说似乎很糟糕,因为我可能会意外地引入我不想要的依赖项。
  • 另一方面,我不知道是否有一个SeekerWire,一个PlayerWire,一个EnemyWire等,因为它们几乎是“相同”的,所以我不得不重复功能它们。

我该怎么办?

3 个答案:

答案 0 :(得分:7)

抑制monoid e抑制例外的类型。这不是电线产生的东西,而是与e中的Either e a具有相同的作用。换句话说,如果按<|>组合导线,则输出类型必须相等。

假设您的GUI事件通过输入传递给线路,并且您有一个连续的按键事件。建模的一种方法是最直接的:

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

此线路将当前游戏状态作为输入,如果按住键,则产生()。虽然没有按下按键,但它只是禁止按键。大多数应用程序并不真正关心为什么导线抑制,因此大多数导线都会被mempty抑制。

表达此事件的更方便的方法是使用阅读器monad:

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

这个变体真正有用的是,现在你不必将游戏状态作为输入传递。相反,这条线在偶数发生时就像身份线一样,并且当它不发生时就会起作用:

quitScreen . keyDown Escape <|> mainGame

这个想法是当按下转义键时,事件导线keyDown Escape会暂时消失,因为它的作用类似于标识线。所以整条线就像quitScreen一样,假设它不会抑制自己。释放密钥后,事件线会禁止,因此带有quitScreen的组合也会抑制。因此整条线的作用就像mainGame

如果你想限制电线可以看到的游戏状态,你可以轻松地为它编写一个线组合器:

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

这允许您应用withReaderT

trans (withReaderT fullGameStateToPartialGameState)

答案 1 :(得分:2)

这有一个非常简单和通用的解决方案。关键的想法是你永远不会合并不同类型的来源。相反,您只合并相同类型的源。使这项工作的诀窍在于您将所有不同来源的输出包装在代数数据类型中。

我对netwire并不熟悉,所以如果您不介意,我会以pipes为例。我们想要的是一个merge函数,它接受一个源列表并将它们组合成一个源,它同时合并它们的输出,当它们全部完成时结束。密钥类型签名是:

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

这只是说它需要列出Producera类型的值,并将它们组合成Producer类型a的{​​{1}}个值。以下是merge的实现,如果您感到好奇并希望继续:

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 ()

现在,让我们假设我们有两个输入源。第一个生成从110的整数,间隔为一秒:

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

第二个来源从用户输入读取三个String

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

我们希望将这两个源组合在一起,但它们的输出类型不匹配,因此我们定义一个代数数据类型以将它们的输出统一为一个类型:

data Merge = UserInput String | AutoInt Int deriving Show

现在我们可以将它们的输出包装在我们的代数数据类型中,将它们组合成一个相同类型生成器的列表:

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

我们可以非常快速地测试它:

>>> 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"
>>>

现在你有一个综合来源。然后,您可以编写您的游戏引擎,只需从该源读取,输入上的模式匹配,然后表现得恰当:

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

我们试一试:

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

我认为同样的技巧适用于netwire

答案 2 :(得分:2)

榆树有一个Automatons的图书馆,我认为它与你正在做的类似。

您可以为想要访问的每种类型的州使用类型类。然后为游戏的整个状态实现每个类(假设你有一个大胖对象保存所有东西)。

-- 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

然后,当您创建使用数据的函数时,只需指定这些函数将使用的类型类。

{-#LANGUAGE RankNTypes #-}

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

data Projectile i = Projectile
    {projectileRun :: (HasPositionState i) => (i -> Projectile i)}