8

我正在尝试进入 Netwire,我已经挖掘找到文档、介绍、教程和诸如此类的东西,但是几乎每个教程和现有代码对于 Netwire 5 来说都是过时的,并且使用 Netwire 4 中不再使用的功能我们。自述文件很有帮助,但并非所有内容都可以编译,而且它仍然几乎没有提供足够的信息来开始使用。

我要求解释或示例只是为了让游戏循环运行并能够响应事件,所以我寻求信息以便最终知道:

  1. 基本结构(例如在响应式香蕉中,您如何启动使用处理程序、定义行为和对事件做出反应的网络描述)。
  2. 它最终如何进入main
  3. 如何处理 IO 事件(如鼠标单击、按键或游戏循环回调),事件如何进入会话等。

还有其他相关的。

我认为从那里我可以运行一些东西,所以我可以通过实验来学习其余的东西(因为第 5 版中的文档和教程的状态非常不存在,我希望很快就会出现一些)。

谢谢!

4

2 回答 2

10

免责声明:我还没有找到任何使用 Netwire 的大型程序,所以我将要写的所有内容都应该对你有所保留,因为它是基于我自己使用 Netwire 的经验。我在这里使用的示例大多取自我自己的库,并尝试使用 FRP 编写游戏,并且可能不是做事的“正确方法”。


问题 1:基本结构(例如在响应式香蕉中,您如何执行使用处理程序、定义行为和对事件做出反应的网络描述)。

Sessions: netwire 库的作者对netwire 程序的基本结构给出了非常好的回答。由于有点老了,我将在这里概述一些要点。在看wire之前,我们先来看看netwire是如何处理时间的,FRP的底层驱动。在不使用测试工具的情况下提前时间的唯一方法testWire是生成一个Session将有状态地返回时间增量的方法。Sessions保存状态的方式被封装在它们的类型中:

newtype Session m s = Session { stepSession :: m (s, Session m s) }

在这里, aSession位于 Monad 中(通常是IO),每次评估它时,都会返回一个类型为“时间状态”的值s和一个 new Session。通常,任何有用的状态s都可以写成一个Timed可以返回一些实例的值Real t

data Timed t s
class (Monoid s, Real t) => HasTime t s | s -> t where
    -- | Extract the current time delta.
    dtime :: s -> t
instance (Monoid s, Real t) => HasTime t (Timed t s)

例如,在游戏中,您通常需要一个固定的时间步来执行更新调用。netwire 将这个概念编码为:

countSession_ :: Applicative m => t -> Session m (Timed t ())

AcountSession_将时间步长作为输入,在本例中为 type 的固定值t,并生成Session其状态值为 type 的a Timed t ()。这意味着它们只对 type 的单个值进行编码t,并且不携带任何附加状态()。在我们讨论了电线之后,我们将看到这在评估电线方面是如何发挥作用的。

电线: Netwire 中“电线”的主要类型是:

Wire s e m a b

这条线描述了一个类型的反应值,b它执行以下操作:

  • 将类型的反应值作为输入a
  • 在 Monad 中运行m
  • 可能抑制或不产生值,产生类型的抑制值e
  • 假设时间状态由s

由于作为电抗值的性质,线可以被认为是时变函数。因此,每条线都被编码为时间(或时间状态s)的函数,在该时刻产生一个新的 type 值b,以及一个用于评估下一个 type 输入的新线a。通过返回一个值和一个新的连线,函数可以通过函数定义传播状态来包含状态。

此外,电线可能会禁止或不产生值。这对于未定义计算(例如当鼠标位于应用程序窗口之外)时很有用。这允许您实现类似的东西switch,其中一条线更改为不同的线以继续执行(例如玩家完成他的跳跃)。

有了这些思路,我们就可以看到netwire中wire的主要驱动:

stepWire :: Monad m => Wire s e m a b -> s -> Either e a -> m (Either e b, Wire s e m a b)

stepWire wire timestate input完全按照我们之前所说的那样做:它需要 awire并将电流timestateinput前一根电线传递给它。然后,在底层 Monadm中,它要么产生一个值,Right b要么用一个值抑制Left e,然后给出下一条线用于计算。

问题2:它最终如何进入主要。

有了 and 类型的值SessionWire我们可以构造一个循环,一遍又一遍地做两件事:

  1. 步进会话以接收新的时间状态
  2. 使用新的时间状态来步进导线

下面是一个程序示例,该程序将固定计数器更改为永远以二为单位计数:

import Control.Wire

-- My countLoop operates in the IO monad and takes two parameters:
--   1. A session that returns time states of type (Timed Int ())
--   2. A wire that ignores its input and returns an Int
countLoop :: Session IO (Timed Int ()) -> Wire (Timed Int ()) () IO a Int -> IO ()
countLoop session wire = do
  (st, nextSession) <- stepSession session
  (Right count, nextWire) <- stepWire wire st (Right undefined)
  print count
  countLoop nextSession nextWire

-- Main just initializes the procedure:
main :: IO ()
main = countLoop (countSession_ 1) $ time >>> (mkSF_ (*2))

问题 3:如何处理 IO 事件(如鼠标单击、按键或游戏循环回调),事件如何进入会话等。

关于如何做到这一点存在一些争论。我认为在这种情况下,最好利用底层的 Monad m,并将当前状态的快照简单地传递给stepWire函数。在这样做时,我的大多数输入线看起来像这样:

mousePos :: Wire s e (State Input) a (Float, Float)

电线的输入被忽略,鼠标输入是从State单子中读取的。我使用State而不是Reader为了正确处理键去抖动(因此单击 UI 不会同时单击 UI 下方的某些内容)。状态在我的main函数中设置并传递给runState,它也执行导线步进。像这样的电线的抑制行为可以编写一些优雅的代码。例如,假设您有电线rightleft箭头键,如果按下键会产生一个值,否则会抑制。您可以使用如下所示的线创建角色移动:

(right >>> moveRight) <|> (left >>> moveLeft) <|> stayPut

由于线是 的一个实例Alternative,如果right禁止,它将继续移动到下一条可能的线。a <|> b仅当两者都抑制时才会a抑制b

您也可以编写代码以利用 netwire 的Event系统,但您必须自己制作返回Eventusing的线路Control.Wire.Unsafe.Event。话虽如此,我还没有发现这种抽象比简单的抑制更有用。

于 2014-08-16T12:42:25.187 回答
0

回答这个问题可能真的太晚了,但我在这里有一个答案,其中包含 Netwire 5 程序的(简约)结构:Netwire中的控制台交互性?

于 2015-09-29T22:28:54.740 回答