对于一个视频游戏软件,涉及到宇宙飞船、太空站,小型星是否存在碰撞的风险。先写一个继承类:
class GameObject{...};
class SpaceShip: public GameObject{...};
class SpaceStation: public GameObject{...};
class Asteroid: public GameObject{...};
写个函数来检测并处理对象碰撞:
void checkForCollision(GameObject& object1,GameObject& object2)
{
if(theyJustCollided(object1,object2))
processCollision(object1,object2);
else
...
}
当调用processCollision,你知道object1和object2彼此碰撞,但是你不知道这两个对象具体是什么物体,你只知道是GameObjects。如果碰撞只依据object1的动态类型决定,你可以令processCollision成为GameObjects的虚函数,然后调用object1.processCollision(object2)。如果依据object2的动态类型决定一样。但是如果同时两者的动态类型决定,这样是不行的。你需要某种函数,其行为视一个以上的对象的动态类型决定。C++没提供这样的函数。这种任务叫做“double dispatching”。更广泛的情况则称为“multiple dispatch”。
- 虚函数 + RTTI(运行时期类型辨别)
虚函数实现一个single dispatching:声明一个collide,并在子类中重载:
class GameObject
{
public:
virtual void collide(GameObject& otherObject) = 0;
...
};
class SpaceShip: public GameObject
{
public:
virtual void collide(GameObject& otherObject);
...
};
这里只写了SpaceShip,SpaceStation和Asteroid形式完全一样。
实现double dispatching最常见的方法就是就只是在虚函数中使用if-then-else:
class CollisionWithUnkownObject{
public:
CollisionWithUnkownObject(GameObject& whatWeHit);
...
};
void SpaceShip::collide(GameObject& otherObject)
{
const type_info& = typeid(otherObject);
if(objectType == typeid(SpaceShip))
{
SpaceShip& ss = static_cast<SpaceShip&> (otherObject);
process a SpaceShip-SpaceShip collision;
}
else if(objectType == typeid(SpaceStation))
{
SpaceStation& ss = static_cast<SpaceShip&> (otherObject);
process a SpaceShip-SpaceStation collision;
}
else if(objectType == typeid(Asteroid)))
{
Asteroid& ss = static_cast<SpaceShip&> (otherObject);
process a SpaceShip-Asteroid collision;
}
else
{
throw CollisionWithUnkownObject(otherObject);
}
}
这里我们只检查了otherObject的类型,另一个是*this。它的类型由虚函数体系决定。我们整处于SpaceShip的成员函数,所以*this肯定是一个SpaceShip对象,上述方法没维护性,不易扩展。
- 只是用虚函数
只是用虚函数来解决double dispatching。这个和RTTI这样的架构差不多。collide函数被声明为virtual,并被所有的派生类重新定义,此外,它还被每个类重载,每个重载处理一个派生类:
class SpaceShip;
class SpaceStation;
class Asteroid;
class GameObject
{
public:
virtual void collide(GameObject& otherObject) = 0;
virtual void collide(SpaceShip& otherObject) = 0;
virtual void collide(SpaceStation& otherObject) = 0;
virtual void collide(Asteroid& otherObject) = 0;
...
};
class SpaceShip: public GameObject
{
public:
virtual void collide(GameObject& otherObject);
virtual void collide(SpaceShip& otherObject);
virtual void collide(SpaceStation& otherObject);
virtual void collide(Asteroid& otherObject);
};
基本想法是,将double-dispatching以两个single dispatching实现出来,其一用来决定第一个个对象的动态类型,其二用来决定第二个对象的动态类型。和先前一样,第一个虚函数调用动作针对的是“接获GameObjects&参数”的collide,实现如下:
void SpaceShip::collide(GameObject& otherObject)
{
otherObject.collide(*this);
}
上述调用的将是参数SapceShip&类型的虚函数:
void SpaceShip::collide(SpaceShip& otherObject)
{
process a SpaceShip-SpaceShip collision;
}
void SpaceShip::collide(SpaceStation& otherObject)
{
process a SpaceShip-SpaceStation collision;
}
void SpaceShip::collide(Asteroid& otherObject)
{
process a SpaceShip-Asteroid collision;
}
但是这样的代码缺点是,和上述RTTI一样,一旦有新的class加入,代码必须修改。
- 自行仿真虚函数表格(virtual Function Tables)
我们可以利用函数指针数组(vtbl)来实现虚函数;有两人vtbl,编译器就不用执行那么一大串if-then-else运算,并得以在所有的虚函数调用端产生相同的代码,用以(1)决定正确的vtbl索引,(2)调用vtbl中索引位置内所指的函数。
void SpaceShip::hitSpaceShip(SpaceShip& otherObject)
{
process a SpaceShip-SpaceShip collision;
}
void SpaceShip::hitSpaceStation(SpaceStation& otherObject)
{
process a SpaceShip-SpaceStation collision;
}
void SpaceShip::hitAsteroid(Asteroid& otherObject);
{
process a SpaceShip-Asteroid collision;
}
在SpaceShip::collide函数内,我们需要一种方法,将参数otherObject的动态类型映射到某个member function指针,指向适当的碰撞处理函数,一种简单的做法产生一个关系型数组,只要获得class名称,便能导出适当的member function指针。直接使用这个数组来实现collide是有可能的,但如果加上一个中介函数lookup,会更容易。lookup获得一个GameObject并返回适当的member function指针:
class SpaceShip: public GameObject
{
private:
typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
static HitFunctionPtr lookup(const GameObject& whatWeHit);
...
};
void SpaceShip::collide(GameObject& otherObject)
{
HitFunctionPtr hfp = lookup(otherObject);
if(hfp)
(this->*hfp)(otherObject);
else
throw CollisionWithUnknowObject(otherObject);
}
lookup实现如下:
SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
static HitMap collisionMap;
HitMap::iterator mapEntry = collisionMap.find(typeid(whatWeHit).name());
if(mapEntry == collisionMap.end()) return 0;
return (*mapEntry).second;
}
- 将自行仿真的虚函数表格(Virtual Function Tables)初始化
现在面临collisionMap初始化的问题,或许可以这么做:
//一个不正确的做法
SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
static HitMap collisionMap;
collisionMap["SpaceShip"] = &hitSpaceShip;
collisionMap["SpaceStation"] = &hitSpaceStation;
collisionMap["Asteroid"] = &hitAsteroid;
...
}
但是这会在每次lookup被调用时“将member function指针”安插到collisionMap内,这是不必要的。此外上述做法也无法编译。
此时,我们只需要的一个方法,将“member function指针”放进collisionMap内一次就好——在collisionMap诞生时刻。wo们只需要编写一个private static member function的名为initializeCollisionMap,用以产生并初始化我们的map,然后initializeCollisionMap的返回collisionMap的的初值即可:
class SpaceShip: public GameObject
{
private:
static HitMap initializeCollisionMap();
...
};
SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
static HitMap collisionMap = initializeCollisionMap();
...
}
我们可以使用智能指针来代替initializeCollisionMap返回的值,这样避免了返回对象的开销,返回指针的泄露问题:
class SpaceShip: public GameObject
{
private:
static HitMap* initializeCollisionMap();
...
};
SpaceShip::HitFunctionPtr
SpaceShip::lookup(const GameObject& whatWeHit)
{
static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
...
}
最明直接而明显initializeCollisionMap实现方法是这样:
SpaceShip::HitMap* SpaceShip::initializeCollisionMap()
{
HitMap* phm = new HitMap;
(*phm)["SpaceShip"] = &hitSpaceShip;
(*phm)["SpaceStation"] = &hitSpaceStation;
(*phm)["Asteroid"] = &hitAsteroid;
return phm;
}
上述无法通过编译,因为函数指针的形参数不一样。为了通过编译,你可一用reinterpret_cast来“欺骗”编译器,通常在两个函数指针之间做转型的一个选择:
SpaceShip::HitMap* SpaceShip::initializeCollisionMap()
{
HitMap* phm = new HitMap;
(*phm)["SpaceShip"] = reinterpret_cast<HitFunctionPtr> (&hitSpaceShip);
(*phm)["SpaceStation"] = reinterpret_cast<HitFunctionPtr>(&hitSpaceStation);
(*phm)["Asteroid"] = reinterpret_cast<HitFunctionPtr>(&hitAsteroid);
return phm;
}
上述转型,如果在那些函数中GamesObjects derived classes运用了多重继承或者拥有虚拟基类,会导致问题(unnatural polymorphism)。
现在只有一个办法解决冲突,将上述函数指针的参数统一为GameObject:
class GameObject
{
public:
virtual void collide(GameObject& otherObject) = 0;
...
};
class SpaceShip:public GameObject
{
public:
virtual void collide(GameObject& otherObject);
virtual void hitSpaceShip(GameObject& spaceShip);
virtual void hitSpaceStation(GameObject& spaceShip);
virtual void hitAsteroid(GameObject& spaceShip);
};
现在可以用希望的initializeCollisionMap形式了:
SpaceShip::HitMap* SpaceShip::initializeCollisionMap()
{
HitMap* phm = new HitMap;
(*phm)["SpaceShip"] = &hitSpaceShip;
(*phm)["SpaceStation"] = &hitSpaceStation;
(*phm)["Asteroid"] = &hitAsteroid;
return phm;
}
很可惜的是,我们将撞击的参数统一了,但是非它们所期望的精确的derived class参数,为了做到这个,我们必须在函数中使用dynamic_cast:
void SpaceShip::hitSpaceShip(GameObject& spaceShip)
{
SpaceShip& otherShip = dynamic_cast<SpaceShip&> (spaceShip);
process a SpaceShip-SpaceShip collision;
}
void SpaceShip::hitSpaceStation(GameObject& spaceShip)
{
SpaceStation& otherShip = dynamic_cast<SpaceStation&> (spaceShip);
process a SpaceShip-SpaceStation collision;
}
void SpaceShip::hitAsteroid(GameObject& spaceShip);
{
Asteroid& otherShip = dynamic_cast<Asteroid&> (spaceShip);
process a SpaceShip-Asteroid collision;
}
- 使用“非成员(non-Member)函数”的碰撞处理函数
上述做法使用,数组指针指向的member function,所以有新的GameObject类型加入这个游戏,我们就要修改class的定义,这和单纯使用虚函数来解决double-dispatching一个道理,导致重新编译问题。如果关系数组内含的指针指向的是non-member functions,那么就可以消除这个问题,还可以解决一个设计的问题,如果两个不同的物体相撞,到底哪一个class应该负责处理?
·如果将碰撞处理函数移除classes之外,processCollision函数如下:
#include "SpaceShip.h"
#include "SpaceStation.h"
#include "Asteroid.h"
namespace
{
//主要的碰撞处理函数
void shipAsteroid(GameObject& spaceShip,GameObject& asteroid);
void shipStation(GameObject& spaceShip,GameObject& spaceStaion);
void asteroidStation(GameObject& asteroid,GameObject& spaceStaion);
...
//次要的碰撞处理函数
//只是为了实现对称性,对调参数为主,然后调用主要的碰撞处理函数
void asteroidShip(GameObject& asteroid,GameObject& spaceShip)
{
shipAsteroid(spaceShip,asteroid);
}
void stationShip(GameObject& spaceStaion,GameObject& spaceShip)
{
shipStation(spaceShip,spaceStaion);
}
void stationAsteroid(GameObject& spaceStaion,GameObject& asteroid)
{
asteroidStation(asteroid,spaceStaion);
}
...
typedef void (*HitFunctionPtr)(GameObject&,GameObject&);
typedef map< pair<string,string>,HitFunctionPtr> HitMap;
pair<string,string> makeStringPair(const char* s1,const char* s2);
HitMap* initializeCollisionMap();
HitFunctionPtr lookup(const string& class1,const string& class2);
}
void processCollision(GameObject& object1,GameObject& object2)
{
HitFunctionPtr phf = lookup(typeid(object1).name(),typeid(object2).name());
if(phf) phf(object1,object2);
else throw UnknowCollision(object1,object2);
}
initializeCollisionMap和makeStringPair还有lookup实现如下:
namespace
{
pair<string,string> makeStringPair(const char* s1,const char* s2)
{
return pair<string,string>(s1,s2);
}
}
namespace
{
HitMap* initializeCollisionMap()
{
HitMap* phm = new HitMap;
(*phm)[makeStringPair("SpaceShip","Asteroid")]
= &shipAsteroid;
(*phm)[makeStringPair("SpaceShip","SpaceStation")]
= &shipStation;
...
return phm;
}
}
namespace
{
HitFunctionPtr lookup(const string& class1,const string& class2)
{
static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
HitMap::iterator mapEntry =
collisionMap->find(make_pair(class1,class2));
if(mapEntry == collisionMap.end()) return 0;
return (*mapEntry).second;
}
}
- 将自行仿真的虚函数表格初始化(再度讨论)
我们希望能够对撞击函数做新增、移除、修改等动作。我们可以利用一个map来存储撞击处理函数,放进某个class内,该class提供一些member functions,让我们动态修改map的内容:
class CollisionMap
{
public:
typedef void (*HitFunctionPtr)(GameObject&,GameObject&);
void addEntry(const string& type1,
const string& type2,
HitFunctionPtr collisionFunction,
bool symmtric = true);
void remove(const string& type1,const string& type2);
HitFunctionPtr lookup(const string& type1,const string& type2);
//仅仅只有一个map
static CollisionMap& theCollisionMap();
private:
//放置产生多个maps
CollisionMap();
CollisionMap(const CollisionMap&);
};
有了这个CollisionMap class,用户可以这样直接的伪map加上一个条目:
void shipAsteroid(GameObject& spaceShip,GameObject& asteroid);
CollisionMap::theCollisionMap().addEntry("SpaceShip","Asteroid",
&shipAsteroid);
void shipStation(GameObject& spaceShip,GameObject& spaceStaion);
CollisionMap::theCollisionMap().addEntry("SpaceShip","spaceStaion",
&shipStation);
void asteroidStation(GameObject& asteroid,GameObject& spaceStaion);
CollisionMap::theCollisionMap().addEntry("asteroid","spaceStaion",
&asteroidStation);
确保map在对应的任何碰撞发生之前就加入map之中。办法之一就是令GameObjects subclasses的constructors加以检查,看对象产生之际已有适当的map条目加入,还有一种做法就是产生一个人RegisterCollisionFunction class:
class RegisterCollisionFunction
{
public:
RegisterCollisionFunction(
const string& type1,
const string& type2,
CollisionMap::HitFunctionPtr collisionFunction,
bool symmetric = true)
{
CollisionMap.theCollisionMap.addEntry(type1,type2,
collisionFunction,
symmetric);
}
};
client可以利用这种类型的全局对象来自动注册它们所需的函数:
RegisterCollisionFunction cf1("SpaceShip","Asteroid",&shipAsteroid);
RegisterCollisionFunction cf2("SpaceShip","SpaceStation",&shipStation);
RegisterCollisionFunction cf3("Asteroid","SpaceStation",&asteroidStation);
...
int main()
{
...
}
由于这些全局对象都在main之前产生的,它们的constructors所注册的函数也会在main被调用前加入map。如果有加入一个新的derived class:
class Satellite: public GameObject{...};
并写一个或多个新的碰撞处理函数:
void satelliteShip(GameObject& satellite,GameObject& spaceShip);
void satelliteAsteroid(GameObject& satellite,GameObject& asteroid);
这些新函数可以类似方法加入到map之中,不需要改动原有的代码:
RegisterCollisionFunction("Satellite","SpaceShip",&satelliteShip);
RegisterCollisionFunction("Satellite","Asteroid",&satelliteAsteroid);