Complete roguelike tutorial using C++ and libtcod - part 6: going berserk!

From RogueBasin
Jump to: navigation, search
Complete roguelike tutorial using C++ and libtcod
-originally written by Jice
Text in this tutorial was released under the Creative Commons Attribution-ShareAlike 3.0 Unported and the GNU Free Documentation License (unversioned, with no invariant sections, front-cover texts, or back-cover texts) on 2015-09-21.


In this article, we'll actually implement melee fighting. It's significantly bigger and harder than the previous articles.

View source here

Contents

Preparing for a huge diversity of actors

Those orcs and trolls have been teasing us for too long. It's time to slice them and repaint this dungeon in red... With actual fight comes the primordial question of how we handle actors specifities. Our Actor class represents everything that has a position on the map. It's easy to see that it will encompass a wide range of different entities : items, traps, doors, monsters... The question is how do we handle this in our class hierarchy ?

The old school way would be to stuff every property in the Actor class and use flags to determine what feature is active on a specific actor. This is great because any feature can be enabled on any actor. But there's a lot of wasted memory because every actor stores a lot of properties it doesn't use.

Another way is to make a base Actor class with all common attributes and a derived class for each type of actor. Item, Trap, Door, Monster. That's more efficient as each class only stores the attributes it needs. But it's very rigid. No way an item can also be a monster. What if you want to wear an acid spitting critter like a weapon ? Or populate the dungeon with enchanted flying knives that attack you on sight ?

There's a way to get most of the advantages and almost no drawback : composition. The Actor class will contain a pointer to every "pack" of features it needs. If a pack is not needed, the pointer is NULL and only waste 4 bytes (8 on a 64 bits OS).

Destructible actors, attacking actors and thinking actors

For this article, we'll create three actor features :

  • Destructible : something that can take damage and potentially break or die
  • Attacker : something that can deal damage to a Destructible
  • Ai : something that is self-updating

Now it's obvious that these classes will have to access the Actor they're linked with. We can either add an Actor * pointer field to each class (but that's 4/8 bytes wasted for each actor feature) or pass the Actor as parameter of the feature methods. We'll use the second solution.

Now that the monsters can die, some of the Actors will block the path (living creatures) and some won't (dead creatures). To make this simple, we'll add a block boolean to the Actor class.

Actor.hpp :

class Actor {
public :
   int x,y; // position on map
   int ch; // ascii code
   TCODColor col; // color
   const char *name; // the actor's name
   bool blocks; // can we walk on this actor?
   Attacker *attacker; // something that deals damage
   Destructible *destructible; // something that can be damaged
   Ai *ai; // something self-updating
     
   Actor(int x, int y, int ch, const char *name,
       const TCODColor &col);
   void update();
   void render() const;
};

Note that we removed the moveOrAttack function. This will be handled by the Ai class from now on.

The implementation is trivial. The constructor initializes all the features with the NULL value.

Actor::Actor(int x, int y, int ch, const char *name, 
   const TCODColor &col) :
   x(x),y(y),ch(ch),col(col),name(name),
   blocks(true),attacker(NULL),destructible(NULL),ai(NULL) {
}

The update function now delegates the processing to the Ai class (if the actor has one) :

void Actor::update() {
   if ( ai ) ai->update(this);
}

If ( ai ) is equivalent to if ( ai != NULL ). Remember an integer value in C is false if ==0, else true. Since a NULL pointer is an integer with value 0, it's false.

One header to include them all

With 3 new headers, Destructible.hpp, Attacker.hpp and Ai.hpp, the header dependency management is becoming tedious. There are several ways to deal with that :

  1. either each cpp must include all the required headers with their dependencies in the right order (what we've done so far)
  2. either each header includes its own dependencies
  3. or all cpp files use a single main.hpp header

With the project becoming bigger and bigger, we see that solution 1 is quickly becoming tedious and each cpp file will have a huge list of included headers. We definitely don't want that.

Solution 2 implies that each header protects itself from being included more than once with a preprocessor trick :

#ifndef MAP_HPP
#define MAP_HPP

... content of Map.hpp

#endif

The first time the header is included, the macro MAP_HPP is defined. The second time, the header content is ignored by the compiler. It's quite handy but you still have to manage dependencies in every header.

The third solution is the simplest from the source code point of view. But it means that every cpp includes every header. With a very big project, this might have a significant impact on the compilation time.

With the current state of the project, all three solutions result in the same compilation time, and since currently, almost every cpp file requires almost every header anyway, we'll use the third solution.

Put all the headers in a main.hpp and simply include that in every cpp file.

main.hpp

#include "libtcod.hpp"
class Actor;
#include "Destructible.hpp"
#include "Attacker.hpp"
#include "Ai.hpp"
#include "Actor.hpp"
#include "Map.hpp"
#include "Engine.hpp"

The standard stdio.h or math.h header are still only included in the cpp files that actually need them.

Also note our first forward declaration :

class Actor;

The Destructible, Attacker and Ai features all need to manipulate Actors. But the Actor class also contains pointers to these classes. That's a circular dependency. We deal with that by forward declaring the Actor class so that the compiler knows that this class exists. We can then use Actor pointers and references in our declarations (because the compiler knows the size of a pointer). But we cannot use the Actor class itself until the compiler knows its content. So this works:

class Actor;
class Foo {
   Actor *actorField;
};

but this doesn't :

class Actor;
class Foo {
   Actor actorField;
};

The dependencies for the Actor.cpp file are now :

#include "main.hpp"

We can also update the Map::canWalk function to use the new Actor::blocks field :

Actor *actor = *iterator;
if ( actor->blocks && actor->x == x && actor->y == y ) {
   // there is a blocking actor here. cannot walk
   return false;
}

Destructible actors

Let's create the Destructible.hpp file :

class Destructible {
public :
   float maxHp; // maximum health points
   float hp; // current health points
   float defense; // hit points deflected
   const char *corpseName; // the actor's name once dead/destroyed

The health points start at maxHp. Each time the class takes x damage points, the health is decreased by x-defense. We also store a corpse name that will be used to update the actor's name once hp reaches 0.

Then we define the constructor and some helper function :

Destructible(float maxHp, float defense, const char *corpseName);
inline bool isDead() { return hp <= 0; }

This is the first function where the implementation is in the header rather than in the cpp file. The inline keyword tells the compiler to replace calls to this function directly by the function code, resulting in faster execution times. You can inline trivial functions that are frequently called. You shouldn't inline functions that have more than a few lines of code.

float takeDamage(Actor *owner, float damage);

This is the function that handles damage given to the Destructible. The owner is the actor targetted by the attack, the one containing the Destructible class. The function returns the number of hit points actually taken (damage - defense). It's not used yet but it might be useful for the attacker to know how many hit point were dealt.

virtual void die(Actor *owner);

And here is, again, something new. The Destructible class will take care of what happens when hp reaches 0 in the die function. But it's pretty obvious the player actor's die function will be completely different from some random monster's one. We could simply make a test in Destructible::die :

if ( owner == engine.player ) {
...
} else {
...
}

But we'll use class inheritance to handle it in a more evolutive way. We'll create MonsterDestructible and PlayerDestructible classes that inherit Destructible and overload the die method :

class MonsterDestructible : public Destructible {
public :
   MonsterDestructible(float maxHp, float defense, const char *corpseName);
   void die(Actor *owner);
};

class PlayerDestructible : public Destructible {
public :
   PlayerDestructible(float maxHp, float defense, const char *corpseName);
   void die(Actor *owner);
};

The problem is that the Engine will manipulate Destructible pointers, not MonsterDestructible or PlayerDestructible pointers. To be able to call the correct die method when dealing with a Destructible pointer without knowing whether the pointed object is a Monster or a Player, we have to make this method virtual. Note that you only have to use the virtual keyword in the base class.

Destructible implementation

#include <stdio.h>
#include "main.hpp"

Destructible::Destructible(float maxHp, float defense, const char *corpseName) :
   maxHp(maxHp),hp(maxHp),defense(defense),corpseName(corpseName) {
}

The takeDamage method subtracts the defense points and print an appropriate message. If hp is <= 0, it calls the die method.

float Destructible::takeDamage(Actor *owner, float damage) {
   damage -= defense;
   if ( damage > 0 ) {
       hp -= damage;
       if ( hp <= 0 ) {
           die(owner);
       }
   } else {
       damage=0;
   }
   return damage;
}

The Destructible::die method handle the part of dying that is common to both player and monsters :

void Destructible::die(Actor *owner) {
   // transform the actor into a corpse!
   owner->ch='%';
   owner->col=TCODColor::darkRed;   
   owner->name=corpseName;
   owner->blocks=false;
   // make sure corpses are drawn before living actors
   engine.sendToBack(owner);
}

We replace the actor's character with a bloody red %, change its name to the corpse name and set it as non blocking (living creatures can trample corpses...).

Since actors are drawn in their order in the list, a corpse my be drawn on top of a living actor. To keep that from happening, we simply move the dead actors to the beginning of the list in the Engine::sendToBack function :

In Engine.hpp :

void sendToBack(Actor *actor);

And the implementation :

void Engine::sendToBack(Actor *actor) {
   actors.remove(actor);
   actors.insertBefore(actor,0);
}

The MonsterDestructible and PlayerDestructible constructor are just copies of the base class constructor :

MonsterDestructible::MonsterDestructible(float maxHp, float defense, const char *corpseName) :
   Destructible(maxHp,defense,corpseName) {
}

PlayerDestructible::PlayerDestructible(float maxHp, float defense, const char *corpseName) :
   Destructible(maxHp,defense,corpseName) {
}

The Monster dying method displays a message, then call the father class die method :

void MonsterDestructible::die(Actor *owner) {
   // transform it into a nasty corpse! it doesn't block, can't be
   // attacked and doesn't move
   printf ("%s is dead\n",owner->name);
   Destructible::die(owner);
}

The Player dying method is almost the same. The message is slightly different, but it also change the game status to defeat.

void PlayerDestructible::die(Actor *owner) {
   printf ("You died!\n");
   Destructible::die(owner);
   engine.gameStatus=Engine::DEFEAT;
}

Attacker

This is the simplest class so far :

Attacker.hpp :

class Attacker {
public :
   float power; // hit points given

   Attacker(float power);
   void attack(Actor *owner, Actor *target);
};

Attacker.cpp :

#include <stdio.h>
#include "main.hpp"

Attacker::Attacker(float power) : power(power) {
}

void Attacker::attack(Actor *owner, Actor *target) {
   if ( target->destructible && ! target->destructible->isDead() ) {
       if ( power - target->destructible->defense > 0 ) {
           printf("%s attacks %s for %g hit points.\n", owner->name, target->name,
               power - target->destructible->defense);
       } else {
           printf("%s attacks %s but it has no effect!\n", owner->name, target->name);            
       }
       target->destructible->takeDamage(target,power);
   } else {
       printf("%s attacks %s in vain.\n",owner->name,target->name);
   }
}

First we check that the target is actually destructible and not yet dead. Then we try to attack it and print an appropriate message, whether the attack was successful or not.

A player with some intelligence

The player, like the monsters, will get his own AI. We create a base Ai class that is rather minimalist :

class Ai {
public :
   virtual void update(Actor *owner)=0;
};

We know that the update method will be overloaded in the PlayerAi and MonsterAi classes, so we make it virtual. But what's this funny looking =0 ? It means that the method is a pure virtual method, or an abstract method. The Ai class has no implementation for this method. Thus, it is an abstract class that can't be instanciated.

The player AI will only handle the keyboard input and not make decisions on its own. We'll have to bring back code from Engine::update and Actor::moveOrAttack :

class PlayerAi : public Ai {
public :
   void update(Actor *owner);

protected :
   bool moveOrAttack(Actor *owner, int targetx, int targety);
};

The update function first checks if you're still alive to keep your cadaver from wandering in the dungeon :

void PlayerAi::update(Actor *owner) {
   if ( owner->destructible && owner->destructible->isDead() ) {
       return;
   }

Then you put the old code from Engine::update :

   int dx=0,dy=0;
   switch(engine.lastKey.vk) {
   case TCODK_UP : dy=-1; break;
   case TCODK_DOWN : dy=1; break;
   case TCODK_LEFT : dx=-1; break;
   case TCODK_RIGHT : dx=1; break;
       default:break;
   }
   if (dx != 0 || dy != 0) {
       engine.gameStatus=Engine::NEW_TURN;
       if (moveOrAttack(owner, owner->x+dx,owner->y+dy)) {
           engine.map->computeFov();
       }
   }
}

The moveOrAttack function is inspired from its old implementation but it now uses the Attacker/Destructible features :

bool PlayerAi::moveOrAttack(Actor *owner, int targetx,int targety) {
   if ( engine.map->isWall(targetx,targety) ) return false;
   // look for living actors to attack
   for (Actor **iterator=engine.actors.begin();
       iterator != engine.actors.end(); iterator++) {
       Actor *actor=*iterator;
       if ( actor->destructible && !actor->destructible->isDead()
            && actor->x == targetx && actor->y == targety ) {
           owner->attacker->attack(owner, actor);
           return false;
       }
   }

If nobody was attacked, we display the corpses on the destination cell :

// look for corpses
for (Actor **iterator=engine.actors.begin();
   iterator != engine.actors.end(); iterator++) {
   Actor *actor=*iterator;
   if ( actor->destructible && actor->destructible->isDead()
        && actor->x == targetx && actor->y == targety ) {
       printf ("There's a %s here\n",actor->name);
   }
}

and finally move to the cell, returning true to update the field of view :

   owner->x=targetx;
   owner->y=targety;
   return true;
}

Artificial dumbness for orcs and trolls

The MonsterAi class declaration is similar to PlayerAi, except that we don't need to know if moveOrAttack resulted in a movement :

class MonsterAi : public Ai {
public :
   void update(Actor *owner);

protected :
   void moveOrAttack(Actor *owner, int targetx, int targety);
};

The update function also checks that we're dealing with a living actor :

void MonsterAi::update(Actor *owner) {
   if ( owner->destructible && owner->destructible->isDead() ) {
       return;
   }

then it will try to move towards the player as soon as he's in fov. Fortunately, we don't have to compute the field of view from the monster point of view. The monster can see the player only if he is in the player's field of view.

   if ( engine.map->isInFov(owner->x,owner->y) ) {
           // we can see the player. move towards him
       moveOrAttack(owner, engine.player->x,engine.player->y);
   }
}

The moveOrAttack method will use a totally naive pathfinding method : go straight to the player until we can't. We'll improve that in the end of the article.

void MonsterAi::moveOrAttack(Actor *owner, int targetx, int targety) {
   int dx = targetx - owner->x;
   int dy = targety - owner->y;
   float distance=sqrtf(dx*dx+dy*dy);

First we compute the distance to the player. If we're out of melee range, try to walk towards him :

if ( distance >= 2 ) {
   dx = (int)(round(dx/distance));
   dy = (int)(round(dy/distance));

Here we're taking the monster->player vector and normalizing it (dividing it by its length so that its length is 1), then rounding its x,y component to get an integer deplacement vector. In the end, the possible values for dx,dy are -1,0 and 1.

if ( engine.map->canWalk(owner->x+dx,owner->y+dy) ) {
   owner->x += dx;
   owner->y += dy;

If the destination cell is walkable, walk !

       }
   } else if ( owner->attacker ) {
       owner->attacker->attack(owner,engine.player);
   }
}

If we're at melee range and have an Attacker feature, attack the player !

Update the engine

First a tiny improvement : we pass the console size as constructor parameters and store them in the class. We'll need that to draw the GUI. We also store the last key pressed so that PlayerAi can use it.

In Engine.hpp :

   int screenWidth;
   int screenHeight;
   TCOD_key_t lastKey;

   Engine(int screenWidth, int screenHeight);

We have to change the player creation in the Engine constructor :

Engine::Engine(int screenWidth, int screenHeight) : gameStatus(STARTUP),fovRadius(10),
   screenWidth(screenWidth),screenHeight(screenHeight) {
   TCODConsole::initRoot(screenWidth,screenHeight,"libtcod C++ tutorial",false);
   player = new Actor(40,25,'@',"player",TCODColor::white);
   player->destructible=new PlayerDestructible(30,2,"your cadaver");
   player->attacker=new Attacker(5);
   player->ai = new PlayerAi();
   actors.push(player);

We give the player 30 health points, 2 in defense and 5 in attack.

The update function is much simpler as it only has to call the actor's update function :

void Engine::update() {
   if ( gameStatus == STARTUP ) map->computeFov();
   gameStatus=IDLE;
   TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS,&lastKey,NULL);
   player->update();
   if ( gameStatus == NEW_TURN ) {
       for (Actor **iterator=actors.begin(); iterator != actors.end();
           iterator++) {
           Actor *actor=*iterator;
           if ( actor != player ) {
               actor->update();
           }
       }
   }
}

And now, as a GUI embryo, we'll display the player health in the bottom of the screen in the render function :

player->render();
// show the player's stats
TCODConsole::root->print(1,screenHeight-2, "HP : %d/%d",
   (int)player->destructible->hp,(int)player->destructible->maxHp);

Finally we have to change the Engine declaration in main.cpp :

Engine engine(80,50);

Monsters in the map

The monsters creation in the addMonster function now uses the actor features :

if ( rng->getInt(0,100) < 80 ) {
   // create an orc
   Actor *orc = new Actor(x,y,'o',"orc",
       TCODColor::desaturatedGreen);
   orc->destructible = new MonsterDestructible(10,0,"dead orc");
   orc->attacker = new Attacker(3);
   orc->ai = new MonsterAi();
   engine.actors.push(orc);
} else {
   // create a troll
   Actor *troll = new Actor(x,y,'T',"troll",
        TCODColor::darkerGreen);
   troll->destructible = new MonsterDestructible(16,1,"troll carcass");
   troll->attacker = new Attacker(4);
   troll->ai = new MonsterAi();
   engine.actors.push(troll);
}

The orc has 10 health points, no defense and 3 attack points. The troll, 16, 1 and 4.

A little more smartness

You can now compile and test the game but you'll see that monsters can be easily abused because they stop tracking you as soon as they're out of fov, and they can easily be blocked at a wall corner.

We're going to improve slightly their Ai code.

First, we want the monster to keep tracking the player for a few turns after he's out of sight. We add a moveCount field in the MonsterAi class to keep track of this number of turns :

protected :
   int moveCount;

In the implementation, we set that number of turns to 3 :

// how many turns the monster chases the player
// after losing his sight
static const int TRACKING_TURNS=3;

Each time the player is in FOV, we reset the moveCount to TRACKING_TURNS. When the player is out of sight, we decrease that count. We track the player until moveCount is <= 0. Let's update the MonsterAi::update function :

void MonsterAi::update(Actor *owner) {
   if ( owner->destructible && owner->destructible->isDead() ) {
       return;
   }
   if ( engine.map->isInFov(owner->x,owner->y) ) {
       // we can see the player. move towards him
       moveCount=TRACKING_TURNS;
   } else {
       moveCount--;
   }
  if ( moveCount > 0 ) {
   moveOrAttack(owner, engine.player->x,engine.player->y);
  }
}

Now we're going to improve the movement to add wall sliding :

void MonsterAi::moveOrAttack(Actor *owner, int targetx, int targety) {
   int dx = targetx - owner->x;
   int dy = targety - owner->y;
   int stepdx = (dx > 0 ? 1:-1);
   int stepdy = (dy > 0 ? 1:-1);
   float distance=sqrtf(dx*dx+dy*dy);
   if ( distance >= 2 ) {
       dx = (int)(round(dx/distance));
       dy = (int)(round(dy/distance));
       if ( engine.map->canWalk(owner->x+dx,owner->y+dy) ) {
           owner->x += dx;
           owner->y += dy;
       } else if ( engine.map->canWalk(owner->x+stepdx,owner->y) ) {
           owner->x += stepdx;
       } else if ( engine.map->canWalk(owner->x,owner->y+stepdy) ) {
           owner->y += stepdy;
       }

If the normal route is blocked, we try either only the horizontal direction, or only the vertical.

The end

Ok it's finally time to compile.

Windows

> g++ src/*.cpp -o tuto -Iinclude -Llib -ltcod-mingw -static-libgcc -static-libstdc++ -Wall

Linux

> g++ src/*.cpp -o tuto -Iinclude -L. -ltcod -ltcodxx -Wl,-rpath=. -Wall

Have a nice death and don't forget to launch the game from the terminal if you want to see the messages !

Personal tools