9

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

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

  • 一个想法是让 ObjectInput 类型传递给所有东西,但这对我来说似乎很糟糕,因为我可能会不小心引入我不想要的依赖项。
  • 另一方面,我不知道拥有 SeekerWire、PlayerWire、EnemyWire 等是否是个好主意,因为它们几乎“相同”,因此我必须在它们之间复制功能。

我应该怎么办?

4

3 回答 3

7

抑制幺半群e抑制异常的类型。e它不是电线产生的东西,但与in的作用大致相同Either e a。换句话说,如果您通过 组合线<|>,则输出类型必须相等。

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

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

这条线将当前游戏状态作为输入,并()在按键被按下时产生一个。当键没有被按下时,它只是禁止。大多数应用程序并不真正关心为什么电线禁止,所以大多数电线禁止使用mempty.

表达此事件的更方便的方法是使用 reader 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)
于 2013-02-03T21:55:23.197 回答
2

Elm 有一个自动机库,我相信它与您正在做的类似。

您可以为您想要访问的每种状态类型使用一个类型类。然后为您的游戏的整个状态实现这些类中的每一个(假设您有 1 个大胖对象容纳所有东西)。

-- 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)}
于 2013-02-03T12:16:40.490 回答
2

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

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

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

这只是说它需要一个Producertype 值的列表a,并将它们组合成一个Producertype 值a。下面是 的实现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 ()

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

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从用户输入中读取三个 s:

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.

于 2013-02-03T07:17:49.583 回答