Making a 3D game with Ogre - Adding Enemies

Dec 23rd, 2009 by mcasperson

In this tutorial we add some enemies to the level.

DOWNLOAD THE DEMO AND SOURCE CODE FOR WINDOWS

DOWNLOAD THE DEMO AND SOURCE CODE FOR LINUX

RETURN TO THE TUTORIAL INDEX

Enemies are added into the game based on a time line. The effect we achieve is the ability to say “after 10 seconds add enemy of type x to the screen”. Just like the weapons, enemies will be created and added to a pool, allowing shut down enemy classes to be reused. Enemies will also be created by a database class, called EnemyDatabase in this case.

To start off with we define a base class for the enemies. This Enemy class is very similar to the Weapon class.

Enemy.h

/*
 *  Enemy.h
 *
 *  Author:  Matthew Casperson
 *  Email: matthewcasperson@gmail.com
 *  Website: http://www.brighthub.com/hubfolio/matthew-casperson.aspx
 */

#ifndef ENEMY_H_
#define ENEMY_H_

#include "PersistentFrameListener.h"
#include "map"

typedef std::map StringMap;


class Enemy:
 public PersistentFrameListener
{
public:
 Enemy();
 virtual ~Enemy();

 virtual void Startup(StringMap settings);
 virtual void Shutdown();
 virtual bool FrameStarted(const FrameEvent& evt);

protected:
 void InitialiseVariables();
 SceneNode*    enemySceneNode;
 int     shields;
 int     score;

};

#endif

Enemy.cpp

#include "Enemy.h"
#include "GameLevel.h"
#include "GameConstants.h"

Enemy::Enemy()
{
 InitialiseVariables();
}

Enemy::~Enemy()
{

}

void Enemy::InitialiseVariables()
{
 enemySceneNode = NULL;
 shields = 0;
 score = 0;
}

One of the big differences between an Enemy and a Weapon is that the enemies are defined, in the XML file, long before they are created. This means we need a way to stored the information that defines what type of enemy will be created in between that information being read from the XML file, and the Enemy object being created in the game. To do this the EnemyDatabase class (which will be described later) will store a mapping of the enemy XML attribute names and values. In this way the enemy classes will interpret these strings instead of the DotSceneManager class.

Here in the Startup function the Enemy class converts the XML attributes startX, startZ, scaleX, scaleY, scaleZ, score and shields into the values that are either stored in member variables, or used in the construction of the SceneNode. Note that the startX and startZ values are relative to the width and height of the screen (give or take). So by specifying a startZ value of 0 and a startX value of 0.5, the enemy will start in the middle of the top of the screen.

void Enemy::Startup(StringMap settings)
{
 PersistentFrameListener::Startup();
 
 Vector3 startingPosition(
  StringConverter::parseReal(settings["startX"]) * WEAPON_SCREEN_X * 2 - WEAPON_SCREEN_X,
  GAMELEVEL.GetPlayerSceneNode()->getPosition().y,
  StringConverter::parseReal(settings["startZ"]) * WEAPON_SCREEN_Z * 2 - WEAPON_SCREEN_Z);

 Vector3 scale(
  StringConverter::parseReal(settings["scaleX"]),
  StringConverter::parseReal(settings["scaleY"]),
  StringConverter::parseReal(settings["scaleZ"]));

 if (scale.x == 0) scale.x = 1;
 if (scale.y == 0) scale.y = 1;
 if (scale.z == 0) scale.z = 1;

 enemySceneNode = GAMELEVEL.GetPlayerSceneNode()->createChildSceneNode(startingPosition);
 enemySceneNode->yaw(Angle(180));
 enemySceneNode->scale(scale);

 shields = StringConverter::parseInt(settings["shields"]);
 score = StringConverter::parseInt(settings["score"]);
}

void Enemy::Shutdown()
{
 GAMELEVEL.GetPlayerSceneNode()->removeAndDestroyChild(enemySceneNode->getName());
 InitialiseVariables();
 PersistentFrameListener::Shutdown();
}

bool Enemy::FrameStarted(const FrameEvent& evt)
{
 const Vector3& position = enemySceneNode->getPosition();
 if (position.x  WEAPON_SCREEN_X ||
  position.z  WEAPON_SCREEN_Z )
 {
  this->Shutdown();
 }

 return true;
}

The BasicEnemy class extends the Enemy class to define a simple enemy that moves in a straight line. Again this code is very similar to the Weapon/Bullet classes, with the exception being that the BasicEnemy class has to convert the XML attribute strings into C++ varibles like ints, floats and vectors.

BasicEnemy.h

/*
 *  BasicEnemt.h
 *
 *  Author:  Matthew Casperson
 *  Email: matthewcasperson@gmail.com
 *  Website: http://www.brighthub.com/hubfolio/matthew-casperson.aspx
 */

#ifndef BASICENEMY_H_
#define BASICENEMY_H_

#include "Enemy.h"

class BasicEnemy :
 public Enemy
{
public:
 BasicEnemy();
 ~BasicEnemy();

 void Startup(StringMap settings);
 void Shutdown();
 virtual bool FrameStarted(const FrameEvent& evt);

protected:
 void InitialiseVariables();
 Entity*   mesh;
 Vector3   direction;
};

#endif

BasicEnemy.cpp

#include "BasicEnemy.h"
#include "GameLevel.h"
#include "Utilities.h"

BasicEnemy::BasicEnemy()
{
 InitialiseVariables();
}

BasicEnemy::~BasicEnemy()
{

}

void BasicEnemy::InitialiseVariables()
{
 mesh = NULL;
 direction = Vector3::ZERO;
}

void BasicEnemy::Startup(StringMap settings)
{
 Enemy::Startup(settings);

 mesh = GAMELEVEL.GetSceneManager()->createEntity(Utilities::GetUniqueName("BasicEnemy"), settings["filename"]);
 enemySceneNode->attachObject(mesh);

 direction.x = StringConverter::parseReal(settings["directionX"]);
 direction.y = 0;
 direction.z = StringConverter::parseReal(settings["directionZ"]);
}

void BasicEnemy::Shutdown()
{
 enemySceneNode->detachObject(mesh);
 GAMELEVEL.GetSceneManager()->destroyEntity(mesh);
 InitialiseVariables();
 Enemy::Shutdown();
}

bool BasicEnemy::FrameStarted(const FrameEvent& evt)
{
 enemySceneNode->translate(direction * evt.timeSinceLastFrame);
 return Enemy::FrameStarted(evt);
}

The EnemyDatabase class serves two purposes. It maintains a pool of enemy objects, allowing shutdown objects to be reused. It also maintains the timeline which indicates at what time in the level enemies are added.

EnemyDatabase.h

/*
 *  EnemyDatabase.h
 *
 *  Author:  Matthew Casperson
 *  Email: matthewcasperson@gmail.com
 *  Website: http://www.brighthub.com/hubfolio/matthew-casperson.aspx
 */

#ifndef ENEMYDATABASE_H_
#define ENEMYDATABASE_H_

#include "Enemy.h"
#include "map"
#include "list"

#define ENEMYDATABASE EnemyDatabase::Instance()

typedef std::list EnemyList;
typedef std::map EnemyMap;
typedef std::list StringMapList;
typedef std::map EnemyPlacementDatabase;

#include "PersistentFrameListener.h"

class EnemyDatabase :
 public PersistentFrameListener
{
public:
 ~EnemyDatabase();
 static EnemyDatabase& Instance()
 {
  static EnemyDatabase instance;
  return instance;
 }

 void Startup();
 void Shutdown();
 void AddEnemyPlacement(float time, StringMap details);

 bool FrameStarted(const FrameEvent& evt);

protected:
 EnemyDatabase();
 void InitialiseVariables();
 Enemy* GetEnemy(std::string type);
 Enemy* CreateEnemy(std::string type);
 EnemyMap       enemyMap;
 EnemyPlacementDatabase    enemyPlacementDatabase;
 EnemyPlacementDatabase::iterator nextEnemyPlacement;
 float        currentTime;
};

#endif

EnemyDatabase.cpp

#include "EnemyDatabase.h"
#include "GameConstants.h"
#include "GameLevel.h"
#include "BasicEnemy.h"

EnemyDatabase::EnemyDatabase()
{
 InitialiseVariables();
}

EnemyDatabase::~EnemyDatabase()
{

}

void EnemyDatabase::InitialiseVariables()
{
 currentTime = 0;
 nextEnemyPlacement = enemyPlacementDatabase.end();
}
 
void EnemyDatabase::Startup()
{
 PersistentFrameListener::Startup();
}

void EnemyDatabase::Shutdown()
{
 enemyMap.clear();
 InitialiseVariables();
 PersistentFrameListener::Shutdown();
}

The AddPlacement function takes a time and a collection of XML attribute strings and stores them in a timeline database, which is really a std::map that maps floats (the time an enemy is to be created) with a std::list of XML attribute strings (itself a std:map mapping two std::string objects). Because the std::map is sorted we make sure that the nextEnemyPlacement variable, which is an iterator over the timeline std::map, always references the first enemy placement definition.

void EnemyDatabase::AddEnemyPlacement(float time, StringMap details)
{
 enemyPlacementDatabase[time].push_back(details);
 nextEnemyPlacement = enemyPlacementDatabase.begin();
}

In the FrameStarted function we maintain a record of the total time spent in the level in the currentTime variable. When this time is greater than or equal to the time of the enemy placement referenced by the nextEnemyPlacement iterator we call the GetEnemy function to get the appropriate enemy object, and then start it up, passing in the XML attribute strings.

In this way we can define how the enemy objects are initialised without having to create then at the time the XML file is parsed.

bool EnemyDatabase::FrameStarted(const FrameEvent& evt)
{
 currentTime += evt.timeSinceLastFrame;

 while (nextEnemyPlacement != enemyPlacementDatabase.end() &&
  nextEnemyPlacement->first second.begin();
    iter != nextEnemyPlacement->second.end(); ++iter)
  {
   StringMap& map = *iter;
   Enemy* enemy = GetEnemy(map["type"]);
   if (enemy != NULL)
    enemy->Startup(map);  
  }

  ++nextEnemyPlacement;
 }

 return true;
}

The GetEnemy function should look familiar by now, in that it returns an unused enemy object, or creates a new one, adds it to the pool, and returns it.

Enemy* EnemyDatabase::GetEnemy(std::string type)
{
 for (EnemyList::iterator iter = enemyMap[type].begin(); iter != enemyMap[type].end(); ++iter)
 {
  Enemy* enemy = *iter;
  if (!enemy->IsStarted())
   return enemy;
 }

 Enemy* enemy = CreateEnemy(type);
 if (enemy != NULL)
 {
  enemyMap[type].push_back(enemy);
  return enemy;
 }

 return NULL;
}

The CreateEnemy function includes the logic required to create a specific enemy object when supplied with an enemy type string. We have only the one enemy type now, but this function would grow as more enemies are programmed in.

Enemy* EnemyDatabase::CreateEnemy(std::string type)
{
 if (type == "basic")
  return new BasicEnemy();

 return NULL;
}

The DotSceneLoader gets a new function called parseEnemy, which collects the XML attributes strings and the enemy placement time and calls the EnemyDatabase AddPlacement function.

void DotSceneLoader::processEnemy(TiXmlElement *XMLNode)
{
 float time = getAttribReal(XMLNode, "time");
 StringMap attributes;

 TiXmlAttribute* attribute = XMLNode->FirstAttribute();
 while (attribute)
 {
  attributes[attribute->Name()] = attribute->Value();
  attribute = attribute->Next();
 }

 ENEMYDATABASE.AddEnemyPlacement(time, attributes);
}

At this point we can load a complete level from the XML file, complete with terrain, sound effects, particle systems, additional models, and now enemy placements. This ability to define a level without modifying the source code will save time and allow non-programmers to design levels.

mcasperson

Written by mcasperson

Rate this Article:

Be the first to rate me.

Add new comment

* You must be logged in order to leave comments, please Sign in or join us.

Comments

No comments yet, be the first to comment on this article.

Related Content