问题来源:假设正在编写一个小游戏,游戏的背景是发生在太空,有宇宙飞船、太空船和小行星,它们可能会互相碰撞,而且其碰撞的规则不同,如何用C++代码处理物体间的碰撞。代码的框架如下:
1 class GameObject{...};
2 class SpaceShip:public GameObject{...};
3 class SpaceStation:public GameObject{...};
4 class Asteroid:public GameObject{...};
5
6 void checkForCollision(GameObject& obj1,GameObject& obj2)
7 {
8 if(theyJustCollided(obj1,obj2))
9 {
10 processCollision(obj1,obj2);
11 }
12 else
13 {
14 ...
15 }
16 }
正如上述代码所示,当调用processCollision()时,obj1和obj2的碰撞结果取决于obj1和obj2的真实类型,但我们只知道它们是GameObject对象。相当于我们需要一种作用在多个对象上的虚函数。这类型问题,在C++中被称为二重调度问题,下面介绍几种方法解决二重调度问题。
1.虚函数加RTTI
虚函数实现了一个单一调度,我们只需要实现另一调度。其具体实现方法:将processCollision()定义为虚函数,解决一重调度,然后只需要检测一个对象类型,利用RTTI来检测对象的类型,再利用if...else语句来调用不同的处理方法。具体实现如下:
1 class GameObject{
2 public:
3 virtual void collide(GameObject& otherObject) = 0;
4 ...
5 };
6 class SpaceShip:public GameObject{
7 public:
8 virtual void collide(GameObject& otherObject);
9 ...
10 };
11
12 class CollisionWithUnknownObject{
13 public:
14 CollisionWithUnknownObject(GameObject& whatWehit);
15 ...
16 };
17 void SpaceShip::collide(GameObject& otherObject)
18 {
19 const type_info& objectType = typeid(otherObject);
20 if(objectType == typeid(SpaceShip))
21 {
22 SpaceShip& ss = static_cast<SpaceShip&>(otherObject);
23 process a SpaceShip-SpaceShip collision;
24 }
25 else if(objectType == typeid(SpaceStation))
26 {
27 SpaceStation& ss = static_cast<SpaceStation&>(otherObject);
28 process a SpaceShip-SpaceStation collision;
29 }
30 else if(objectType == typeid(Asteroid))
31 {
32 Asteroid& a = static_cast<Asteriod&>(otherObject);
33 process a SpaceShip-Asteroid collision;
34 }
35 else
36 {
37 throw CollisionWithUnknownObject(otherObject);
38 }
39 }
该方法的实现简单,容易理解,其缺点是其扩展性不好。如果增加一个新的类时,我们必须更新每一个基于RTTI的if...else链以处理这个新的类型。
2.只使用虚函数
基本原理就是用两个单一调度实现二重调度,也就是有两个单单独的虚函数调用:第一次决定第一个对象的动态类型,第二次决定第二个对象动态类型。其具体实现如下:
1 class SpaceShip;
2 class SpaceStation;
3 class Asteroid;
4 class GameObject{
5 public:
6 virtual void collide(GameObject& otherObject) = 0;
7 virtual void collide(SpaceShip& otherObject) = 0;
8 virtual void collide(SpaceStation& otherObject) = 0;
9 virtual void collide(Asteroid& otherObject) = 0;
10 ...
11 };
12 class SpaceShip:public GameObject{
13 public:
14 virtual void collide(GameObject& otherObject);
15 virtual void collide(SpaceShip& otherObject);
16 virtual void collide(SpaceStation& otherObject);
17 virtual void collide(Asteroid& otherObject);
18 ...
19 };
20
21 void SpaceShip::collide(GameObject& otherObject)
22 {
23 otherObject.collide(*this);
24 }
25 void SpaceShip::collide(SpaceShip& otherObject)
26 {
27 process a SpaceShip-SpaceShip collision;
28 }
29 void SpaceShip::collide(SpaceStation& otherObject)
30 {
31 process a SpaceShip-SpaceStation collision;
32 }
33 void SpaceShip::collide(Asteroid& otherObject)
34 {
35 process a SpaceShip-Asteroid collision;
36 }
与前面RTTI方法一样,该方法的缺点扩展性不好。每个类都必须知道它的同胞类,当增加新类时,所有的代码都必须更新。
3.模拟虚函数表
编译器通常创建一个函数指针数组(vtbl)来实现虚函数,并在虚函数被调用时在这个数组中进行下标索引。我们可以借鉴编译器虚拟函数表的方法,建立一个对象到碰撞函数指针的映射,然后在这个映射中利用对象进行查询,获取对应的碰撞函数指针,进行函数调用。具体代码实现如下:
1 namespace{
2 void shipAsteroid(GameObject& spaceShip,GameObject& asteroid);
3 void shipStation(GameObject& spaceShip,GameObject& spaceStation);
4 void asteroidStation(GameObject& asteroid,GameObject& spaceStation);
5 ...
6 //implement symmetry
7 void asteroidShip(GameObject& asteroid,GameObject& spaceShip)
8 { shipAsteroid(spaceShip,asteroid);}
9 void stationShip(GameObject& spaceStation,GameObject& spaceShip)
10 { shipStation(spaceShip,spaceStation);}
11 void stationAsteroid(GameObject& spaceStation,GameObject& asteroid)
12 { asteroidStation(asteroid,spaceStation);}
13
14 typedef void(*HitFunctionPtr)(GameObject&,GameObject&);
15 typedef map<pair<string,string>,HitFunctionPtr> HitMap;
16 pair<string,string> makeStringPair(const char *s1,const char *s2);
17
18 HitMap* initializeCollisionMap();
19 HitFunctionPtr lookup(const string& class1,const string& class2);
20 }
21
22 void processCollision(GameObject& obj1,GameObject& obj2)
23 {
24 HitFunctionPtr phf = lookup(typeid(obj1).name(),typeid(obj2).name());
25 if(phf)
26 phf(obj1,obj2);
27 else
28 throw UnknownCollision(obj1,obj2);
29 }
30
31 namespace{
32 pair<string,string> makeStringPair(const char *s1,const char *s2)
33 {
34 return pair<string,string>(s1,s2);
35 }
36
37 HitMap* initializeCollisionMap()
38 {
39 HitMap *phm = new HitMap;
40 (*phm)[makeStringPair("SpaceShip","Asteroid")] = &shipAsteroid;
41 (*phm)[makeStringPair("SpaceShip","SpaceStation")] = &shipStation;
42 ...
43 return phm;
44 }
45
46 HitFunctionPtr lookup(const string& class1,const string& class2)
47 {
48 static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
49 HitMap::iterator mapEntry = collisionMap->find(make_pair(class1,class2));
50 if(mapEntry == collisionMap->end())
51 return 0;
52 return (*mapEntry).second;
53 }
54 }
如上述代码所示,使用非成员函数来处理碰撞过程,根据obj1和obj2来查询初始化之后映射表,来确定对应的非成员函数指针。利用模拟虚函数表的方法,基本上完成了基于多个对象的虚拟化功能。但是为了更方便的使用代码,更方便的维护代码,我们还需要进一步完善其实现过程。
4.将映射表和注册映射表过程封装起来
由于具体应用的过程,映射表的映射关系存在着增加和删除的操作,因而需要把映射表封装类体,提供增加,删除等接口。具体实现如下:
1 class CollisionMap{
2 public:
3 typedef void (*HitFunctionPtr)(GameObject&,GameObject&);
4 void addEntry(const string& type1,const string& type2,HitFunctionPtr collisionFunction,bool symmetric = true);
5 void removeEntry(const string& type1,const string& type2);
6 HitFunctionPtr lookup(const string& type1,const string& type2);
7
8 static CollisionMap& theCollisinMap();
9 private:
10 CollisionMap();
11 CollisinMap(const CollisionMap&);
12 };
在应用中,我们必须确保在发生碰撞前将映射关系加入了映射表。一个方法是让GameObject的子类在构造函数中进行确认,这将导致在运行期的性能开销,另外一个方法创建一个RegisterCollisionFunction类,用于完成映射关系的注册工作。RegisterCollisionFunction相应的代码如下:
1 class RegisterCollisionFunction{
2 public:
3 RegisterCollisionFunction(const string& type1,const string& type2,CollisionMap::HitFunctionPtr collisionFunction,bool symmetric = true)
4 {
5 CollisionMap::theCollisionMap().addEntry(type1,type2,collisionFunction,symmetric);
6 }
7 };
8 //利用此类型的全局对象来自动地注册映射关系
9 RegisterCollisionFunction cf1("SpaceShip","Asteroid",&shipAsteroid);
10 ...
参考资料:More Effective C++
原文链接: https://www.cnblogs.com/dwdxdy/archive/2012/08/23/2652875.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/60683
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!