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.
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.
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;
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
.
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.
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).
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
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; } };
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?struct
function for the Player's armor with fields for name, armor class, and durability rating.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.
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:
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:
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).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 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
.
3.144.242.235