Get up to 80 % extra points for free! More info:

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 &die;
};
#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 &die;
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 &die;
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++

 

Previous article
The this pointer in C++
All articles in this section
Object-Oriented Programming in C++
Skip article
(not recommended)
Arena with warriors in C++
Article has been written for you by David Capka Hartinger
Avatar
User rating:
No one has rated this quite yet, be the first one!
The author is a programmer, who likes web technologies and being the lead/chief article writer at ICT.social. He shares his knowledge with the community and is always looking to improve. He believes that anyone can do what they set their mind to.
Unicorn university David learned IT at the Unicorn University - a prestigious college providing education on IT and economics.
Activities