Welcome to day 2 of the XNA Game Architecture series!
I have thought hard about whether I should just assume a certain level of object oriented programming knowledge in this series. People picking up these articles likely already have some knowledge about objects and design, so I settled on a quick run-over of the principles that hopefully won’t bore the seasoned developers and provide a good reference for people just starting out!
If you already know all this, feel free to skip ahead until it becomes interesting again or to the next chapter ;)!
Modularization
Many programmers using XNA to learn programming end up with something like this:
That’s quite okay for someone doing his first steps, but obviously, as the code grows larger, this kind of design becomes pretty hard to maintain and it will be increasingly difficult to remember what all those fields are there for and where the code for doing something needs to go.
No programmer is capable of keeping every method and every field of even a moderately sized game in his head at once, so we break the problem down into small parts that we can understand – using methods and classes. It’s a bit more work to write those classes and to wire them up to each other, but if we do our job well, the project is much easier to maintain and we have more fun working on it, too!
Here’s what might become of above class with some modularity applied:
Knowing what to turn into classes and how to make them interact with each other comes with experience. And hopefully, by the end of this series, you will have a bit more of that! ;)
Encapsulation
As the game grows, the number of classes will also grow until there are too many of them interacting with each other to keep track of. So classes alone are not the final solution to keeping a game’s code base manageable.
One method of combatting this complexity is to limit the amount of members exposed by a class. This is best illustrated with an example: Imagine you were writing a DVD burning application. At some point, you will have to delve into the gory details of the some obscure API used to control the DVD drive.
One could sprinkle bits of this code all over the application. The drive selection dialog would contain some code that looks at what DVD drives are available, the burn dialog would contain some code that sends the data to be burned to the DVD drive bit by bit, and so on — like this:
But this makes it hard to keep track of what’s going on. Worse even, if you later port that application, say from WinForms to WPF, you have to isolate all those pieces of code from its dialogs and re-add them to the WPF code. And if you decide to also create a command-line version of the application, oh well…
Instead of spreading the DVD drive control code everywhere, you can encapsulate all that complicated stuff behind a small class:
class DvdWriter {
public DvdDrive[] GetDrives();
public void WriteIso(DvdDrive drive, string isoPath);
public void VerifyDvd(DvdDrive drive);
}
There might be a lot going on behind those three methods, there might even be multiple classes involved – the point is, from the outside (meaning the rest of the application), only those three methods are visible.
-
When modifying the
DvdWriter
class, you only need to be concerned with doing what the three methods promise to do. You can completely eradicate the rest of the application’s code from your mind. - Vice versa, when working on other parts of the application, you don’t have to remember the details of how the DVD drive control code works. There are three simple methods to call and their names reveal exactly what they do.
Of course, the art lies in finding appropriate places where the interactions between classes can be trimmed down to just a few methods. This, too, is a matter of experience. Good programmers intentionally design towards making it possible to build self-contained, encapsulated classes. One trick is to look for where the concerns change: the dialog is concerned with managing the user interface, the DVD drive control code is concerned with burning data to a DVD – thus, these two don’t belong together.
Abstraction
Quite often, classes have to interact with each other. As an example, one
class might manage your particle systems,
another might manage
your graphics device (like XNA’s
GraphicsDeviceManager
class ;). That particle system manager needs
to access the graphics device to render its particle systems. But you don’t want
to let the classes directly access each other, otherwise you’d end up with this:
Where is the problem with that?
-
By directly accessing the
GraphicsDeviceManager
, theParticleSystemManager
would become dependent upon this very class. Should you later decide to create a particle system editor in WinForms which doesn’t use theGraphicsDeviceManager
, you would have to rewrite a lot of code to get it working. -
The
ParticleSystemManager
has access to everything theGraphicsDeviceManager
provides. Does it make sense that theParticleSystemManager
is theoretically able to toggle between full screen and windowed mode?This concern might not seem terribly obvious (after all, the .NET Framework gives any part of your program access to lots of silly things it wouldn’t want to do), but the idea is that exposing only what’s needed makes it easier to change used classes or to change or replace them.
What you need then is an interface for the GraphicsDeviceManager
.
One that lets other classes access only those parts they’re supposed to use.
Luckily, the XNA Framework provides just such an interface. It’s called
IGraphicsDeviceService
and the
GraphicsDeviceManager
implements it. So instead of directly
accessing the GraphicsDeviceManager
, you can do this:
Now the ParticleSystemManager
will accept any class
that implements the IGraphicsDeviceService
interface. If you
decided to write aforementioned particle system editor, you could implement
IGraphicsDeviceService
in a Control
or
Form
and the ParticleSystemManager
could work
with it – without changing a single line of code.
Next Chapter
In the next chapter, I will finally start writing some code and explain the concept of Dependency Injection and how to wire up a game’s classes with each other using an Inversion of Control container.