Monday, November 12, 2012

In this blog post, the breakout example from the last Post has been improved, giving it more features:

The Paddle and Blocks have rounded edges. The ball bounces of them depending on the surface normal where it hits.

The blocks are fading out when destroyed.

The paddle can shot to destroy blocks. When the game starts one shot is available, shots can be gained by destroying green blocks.

The game is aware when the player lost or won and displays this information when the game ends.

But that is not all! Instead of using the simple Coroutines we are now using a full blown FRP library called netwire. But more about that later, here is the preview. As alwyas you have to click the canvas to get input focus. If you are not viewing this blog article on blogspot and the application does not work, try the original aricle page.

I had a lot of help over the haskell beginners mailing list. I will try to add links to the specific topics whenever I am writing something I had help with.

As a final note before I start: Being a haskell beginner, I might not do everything here the best way. I encourage you to comment if you think something could be done better! Of course, I also encourage you to comment if you have any questions.

About Netwire

Netwire is a arrowized functional reactive programming (AFRP) library for haskell and the version 4 of the library has recently been released on hackage. Since it uses Arrows, some of the things we did with Coroutines can be done the same way with netwires, but it has tons of other features. Here is a short introduction to netwire, but I will try to explain all the features when I use them.

Also I will explain some of netwires usage here, this is by no means a complete tutorial to netwire. One obvious reason for this is, that I myself do not (yet?) understand all the features and Ideas of netwire (remember, I am still a haskell beginner doing this for my own education). Maybe some of this will be useful for someone wanting to start with netwire.

To install netwire, just type

haste-inst install netwire

This will most likely fail on lifted-base and time. There is a (potentially dangerous) workaround here, that should work for now.

New Javascript functions

To Draw a rounded rectangle a new function "fillRoundedRect" is defined in JavaScript.hs. Also a new type for Colors has been added:

This allows us to apply the collision functions directly to our game objects (when they are instances of the corresponding class) without always explicitly extracting the shape. In other words, instead of writing:

circleRectCollision (ballCircle ball) (blockRect block)

we can write

circleRectCollision ball block

The type classes return maybe types, because some objects might become "shapeless" and should not collide (for example a block that is fading out).

It returns a "Maybe Collision" because a collision might not take place. Notice the "do" notation. We are in the "Maybe" monad, which causes the function to automatically return Nothing if one of out circle shapes return Nothing (if you do not understand, see here). So we are getting the vector between the center positions and testing its square against the square of the sums of the radian of the circles. The guard function (from Control.Monad) causes the monad to return with "Nothing" if the circles are not close enough. Then we return the normalized vector as the normal. Notice that the normal always points from the first circle to the second.

So how do we test a circle against a rounded rectangle? A rounded rectangle is rectangle where the corners have been replaced by quarter circles. We have do test against these circles or the "inner" rectangle depending on where the colliding circle is, see this picture:

Areas of rounded rectangle

When the center of the colliding circle is in one of the red areas, collision testing is done with the corresponding corner circles. Otherwise collision is done against the "unrounded" rectangle (which is the same as rounded rectangle when we not in one of the red areas). The normal is then determined by the normal of the closest rectangle side. Here is the code:

I am a bit unhappy that I have to define the inner function "circleRoundedRectCollision'", but I do not know how else I could use this nice pattern guards.

Wire helpers

To handle bullets and blocks we need some way to manage a set of objects where objects can be removed. For this I got a lot of help here and here. The code is here. Let's look at the type of a wire:

dataWire e m a b

The m parameter is the underlying monad. We will set it to Identity and be fine with it. "a" is the input type. Quoting from here: From these inputs it (the wire)

either produces an output value of type "b" or inhibits with a value of type "e",

produces a new wire of type Wire e m a b.

When a wire produces, it is the same as our Coroutines producing output. The possibility that a wire can inhibit is often used to switch to different wires. See here. We will explore this possibility a little bit further down.

dynamicSet

When a wire inhibits, there are several combinators which allows to switch to other wires (permanently or just for one instance). Here inhibiting wires will be removed from the set. To create new wires a creator function and an additional input will be used.

mkGen is passed a function that is turned into a wire. The parameters for this function are the time delta (dt) and the input (i,new) of the wire. We use the do notation because we are in the inner Monad "m" (of which we know nothing but that it is a monad). After we stepped all wires ("stepWire" steps a wire ,see netwire tutorial) we filter those that produced (by returning a right value) and return there outputs as list. The new wire is again a dynamics set with the ramaing wires and the newly created ones using the creator function.

dynamicSetMap

To use dynamic set in the breakout game, we assign each wire in the set a unique key (Int) and change the input to a Map that maps from the key to the input values of the individual wires. Since a map lookup may fail, the input of the wires will be Maybes.

To archive this we define a wire that takes a list as inputs and pairs it with a given (infinite) list (which will be our keys):

The StartScreen constructor of the GameState is to show a message when the game is not running (in the beginning and when the player won or lost). We gave the ball the ballSpeed property (which is not necessary for viewing the game state) because it will be needed outside the balls own wires later. You will see. The Double parameter for a Dying block is the fade level (going from 1.0 to 0.0 as the block is removed). A Block now also as a BlockType. A PowerBlock is a block that gives the player ammo when destroyed.

The canvas name is the same name as defined in the outer html where the canvas is located.

Startup and key events

As said earlier, we step the main wire on every key event. But besides that the key event and startup functions look very similar to the last post. Also the drawing function has been extended to draw bullets and fading blocks. To produce the game state to draw, the wire is step with "Update". See here if you want to see the code.

Key events

In netwire an Event is a Wire that behaves as the identity wire when the event occurs and inhibits when the event does not occure. There are many functions to create events in netwire. Most require the inhibition type of the wire to be a monoid. That is very useful for switching on events. For now just accept that, you will see later.

So we create events that produce when the input event is a certain key down or release event:

Notice the use of accum1. In difference to accum, accum1 does not delay its output by one invocation. accum1Fold does the same as accum1 but takes a list as input over which it folds. Here it is used to fold over the incomming collision events.

What happens when the ball collides with an object? Assuming the collision is fully elastic, the velocity along the collision normal is inverted. The velocity (v0) along the collision normal (n) is <n,v0> (the scalar product of n and v0). Expressed with vector space, this is n <.> v0. To invert this part of v0, we have to substract this twice from v0. This gives us: v0 - (2.0 * (n <.> v0)) *^ n.

Blocks

A block behaves as its initial state, removing a live whenever it is hit (its input is not Nothing). When the lives are out, the block changes into the Dying state. And fades out in 30.0 "time units". Afterwards the block wire inhibts (so it is removed from the set).

Notice the expression "(pure 1.0) - (time / (pure 30.0)))" for the fading level. We can use "-" and "/" because wires are members of the Fractional and Num type classes. We could even leave out the "pure" and write "(1.0) - (time / (30.0)))". At present this does not work with haste because "framRational" needs some not yet supported primOps (see here).

When a "PowerBlock" is destroyed, the player is supposed to gain ammo. Therefore there is a blockAmmoWire that returns the number of ammo the player should gain. For a normal block it returns always 0. For a PowerBlock it returns 0 except the moment the block is destoryed (the input is not Nothing).

Putting it all together, the main wire

Switching game state

Let's first look at the outer wire, that manages when the game starts and when to show the start screen. It should behave like this:

In the beginnig it shows "Press Enter to start (click canvas to focus)". When the user pressed enter the game is started.

When the player looses, is shows "Sorry, you loose! Press Enter to restart." and lets the user press enter to restart.

Similar when the player wins, with the message "Congratulations, you won! Press Enter to restart."

So when the game ends we need to switch differently depending on if the game was lost or won. Remember that the inhibition if wires can be used to switch wires (for example with "-->"). If we want to have different wire we have to encode this in the inhibition Type (see also this thread):

First the paddle is updated using the input. The fireRequests are build by accumulating all presses of the fireing key. These are later filtered in the gun, so that no more bullets are fired than there is ammo. When an Update event is issued the queue is purged. Remember that accum delays by one, so that when the input event is "Update", fireRequests is purged one invocation later.

The rest of the wire is only invoked when the input event is "Update" (otherwise Nothing is returned). Note that we can use "if" to invoke a different wire depending on some condition. Creating the collision data is done using the functions introduced earlier. Note the filter with "validCollDir". Due to the rounded edges, it can happen that the ball collides with a block in a way that the ball is not outside the block the next frame. To prevent "double collisions" all those collision events, that are not directed against the moving direction of the ball are filtered.

If we would have used "accum" instead of "accum1" in a couple of places, the output of all the game objects would be delayed by 1 and we would not need the "old..." objects. This is personal preference, I find the use of the "old.." objects more transparent to what is happening.

Conclusion

I am getting more confortable with haskell and its getting easier for me to read haskell code. Netwire seems to be a nice library, but I feel like I have so far only scratched its surface. I wonder what cool things one could do if one would use the inner monad. Also I wonder how Arrowrized FRP compares with FRP without arrows. Unfortantly reactive banana does not yet work with haste. I had a quick peek at elerea but it also needs some PrimOps not supported by haste.

Again: I encourage you to comment if you think something could be done better. For a lot of things I might not use a better alternative because I am simply not aware of it. After all I am still a haskell beginner.