This post is the third in a series of posts about the code behind PaintWars. In this series, I will be talking about how the design and implementation of the game differed in C# and F#. Along the way, I’ll also be talking about some of the fun and exciting features of the F# language and examine how they can be used to solve practical software problems. For a description of the game, see this post.
It’s no secret that most games and graphics intensive applications are written in imperative languages. It’s also no secret that until recently, functional languages have only found widespread adoption in academia. That being said, it is not surprising that one of the few examples of writing games in functional languages before Paint Wars was a thesis paper.
With the design exploring relatively uncharted waters, what did the F# design of Paint Wars look like?
Back to the Basics
Functional languages have deep roots in mathematics, so the design of Paint Wars also resembles a series of mathematical equations.
Think of the equation "2 + 3 = 5". One way of breaking down this equation is that "+ 3" is a transform to 2. When we apply this transform to an input of 2, we get an output of 5. Similarly, we can apply a transform of "+ 4" to an input of 5 to get an output of 9.
Now, let’s apply this analogy to the game of Paint Wars. We will replace 2 in the "2 + 3 = 5" equation with a game state, and we will replace "+ 3" with a game state transform. Let’s say we have a game state of "one blob at position (1,1)" and a transform of "kill all of the blobs at position (1,1)". We can plug this into our equation format and get something that looks like this:
"one blob at position (1,1)" + "kill all of the blobs at position (1,1)" = "no blobs"
This approach to design may seem like a trivial shift from traditional imperative design, but it has powerful implications such as:
- Testing becomes a lot easier. This is because for any given input state and any given transform, we know that the output state will always be the same, just like we always know that adding 3 to 2 equals 5.
- It is much easier to reason about code written in a mathematical style. Basically, it’s easier to write code that "gets it right".
- Writing code in this fashion means that some steps in the game loop can be performed in parallel. More on this below.
Getting Practical – The Game Loop
All that math rubbish might sound smart, but it does not make for a complete game loop, so lets fill in the missing pieces. As I talked about in the last post, XNA loops are normally split into an update step and a draw step. In the functional version, we will keep the update and draw steps separate, but we’ll split the update into a couple steps.
First, we produce a list of transforms to the game state. This can be done in parallel since the process for generating transforms depends only on an input game state and not on other transforms.
Next, we apply them to the input game state to produce a new game state. We can’t go parallel here, since we need the output state of one transform to pass as input to the next.
Once we’re done applying all of the transforms, we can pass the final game state to our draw function. After drawing, the input state gets passed to the update function as input to produce the next game state.
To be Continued
What I outlined above is the core of the design, but there are a few details that I glossed over. How do the transforms get generated? How do they get applied? Does this approach work in practice? Where is the code?
I’ll have more on all of those questions in the next post where I will talk about meshing ideas from the object oriented and the functional designs.