Complete roguelike tutorial using C++ and libtcod - part 5: preparing for combat

From RogueBasin
Revision as of 18:08, 20 December 2015 by Joel Pera (talk | contribs) (added source link)
Jump to navigation Jump to 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 part, we'll do some refactoring to prepare the monster bashing. This includes detecting actor collisions (an actor trying to step on a tile where there already is an actor) and properly handling game turns.

View source here

Don't step on my shoes

First, we want to detect when the player tries to walk on a Tile occupied by another actor as this is the basis for melee combat. We keep the Map::isWall function but we add a Map::canWalk that handles both walls and actors.

In Map.hpp :

bool canWalk(int x, int y) const;

And the implementation in Map.cpp :

bool Map::canWalk(int x, int y) const {
   if (isWall(x,y)) {
       // this is a wall
       return false;
   }
   for (Actor **iterator=engine.actors.begin();
       iterator!=engine.actors.end();iterator++) {
       Actor *actor=*iterator;
       if ( actor->x == x && actor->y == y ) {
           // there is an actor there. cannot walk
           return false;
       }
   }
   return true;
}

First, the function checks if there is a wall. Else, it scans the actor list and returns false if there is an actor at given position.

Fill the rooms with monsters

Now we'll replace the inoffensive @ NPCs with ugly smelly orcs and trolls. Let's first define the maximum number of monsters per room.

static const int MAX_ROOM_MONSTERS = 3;

We add a createMonster function to place a new monster somewhere on the map :

Map.hpp :

void addMonster(int x, int y);

Map.cpp :

void Map::addMonster(int x, int y) {
   TCODRandom *rng=TCODRandom::getInstance();
   if ( rng->getInt(0,100) < 80 ) {
       // create an orc
       engine.actors.push(new Actor(x,y,'o',"orc",
            TCODColor::desaturatedGreen));
   } else {
       // create a troll
       engine.actors.push(new Actor(x,y,'T',"troll",
            TCODColor::darkerGreen));               
   }
}

We create an orc in 80% of the cases, or a troll.

Now we change the createRoom function :

TCODRandom *rng=TCODRandom::getInstance();
int nbMonsters=rng->getInt(0,MAX_ROOM_MONSTERS);
while (nbMonsters > 0) {
   int x=rng->getInt(x1,x2);
   int y=rng->getInt(y1,y2);
   if ( canWalk(x,y) ) {
       addMonster(x,y);
   }
   nbMonsters--;
}

First we get a random number of monsters and for each one, get a random position inside the room. If the tile is empty (canWalk) we create a monster.

Improved actors

As you may have noticed, we added a name to the Actor constructor. We also add two new methods.

Actor.hpp :

const char *name; // the actor's name

Actor(int x, int y, int ch, const char *name, const TCODColor &col);
void update();
bool moveOrAttack(int x,int y);

We're storing the actor's name in a const char pointer. A char pointer or a char array is how strings are manipulated in C. We could use the C++ string class but it's very tricky to use if you want to avoid uncontrolled object copies. We use a const keyword to be able to affect static strings like "a name". But that means that the actor won't be able to change his name later. If we want to be able to do that, we'd better store the string in the Actor class (and strcpy it from the name in the constructor) :

char name [ MAX_NAME_LENGTH ];

update() will handle the monster turn. It will only display a debug message for now.

moveOrAttack() will handle the player move. It returns true if the player actually moved, false it he hits a wall or another creature.

And now the implementation. First, the updated constructor :

Actor::Actor(int x, int y, int ch, const char *name,
   const TCODColor &col) :
   x(x),y(y),ch(ch),col(col),name(name) {
}

If you store the name in the actor to be able to change it later, the constructor is rather :

Actor::Actor(int x, int y, int ch, const char *name,
   const TCODColor &col) :
   x(x),y(y),ch(ch),col(col) {
   strcpy(this->name, name);
}

The monster update :

void Actor::update() {
   printf ("The %s growls!\n",name);
}

The printf function is the first function from the C standard library that we're using. It's defined in a stdio.h header that is in the compiler headers path. That's why we use a different syntax to include it. printf outputs a formatted string to the program standard output (in the Linux/Msys terminal).

And the player movement function :

bool Actor::moveOrAttack(int x,int y) {
 if ( engine.map->isWall(x,y) ) return false;
 for (Actor **iterator=engine.actors.begin();
    iterator != engine.actors.end(); iterator++) {
    Actor *actor=*iterator;
    if ( actor->x == x && actor->y ==y ) {
       printf("The %s laughs at your puny efforts to attack him!\n",
           actor->name);
       return false;
    }
 }
 this->x=x;
 this->y=y;
 return true;
}

Again, we scan the actor list to find a monster. If there is one, we display a debug message.

Note how we're using this->x to reference the Actor's x field whereas x references the function parameter. It's generally a bad thing to shadow a class field with a parameter using the same name.

Since Actor is now using the Engine class and the stdio printf function, we need to update the headers list :

#include <stdio.h>
#include "libtcod.hpp"
#include "Actor.hpp"
#include "Map.hpp"
#include "Engine.hpp"

Upgrading the Engine

We have to track whether the monster must be updated or not. There is a new turn each time the player does an action that takes some time. Instead of adding another boolean, we're going to use some enumeration, in Engine.hpp. This might seems a bit convoluted right now but it will make things easier for the next chapter.

public :
   enum GameStatus {
       STARTUP,
       IDLE,
       NEW_TURN,
       VICTORY,
       DEFEAT
   } gameStatus;

This replaces the computeFov field. The gameStatus field is an integer with only a few possible values :

  • STARTUP (==0) : first frame of the game
  • IDLE (==1) : no new turn. Redraw the same screen.
  • NEW_TURN (==2) : update the monsters position
  • VICTORY (==3) : the player won
  • DEFEAT (==4) : the player was killed

Now we can update the Engine constructor :

Engine::Engine() : gameStatus(STARTUP), fovRadius(10) {
   TCODConsole::initRoot(80,50,"libtcod C++ tutorial",false);
   player = new Actor(40,25,'@',"player",TCODColor::white);
   actors.push(player);
   map = new Map(80,45);
}

Note that since the enum is declared inside the Engine class (or namespace), we have a direct access to its constants (STARTUP) in the Engine methods. From another class, we would have to specify the complete namespace :

Engine::STARTUP.

The update function must ensure the FOV is computed on first frame :

void Engine::update() {
   TCOD_key_t key;
   if ( gameStatus == STARTUP ) map->computeFov();
   gameStatus=IDLE;

Then it will compute the player movement :

TCODSystem::checkForEvent(TCOD_EVENT_KEY_PRESS,&key,NULL);
int dx=0,dy=0;
switch(key.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 some movement key was pressed, we set gameStatus to NEW_TURN and try to move the player. Only if this succeeds, we recompute the FOV :

if ( dx != 0 || dy != 0 ) {
   gameStatus=NEW_TURN;
   if ( player->moveOrAttack(player->x+dx,player->y+dy) ) {
       map->computeFov();
 }
}

If this is a new turn, we iterate over all the actors (but the player) and call the update function. This must occur after the fov computation because monsters may need to know whether they're in the player field of view. Note that even if the player didn't actually move (he hit a wall or a creature), there is a new turn.

if ( gameStatus == NEW_TURN ) {
   for (Actor **iterator=actors.begin(); iterator != actors.end();
       iterator++) {
       Actor *actor=*iterator;
       if ( actor != player ) {
           actor->update();
       }
   }
}

Compilation

As usual, compile and enjoy inoffensive monster bashing :

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