At the forefront of Artificial Intelligence
  Home Articles Reviews Interviews JDK Glossary Features Discussion Search
Home » Articles » Programming » General

Object-Orientation and Gaming

Object-orientation is becoming more and more popular as a method of creating games. It has been proven that procedural programming language (Fortan, Pascal, C) tend to break down after a certain threshold is reached. After around 105 lines of code, the maintenance cost of correcting bugs and updating features takes over from the development cost. Object-orientation allows programmers to attack problems via the ancient "divide-and-conquer" technique.

A hundred thousand lines of code seems to be an incredible amount, but with modern games reaching astounding complexity, their code is approaching this level. With the fast paced world of computers, quick bug fixing, patches and updates are expected from all game companies to resolve problems. Object-orientation helps with all of these aspects - and with new compilers, C++ code is as efficient as its C counterparts (if not more so for complex systems).

This essay will look at various C++ techniques that can be utilized when planning and creating your game. I cannot place enough emphasis on the importance of planning out your game as thoroughly as possiblie before you start programming. Accounting for all entities in the game, all possible events, variables and storyline developments will help create your classes in the most efficient ways possible.

Class Inheritance

Inheritance can be great way to save time, and also improve uniformity of code. Treat inheritance exactly like you would a type-of statement. For example, if a soldier is a type of monster, then derive CSoldier from CMonster. CMonster can have all the basic functions of a monster, and all the variables that all monsters require. Let us look at some code for our imaginary game:
// Class CMonster.
class CMonster {
	public:
		CMonster();
		~CMonster();

		...

	protected:
		int	m_iHealth;
		int	m_iAmmoType;
		int   m_iMonsterType;

		...
};

// Derive CSoldier from CMonster.
class CSoldier : public CMonster {
	public:
		CSoldier();
		~CSoldier();

	protected:
		int	m_iUnit;
};
Note how CSoldier doesn't require declarations for health, monster type and ammo used, since that is all handled by the monster class. In this example, the immediate advantages are small, but base classes can be as big (or small) as you want, and can make derived classes very concise.

Constructors and Initializer Lists
Remember that when a class is derived, its constructor gets called, then all base class constructors are also called. Therefore in our example, CSoldier would be called, then CMonster would be called after that. Therefore, we can initialize our class variables in their respective constructors:

// In monster.cpp
CMonster::CMonster() {
	m_iHealth = 100;
	m_iAmmoType = AMMO_BULLET;		// Flags declared elsewhere.
	m_iMonsterType = MONSTER_GENERIC;	// Ditto.
}

// In soldier.cpp
CSoldier::CSoldier() {
	m_iUnit = 0;
}
You will see though the all monster will have bullets for ammo, and a generic type for a monster type. This is probably not we want. What we could do is simply initialize them in CSoldier, couldn't we? No, because they would be reinitialized in the CMonster constructor call. We could take them out of the CMonster constructor and initialize them in the soldier constructor. Thing is, that has an additional danger. All classes that are derived from CMonster will have uninitialized variables - this can be dangerous if we forget to initialize them in the derived class constructors. A workaround I take is using initializer lists.

Initializer lists are parameters that are passed from a derived class to a base class during construction. They are often used to help construct embedded classes (I'll stay away from that). We will use to merely initialize our monster variables. This does require use to change nearly all the code we've looked at so far. This is why I emphasize planning! Firstly, we must alter the CMonster constructor to take parameters, and add the initializer list to the CSoldier class:

// In monster.h
class CMonster {
	CMonster(int, int);
	~CMonster();
	...
};

// In monster.cpp
CMonster::CMonster(int ammo, int monster) {
	m_iHealth = 100;		// Universal to all monsters.
	m_iAmmoType = ammo;		// We use parameters passed.
	m_iMonsterType = monster;	//
}

// In soldier.cpp
CSoldier::CSoldier() : CMonster(AMMO_SHELLS, MONSTER_SOLDIER) {
	m_iUnit = 0;
}
We will extend this all one level further to help clarify everything. Any of you how have played Quake2 will now that they are several types of soldiers, ones that use blasters, one that uses a shotgun and another that uses a machine gun. Let us imagine that we are doing something similar and require two additional types of soldier. One that uses bullets, and another that uses shells. We have to change CSoldier to take parameters, and add two new classes:
// In soldier.h
class CSoldier : public CMonster {
	CSoldier(int);
	...
};

// In soldier.cpp
CSoldier::CSoldier(int ammo) : CMonster(ammo, MONSTER_SOLDIER) {
	m_iUnit = 0;
}

// New monsters derived from CSoldier. 
class CGunSoldier : public CSoldier {
	...
};

class CShellSoldier : public CSoldier {
	...	
};

// In gunsoldier.cpp
CGunSoldier::CGunSoldier() :	CSoldier(AMMO_BULLETS) { }

// In shellsoldier.cpp
CShellSoldier::CShellSoldier() : CSoldier(AMMO_SHELLS) { }
Hopefully, you know understand the idea of initializer lists. I use this approach because it is safe - we will not have any uninitialized variables. You are forced at compile-time into initializing the variables from derived classes.
newsoldier = new CSoldier;		// Illegal, no ammotype.
newsoldier = new CGunSoldier;		// Legal.
Of course, in the above example, it seems a little redundant to create a completely new class because you can specify the ammo type in the code itself, but I am assuming there are other discrepancies between CSoldier and CGunSoldier that warrant the creation of a new class.

Tip: Remember that when you plan you base class header files to keep all function bodies out of the header file and in the .cpp file no matter how simple. If you want to change even the smallest thing, any file with the remotest connection (includes a file that includes it etc.) to it will be recompiled!

Virtual Functions

We have kept our eyes at construction level, without much look into the actual coding of functions. Here we will look at some excellent ways of reducing code and increasing clarity using object-orientation.

Let us say we keep track of all monsters in our game via an array (not a good idea, but makes examples simplier). How, how would you do this? Using normal techniques, this would be impossible, but using C++ you can use a pointer to a base class to refer to any derived class. Therefore, this code is perfectly valid:

CMonster monsters[4];

monsters[0] = new CMonster(0,0);
monsters[1] = new CSoldier(0);
monsters[2] = new CGunSoldier;
monsters[3] = new CShellSoldier;
The problem with this though, is that all the pointers to the classes are pointers to CMonster and not the real class they represent. Therefore, this is invalid:
int unit = monsters[1]->m_iUnit;
This is invalid for two reasons, m_iUnit is a member variable of CSoldier not CMonster, so the compiler would give a 'undeclared identifier' error. On an aside note, even if it was valid as a variable, object-orientation does not allow classes/functions outside the class to access protected member functions or variables unless declared as a friend (see later).

So how do we know what type of monster we are looking at? Most people will see that we can use the monstertype variable, then use a pointer cast to the correct type. This works perfectly, and would be necessary if we wanted to access the unit variable as above. For example (disregarding the fact we are accessing protected variables), we could do:

if (monsters[1].monstertype == MONSTER_SOLDIER) {	// Check type.
	int unit = dynamic_cast<CSoldier *>(monsters[1])->m_iUnit;  // Pointer cast.
}
Let us suppose though that every monster has a function that will update the monsters position, health and everything else that is required. Therefore, we add an Update() function to the CMonster class with. We then override it in every derived class. Now, if we use the above method, our code could look something like this:
// In monster.h
class CMonster {
	...

	void Update() {};
	...
};

// Elsewhere in program.
// Assume Update() is overloaded in all classes shown.
for (int i=0;i<4;i++) {
    CMonster *monster = monsters[i];

    switch(monster.monstertype) {
	  case MONSTER_SOLDIER: dynamic_cast<CSoldier *>(monster)->Update(); break;
	  case MONSTER_GUNNER:  dynamic_cast<CGunner *>(monster)->Update();  break;
	  case MONSTER_JAMES:   dynamic_cast<CJames *>(monster)->Update();   break;
	  ...
	  ...
    }
}
While this works, there are a few problems. Firstly, there is the obvious problem that all monsters will have to have their own line of code, therefore if you add a monster you will have to add the relevant code whereever you use a loop like this. Secondly it is quite inefficient, because you need to get the monster type and then use a switch or many if-then-else statements to convert the pointer, THEN call the function.

This is where virtual functions come in. Virtual functions are functions designed to be overridden and called in circumstances like the above. What the compiler does is generate a table (v-table) of the necessary functions for the classes, so no matter what the pointer of the class is pointing to, the correct function is called. Therefore, with a tiny addition to monster.h, we very much improve the code:

// In monster.h
class CMonster {
	...

	virtual void Update() {};
	...
};

// Elsewhere.
for (int i=0;i<4;monsters[i++]->Update()) ;
Rather nice, isn't it? Note that if it isn't overridden, CMonster::Update() is called (which does nothing). Plus, if you add additional classes derived from CMonster, no modification to the code is required! As long as you override Update() in your new class, it will be called.

Note: Although you may not specify it explictly, member functions for virtual function in derived classes are also virtual. For example, if CGunSoldier has an Update() function it will be called, if not, CSoldier::Update() is called. Also, note that you do not have to specify a body for the virtual function in the base class, instead declare the function like this:
   virtual void Update() = 0;
Note, though that you will not be able to instantiate a class with a pure member function (shown above).

Messaging

What I am about to talk about is not part of C++ by design. It is nevertheless often associated with object-orientation and OO-architectures. For example, in SmallTalk any member function is refered to as a 'message', or Microsoft Windows (a highly object orientated environment) is based heavily upon messaging - thus the term 'event (message)-driven programming.' Why is messaging so important?

Creating complex hierarchies of objects often leads to a few difficulties when it comes to information exchange. Yet, with some well designed functions, this can be very easy. Planning is key! It is this part that is hardest to talk about in a general sense, because nearly every single game will have different requirements. I will look at some generic ideas, focussing on our example.

Encapsulation serves as a way of protecting information from modification outside of the class itself. This does though prove awkward at times because even retrieval of the variable is invalid. A simple way around things like this are to create inline functions that return the value:

class CMonster {
	...
	inline int Health() { return m_iHealth; }
};
For more complicated information 'queries' I often employ a messaging system within the program. Like I've said, this is inspired by Windows programming - in Windows you cannot access functions that will redraw the screen, or close the program directly, but what you can do is send a message to the window in question. Each window has its own message queue, and messages are handled on a FILO basis. This serves as a great way (in some games) to communicate between classes. The first advantage is it is realistic - objects communicating in a 'real' way will act more realistically (this is the idea behind emergent behaviour). Messaging also allows a certain degree of 'fake' parallel processing.

Let us look at an example. Let us assume the monsters in our game are connected via a 'radio link' - that is, if they encounter an enemy, they alert everyone else. Or if a monster doesn't respond to a random check, another monster is sent over to check. This kind of behaviour requires a lot of knowledge of the game world - but this information is not available to individual monsters. Therefore, if we wanted to alert everyone in the world, we could send a message to the world (analogous to broadcasting over the radio) - the world would then take care of alerting other monsters. Or for the latter example, messages could be sent to monsters and if they don't respond (quite literally) then an additional message is sent out to check the coordinates the monster was last at.

What are the advantages of messaging over function calling? In this case they are not too many - if there are a lot of cases to deal with, the messaging system can be simpiler because only one function is required that handles everything (this of course, can be broken up). The messaging system can be very useful in other cases - for example, I am creating a game whereby many messages (twenty to thirty or more every game cycle) are sent - but only this, at various stages of the game, the messages are handled by upto 5 different things - the player or 4 different AI agents. Now, using functions, my code would be much more complicated than its messaging counterpart I am using.

Like I have said, there are no functions/construct within C++ that directly support messaging, but it is a very useful programming technique if correctly implemented.

Conclusion

I am a huge C++/Object-orientation advocate, and I feel that with the advent of efficient C++ compilers, more games should be written in C++. I have discussed how to use class inheritance, virtual functions and messaging in your games. Remember, that when developing a object-orientated game plan, plan, plan!

Last Updated: 08/05/2000

Article content copyright © James Matthews, 2000.
 Article Toolbar
Print
BibTeX entry

Search

Latest News
- The Latest (03/04/2012)
- Generation5 10-year Anniversary (03/09/2008)
- New Generation5 Design! (09/04/2007)
- Happy New Year 2007 (02/01/2007)
- Where has Generation5 Gone?! (04/11/2005)

What's New?
- Back-propagation using the Generation5 JDK (07/04/2008)
- Hough Transforms (02/01/2008)
- Kohonen-based Image Analysis using the Generation5 JDK (11/12/2007)
- Modelling Bacterium using the JDK (19/03/2007)
- Modelling Bacterium using the JDK (19/03/2007)


All content copyright © 1998-2007, Generation5 unless otherwise noted.
- Privacy Policy - Legal - Terms of Use -