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.
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:
We can pass the Car object around and access its members, the class variables that describe different aspects of the object:
Object-oriented design compartmentalizes code so we can have lots of objects, each with its own set of parameters that describe that object.
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:
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:
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:
In the class constructor, we'll set the initial values of these members:
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:
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:
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.
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:
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:
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:
So when a bullet hits an enemy and causes enough damage to kill it, the following functions will be called in the order below:
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.
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:
- We can create new classes that just add or modify a couple of functions, without writing a lot of new code.
- 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
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.