Lesson 6 - Arena Warrior - Encapsulation
In the previous lessons, The this pointer in C++, we explained the this
keyword. We already have one fully-featured object which us a rolling die. We're
going to finish up our arena in the next two lessons. We already have a rolling
die, but we're still missing two essential objects: the warrior and the arena
itself. Today, we're going to focus mainly on the warrior. First, we'll decide
what he (or she) will be able to do, and then we'll write our code.
To begin with, please delete printing to the console in the
RolligDie
constructors and destructor. For further work, these logs
would mess with our output.
Fields
The warrior will have HP (which stands for health points/hit points, e.g. 80hp). We'll store his maximum health, which will vary each instance, and his current health, e.g. a wounded warrior will have 40hp from 80 total. The warrior will have a damage and defense, which will be both defined in hp. When a warrior, with 20hp damage, attacks another warrior with 10hp defense, he takes 10 points of his health (we'll improve this formula later). The warrior will have a reference to the rolling die instance. We'll always roll the die and add a particular random number to his attack/defense to make the game more unpredictable. Of course, each warrior could have their own rolling die, but I wanted this to be as close to a real board game as possible and show you how OOP simulates reality. The warriors will share a single rolling die instance, which will add an element of randomness to the game and make the game a bit more realistic. Last of all, we'll make the warriors send messages about what is happening in the fight. The message will look something like this: "Zalgoren attacks with a hit worth 25 hp." However, we'll put the message part off for now, we'll mainly focus on creating the warrior object.
Now when we've got a good idea of what we want, let's get right into it! Let's add a Warrior
class to the arena project and add fields to it accordingly:
Warrior.h
#ifndef __WARRIOR_H_ #define __WARRIOR_H_ #include <string> #include "RollingDie.h" using namespace std; class Warrior { public: float health; float max_health; float damage; float defense; RollingDie ¨ }; #endif
We have deleted the constructor and destructor so far. Likewise, we must not
forget to include RollingDie.h
for the warrior.
Methods
Let's start off by creating a constructor for the fields. It won't be hard.
We'll want to set all the fields the class has. Let's add declaration to the
Warrior.h
header file and initialize the fields in the
implementation file:
Warrior.cpp
Warrior::Warrior(float health, float damage, float defense, RollingDie &die) : die(die) { this->health = health; this->max_health = health; this->damage = damage; this->defense = defense; }
We assume that the warrior has a full health once he's created, so the
constructor doesn't need a maxHealth
parameter. It's easier to just
set the maxHealth
to whatever the starting health is.
Next, notice the reference initialization. We pass the die by reference to be able to have only one die in the program. If the die wasn't a reference (or pointer), then each warrior would have its own die. As we said in the Reference lesson, we must initialize the reference when we create it. And as we know from previous lessons, that is before the constructor is called. That's why we need to use this syntax. In general, all fields should be initialized this way if it's possible. Therefore, we'll now modify the constructor:
Warrior::Warrior(float health, float damage, float defense, RollingDie &die) : die(die), health(health), max_health(health), damage(damage), defense(defense) {}
Now it's according to good practices.
Let's move on to the methods. Apparently we're going to need some
alive()
method to see whether the warrior is still alive.
Similarly, we're going to need the attack()
method to attack
another warrior. First, we'll look at the alive()
method. We'll
figure out whether the warrior has any health left. If he doesn't, then he's
apparently dead.
bool Warrior::alive() { if (this->health > 0) return true; else return false; }
Due to the fact that the expression this->health > 0
is
actually a logical value, we can return it and the code will become shorter:
bool Warrior::alive() { return this->health > 0; }
I often meet such an unnecessary condition very often even in code written be
experienced programmers, that's why I'm pointing this out. The
if
-else
construct in this case only says how
unexperienced the programmer probably is.
Now let's look at the attack()
method. Based on the defense, die
roll and attack, it calculates the damage as we described it at the
beginning.
void Warrior::attack(Warrior & second) { float defense_second = second.defense + second.die.roll(); float attack_first = this->damage + this->die.roll(); float injury = attack_first - defense_second; if (injury < 0) injury = 0; second.health -= injury; }
The calculation should be easy. We'll calculate the defender's defense (including the number rolled on the die), then the attacker's damage (again including the rolled number) and subtract these values - we get the final injury. We must also reckon with a situation where the defense is higher than the damage - hence the condition (if it wasn't there, the defender's health would be increased). Finally, we subtract the damage from the defender's health and we're done.
Visibility
Now we have warriors who can fight each other. But what if one of the players
wants to cheat and wants to take more health? If it was a programmer who used
our warrior (for example, because it's included in some library), he could do
that, because we allowed programmers to change the health freely. One of the
fundamentals of OOP is encapsulation, i.e. to keep the fields
for the class itself and to expose only the methods outside. We'll handle this
by that magic public:
part at the beginning of the class.
Please edit your Warrior
class as follows:
Warrior.h
class Warrior { private: float health; float max_health; float damage; float defense; RollingDie ¨ public: Warrior(float health, float damage, float defense, RollingDie &die); bool alive(); void attack(Warrior &second); };
Note the use of private:
at the beginning of the class. All
fields (and methods) following this construct will not be visible from the
outside. For example, we won't be able to execute the following code after this
change:
Warrior me(100, 8, 4, &die); Warrior enemy(100, 8, 4, &die); me.health = 99999; // Haha, I'm almost immortal me.defense = 99999; // Haha, I'm invincible enemy.damage = 0; // Haha, you wouldn't hurt a fly enemy.health = 1; // Haha, you are weak
The programmer won't have access to either of the fields because they are
private. But perhaps someone might want to know how much health the warrior
still has. We use getters and setters for this. These are methods starting with
get
or set
, then the field name follows. In principle,
these are fully-featured methods, returning or setting the value of a private
field. It's only this naming convention which makes them getters and
setters.
For example, for health, these methods would look like this:
Warrior.h
class Warrior { private: float health; float max_health; float damage; float defense; RollingDie ¨ public: Warrior(float health, float damage, float defense, RollingDie &die); bool alive(); void attack(Warrior &second); float getHealth(); // getter void setHealth(float health); // setter };
Warrior.cpp
float Warrior::getHealth() { return this->health; } void Warrior::setHealth(float health) { if (health < 0) // validation return; this->health = health; }
Using the getHealth()
method, we can now get the life of the
warrior, even if the field is private. On the contrary, with the setter we can
set the health. Note that it's not possible to set the health to less than
0
due to the condition in the setter. We don't want a setter for
the health because the injury is handled by the attack()
method,
but I also wanted to show it to demonstrate how we can validate the input by it.
If the field was publicly available, we won't have be able to ensure this
validation process and someone could easily set a negative health.
We can access fields and methods marked as public
from outside.
On the contrary, fields and methods marked as private
are secured
and we can be sure that the only one who sees them is us. We can access these
methods and fields from other methods within the same class (including
public
ones) and through the this
pointer. This is the
encapsulation principle - to protect our own data from being
changed.
Fight
Now we can make such a small fight. In main.cpp
, we'll create
two fighters and fight each other until one of them dies (we don't forget to
include Warrior.h
).
int main() { RollingDie die; Warrior first(100, 8, 4, die); Warrior second(100, 8, 4, die); while (first.alive() && second.alive()) { first.attack(second); if (second.alive()) second.attack(first); } if (first.alive()) cout << "The first warrior won with " << first.getHealth() << " hp left" << endl; else cout << "The second warrior won with " << second.getHealth() << " hp left" << endl; cin.get(); return 0; }
Console application
The second warrior won with 22 hp left
Conventions
You've certainly noticed that methods and fields are written in a different
style (such as letter case, spaces, etc.). In C++ (as opposed to Java or C#, for example), there are no
rules on how to write code. The developers didn't even agree on how to write
braces - whether after the method name (as Java does for example) or under the
method name (as C# does). I personally follow the convention stated in the
series, i.e. the field names and variables are in the snake-case notation: in
lowercase and separated by underscores (int my_variable
,
string player_name
). I write methods in the camelCase notation
(myMethod()
, roll()
). Then constants in ALL_CAPS
notation (MAX_PLAYER_COUNT
, LEVEL_COUNT
). As for
brackets, I use the Allman style (braces go under the method name), you can find
different conventions on Wikipedia. In this, C++ is
not unified and it's up to ebery programmer to choose his style.
That would be all for today's lesson. In the project, we have modified visibility for other classes and added getters and setters. If you want to be sure that we are working with the same code, please download the source code below the article.
We'll continue with it in the next lesson. And what's next? In the, Arena with warriors in C++, lesson, we'll write some basic arena functionality to make the program finally do something.
Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.
Download
By downloading the following file, you agree to the license terms
Downloaded 6x (1003.99 kB)
Application includes source codes in language C++