Program Structure
As we've covered in previouus lectures, putting all of your code into the main Game class is a really, really bad idea. The main game class should just be a container for all of the various game components. It should create all of the main classes, and can do some setup work (though really, each component should set itself up as much as possible), and then should just get out of the way -- on each update and draw, it should just make calls to the various update and draw methods of its contained components. Your main class should probably look something like the following:namespace MyGameNamespace { public class MyGameName : Microsoft.Xna.Framework.Game { GraphicsDeviceManager mGraphics; // Save components as instace variables public MyGameName() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; // Create all of your components // Either add them to the Components of the superclass } protected override void Initialize() { // If you need to do any initialization of your components, it can be done here base.Initialize(); } protected override void LoadContent() { // Load content as necessary // As much as possible, each component should load its own content // Put calls in here to the approprate LoadContent methods of the // appropriate components } protected override void UnloadContent() { // Likely not to need this for your games } protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); // Call update on each of your components // (or let the system do it for you, if you added them to the superclass's Components // collection) base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); // Call update on each of your components // (or let the system do it for you, if you added them to the superclass's Components // collection) base.Draw(gameTime); } } }
The key to notice here is that very little is happening in the main program -- each component is taking care of itself
Component Structure
What should be a component? As always, we should avoid the two extremes:
- One component that does everything: If we put all logic in one component, then we are essentially in the same situation as if we put everything into the main game class, except that now there is an extra level of indirection. We've made the program more complex, not simplier.
- A component for every single object in the game: If every object in the world is its own component, then we will need to manage every single element in our main class, which quickly becomes daunting. There will need to either be a lot of extra logic in the main class (to manage inter-component communication), or each component will need to do a whole lot of communication with other components, leading to way too many interconnects between components (We want the components to be as separate as possible)
So how should we break our game into componets? Like any other software engineering project, we should look at how the components function. Each component should handle a single aspect of the gameplay, and be as independent as possible from the other components in the game. Some posibilities for components are:
- Player Component To handle player input and the player avatar
- AI Component To handle all of the AI elements
- HUD component To handle any Heads-up display items -- heath, score, subtitles, etc
- World Component To handle non-AI objects in the world. Depending upon how your game is set up, the world component may also handle all of your collision detection, with callbacks to other components when elements collide
- Projectile Component To handle any projectiles your game creates. Projectiles can also be handled by the world component, instead of having their own separate manager. If your projectiles are doing anything particularly interesting (homing, etc), it may be a good idea for them to have their own manager. It is probably a bad idea for projectiles to be handled in more than one place (by the player and the AI, for instance).
Components != Classes
While Components will be implemented as classes, they should not be the only classes in your program! You will likely need classes for animation, enemies (perhaps with subclasses for different kinds of enemies), pojectiles, world elements (platforms, obsticles), and so on. A component represents a top-level functional chunk of your game. A class is used to define any object in your game.Component Communication
The above structure is all well and good if the componets don't need to interact with each other, but that is usally not the case. You will want to design your components so that you limit communication, but you will not be able to remove it completely -- a game where enemies can not interact with the player in any way would not be a very interesting game! So, how should the componets communicate with each other? There are several models:
- Every component has a pointer to the containing game, and the game has access methods to get at each component. To communicate with another component, go through the game, get the appropriate component, call the appropriate method.
- Each component has a pointer to any other component that it needs to communicate with. To communicate with another component, use the appropriate pointer to get to that component. The pointers can be set up in the initialization of the game
- All communication goes through a centeral intermediary. This method works well if there is a single World class, that keeps track of all elements in the world. When the player (or AI) wants to move, it calls a method on the world to set the position (or velocity, or apply forces to) the object in the world. The world handles collisions, making the appropriate callback functions to elements that collide. The world owns the positions of each object (regardless of where that data is actually stored) Actions like "notify every element within a 10 unit distance of a certain point" (to handle explosins, grenades, etc) becomes much eaiser with this method
Singleton Design Pattern
If everyone is going to need access to a particular component, and there will only ever be one instance of that component, it might make sense to use a singleton design pattern:class SingletonClass { private static SingletonClass instance = null; private SingletonClass() { } public static SingletonClass GetInstance() { if (instance == null) { instance = new SingletonClass(); } return instance; } }Of course, you can make this as complicated as you like:
class SingletonClass { public static SingletonClass GetInstance() { if (instance == null) { instance = new SingletonClass(); } return instance; } public void Initialize() { initialized = true; } int SampleIntValue { get { CheckInitialized(); return mIntValue; } set { CheckInitialized(); mIntValue = value; } } private bool initialized; private int mIntValue; private void CheckInitialized() { if (!initialized) throw new InvalidOperationException(); } private static SingletonClass instance = null; private SingletonClass() { } }
Using a World Component
One method of handling communication across componets is to have a world component that keeps track of all objects in the world. This component handles object intersection, as well as object location. If you are going to use a physics engine of some sort, you will need to follow a similiar pattern. There are many, many ways to do this properly, this is just one example:namespace MyGameNamespace { public enum Faction { Player, Enemy, Neutral, } [Flags] public enum CollisionFlags { None = 0, StaticObjects = 0x01, SameFaction = 0x02, DifferentFaction = 0x04, All = StaticObjects | SameFaction | DifferentFaction, } interface CollisionCallBackHandler { void HandleCollision(DynamicWorldObject o1, DynamicWorldObject o2); } interface DynamicWorldObject { // You'd want something more sopisticated than just an AABB bounding box // to handle collisions properly. Rectangle AABB(); CollisionFlags CollisionFlags(); Faction Faction(); // Other methods common to dynamic objects } class World { void AddObject(DynamicWorldObject o, CollisionCallBackHandler collisionCallBack) { } void RemoveObject(DynamicWorldObject o) { } void SetVelocity(DynamicWorldObject o) { } void GetPosition(DynamicWorldObject o) { } void GetVelocity(DynamicWorldObject o) { } } }On the Update method for the world, it would modify the positions of the objects based on their current velocity, handle any collisions based on the collision flags of the objects, and make any approriate callbacks if collisions occured. Given the above defintions, a collision of Object1 and Object2 would get two callbacks -- one the the collision handler for object1 (passing in Object1 and Object2), and one for the collision handler for object2 (which might be the same as object1) passing in object2 and object1.
Coding Conventions
Now that we've tamed the basic structure of our game, it's time to talk a bit about writing the actual code in a way that is extensible, maintainable, and easy to debug. Let's start with the basics:Formatting & Naming
It may seem like a little thing, but consistent naming and formatting of your code will save you countless headaches down the road. It doesn't particually matter what coding standards you use (though some are certainly better than others!) as long as you pick a style and stick to it.- Prefixes and Identifier Capitalization Capitalization is a tool -- it allows you to quickly see what kind of an object an identifier refers to. You can use any system that you like, as long as you stick to it. When programming in C#, I like to use
- Uppercase letters for Class defintions and Method names (including properties, which are just accessor methods)
- "m" prefix for instance variables (variables defined in the class). Second letter capitalized for readability
- lower case letters for local variables
- Naming Names are important for easy readability, and can aid in debugging
- Avoid similiar names! Identifiers that have subtly different names can lead to hard to track down bugs. Looking at a piece of code, you will often see what you expect to see, instead of what is actually there. The more similar the names are, the more likely this is. A common trap (that I have fallen into many times) is using i and j for loop indices. In some ways, i and j are fine generic loop indices -- programmers instantly know what those variables are (and if you ever name a variable that is not a local loop index i or j, you will certainly reget it!) However, i and j look a lot alike -- especially bad for doubly nested loops! Try using i and k instead
- Method names should be verbs Your code should read as much like English as possible
- Predicate functions should have consisitent names. Using a set prefix like Is (IsAlive, IsOnGround, etc) for predicate functions (that is, functions that retur makes it easier to see what is going on.
- Use whitespace and curly braces {} consistently.
Instance variable access
You should always access instance variables in a class through getters / setters (the C# Properties are just syntactic sugar for getters and setters). You can change the underlying data representation easily (without changing every other piece of code that uses the class), and you can also determine when a value is being changed -- slap a breakpoint into the setter, and you can see who is modifying the value when. You don't need to look through the entire codebase (which will end up begin quite large for a substantial game) in order to see who is modifying the values of instance variables, and you don't need to deal data watchpointsFunctional Decomposition
Alas, there is no algorithmn for decomposing your code into functions. There are some good rules of thumb, however:- Each function should do only one thing. If you can't describe what the function is doing as a single high-level task, then the function is probably doing too much. If you are having trouble giving your function a name (that is, there isn't a nice verb that describes what the function does), then the function should probably be refactored
- You need to be able to hold the entire function in your head at once. People have trouble juggling more than about 7 pieces of information at the same time -- keep this in mind when writing functions. If your function takes up more than about a screenful of code, seriously consider breaking it down into smaller chunks
- Your code should read like English. You should be anle to look at a single function in isolation, and figure out what is going on without looking at the definitions of functions that are being called
Documentation
Commenting you code -- how much is enough? How much is too much? To some extent, this is an art rather than a science, but there are good guidelines- Header comments: Each function (and certainly each pubic function!) should have a comment that lists the arguments to the function, output parameters (if any), return value, and what the function does. Be sure to list edge cases / error conditions -- what assumptions does the function make about the input? Are there any conditions that cause the function to behave in a different way? Someone should be able to use this function correctly in all cases without looking at the source code for the function. If you are having trouble writing a header comment for a function, consider refactoring! If you use /// for your header comments in C#, then you can auto-generate documentation (like javaDoc), assumuing that the comments follow the XML conventions for the C# comment generator. Fortunately, Visual Studio auto-generates XML if you type /// in front of a function name.
- Comments Describing variables: The following comment is pretty silly:
const float PI = 3.14159 // Constant value for PI
Without the comment, we can easily see that this is a constant representing the value of PI -- the comment is entirely superfluous. Likewise:int textureWidth; // Width of the texture for this object
What if the comment does tell us something interesting? If we have something like the following:int mx; // Maximum value for the x position of the object
The comment tells us something, but only because the name of the variable was so terribly bad! It would be preferable to just give the variable a better name:int maximumPositionX;
- Descriptive comments in the body of the code: If you are doing something tricky (especially some clever hack), please document it in the code. If the code is making some assumptions about the data, documenting them is not a bad idea. Anything that might trip you up later shouild be documented. High-level documentation about what a section of code is supposed to do can also be a good idea. Repeating what the code says in comments is not helpful. Well formulated, well decomposed functions can often do without internal documentation
- Notes to self (or others): If you know you will need to fix a bit of code, or if you have only implemented part of the functionality, puting a comment in can save time later. There are commonly used keywords that you may want to adopt
- TODO: To note that the functionality of a particular method is not quite complete, or there are some edge cases that are not quite taken care of yet
- FIXME: To note a bug or problem in the code. Of course, it is always better to fix a bug than to document it, but if you don't have time to fix a bug, it is better to note it than ignore it!
- NOTE: Highlight a particularly tricky bit of code, or to warn of pitfalls if someone is going to modify the code