如何在Haskell中编写游戏事件循环(例如setTimeout)?

时间:2014-05-21 05:54:11

标签: haskell timeout

第一

在Haskell中实现setTimeout函数是否存在或有多大麻烦?

主要想法

setTimeout someFunction 1000 --someFunction will be called in 1000ms = 1s

我是Haskell的新手,可能不会使用它;我正在学习Haskell的一脚。


第二

游戏的事件循环通常需要您

  1. 根据规则(通常是物理)修改您的内部游戏对象
  2. 更新图片
  3. 这里管理时间很重要(因此setTimeout)以确保不变的FPS。

    如何在Haskell上看到这样的模式?

3 个答案:

答案 0 :(得分:5)

  

在Haskell中实现setTimeout函数是否存在或有多大麻烦?

JavaScript setTimeout和其他类似的方法(如Qt'} QTimer通常在单个线程中的单个事件循环中工作(不包括网络工作者或QThread )。使用Haskell建模确切行为有点困难,尽管并非不可能,因为GHC已经提供了(内部)implementation

如果你不关心实际的单线程行为(这也意味着两个具有几乎相同超时的函数可能同时执行),那么你可以简单地分叉一个新线程并延迟它动作:

doLater :: Int -> IO () -> IO ThreadId
doLater ms io = forkIO $ threadDelay (ms * 1000) >> io

对于任何进一步的想法,我们实际上需要了解有关您的特定事件循环的更多信息。

  

这样的模式如何看待Haskell?

非常,非常一般化,你需要这样的东西:

mkGame :: World 
          -> (Input -> World -> World) 
          -> (TimeDiff -> World -> World)
          -> (World -> GraphicalRepresentation)
          -> IO ()
mkGame initialWorld handleInput handleProgression drawWorld = undefined

请注意,您可能会在其中抛出其他参数,例如每秒的最大世界更新次数。

我们现在如何建模setTimeout?假设World类似于:

data World = World { 
     getWorldData    :: WorldData, 
     getCurrentTime  :: TimePoint, -- would get updated by your loop
     getTimeouts     :: Queue TimePoint (World -> World)
}

其中Queue是任何工作优先级队列(pick one,有很多,或构建自己的队列)。现在您可以使用

简单地设置超时
setTimeout world delay action = world { getTimeouts = timeouts' }
    where timeouts' = insert (getTimeouts world) t action
          t         = delay + getCurrentTime world

-- insert :: Queue p v -> p -> v -> Queue p v

如果您希望取消超时,还需要Queue上的密钥,请查看GHC.Event.PSQ获取灵感。

除此之外,您现在可以通过简单地浏览队列并应用所有功能来检查时间是否已经过去并在游戏循环中采取相应的行动。

这基本上是一个非常简单粗暴的基础,可以用来激发你自己的工作。根据你真正想做的事情,你可能想看看gloss,它已经实现了一个非常相似的概念,虽然没有超时,但在这种情况下,你仍然可以添加超时和总时差在您的世界中,只需使用拟合更新功能(TimeDiff -> World -> World部分)。

答案 1 :(得分:4)

这个问题对纯函数没有意义,因为Haskell做了懒惰的评估。当需要其结果时,将调用函数,而不是之前的函数,而不是更晚的函数。

但是,如果您想要延迟某些IO操作,则可以在GHC中使用threadDelay中的Control.Concurrent函数。然后,您的setTimeout将实现为

setTimeout :: IO () -> Int -> IO ThreadId

setTimeout ioOperation ms =
  forkIO $ do
    threadDelay (ms*1000)
    ioOperation

答案 2 :(得分:1)

这是一个老问题,但我对答案并不满意(并且没有一个被标记为正确),因为过分关注重新实现setTimeout函数,我认为这是对...的错误解释。什么setTimeout用于使用javascript制作游戏。

我可以假设你想要替换这个javascript模式吗?

function update() { 
    undefined;
    setInterval(update, 1000)
}

Javascript之外的游戏循环通常不是使用setTimeout编写的,而setTimeout是从浏览器中JS的异步性质开始的。 另外,建议不要将setTimeout用于时间关键的可视代码,requestAnimationFrame是最佳功能。

编写游戏循环的惯用方法是一个简单的循环,伪haskell代码看起来像这样:

draw :: World -> IO ()
draw w = do
    undefined
    glSwapBuffers

updateWorld :: World -> World
updateWorld w = undefined

loop World -> Time -> IO ()
loop world time = do
    inputs <- getInputs
    time' <- getTime
    let delta = time' - time
    let world' = updateWorld delta world inputs
    draw world'
    loop world' time'

setup :: IO World
setup = do
    undefined -- window setup etc
    world <- undefined
    return world

main :: IO ()
main = do
    w <- setup
    t <- getTime
    loop w t

您的draw函数应该调用glSwapBuffers(或等效函数),这在大多数设备上会阻塞直到vsync,这将保持平滑的fps,但不要假设所有计算机都启用了vsync,并且不要假设所有显示器都是60hz,144hz,并且可变帧速率技术现在在游戏玩家社区中很普​​遍。

这样的循环也会安全地降级,如果它不能制作16.67ms窗口(不像使用forkIO构建循环的那些),虽然haskell的微线程,使用它们编程不是疯了,但要确保使用它们是线程安全,例如使用STM之类的东西。

clocktime包将提供getTime函数。

Zeta的mkGame是更具功能性的风格(因为它将函数作为参数),并且可以很容易地适应我的代码。

另请注意,像sdl这样的库提供了自己的循环和输入以及计时方法,这可能会简化(或复杂化)代码。