This post is the fourth 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.
In my previous two PaintWars posts, I covered the design of Paint Wars from both an OOP and a functional standpoint. In this post, I'll get practical and combine the two disciplines to produce a real result. We'll use F# to do it because it excels at being multi-paradigm.
We're going to dive right into code. I won't go into details about the syntax here, but the code below forms the basis of all of the interactions in the game.
type IGameObject =
abstract Id : Id
abstract Position : Vector
abstract Attackable : bool
abstract Color : Color
abstract Texture : Texture2D
abstract DrawOffset : Vector
abstract BoundingObject : BoundingObject option
abstract Solid : bool
abstract GetFrame : Time -> Rectangle
abstract Update : GameState -> GameStateTransform List
abstract ApplyTransform : GameState-> ObjectTransform -> IGameObject
The first thing that's we define is an interface for game objects. This is similar to the abstract base class that we had in the OOP approach. The most important functions here are the Update and ApplyTransform functions since they are the plumbing for most of the engine. As you can infer from the interface definition, none these functions modify the state of the object.
and ObjectTransform =
| MoveAbsolute of Vector
| MoveRelative of Vector
| ChangeAnimation of Animation
| GetPickedUp of Color
| GetPutDown of Vector
| StartPickingUp
| StopPickingUp
| AcquireObject of IGameObject
| DropChildren
| TakeDamage of (Color * HitPoints)
| SetSpawnTicks of int
| SetPaintRechargeTime of Time
| Die
| WaitForNextSpawn
Next we describe the set of transforms that can be applied to a GameObject. I talked about how the transforms work in the last post, but these transforms are used by a GameObject's Update function to produce a new GameObject from an existing one. In the OOP approach, these ObjectTransforms would be Abstract methods on the base object.
and GameState =
{ PlayingArea : Rectangle
ObjectsById: Dictionary<int, IGameObject>
Partition : PartitionElement
Canvas : Canvas
CanvasState: CanvasState
PaintList : PaintableGameObject List
TimeSinceLastState : Time
Time: Time }
The next thing that we define is the GameState record. This contains the state of the game before and after an update. Most of the members should be self explanatory, The Canvas, CanvasState, and PaintList all deal with how the paint gets drawn on the map, and the Partition member deals with the spatial partitioning algorithm used for collision detection. We'll ignore those for the purpose of this post. Also of interest is the ObjectsByID dictionary. It's mutable for performance reasons, but an immutable container would work if we wanted to stay pure.
and GameStateTransform =
| AreaOfEffect of (BoundingObject * ObjectTransform)
| ById of (Id * ObjectTransform)
| Remove of Id
| Add of IGameObject
| Paint of PaintableGameObject
Finally, we define the GameStateTransform type which lists all of the transforms that can be performed on a GameState.
state.ObjectsById.Values
|> ParallelSeq.map (fun gameObject -> gameObject.Update state)
|> Seq.reduce (fun currentList toAppend -> currentList @ toAppend)
|> Seq.fold ApplyTransform state
Now we get to the fun part. After doing all the hard work of defining the above types and the IGameObject implementors, we can run them with the above code. Let's go line by line to explain exactly what is happening.
|> ParallelSeq.map (fun gameObject -> gameObject.Update state)
This bit of code generates a list of list of GameStateTransforms from the list of objects on the previous state. As described in the last post, we can do this in parallel since none of the generator functions share any mutable state. Note that the ParallelSeq function is just a wrapper around map/select in the Task Parallel Library.
|> Seq.reduce (fun currentList toAppend -> currentList @ toAppend)
This line just transforms the list of lists into a flat list of GameStateTransforms.
|> Seq.fold ApplyTransform state
Lastly, we just apply each transform to the GameState to produce our state for the next frame of gameplay. The ApplyTransform function is defined elsewhere, but as it's name indicates, all it does is apply a GameStateTransform to a Gamestate to produce an updated GameState.
Although there is a lot of other code in the PaintWars project, most of the engine is written out above. It is possible to write in such a small space because F# code can be both terse and multi-paradigm. This combination is also one of the reasons that F# is such a fun language to program in.