Object-Oriented Game Design

Published May 18, 2013 by Josh Klint, posted by Josh Klint
Do you see issues with this article? Let us know.
Advertisement
C++ can be intimidating to new programmers. The syntax does at first glance look like it was designed for robots to read, rather than humans. However, C++ has some powerful features that speed up the process of game design, especially as games get more complex. There's good reason C++ is the long-standing standard of the game industry, and we'll talk about a few of its advantages in this lesson.

Object-Oriented Design

C++ is an object-oriented language. This means that instead of using a lot of variables for different aspects of each object, the variables that describe that object are stored in the object itself. For example, a simple C++ class might look like this: class Car { public: float speed; float steeringAngle; Model* tire[4]; Model* steeringWheel }; We can pass the Car object around and access its members, the class variables that describe different aspects of the object: Car* car = new Car; float f = car->speed Object-oriented design compartmentalizes code so we can have lots of objects, each with its own set of parameters that describe that object.

Inheritance

In C++, we can create classes that are built on top of other classes. The new class is derived from a base class. Our derived class gets all the features the base class has, and then we can add more in addition to that. We can also override class functions with our own, so we can just change the bits of behavior we care about and leave the rest. This is extremely powerful because:
  1. We can create new classes that just add or modify a couple of functions, without writing a lot of new code.
  2. We can make modifications to the base class, and all our derived objects will be automatically updated. We don't have to change the same code for each different class.
In the previous lesson we created a base class all our game classes would be derived from. All the features of the base GameObject class are inherited by the derived classes. Now I'll show you some of the cool stuff you can do with inheritance. Consider a bullet object, flying through the air. Where it lands, nobody knows, but it's a good bet that it's going to do some damage when it hits. Let's use the pick system in Leadwerks to continually move the bullet forward along its trajectory, and detect when it hits something: void Bullet::Update() { PickInfo pickinfo; Vec3 newPosition = position + velocity / 60.0; void* userData; //Perform pick and see if anything is hit if (world->Pick(position, newPosition, pickinfo)) { //Get the picked entity's userData value userData = pickinfo.entity->GetUserData(); //If the userData has been set, we know it's a GameObject if (userData!=NULL) { //Get the GameObject associated with this entity GameObject* gameobject = (GameObject*)userData; //================================== //What goes here??? //================================== //Release the bullet, since we're done with it Release(); } } else { position = newPosition; } } We can assume that for all our GameObjects, if a bullet hits it, something will probably happen. Let's add a couple of functions to the base GameObject class that can handle this situation. We'll start by adding two members to the GameObject class in its header file: int health; bool alive; In the class constructor, we'll set the initial values of these members: GameObject::GameObject() : entity(NULL), health(100), alive(true) { } Now we'll add two functions to the GameObject class. This very abstract still, because we are only managing a health value and a live/dead state: void GameObject::TakeDamage(const int amount) { //This ensures the Kill() function is only killed once if (alive) { //Subtract the specified amount from the object's health health -= amount; if (health<=0) { Kill(); } } } //This function simply sets the "alive" state to false void GameObject::Kill() { alive = false; } The TakeDamage() and Kill() functions can now be used by every single class in our game, since they are all derived from the GameObject class. Since we can count on this function always being available, we can use it in our Bullet::Update() function: //Get the GameObject associated with this entity GameObject* gameobject = (GameObject*)userData; //Add 10 damage to the hit object gameobject->TakeDamage(10); //Release the bullet, since we're done with it Release(); At this point, all our classes in our game will take 10 damage every time a bullet hits them. After being hit by 10 bullets, the Kill() function will be called, and the object's alive state will be set to false.

Function Overriding

If we left it at this, we would have a pretty boring game, with nothing happening except a bunch of internal values being changed. This is where function overriding comes in. We can override any function in our base class with another one in the extended class. We'll demonstrate this with a simple class we'll call Enemy. This class has only two functions: class Enemy : public GameObject { public: virtual void TakeDamage(const int amount); virtual void Kill(); }; Note that the function declarations use the virtual prefix. This tells the compiler that these functions should override the equivalent functions in the base class. (In practice, you should make all your class functions virtual unless you know for sure they will never be overridden.) What would the Enemy::TakeDamage() function look like? We can use this to add some additional behavior. In the example below, we'll just play a sound from the position of the character model entity. At the end of the function, we'll call the base function, so we still get the handling of the health value: void Enemy::TakeDamage(const int amount) { //Play a sound entity->EmitSound(sound_pain); //Call the base function GameObject::TakeDamage(amount); } Once the enemy takes enough damage, the GameObject::TakeDamage() function will call the Kill() function. However, if the GameObject happens to be an Enemy, it will kill Enemy::Kill() instead of GameObject::Kill(). We can use this to play another sound. We'll also call the base function, which will manage the object's alive state for us: void Enemy::Kill() { //Play a sound entity->EmitSound(sound_death); //Call the base function GameObject::Kill(); } So when a bullet hits an enemy and causes enough damage to kill it, the following functions will be called in the order below:
  • Enemy::TakeDamage
  • GameObject::TakeDamage
  • Enemy::Kill
  • GameObject::Kill
We can reuse these functions for every single class in our game. Some classes might act differently when the health reaches zero and the Kill() function is called. For example, a breakable object might fall apart when the Kill() function is called, and get replaced with a bunch of fragments. A shootable switch might open a door. The possibilities are endless. The Bullet class doesn't know or care what the derived classes do. It just calls the TakeDamage() function, and the behavior is left to the different classes to implement.

Conclusion

C++ is the long-standing game industry standard for good reason. In this lesson we learned some of the advantages of C++ for game development, and how object-oriented game design can be used to create a system of interactions. By leveraging these techniques, you can create wonderful worlds of rich interaction and emergent gameplay.
Cancel Save
0 Likes 16 Comments

Comments

Sollum

Good morning.

Nice article.

I am not a nitpicky person, but from university days i have this feature, given me by lector, where i notice random, unclear "numbers" :S

Vec3 newPosition = position + velocity / 60.0;

GameObject::GameObject() : entity(NULL), health(100), alive(true)

Educated guess says that 60 is fps. But if somebody new to this stuff starts using this code, changing all 60's to 58's will be intimidating :S Maybe making some static method calls would be better? Like Config.GetFPS();

Again, no offense.

May 17, 2013 06:01 AM
Alpha_ProgDes

Or 60.0 can be replaced with FPS and 100 can be replaced with FULL_HEALTH.

I wonder however if the author will be focusing on composition as much as inheritance in the upcoming articles.

May 17, 2013 11:56 AM
Gaiiden

random, unclear "numbers" :S

I've always called them "magic numbers" myself :) And yea, not great practice. Not really a big deal in this article IMO but certainly something worth pointing out

May 17, 2013 01:19 PM
tivolo

"In practice, you should make all your class functions virtual unless you know for sure they will never be overridden."

I beg to differ.

It might make sense in this context (inheritance-based entities, everything derives from a common base class), but generally speaking, I would advise against doing so.

May 17, 2013 03:16 PM
Puyover

I know this results in educative purpose, but I would like to state that people should use more about C++ has to offer, and I mean by this the use of references and smart pointers for example.

May 17, 2013 06:15 PM
Josh Klint

"In practice, you should make all your class functions virtual unless you know for sure they will never be overridden."

I beg to differ.

It might make sense in this context (inheritance-based entities, everything derives from a common base class), but generally speaking, I would advise against doing so.

Are there any reasons you recommend this, aside from speed? I recommend virtuals in most cases, because it's easy to forget to change them when needed.

I've always called them "magic numbers" myself smile.png And yea, not great practice. Not really a big deal in this article IMO but certainly something worth pointing out

Thanks for the suggestion. It's funny how those things become invisible to you after years of using them.

May 18, 2013 03:04 AM
tivolo

"In practice, you should make all your class functions virtual unless you know for sure they will never be overridden."

I beg to differ.

It might make sense in this context (inheritance-based entities, everything derives from a common base class), but generally speaking, I would advise against doing so.

Are there any reasons you recommend this, aside from speed? I recommend virtuals in most cases, because it's easy to forget to change them when needed.

Well, performance is one reason. We all know that virtual functions can cause I$- and D$-misses, and cannot be inlined 99% of the time. But there are also other issues with it.

First, lots of virtual functions make it harder to reason about what really happens in the code. The prime example is having all GameObjects stored in an array, calling go->Update() for each. What does the code do? Which functions are called? You cannot really tell unless stepping through the loop with a debugger.

Second, and this is a direct consequence from the point above, it is very hard and sometimes even impossible to thread code written like this without huge amounts of refactoring. Just by looking at a virtual Update() call you cannot tell which global state is touched, which mutable state is going to be changed, etc. Code that was written with polymorphic base classes and homogeneous arrays/vectors is much, much harder to thread than, say, component-based systems that only deal with heterogeneous, contiguous chunks of data.

Third, and I guess that's really my main point: I would never advise making each and every function virtual, unless I give this advice in a very, very specific context. Of course, virtuals still have their reasons for existence, but IMHO they should be the exception rather than the norm. After all, when do you *really* need objects to behave polymorphically? Without context, I fear novice programmers might get the wrong idea, and just put the virtual keyword in front of every function.

May 18, 2013 05:11 PM
Josh Klint

I tend to favor making the code as flexible as possible; I write with the assumption any function may be overridden later on (with the exception of computationally intensive code I am specifically optimizing). But I see your points.

May 18, 2013 05:21 PM
Eastfist

I love how well c++ runs (when the code is good). But I also understand why it's intimidating to new programmers (because c++ is not my first programming language). But how I learned it coming from Visual Basic was to look for the patterns in language syntax. Imagine one day when we'll all be programming with full graphical rebus language, looking like some kind of code we dial into Mortal Kombat. :P

May 18, 2013 06:08 PM
CaseyHardman

I haven't learned C++, though I use C#. The thing that really makes my mind go bleh when I see C++ code is usually just the way everything seems to be named, more so than the actual syntax itself.

It's not this way in this article or the code samples provided here, but a lot of people seem to think names have to look 'techy' and be non-descriptive for them to be proper. It seems to happen most in C++, though I've seen it in a lot of other places as well. It's like they try to shorten the name as much as possible, and end up making it unnatural and confusing.

Oh, and about virtual functions...

If you made a function and later found out you wanted it to be virtual, couldn't you easily go back and make it virtual? If so, wouldn't it be best to just make them virtual only if you're sure you're going to override them?

May 19, 2013 03:17 AM
Josh Klint

It's not this way in this article or the code samples provided here, but a lot of people seem to think names have to look 'techy' and be non-descriptive for them to be proper. It seems to happen most in C++, though I've seen it in a lot of other places as well. It's like they try to shorten the name as much as possible, and end up making it unnatural and confusing.

...

If you made a function and later found out you wanted it to be virtual, couldn't you easily go back and make it virtual? If so, wouldn't it be best to just make them virtual only if you're sure you're going to override them?

Thanks for the compliment. I agree with you on naming. I always just use all lower-case for members and local variables, in my own code.

Yes, you can go back and change a function to be virtual later, assuming you remember to do it! I make a lot of mistakes and can't rely on knowing the status of each function.

May 19, 2013 06:07 AM
rpiller

Of course, virtuals still have their reasons for existence, but IMHO they should be the exception rather than the norm. After all, when do you *really* need objects to behave polymorphically? Without context, I fear novice programmers might get the wrong idea, and just put the virtual keyword in front of every function.

I wouldn't put it on every function, but I wouldn't say they are the exception at all. I don't understand some people's fear of the benefits of C++. Of course using the main features of C++ is going to be closer than say C, but they add other benefits which is why they exist. I will never understand someone who uses C++ and complains/hardly ever uses the main benefits it adds. Polymorphism is one of the main benefits of C++ along with inheritance and encapsulation, yet 2 of the 3 I'm constantly hearing game programmers complain about. Then why use C++. Just use C instead.

May 19, 2013 05:13 PM
tivolo

Of course, virtuals still have their reasons for existence, but IMHO they should be the exception rather than the norm. After all, when do you *really* need objects to behave polymorphically? Without context, I fear novice programmers might get the wrong idea, and just put the virtual keyword in front of every function.

I wouldn't put it on every function, but I wouldn't say they are the exception at all. I don't understand some people's fear of the benefits of C++. Of course using the main features of C++ is going to be closer than say C, but they add other benefits which is why they exist. I will never understand someone who uses C++ and complains/hardly ever uses the main benefits it adds. Polymorphism is one of the main benefits of C++ along with inheritance and encapsulation, yet 2 of the 3 I'm constantly hearing game programmers complain about. Then why use C++. Just use C instead.

It's not fear of benefits. Don't get me wrong - I use virtual functions and polymorphism *where it makes sense*. Making every function virtual IMO doesn't make sense. Using interfaces/abstract base classes even if you never need polymorphism doesn't make sense.

Ever had to optimize code on the PS3? Ever tried threading this kind of generic gameObject->Update() code? Ever tried putting something like that on the SPUs? Have fun, you won't enjoy it.

Use the right tools for the job. You want to use virtual functions in your GUI/HUD code? Sure, if it's gonna be 100 calls per frame, please do use them!

You want to hide every low-level engine class like Texture, VB, IB, behind an abstract interface because somebody taught you it is OOP and has to be done that way? Nope, or you will die the death of 1000 needles.

The thing is: yes, polymorphism is one of the major features that C++ has in comparison to C. Still, does that mean you have to use it all the time? I doubt you're using templates for *everything*, just because you can.

May 19, 2013 06:13 PM
rpiller

Things like "all the time" or "exception" are so vague that continuing a discussion around this is difficult. I think we both agree not EVERY method should be virtual. That's not even logical in any way. However having abstract base classes with methods like Update()/Draw()/OnThink()/whatever being virtual with a detailed class hierarchy is perfectly fine as long as you know you won't be pushing the envelope on the hardware your game plans to run on. You have to realize the trade-off between speed, flexibility, and maintainability which is really the underlying topic here. There are actually very few games (when just looking at numbers between every platform) that push the envelope. So again, the trade-off begins and we all have to make that choice.

May 19, 2013 07:52 PM
CaseyHardman

It's not this way in this article or the code samples provided here, but a lot of people seem to think names have to look 'techy' and be non-descriptive for them to be proper. It seems to happen most in C++, though I've seen it in a lot of other places as well. It's like they try to shorten the name as much as possible, and end up making it unnatural and confusing.

...

If you made a function and later found out you wanted it to be virtual, couldn't you easily go back and make it virtual? If so, wouldn't it be best to just make them virtual only if you're sure you're going to override them?

Thanks for the compliment. I agree with you on naming. I always just use all lower-case for members and local variables, in my own code.

Yes, you can go back and change a function to be virtual later, assuming you remember to do it! I make a lot of mistakes and can't rely on knowing the status of each function.

I usually do lowercase, but capitalize the first letter of new words, e.g. I'd do "fullHealth", whereas I wouldn't be surprised if some people would use "flhlth" just to sound 'programmer-like'.

As a side note, the name of this article sounds kind of confusing. Maybe it would be more appropriate as "Object-Oriented Programming with C++" or something? The current title makes it sound like you're talking about game design as in the game mechanics and such, not programming.

May 20, 2013 01:32 AM
dmitsuki

This article is kind of confusing. The title says Object-Oriented Game Design but the text implies that these things are C++ specific. Basically every modern OO langauge has these features. I don't think these features are the reason people still use C++ because newer langauges do a lot of them more "elegantly" and are designed around them whereas C++ was more of a "I can too" type of effort.

Anyway, another cool feature I think is function overloading. A method name can be the same if it takes a different amount of parameters or different types.

int A = 0;

string B = "yo";

public void DoWork(int a, string b)

{

//look at all this work being done

A = a;

B= b;

}

public void DoWork(int a)

{

//assume "yo" is some default value we expect from the string

DoWork(a, "yo");

}

May 20, 2013 05:07 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

C++ can be intimidating the new programmers. The syntax does at first glance look like it was designed for robots to read, rather than humans. However, C++ has some powerful features that speed up the process of game design, especially as games get more complex. There's good reason C++ is the long-standing standard of the game industry, and we'll talk about a few of its advantages in this lesson.

Advertisement

Other Tutorials by Josh Klint

Advertisement