Quantcast
Channel: GameDev.net
Viewing all articles
Browse latest Browse all 17825

Object-Oriented Game Design

$
0
0
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.

Viewing all articles
Browse latest Browse all 17825

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>