Making a 3D game with Ogre - Collision Detection

Dec 24th, 2009 by mcasperson

In this tutorial we add collision detection to the game.

DOWNLOAD THE DEMO AND SOURCE CODE FOR WINDOWS

DOWNLOAD THE DEMO AND SOURCE CODE FOR LINUX

RETURN TO THE TUTORIAL INDEX

At this point in the demo we have enemies and weapons, but the two do not interact. In order for the bullets to be able to shoot the enemies we need to add collision detection.

Collision detection is a very complicated subject, being the focus of many a research paper. However, we can get away with a very simple collision detection system because the response to all collisions in the game will be the removal of one of the colliding objects. A weapon will be removed when it hits a ship and an enemy ship will be removed if it collides with the player.

Every object that is to collide with other objects extends a base class called CollisionObject.

CollisionObject.h

We only want objects that are active to be involved in collisions. We only want those objects that are active to be involved in a collision, so we extend the PersistentFrameListener class, which gives us access to the IsActive function. This will let the CollisionManager know if the object should participate in a collision.

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

#ifndef COLLISIONOBJECT_H_
#define COLLISIONOBJECT_H_

#include "Ogre.h"
#include "PersistentFrameListener.h"

using namespace Ogre;

class CollisionObject :
 public PersistentFrameListener
{
public:
 CollisionObject(int collisionType);
 virtual ~CollisionObject();

 void Startup();
 void Shutdown();

 int GetCollisionType() const {return collisionType;}

The GetBoundingSphere function is pure virtual, meaning that it has to be implemented by the extending class. This function should return a Sphere in world coordinates that bounds the object on the screen.

 virtual Sphere GetBoundingSphere() = 0;

The Collision function is called when a collision is detected between two objects. It is up to the extending class whether or not to participate in the collision (so an enemy that is colliding with an enemy weapon would not do anything in response to a collision).

 virtual void Collision(CollisionObject* other) {}

protected:
 int   collisionType;
};

#endif

CollisionObject.cpp

#include "CollisionObject.h"
#include "CollisionManager.h"

CollisionObject::CollisionObject(int collisionType) :
 collisionType(collisionType)
{
 COLLISIONMANAGER.AddCollisionObject(this);
}

CollisionObject::~CollisionObject()
{
 COLLISIONMANAGER.RemoveCollisionObject(this);
}

The Startup and Shutdown functions add and remove the local object from the CollisionManager.

void CollisionObject::Startup()
{
 PersistentFrameListener::Startup();
}

void CollisionObject::Shutdown()
{
 PersistentFrameListener::Shutdown();
}

PersistentFrameListener.h

The other big issue with collision detection is making sure objects do not move through each other during the frame. We will address this by setting a maximum time on each frame, which, assuming the colliding objects aren’t too small and moving too fast, will ensure that two objects. This is done in the PersistentFrameListener class, which now also includes a function called FrameEnded that is called once per frame after FrameStarted has been called on all the active objects.

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

#ifndef PERSISTENTFRAMELISTENER_H_
#define PERSISTENTFRAMELISTENER_H_

#include "Ogre.h"
#include "OgreEngineManager.h"
#include "GameConstants.h"

class PersistentFrameListener :
 public FrameListener
{
public:
 PersistentFrameListener() :
  isStarted(false)
 {
  ENGINEMANAGER.GetRoot()->addFrameListener(this);
 }
 
 virtual ~PersistentFrameListener()
 {
  if (ENGINEMANAGER.GetRoot() != NULL)
   ENGINEMANAGER.GetRoot()->removeFrameListener(this);
 }

 void Startup()
 {
  isStarted = true;
 }

 void Shutdown()
 {
  isStarted = false;
 }

 bool frameStarted(const FrameEvent& evt)
 {
  if (this->isStarted)
   return FrameStarted(GetFixedFrameEvent(evt));

  return true;
 }

 bool frameEnded(const FrameEvent& evt)
 {
  if (this->isStarted)   
   return FrameEnded(GetFixedFrameEvent(evt));

  return true;
 }

 virtual bool FrameStarted(const FrameEvent& evt) {return true;}
 virtual bool FrameEnded(const FrameEvent& evt) {return true;}

 bool IsStarted() const {return isStarted;}

protected:
 FrameEvent GetFixedFrameEvent(const FrameEvent& evt)
 {
  FrameEvent fixed;
  fixed.timeSinceLastFrame = evt.timeSinceLastFrame>MAX_FRAME_TIME?
   MAX_FRAME_TIME:
   evt.timeSinceLastFrame;
  return fixed;
 }
 bool   isStarted;
};

#endif /* PERSISTENTFRAMELISTENER_H_ */

The CollisionManager is where all of the objects are tested against each other for collisions.

CollisionManager.h

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

#ifndef COLLISIONMANAGER_H_
#define COLLISIONMANAGER_H_

#include "PersistentFrameListener.h"
#include "CollisionObject.h"
#include "list"

#define COLLISIONMANAGER CollisionManager::Instance()

typedef std::list CollisionObjectList;

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

 void Startup();
 void Shutdown();

 void AddCollisionObject(CollisionObject* object);
 void RemoveCollisionObject(CollisionObject* object);
 
 bool FrameEnded(const FrameEvent& evt);

protected:
 CollisionManager();
 void AddNewObjects();
 void RemoveDeletedObjects();

 CollisionObjectList  collisionObjectList;
 CollisionObjectList  newObjects;
 CollisionObjectList  deletedObjects;
};

#endif

CollisionManager.cpp

#include "CollisionManager.h"

CollisionManager::CollisionManager()
{

}

CollisionManager::~CollisionManager()
{

}

void CollisionManager::Startup()
{
 PersistentFrameListener::Startup();
}

void CollisionManager::Shutdown()
{
 newObjects.clear();
 deletedObjects.clear();
 collisionObjectList.clear();
 PersistentFrameListener::Shutdown();
}

One of the reasons for creating the PersistentFrameListener class was to work around an issue in Ogre where FrameListeners could still have their event function called even after they were removed from the collection maintained by the OgreRoot object with the removeFrameListener function. The CollisionManager shows why this is the case.

In the FrameEnded function the CollisionManager loops through all the CollisionObjects checking for collisions. If a collision is found, the Collision function on the colliding CollisionObjects is called. As was noted earlier, one of the outcomes of a collision can be that one of the colliding objects is removed by calling its Shutdown function. Without a small workaround, this Shutdown function will remove the CollisionObject from the collection maintained by the CollisionManager – the very same one that we were looping over to detect the collision in the first place. This is a problem because if you modify a collection (by say removing an item from it) while looping over it the application would crash.

To avoid this issue all new and removed objects are stored in the temporary collections newObjects and deletedObjects (via the AddCollisionObject and RemoveCollisionObject functions), with the new and deleted objects being synced with the main collection (via the AddNewObjects  and RemoveDeletedObjects  functions) before we start looping over it in the FrameEnded function. This is the same process used with FrameListeners, and a side effect is that removed objects remain in the main collection after being “removed” and have their functions called.

By extending the PersistentFrameListener class and remaining in a pool in a deactivated state (which is what the enemies and weapons already do), objects that extend the CollisionObject class still exist and can have their functions called without crashing the system. The CollisionManager will only call the Collision function if the CollisionObject is active, ensuring that shut down objects do not participate in collisions.

The reason why the collision detection code is in the FrameEnded function is because we want all of our objects to have updated to their new positions before detecting collisions. Because it would be difficult to ensure that the CollisionManagers FrameStarted function was called before or after all of the other game objects, doing the collision detection in the FrameStarted function could lead to an inconsistent situation where half of the game objects updated themselves, the collision detection was calculated, and then the last half of other objects updated themselves.

void CollisionManager::AddCollisionObject(CollisionObject* object)
{
 newObjects.push_back(object);
}

void CollisionManager::RemoveCollisionObject(CollisionObject* object)
{
 deletedObjects.push_back(object);
}

void CollisionManager::AddNewObjects()
{
 for (CollisionObjectList::iterator iter = newObjects.begin();
  iter != newObjects.end(); ++iter)
 {
  collisionObjectList.push_back(*iter);
 }
 newObjects.clear();
}

void CollisionManager::RemoveDeletedObjects()
{
 for (CollisionObjectList::iterator iter = deletedObjects.begin();
  iter != deletedObjects.end(); ++iter)
 {
  collisionObjectList.remove(*iter);
 }
 deletedObjects.clear();
}

bool CollisionManager::FrameEnded(const FrameEvent& evt)
{
 AddNewObjects();
 RemoveDeletedObjects();
 
 for (CollisionObjectList::iterator iter1 = collisionObjectList.begin();
   iter1 != collisionObjectList.end(); ++iter1)
 {
  CollisionObjectList::iterator iter2 = iter1;
  ++iter2;
  
  for ( ; iter2 != collisionObjectList.end(); ++iter2)
  {
   CollisionObject* const object1 = *iter1;
   CollisionObject* const object2 = *iter2;

   if (object1->IsStarted() && object2->IsStarted())
   {
    const Sphere& object1Sphere = object1->GetBoundingSphere();
    const Sphere& object2Sphere = object2->GetBoundingSphere();

    if (object1Sphere.intersects(object2Sphere))
    {
     object1->Collision(object2);
     object2->Collision(object1);
    }
   }
  }
 }

 return true;
}

Classes like Player, Weapon and Enemy that extended the PersistentFrameListener class now extend the CollisionObject class. Below you can see how the Enemy class implements the Collision function.

void Enemy::Collision(CollisionObject* other)
{
 if (other->GetCollisionType() == PLAYER_WEAPON_CT)
 {
  Weapon* weapon = static_cast(other);
  this->shields -= weapon->GetDamage();

  if (this->shields GetCollisionType() == PLAYER_CT)
 {
  Shutdown();
 }
}

The BasicEnemy class implements the GetBoundingSphere function using the built in getWorldBoundingSphere function that is available on all Ogre MovableObjects (essentially all visual Ogre objects).

Sphere BasicEnemy::GetBoundingSphere()
{
 return this->mesh->getWorldBoundingSphere();
}

The changes for the Weapon, Bullet and Player classes are similar. You can take a look at the source code to see the specific changes made to these classes.

Main.cpp

The main function is updated to Startup and Shutdown the CollisionManager class.

#include "OgreEngineManager.h"
#include "WeaponDatabase.h"
#include "EnemyDatabase.h"
#include "GameLevel.h"
#include "CollisionManager.h"
#include "IrrKlangEngineManager.h"

#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"

INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
int main(int argc, char **argv)
#endif
{
 ENGINEMANAGER.AddNewResourceLocation(ResourceLocationDefinition("FileSystem", "../../media", "General"));
 ENGINEMANAGER.AddNewResourceLocation(ResourceLocationDefinition("Zip", "../../media/media.zip", "General"));
 
 if (ENGINEMANAGER.Startup(std::string("plugins.cfg"), std::string("ogre.cfg"), std::string("ogre.log")))
 {
  IRRKLANGENGINEMANAGER.Startup();
  ENEMYDATABASE.Startup();
  WEAPONDATABASE.Startup();
  COLLISIONMANAGER.Startup();
  GAMELEVEL.Startup("Level1.XML");  
  
  ENGINEMANAGER.StartRenderLoop();

  COLLISIONMANAGER.Shutdown();
  WEAPONDATABASE.Shutdown();
  ENEMYDATABASE.Shutdown();
  GAMELEVEL.Shutdown();   
  IRRKLANGENGINEMANAGER.Shutdown();
 }
  
 ENGINEMANAGER.Shutdown();
}

With these changes the enemies can be shot and the player and the enemies can also collide. At the moment the enemies and weapons are simply removed from the level, which is a little unsatisfying, but with the underlying mechanics in place it will be easy to add explosions effects and sounds.

mcasperson

Written by mcasperson

Rate this Article:

Rating: 3.0/5 (2 votes cast)

Add new comment

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

Comments

ogmack, over a year ago
Report comment

Tnx!! its all I need

kanesp, over a year ago
Report comment

thank you very much from Spain

Related Content