Difference between revisions of "Complete roguelike tutorial using C++ and libtcod - part 6: going berserk!"

From RogueBasin
Jump to navigation Jump to search
Line 127: Line 127:


  Actor *actor = *iterator;
  Actor *actor = *iterator;
  if ( actor->blocks && actor->x == x && actor->y == y ) {
  <span style="color:green">if ( actor->blocks && actor->x == x && actor->y == y ) {</span>
     // there is a blocking actor here. cannot walk
     // there is a blocking actor here. cannot walk
     return false;
     return false;

Revision as of 15:35, 6 October 2015

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.

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 damages
   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;
}