首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >基于netwire的游戏实体建模

基于netwire的游戏实体建模
EN

Stack Overflow用户
提问于 2013-02-03 00:17:59
回答 3查看 1.1K关注 0票数 9

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

有一件事我不确定,那就是:我要怎么打电线?并不是所有的实体都有相同的输入,玩家是唯一能够访问关键输入状态的实体,寻找导弹需要目标的位置等等。

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

我该怎么办?

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2013-02-03 21:55:23

抑制类e是抑制异常的类型。它不是连线产生的东西,而是承担着与eEither e a中的角色相同的角色。换句话说,如果通过<|>组合连接,则输出类型必须相等。

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

代码语言:javascript
复制
keyDown :: (Monad m, Monoid e) => Key -> Wire e m GameState ()

这条线以当前游戏状态作为输入,如果按下键,则生成一个()。虽然键不是按下的,但它只是抑制了。大多数应用程序并不真正关心电线抑制的原因,所以大多数应用程序都使用mempty进行抑制。

表达这一事件的一种更方便的方法是使用读取器monad:

代码语言:javascript
复制
keyDown :: (Monoid e) => Key -> Wire e (Reader GameState) a a

这个变体真正有用的是,现在您不必将游戏状态作为输入传递。相反,当偶数发生时,这种电线就像标识线,当不发生时就会抑制:

代码语言:javascript
复制
quitScreen . keyDown Escape <|> mainGame

这样做的想法是,当按下转义键时,事件线keyDown Escape就会暂时消失,因为它的作用类似于标识线。所以整个电线的作用就像quitScreen,假设它不抑制自身。一旦释放了密钥,事件线就会抑制,所以使用quitScreen的组合也会受到抑制。因此,整个电线的作用类似于mainGame

如果您想限制电线可以看到的游戏状态,您可以很容易地为此编写一个连接组合器:

代码语言:javascript
复制
trans :: (forall a. m' a -> m a) -> Wire e m' a b -> Wire e m a b

这允许您应用withReaderT

代码语言:javascript
复制
trans (withReaderT fullGameStateToPartialGameState)
票数 7
EN

Stack Overflow用户

发布于 2013-02-03 07:17:49

对此有一个非常简单和普遍的解决办法。关键的想法是,您永远不会合并不同类型的源。相反,您只合并相同类型的源。完成这项工作的诀窍是,将所有不同来源的输出封装为代数数据类型。

我不太熟悉netwire,所以如果您不介意的话,我将使用pipes作为示例。我们需要的是一个merge函数,它接收一个源列表并将它们合并到一个单一的源中,并发合并它们的输出,当它们都完成时完成。关键类型签名是:

代码语言:javascript
复制
merge
 :: (Proxy p)
 => [() -> Producer ProxyFast a IO r] -> () -> Producer p a IO ()

这就是说,它获取了一个Producera类型值的列表,并将它们组合成一个类型为a的值的Producer。下面是merge的实现,如果您对此感到好奇,并且希望遵循以下内容:

代码语言:javascript
复制
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的整数:

代码语言:javascript
复制
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

代码语言:javascript
复制
source2 :: (Proxy p) => () -> Producer p String IO ()
source2 = getLineS >-> takeB_ 3

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

代码语言:javascript
复制
data Merge = UserInput String | AutoInt Int deriving Show

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

代码语言:javascript
复制
producers :: (Proxy p) => [() -> Producer p Merge IO ()]
producers =
    [ source1 >-> mapD UserInput
    , source2 >-> mapD AutoInt
    ]

我们可以很快地测试出来:

代码语言:javascript
复制
>>> 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"
>>>

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

代码语言:javascript
复制
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

我们试试看:

代码语言:javascript
复制
>>> 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
EN

Stack Overflow用户

发布于 2013-02-03 12:16:40

Elm有一个自动机库,我相信它与你所做的类似。

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

代码语言:javascript
复制
-- 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

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

代码语言:javascript
复制
{-#LANGUAGE RankNTypes #-}

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

data Projectile i = Projectile
    {projectileRun :: (HasPositionState i) => (i -> Projectile i)}
票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/14668075

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档