【问题标题】:How to write a Game Event Loop (e.g. setTimeout) in Haskell?如何在 Haskell 中编写游戏事件循环(例如 setTimeout)?
【发布时间】:2014-05-21 05:54:10
【问题描述】:

首先

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

主要思想

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

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


第二

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

  1. 修改基于规则(通常是物理)的内部游戏对象
  2. 更新您的图形

在这里管理时间很重要(因此 setTimeout)以确保恒定的 FPS。

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

【问题讨论】:

  • 您的第一个问题有点基础,但内容相当完善。另外两个问题要实质性得多,似乎是谷歌搜索的主要目标。你这样做了吗?你想到了什么?结果你试过做什么?您的尝试在哪里失败了?
  • (确保只标记目标语言,除非问题是与多种语言相关的直接比较。)
  • 有点离题,但我很有趣:stackoverflow.com/questions/15236238/…

标签: haskell timeout


【解决方案1】:

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

JavaScript 的 setTimeout 和 Qt 的 QTimer 等其他类似方法通常在单个线程中的单个事件循环中工作(不包括 web worker 或 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 部分)。

【讨论】:

    【解决方案2】:

    这个问题对于纯函数没有意义,因为 Haskell 会进行惰性求值。一个函数会在需要它的结果时被调用,不会更早也不会更晚。

    但是,如果您想要延迟一些 IO 操作,您可以在 GHC 中使用来自 Control.ConcurrentthreadDelay 函数。然后您的setTimeout 将被实现为

    setTimeout :: IO () -> Int -> IO ThreadId
    
    setTimeout ioOperation ms =
      forkIO $ do
        threadDelay (ms*1000)
        ioOperation
    

    【讨论】:

    • 可能应该在里面扔一个forkIO
    • 我同意。 setTimeout 函数是异步的。因此它应该是setTimeout op ms = forkIO $ threadDelay (1000 * ms) >> op。另外,无论如何返回ThreadId 更有意义。 setTimeout 函数在 JavaScript 中返回一个 id。
    • 根据您的回答,我将问题修改为一些新的疑问。有很多东西我还是不明白,主要是因为我对不可变状态缺乏经验。
    • @DanielWagner 我错过了!完成。
    • 有人能把-> IO a最后改成-> IO ThreadId吗?我编辑明显错误的尝试总是被拒绝。通常至少有一位评论者告诉我我改变太多,而至少一位评论者告诉我我改变得太少。
    【解决方案3】:

    这是一个老问题,但我对答案不满意(并且没有一个被标记为正确),因为过于关注重新实现 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.67 毫秒的窗口(与使用 forkIO 构建循环的任何东西不同),这样的循环也会安全地降级,尽管 haskell 的微线程使得使用它们进行编程并不疯狂,但请确保使用它们是线程安全,例如使用 STM 之类的东西。

    clocktime 包将提供 getTime 函数。

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-05-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-12-03
      • 1970-01-01
      相关资源
      最近更新 更多