Jump to content

  • Log In with Google      Sign In   
  • Create Account

Like
21Likes
Dislike

Object-Oriented Game Design

By Josh Klint | Published May 17 2013 07:37 PM in Game Programming
Peer Reviewed by (Josh Vega, jbadams, Dragonsoulj)

c++ oop inheritance

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.



About the Author(s)


Josh Klint is the CEO and founder of Leadwerks Software, an advisor for the IGDA Sacramento chapter, and was a speaker at the GDC 2013 mobile summit.

License


GDOL (Gamedev.net Open License)




Comments

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.

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.

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

"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.

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.

"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.

"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.

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.

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

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?

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.

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.

 

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.

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.

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.

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");

}


Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS