This post is the second 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.
For most readers, this post will not be as exciting as future posts about the design of Paint Wars will be, but every story has to start somewhere, and the story of Paint Wars begins with an object oriented design in C#.
The Structure of an XNA Game
This post is not meant to be an introduction to XNA programming, but I will include an extremely brief introduction to the XNA Game loop for our purposes.
As with most game loops, XNA games consist of two important methods: Update and Draw (in reality, there are a few more setup methods, but they don’t matter yet). The XNA Framework calls the update function so that the game can progress from one state to another based on player input and whatever game logic is required. This usually happens many times per second. After the update function runs, the draw function is called to render the current game state to the screen. The XNA framework runs a game by continually calling these two functions until the end of the game occurs.
The Model Class
In Paint Wars, most of the update and draw logic is handled in the Model class, which is a container for all of the objects in the game. It’s responsibilities include the following:
- Telling all objects in the game to update and draw. Draw order matters, and the Model class handles this.
- Performing collision detection on game objects. The Model class uses basic spatial partitioning to improve the performance of this. I’ll go into more detail about this in a future post.
- Providing access to the Canvas, which is the interface for determining the status of the paint on the game board. Again, more on this in the future.
The above diagram shows the class hierarchy for the C# version of Paint Wars. The base class, WorldObject, has abstract methods for common operations such as drawing, updating, moving, and taking damage. WorldObject is considered to be a fat interface because the extra methods (fat) are not needed by some child classes.
This type of design has the advantage of being easy to consume. When a player presses the nuke button, the Cursor class asks the Model for a list of nearby objects and calls the TakeDamage function on all of the objects. It doesn’t matter if the object can actually be damaged. Objects that can be damaged will override the TakeDamage function. Otherwise, the base class implementation will just "do the right thing".
Similarly, Update and Draw are easy operations for the Model- just iterate the list of WorldObjects, and call either the Update or the Draw function. Polymorphism and Object Oriented Programming 101, right?
Now that all the busy deign work is out of the way, lets make a few observations about the above OO design:
- You don’t have to modify the rest of the code to add new types.
- There is no "switch on type" code (if/else blocks or switch statements) that can introduce subtle bugs when new types of objects are added.
- Separation of concerns and encapsulation means that it’s easy work in teams (you write BlobObject, I’ll write SpawnPoint). It also makes debugging, testing, and maintaining code, a lot less painful.
- Cool diagrams.
- Complexity is handled through inheritance. If you want an object to behave differently, you create a subclass and override the appropriate method. This works well on paper, but in code, it usually becomes difficult to follow.
- There are a lot of classes to worry about. Best practices say that each class belongs in a different file. Without a fancy diagram, it is difficult to see the big picture. A lot of code is wasted on class definitions and plumbing.
- Flexible to a point. Think of a thin wooden rod. It’s very flexible if you only apply a little pressure, but if you put too much pressure on it, it snaps. Similarly, the above class hierarchy appears to be very flexible if you have small requirements changes, but if your requirements change drastically, you will be in a world of pain until you rewrite your plumbing.
- A lot of mutable state hidden inside of classes. Code designed this way will not work in a multithreaded environment without some serious modification.
In the next post, I’ll describe the functional/F# design and examine the tradeoffs between the different designs.