University of Wales Trinity Saint David
ACCB5017 - Games Architecture and Mechanics

Encapsulation

Overview

This lab builds a Character class that keeps its name, health and strength private and reachable only through public getters and setters. You will implement those accessors, then take a plain setHealth that accepts any value at all and add a guard clause that enforces one rule, health can never go below zero, so that every path through the class, including takeDamage, ends up obeying it automatically.

Learning Objectives

By the end of this lab you will be able to:

Concept

A Character in a game needs its health to stay sensible. never negative, since a health bar cannot show a negative amount and an "is this character still alive" check usually just asks whether health is greater than zero. If health were a plain public member, any code anywhere in the project could write character.health = -500; directly, and nothing would stop it. Every piece of code that later reads health would then have to defensively check it before trusting it, because there is no single place where an invalid value could have been caught.

Encapsulation is the practice of hiding a class's data behind an access specifier so it can only be reached through that class's own methods. Marking name, health and strength private makes them invisible to code outside Character. the only way in or out is through the public methods the class chooses to expose. On its own, though, a getter that returns a member and a setter that assigns one changes nothing about validity, a setter that just copies its parameter straight into the member is exactly as unsafe as a public field, it has only moved the unchecked write behind a function call.

The real benefit of encapsulation shows up once a setter adds a guard clause, a conditional check at the top of the function that catches an invalid value before it is assigned, such as if (h < 0) { h = 0; } inside setHealth. Because every write to health is required to go through setHealth, that one guard clause protects every caller, including takeDamage, as long as takeDamage itself calls setHealth instead of assigning to health directly.

Tasks

Task 1 - The Character class: private state and accessors

In Solution Explorer, use Add > Class... and name it Character. This generates a matching Character.h and Character.cpp. Replace their contents with the skeletons below, then complete them.

  1. In Character.h, declare three private members: name as std::string, and health and strength as int.
  2. In Character.cpp, complete the constructor so it assigns each of the three parameters to the matching private member using this->.
  3. Complete getName(), getHealth() and getStrength() so each returns its matching member.

Success condition: constructing a Character and calling all three getters returns exactly the values passed to the constructor.

Character.h
#pragma once
#include <string>

class Character
{
private:
    // TODO: declare three private members: name (std::string),
    // health (int), strength (int)

public:
    Character(std::string name, int health, int strength);
    ~Character();

    std::string getName();
    int getHealth();
    int getStrength();
};
Character.cpp
#include "Character.h"

Character::Character(std::string name, int health, int strength)
{
    // TODO: assign each parameter to the matching private member using this->
}

Character::~Character() {}

std::string Character::getName()
{
    // TODO: return name
}

int Character::getHealth()
{
    // TODO: return health
}

int Character::getStrength()
{
    // TODO: return strength
}

Task 2 - An unguarded setHealth and takeDamage

Open Character.h and add the three setters and takeDamage below to the public section, then open Character.cpp and complete each one.

  1. Implement setName and setStrength to assign their parameter straight to the matching member.
  2. Implement setHealth(int h) the same way for now, assign h straight to health with no check at all.
  3. Implement takeDamage(int damage) to call setHealth(getHealth() - damage).

Then create main.cpp with Add > New Item... and use the reference code below, already complete, to construct a Character and deal it far more damage than its starting health.

Success condition: the project builds and running it prints a health value, whatever setHealth was given, unchecked.

Character.h
#pragma once
#include <string>

class Character
{
private:
    std::string name;
    int health;
    int strength;

public:
    Character(std::string name, int health, int strength);
    ~Character();

    std::string getName();
    int getHealth();
    int getStrength();

    void setName(std::string name);
    void setHealth(int h);
    void setStrength(int s);

    void takeDamage(int damage);
};
Character.cpp
#include "Character.h"

Character::Character(std::string name, int health, int strength)
{
    this->name = name;
    this->health = health;
    this->strength = strength;
}

Character::~Character() {}

std::string Character::getName()
{
    return name;
}

int Character::getHealth()
{
    return health;
}

int Character::getStrength()
{
    return strength;
}

void Character::setName(std::string name)
{
    // TODO: assign the parameter to the name member using this->
}

void Character::setHealth(int h)
{
    // TODO: assign h straight to health, no check yet
}

void Character::setStrength(int s)
{
    // TODO: assign the parameter to the strength member using this->
}

void Character::takeDamage(int damage)
{
    // TODO: call setHealth(getHealth() - damage)
}
main.cpp
#include <iostream>
#include "Character.h"

int main()
{
    Character carys("Carys", 40, 12);

    std::cout << carys.getName() << " has " << carys.getHealth() << " health." << std::endl;

    carys.takeDamage(999);

    std::cout << carys.getName() << " has " << carys.getHealth() << " health after taking 999 damage." << std::endl;

    return 0;
}

Task 3 - Running it and observing the invalid state

Build and run the project. Nothing needs editing in this task, just read the output.

  1. Read the second line the program prints, carys's health after takeDamage(999).
  2. Compare it with what a health value is supposed to mean. a character with negative health has taken more damage than it had health to lose, which no part of the class currently prevents.

Success condition: you can point at the printed number and explain why it is not a value health should ever be allowed to hold, and why setHealth as written in Task 2 let it happen.

Task 4 - Enforcing the invariant inside setHealth

Open Character.cpp and edit setHealth only. Do not touch takeDamage.

  1. At the top of setHealth, add a guard clause. if h is less than 0, set h to 0 before it is assigned to health.

Success condition: rebuilding and rerunning the same main.cpp from Task 2 now prints 0 for carys's health after takeDamage(999), even though takeDamage's own code was never edited. the guard clause inside setHealth protects every path that reaches it, because takeDamage was already written to call setHealth rather than assign to health directly.

Character.cpp
#include "Character.h"

Character::Character(std::string name, int health, int strength)
{
    this->name = name;
    this->health = health;
    this->strength = strength;
}

Character::~Character() {}

std::string Character::getName()
{
    return name;
}

int Character::getHealth()
{
    return health;
}

int Character::getStrength()
{
    return strength;
}

void Character::setName(std::string name)
{
    this->name = name;
}

void Character::setHealth(int h)
{
    // TODO: if h is less than 0, set h to 0 before it reaches the
    // assignment below
    health = h;
}

void Character::setStrength(int s)
{
    this->strength = s;
}

void Character::takeDamage(int damage)
{
    setHealth(getHealth() - damage);
}

Solution

Try the tasks above before revealing the solution.