Chapter 6. Objects, Classes, and Inheritance

In the previous chapter, we discussed functions as a way to bundle up a bunch of lines of related code. We talked about how functions abstracted away implementation details and how the sqrt() function does not require you to understand how it works internally to use it to find roots. This was a good thing, primarily because it saved the programmer time and effort, while making the actual work of finding square roots easier. This principle of abstraction will come up again here when we discuss objects.

In a nutshell, objects tie together methods and their related data into a single structure. This structure is called a class. The main idea of using objects is to create a code representation for every thing inside your game. Every object represented in the code will have data and associated functions that operate on that data. So you'd have an object to represent your player instance and related functions that make the player jump(), shoot(), and pickupItem() functions. You'd also have an object to represent every monster instance and related functions such as growl(), attack(), and possibly follow().

Objects are types of variables, though, and objects will stay in memory as long as you keep them there. You create an object instance once when the thing in your game it represents is created, and you destroy the object instance when the thing in your game it represents dies.

Objects can be used to represent in-game things, but they can also be used to represent any other type of thing. For example, you can store an image as an object. The data fields will be the image's width of the image, its height, and the collection of pixels inside it. C++ strings are also objects.

Tip

This chapter contains a lot of keywords that might be difficult to grasp at first, including virtual and abstract.

Don't let the more difficult sections of this chapter bog you down. I included descriptions of many advanced concepts for completeness. However, bear in mind that you don't need to completely understand everything in this chapter to write working C++ code in UE4. It helps to understand it, but if something doesn't make sense, don't get stuck. Give it a read and then move on. Probably what will happen is you will not get it at first, but remember a reference to the concept in question when you're coding. Then, when you open this book up again, "voilà!" It will make sense.

struct objects

An object in C++ is basically any variable type that is made up of a conglomerate of simpler types. The most basic object in C++ is struct. We use the struct keyword to glue together a bunch of smaller variables into one big variable. If you recall, we did introduce struct briefly in Chapter 2, Variables and Memory. Let's revise that simple example:

struct Player
{
  string name;
  int hp;
};

This is the structure definition for what makes a Player object. The player has a string for his name and an integer for his hp value.

If you'll recall from Chapter 2, Variables and Memory, the way we make an instance of the Player object is like this:

Player me;    // create an instance of Player, called me

From here, we can access the fields of the me object like so:

me.name = "Tom";
me.hp = 100;

Member functions

Now, here's the exciting part. We can attach member functions to the struct definition simply by writing these functions inside the struct Player definition.

struct Player
{
  string name;
  int hp;
  // A member function that reduces player hp by some amount
  void damage( int amount )	
  {
    hp -= amount;
  }
  void recover( int amount )
  {
    hp += amount;
}
};

A member function is just a C++ function that is declared inside a struct or class definition. Isn't that a great idea?

There is a bit of a funny idea here, so I'll just come out and say it. The variables of struct Player are accessible to all the functions inside struct Player. Inside each of the member functions of struct Player, we can actually access the name and hp variables as if they were local to the function. In other words, the name and hp variables of struct Player are shared between all the member functions of struct Player.

The this keyword

In some C++ code (in later chapters), you will see more references to the this keyword. The this keyword is a pointer that refers to the current object. Inside the Player::damage() function, for example, we can write our reference to this explicitly:

void damage( int amount )
{
  this->hp -= amount;
}

The this keyword only makes sense inside a member function. We could explicitly include use of keyword this inside member functions, but without writing this, it is implied that we are talking about the hp of the current object.

Strings are objects?

Yes! Every time you've used a string variable in the past, you were using an object. Let's try out some of the member functions of the string class.

#include <iostream>
#include <string>
using namespace std;
int main()
{
  string s = "strings are objects";
  s.append( "!!" ); // add on "!!" to end of the string!
  cout << s << endl;
}

What we've done here is use the append() member function to add on two extra characters to the end of the string (!!). Member functions always apply to the object that calls the member function (the object to the left of the dot).

Tip

To see the listing of members and member functions available on an object, type the object's variable name in Visual Studio, then a dot (.), then press Ctrl and spacebar. A member listing will pop up.

Strings are objects?

Pressing Ctrl and spacebar will make the member listing appear

Invoking a member function

Member functions can be invoked with the following syntax:

objectName.memberFunction();

The object invoking the member function is on the left of the dot. The member function to call is on the right of the dot. A member function invocation is always followed by round brackets (), even when no arguments are passed to the brackets.

So, in the part of the program where the monster attacks, we can reduce the player's hp value like so:

player.damage( 15 );  // player takes 15 damage

Which isn't that more readable than the following:

player.hp -= 15;      // player takes 15 damage

Tip

When member functions and objects are used effectively, your code will read more like prose or poetry than a bunch of operator symbols slammed together.

Besides beauty and readability, what is the point of writing member functions? Outside the Player object, we can now do more with a single line of code than just reduce the hp member by 15. We can also do other things as we're reducing the player's hp, such as take into account the player's armor, check whether the player is invulnerable, or have other effects occur when the player is damaged. What happens when the player is damaged should be abstracted away by the damage() function.

Now think if the player had an armor class. Let's add a field to struct Player for armor class:

struct Player
{
  string name;
  int hp;
  int armorClass;
};

We'd need to reduce the damage received by the player by the armor class of the player. So we'd type a formula now to reduce hp. We can do it the non-object-oriented way by accessing the data fields of the player object directly:

player.hp -= 15 – player.armorClass; // non OOP

Otherwise, we can do it the object-oriented way by writing a member function that changes the data members of the player object as needed. Inside the Player object, we can write a member function damage():

struct Player
{
  string name;
  int hp;
  int armorClass; 
void damage( int dmgAmount )	
  {
    hp -= dmgAmount - armorClass;
  }
};

Exercises

  1. There is a subtle bug in the player's damage function in the preceding code. Can you find and fix it? Hint: What happens if the damage dealt is less than armorClass of the player?
  2. Having only a number for armor class doesn't give enough information about the armor! What is the armor's name? What does it look like? Devise a struct function for the Player's armor with fields for name, armor class, and durability rating.

Solutions

The solution is in the struct player code listed in the next section, Privates and encapsulation.

How about using the following code:

struct Armor
{
  string name;
  int armorClass;
  double durability;
};

An instance of Armor will then be placed inside struct Player:

struct Player
{
  string name;
  int hp;
  Armor armor; // Player has-an Armor
};

This means the player has an armor. Keep this in mind—we'll explore has-a versus is-a relationships later.

Privates and encapsulation

So now we've defined a couple of member functions, whose purpose it is to modify and maintain the data members of our Player object, but some people have come up with an argument.

The argument is as follows:

  • An object's data members should only ever be accessed only through its member functions, never directly.

This means that you should never access an object's data members from outside the object directly, in other words, modify the player's hp directly:

player.hp -= 15 – player.armorClass; // bad: direct member access

This should be forbidden, and users of the class should be forced to use the proper member functions instead to change the values of data members:

player.damage( 15 );	// right: access thru member function

This principle is called encapsulation. Encapsulation is the concept that every object should be interacted via its member functions only. Encapsulation says that raw data members should never be accessed directly.

The reasons behind encapsulation are:

  • To make the class self contained: The primary idea behind encapsulation is that objects work best when they are programmed such that they manage and maintain their own internal state variables without a need for code outside the class to examine that class' private data. When objects are coded this way, it makes the object much easier to work with, that is, easier to read and maintain. To make the player object jump, you should just have to call player.jump(); let the player object manage state changes to its y-height position (making the player jump!). When an object's internal members are not exposed, interacting with that object is much easier and more efficient. Interact only with an object's public member functions; let the object manage its internal state (we will explain the keywords private and public in a moment).
  • To avoid breaking code: When code outside of a class interacts with that class' public member functions only (the class' public interface), then an object's internal state management is free to change, without breaking any of the calling code. This way, if an object's internal data members change for any reason, all code using the object still remains valid as long as the member functions remain the same.

So how can we prevent the programmer from doing the wrong thing and accessing data members directly? C++ introduces the concept of access modifiers to prevent access of an object's internal data.

Here is how we'd use access modifiers to forbid access to certain sections of struct Player from outside of struct Player.

The first thing you'd do is decide which sections of the struct definition you want to be accessible outside of the class. These section will be labelled public. All other regions that will not be accessible outside of struct will be labelled private, as follows:

struct Player
{
private:        // begins private section.. cannot be accessed 
                // outside the class until
  string name;
  int hp; 
  int armorClass;
public:         //  until HERE. This begins the public section
  // This member function is accessible outside the struct
  // because it is in the section marked public:
  void damage( int amount )
  {
    int reduction = amount – armorClass;
    if( reduction < 0 ) // make sure non-negative!
      reduction = 0;
    hp -= reduction;
  }
};

Some people like it public

Some people do unabashedly use public data members and do not encapsulate their objects. This is a matter of preference, though considered as bad object-oriented programming practice.

However, classes in UE4 do use public members sometimes. It's a judgment call; whether a data member should be public or private is really up to the programmer.

With experience, you will find that sometimes you get into a situation that requires quite a bit of refactoring when you make a data member public that should have been private.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.242.235