FunSpIns - State, the World, the Loop

23 Sep 2013 in software-development | haskell | fun-spin |

Functional space-invaders series

  1. A recap of Rob Ashton’s lessons - Das Intro
  2. Drawing a Rectangle
  3. Moving a Rectangle
  4. No attributes, No vectors, A tiny Workflow and more squares
  5. State, the World, the Loop
  6. The hero must move, the enemies must move smarter
  7. The hero shoots
  8. Collisions, the dead, and a (not so) grateful ending

Inspired by Rob Ashton’s series “Learn functional programming with me

Recreating this episode really had me on the brink of giving up the whole thing. I am not a games programmer, FFS, what am I doing here?

When it comes to things that look mildly imperative or indeed involve mutating state, Haskell keeps doing my head in. Of course Haskell has no troubles with state and its tendency to change, but while other languages have state baked in, in Haskell it is pretty much just another abstraction.

So, I was searching for the right solution, and looked at timers, based on Lazy Foo tutorials on SDL. I looked at how to do Co-routines in Haskell, as I thought that this could be an interesting approach to having, well, coroutines spit out logic that gets rendered. This surely is possible, but right now, the level of abstraction really explodes my current time budget.

To my relief, I did manage to construct a loop that would produce moving rectangles and should allow more interesting things later on. For this, we have the definition of a World:

The world

data World = World 
  {
    canvas :: Surface,
    lastEvent :: Event,
    enemyGridOrigin :: (Int,Int),
    actors :: [World -> (World,[Rect])]
  }
initWorld canvas = World 
  { 
    enemyGridOrigin = (5,5), 
    lastEvent = NoEvent, 
    canvas = canvas, 
    actors = [moveEnemyGrid] 
  }

This object will play the role of keeping the state of our subsequent loops, as well as hosting what I have called actors. These guys must accept the World as input and return the World alongside a number of Rects. Returning the world allows to actually change it within the implementation of an actor

Records

A few words to Haskell’s record-style syntax are probably in order right now. initWorld shows you how to create such a record. You can

  • read out values as if the items of the record are functions and their parameter the record type. (e.g. canvas world would return the Surface stored within)
  • match on the record type in a function declaration, thereby extracting value from it, e.g. foo (World { canvas = c } –binds the canvas value to c)
  • replace one or more values of a record, example just follows…

The enemies

Our enemy grid function now looks like this:

moveEnemyGrid :: World -> (World,[Rect])
moveEnemyGrid w = (newState w, enemies)
  where
    newState w = w { enemyGridOrigin = newOrigin (enemyGridOrigin w) }
    newOrigin (x,y) = (x+5,y)
    enemies = enemyGrid (enemyGridOrigin w) (3, 5)
    enemyGrid (originX,originY) (rows, cols) = 
      map enemy [ (x, y) | x <- take cols [originX,originX+60..], y <- take rows [originY,originY+30..]]
    enemy (x,y) = Rect x y 20 10

You may see that so far this is pretty much just a proof of concept. We change the enemy origin’s x-value by 5 and otherwise output the enemies as we already did in the previous post.

Line 4 shows how we can replace a single value of our world-record. It looks a bit unwieldy, but truth be spoken records don’t seem to be Haskell’s strongest suit. The signature of moveEnemyGrid is very characteristic of a method that mutates something - indeed, anything that should be different after a function has run must be returned by that function.

Let us have a look at the loop…

loop world = do
  event <- FX.pollEvent
  FX.fillRect c Nothing black
  let (newWorld,rects) = foldl runActor (world { lastEvent = event }, []) $ actors world
  mapM paint rects
  FX.flip c
  FX.delay 50
  loop newWorld
  where
    c = canvas world
    paint r = do
      FX.fillRect c (Just r) white
    runActor (wld, rects) actor = (newWorld, rects++moreRects)
      where (newWorld,moreRects) = actor wld

The interaction with our World happens in line 4, everything else is render implementation details. What happens is that the list of actors is folded by successively passing in the World and giving the potentially mutated world to the next actor, meanwhile collecting all Rectangles from the actors that should be drawn to the canvas. At the end of the run we call loop again with the new world.

Chronology

  |  
comments powered by Disqus