I'm trying to get into Netwire, I've dug to find documentations, introductions, tutorials and whatnot, but just about every tutorial & existing-code is outdated as to Netwire 5 and uses functions from Netwire 4 that are just no longer with us. The README is kind of helpful, but not everything compiles and still it barely provides enough information to get started.
I am asking for explanation or an example just to get a game-loop running and being able to respond to events, so I seek information so I'll eventually know:
main
.And anything else relevant.
I figure that from there I could get something running, and so I could learn the rest by experimentation (as the state of documentation and tutorials for this in the 5th version is horribly non-existent, I hope some will appear very shortly).
Thank you!
Disclaimer: I haven't been able to find any large-scale programs that use Netwire, so everything I'm about to write you should take with a grain of salt, as it's based on my own experiences using Netwire. The examples that I use here are mostly taken from my own library and attempt at writing a game using FRP, and might not be "the right way" to do things.
Sessions:
The author of the netwire library gave a really good answer about the basic structure of a netwire program. Since it's a bit old, I will outline some of the points here. Before we look at wires, let's first take a look at how netwire handles time, the underlying driver of FRP. The only way to advance time without using the testing harness testWire
is to produce a Session
that will statefully return time deltas. The way Sessions
preserve state is encapsulated in their type:
newtype Session m s = Session { stepSession :: m (s, Session m s) }
Here, a Session
lies within a Monad (usually IO
) and every time it is evaluated, returns a "time state" value of type s
and a new Session
. Usually, any useful state s
can be written as a Timed
value that can return some instance of 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)
For example, in games, you usually want a fixed timestep to do your update calls. netwire encodes this notion with:
countSession_ :: Applicative m => t -> Session m (Timed t ())
A countSession_
takes as input the timestep, in this case a fixed value of type t
, and produces a Session
whose state values are of type Timed t ()
. This means that they only encode a single value of type t
, and do not carry any additional state with the ()
value. After we discuss wires, we will see how this plays a role in evaluating them.
Wires: The main type of a 'wire' in Netwire is:
Wire s e m a b
This wire describes a reactive value of type b
that does the following:
a
m
e
s
By the nature of being reactive values, wires can be thought of as time-varying functions. Hence, each wire is encoded as a function of time (or the time state s
) that produces, at that instant in time, a new value of type b
, and a new wire with which to evaluate the next input of type a
. By returning a value and a new wire, the function can encompass state by propagating it through the function definitions.
Additionally, wires may inhibit or not produce a value. This is useful for when computations are not defined (such as when the mouse is outside of the application window). This allows you to implement things like switch
, where a wire changes to a different wire to continue execution (such as a player finishing his jump).
With these ideas, we can see the main driver of wires in netwire:
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
does exactly what we said earlier: It takes a wire
and passes it the current timestate
and input
from the previous wire. Then, in the underlying Monad m
, it either produces a value of Right b
or inhibits with a value of Left e
, and then gives the next wire to use for the computation.
Armed with values of type Session
and Wire
, we can construct a loop that does two things over and over:
Here is an example of a program that alters a fixed counter to count by twos forever:
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))
There is somewhat of a debate about how to do this. I think in this situation it's best to take advantage of the underlying Monad m
, and simply pass a snapshot of the current state to the stepWire
function. In doing so, most of my input wires look something like this:
mousePos :: Wire s e (State Input) a (Float, Float)
Where the input to the wire is ignored, and the mouse input is read from a State
monad. I use State
and not Reader
in order to handle key debounces properly (so that clicking on UI doesn't also click on something underneath UI). The state is set in my main
function and passed to runState
, which also does the wire stepping. The inhibition behavior of wires like this can make for some elegant code. For example, suppose you have wires right
and left
for your arrow keys that produce a value if the key is pressed and inhibit otherwise. You can create character movement with a wire that looks like this:
(right >>> moveRight) <|> (left >>> moveLeft) <|> stayPut
Since wires are an instance of Alternative
, if right
inhibits, it'll just move on to the next possible wire. a <|> b
will inhibit only if both a
and b
inhibit.
You could also write your code to take advantage of netwire's Event
system, but you'd have to make your own wires that return an Event
using Control.Wire.Unsafe.Event
. That being said, I have yet to find this abstraction more useful than simple inhibition.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With