More Effective C++: 05技术(30-31)

30:Proxy classes 代理类

         在C++中使用变量作为数组大小是违法的,也不允许在堆上分配多维数组:

int data[dim1][dim2]; 
int *data = new int[dim1][dim2]; // error!

         为了弥补上述缺点,可以设计一个二维数组类:

template<class T>
class Array2D {
public:
    Array2D(int dim1, int dim2);
    ...
};

Array2D<int> data(10, 20);
Array2D<float> *data = new Array2D<float>(10, 20);
void processInput(int dim1, int dim2)
{
    Array2D<int> data(dim1, dim2);
    ...
}

         因为没有operator[][]这样的操作符,为了能以data[3][6]的形式访问该二维数组,所以,这里需要使用proxy类:

template<class T>
class Array2D  {
public:
    Array2D(int dim1, int dim2){
        m_dim1 = dim1;
        m_dim2 = dim2;
        data = new T[dim1*dim2];
    }
    ~Array2D(){delete data;}
    class Array1D  {
    public:
        Array1D(T *data, int begin){
            innerdata = data+begin;
        }
        T& operator[](int index);
        const T& operator[](int index) const;
    private:
        T *innerdata;
    };
    Array1D operator[](int index);
    const Array1D operator[](int index) const;
private:
    T *data;
    int m_dim1, m_dim2;
};

template<class T>
typename Array2D<T>::Array1D Array2D<T>::operator[](int index){
    return Array2D::Array1D(data, index * m_dim2);
}

template<class T>
const typename Array2D<T>::Array1D Array2D<T>::operator[](int index) const{
    return Array2D::Array1D(data, index * m_dim2);
}

template<class T>
T& Array2D<T>::Array1D::operator[](int index){
    return innerdata[index];
}

template<class T>
const T& Array2D<T>::Array1D::operator[](int index) const{
    return innerdata[index];
}

int dim1 = 3, dim2 = 4;
Array2D<int> array(dim1, dim2);
array[0][1] = 1;
array[0][2] = 2;
array[1][0] = 10;
array[2][3] = 23;

         每个Array1D对象表示一个一维数组,而        Array2D的用户不需要知道Array1D的存在。注意上述模板内定义嵌套类的函数定义写法。

         利用proxy类,还可以实现区分operator[]读写操作。上一章讲述的引用计数的String类,其operator[]可以用来读字符,也可以用来写字符。读操作是所谓的右值引用;写操作是左值引用。虽然编译器无法告诉我们operator[]到底是用来读还是写,但是只要将所要处理的动作放缓,直到operator[]的返回结果被使用为止。可以修改operator[],使其返回字符串中字符的proxy,就可以看见该proxy如何被使用:

class String {
public:
    String(const char *value = "");
    
    class CharProxy {
    public:
        CharProxy(String& str, int index);
        CharProxy& operator=(const CharProxy& rhs);   //  左值引用
        CharProxy& operator=(char c);
        operator char() const; //  右值引用
    private:
        String& theString;
        int charIndex;
    };
    const CharProxy operator[](int index) const;
    CharProxy operator[](int index);

friend class CharProxy;

private:
    struct StringValue: public RCObject  {
        char *data;
        StringValue(const char *initValue);
        StringValue(const StringValue& rhs);
        void init(const char *initValue);
        ~StringValue();
    };

    RCPtr<StringValue> value;
};

         有了上面的代码,考虑这条语句:cout<<s1[5],s1[5]产生一个CharProxy对象,但是该对象没有定义output操作符,所以编译器需要找一个合适的隐式类型转换:将CharProxy隐式转换为char,然后进行输出即可。

         而对于s1[5]=’x’,s1[5]返回CharProxy,调用CharProxy::operator=函数即可,这时,被赋值的CharProxy对象被用来作为一个左值。同样的,s1[5]=s1[8]也是一样。

         String的operator[]代码如下:

const String::CharProxy String::operator[](int index) const{
    return CharProxy(const_cast<String&>(*this), index);
}
String::CharProxy String::operator[](int index){
    return CharProxy(*this, index);
}

注意const operator[]返回一个const CharProxy,由于CharProxy::operator=不是一个 const成员函数,const CharProxy因此不能被用来做为赋值的目标物。因此不论是const operator[]  所传回的 proxy,或是该proxy所代表的字符,都不能被用来做为左值。这正是我们希望 const operator[]所具备的行为。

另外,当const operator[] 返回CharProxy对象时,需要对其进行const_cast转换,去除const属性,这是因为CharProxy的构造函数只接受non-const String。

operator[]返回的每一个CharProxy都会记住它所附属的字符串,以及它所在的索引位置,以便将来进行读取或赋值:

String::CharProxy::CharProxy(String& str, int index)
: theString(str), charIndex(index) {}

         CharProxy的char转换函数如下:

String::CharProxy::operator char() const{
    return theString.value->data[charIndex];
}

       该函数以by value的方式返回一个字符,由于C++ 限制只能在右值情境下使用这样的返回值,所以这个转换函数只能用于右值合法之处。

         接下来是CharProxy的operator=操作符:

String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs){
    if (theString.value->isShared()) {
        theString.value = new StringValue(theString.value->data);
    }

    theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex];
    return *this;
}

String::CharProxy& String::CharProxy::operator=(char c){
    if (theString.value->isShared()) {
        theString.value = new StringValue(theString.value->data);
    }
    theString.value->data[charIndex] = c;
    return *this;
}

         与上一章中的non-const String::operator[]比较,会发现它们非常类似。在上一章中悲观地假设所有non-const operator[]的调用都是为了写操作,此处们把完成“写操作”的代码转移到CharProxy的operator=中,避免了non-const operator[]在右值情境下也需付出写操作的昂贵代价。

         尽管我们希望proxy对象能够无间隙的代表它所表示的对象,但是这种想法很难达成,因为除了赋值之外,对象还有很多其他操作,比如:char *p = &s1[5],这条语句有两个报错,s1[5]返回一个临时CharProxy对象,既不能取临时对象的地址,也无法将CharProxy*赋值给char *,这种情况下,需要重载operator&:

const char * String::CharProxy::operator&() const{
    return &(theString.value->data[charIndex]); 
}

char * String::CharProxy::operator&(){
    if (theString.value->isShared()) {
        theString.value = new StringValue(theString.value->data);
    }

    theString.value->markUnshareable();
    return &(theString.value->data[charIndex]);
}

         non-const版本需要返回一个指针,指向一个可能被修改的字符,因此,需要将StringValue成为其专属副本。

         这还仅仅是取地址,原始对象可能还支持+=,++等操作,这些需要左值的操作想要成功,必须一一为proxy类定义相应的操作符(CharProxy转换为char的操作符,返回的是右值,因此无法使用这些操作符)。

         另外,如果proxy代表的对象具有成员函数,则为了能使proxy看起来像原始对象一样能调用相应的成员函数,也需要在proxy中定义相应的函数。

         另外,proxy虽然能隐式转换为它所代表的对象,但是这种隐式转换只能发生一次,如果原始对象A能隐式转换为B,则在需要参数B的函数调用中可以使用A,但不能使用A的proxy。

31:让函数根据一个以上的对象类型来决定如何虚化

         假设你在写一个游戏,游戏中需要处理宇宙飞船、太空站、小行星等的碰撞问题,不同物体之间的碰撞需要做不同的处理。于是有下面的代码:

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的具体类型做不同的处理。这就是所谓的double-dispatching问题。在面向对象程序设计社区,人们把一个“虚函数调用动作”称为一个"message dispatch"。因此某个函数调用如果根据两个参数而虚化,自然而然地就被称为   "double dispatch"。更广泛的情况则被称为multiple dispatch。

        

         最一般化的double-dispatching实现,就是利用虚函数和RTTI:

class GameObject {
public:
    virtual void collide(GameObject& otherObject) = 0;
    ...
};
class SpaceShip: public GameObject {
public:
    virtual void collide(GameObject& otherObject);
    ...
};

class CollisionWithUnknownObject {
public:
    CollisionWithUnknownObject(GameObject& whatWeHit);
    ...
};
void SpaceShip::collide(GameObject& otherObject){
    const type_info& objectType = 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<SpaceStation&>(otherObject);
        process a SpaceShip-SpaceStation collision;
    }
    else if (objectType == typeid(Asteroid)) {
        Asteroid& a = static_cast<Asteroid&>(otherObject);
        process a SpaceShip-Asteroid collision;
    }
    else {
        throw CollisionWithUnknownObject(otherObject);
    }
}

         上面这段代码的缺点,collide函数必须知道其每一个兄弟类(所有继承自GameObject的那些类)。如果有新的类型加入了这个游戏,就必须修改程序中每一个可能遭遇新对象的RTTI-based if-then-else链,这会造成程序难以维护。

         还可以只用虚函数就解决这个问题:

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);
    ...
};
void SpaceShip::collide(GameObject& otherObject){
    otherObject.collide(*this);
}
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;
}

         比较有意思的是接收GameObject&的那个collide函数,当对象的静态类型是GameObject时调用该函数,函数内部,根据参数的动态类型决定调用该类型的哪个虚函数,而函数参数为*this,也就是个SpaceShip类型。

         这种做法的缺点跟上面使用RTTI的类似,每个类都必须知道其兄弟类,一旦有新的类加入,代码就必须修改。而这里的修改又与RTTI解法不同,RTTI解法中,只需要修改每个类的实现部分,也就是collide中的if-else-then,而此处需要修改类的定义,增加一个新的虚函数。然而你并不一定有机会或者权利去修改类的定义式。

         简而言之,如果你需要在你的程序中实现double-dispatching,最好的方向就是修改设计,消除此项需求。如果不能,那么,虚拟函式法比RTTI 法安全一些,但是如果你对头文件的权力不够,这种作法会束缚你的系统扩充性。至于RTTI法,虽不需要重新编译,却往往导至软件难以维护。你总是得付出代价,才能获得机会。

         之前说过,虚函数是通过vtbl实现的。在这个double- dispatching场景中,我们可以自己实现一个vtbl,这比RTTI-based会更有效率,而且可以将RTTI的使用集中在vtbl初始化的地方。

class GameObject {
public:
    virtual void collide(GameObject& otherObject) = 0;
    ...
};

class SpaceShip: public GameObject {
public:
    virtual void collide(GameObject& otherObject);
    virtual void hitSpaceShip(SpaceShip& otherObject);
    virtual void hitSpaceStation(SpaceStation& otherObject);
    virtual void hitAsteroid(Asteroid& otherobject);
    ...
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 CollisionWithUnknownObject(otherObject);
    }
}

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;
}

         这里没有将collide重载,而是用函数名区分不同对象之间的碰撞,原因稍后就会解释。

         在SpaceShip::collide函数内,调用lookup函数,根据GameObject参数查找合适的成员函数指针,一旦找到则调用即可,找不到的时候抛出异常。

         现在考虑lookup函数的实现,在lookup中,需要一个关系型数组,lookup查找该数组,根据对象类型得到某个成员函数指针。这个关系型数组应该在使用之前就产生并初始化了,并在不再需要时进行销毁,可以使用new和delete来产生和销毁数组,但那样容易发生错误。为了保证数组会在使用之前进行初始化,比较好的解决办法就是让关系型数组成为 lookup内的static对象,只有在lookup第一次调用时它才会被产生,而在main结束之后它才会被摧毁:

class SpaceShip: public GameObject {
private:
    typedef map<string, HitFunctionPtr> HitMap;
    ...
};

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;
}

         这里根据typeid(whatWeHit).name()查找collisionMap,但是标准并未明确规定type_info::name的返回值,不同的编译器可能会有不同的行为,比如对于SpaceShip类,有的编译器可能就会返回"class SpaceShip"。一个更好的设计是,以type_info对象的地址来识别class,因为它绝对是独一无二的,此时HitMap类型应该是map<const type_info*, HitFunctionPtr>。

         现在,唯一的问题就是collisionMap的初始化问题了,可以将其初始化放进一个名为initializeCollisionMap的private static成员函数中:

SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
{
    static HitMap collisionMap = initializeCollisionMap();
    ...
}

         但是这种方法会有map的复制成本,因此,这里最高改为指针,为了不操心delete指针的问题,可以使用智能指针:

SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit){
    static unique_ptr<HitMap> collisionMap(initializeCollisionMap());
    ...
}

SpaceShip::HitMap * SpaceShip::initializeCollisionMap(){
    HitMap *phm = new HitMap;
    (*phm)["SpaceShip"] = &hitSpaceShip;
    (*phm)["SpaceStation"] = &hitSpaceStation;
    (*phm)["Asteroid"] = &hitAsteroid;
    return phm;
}

         这个初始化代码还有最后一个问题,map中的value类型是:typedef void (SpaceShip::*HitFunctionPtr)(GameObject&),这种函数的参数为GameObject,但是hitSpaceShip、hitSpaceStation、hitAsteroid它们的参数分别为 SpaceShip、SpaceStation、Asteroid。虽然它们都可以隐式转换为GameObject,但是函数指针之前却不存在这种转换。

         你可能觉得下面的方法可以解决这个问题:

//  一个坏主意…
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;
}

         这是个坏主意,因为它欺骗了编译器,一旦满足某种条件,编译器就会对这种欺骗进行报复:如果SpaceStation、SpaceShip或Asteroid有GameObject之外的其他基类,你可能会发现,你在collide中对碰撞处理函式的呼叫,会导至相当粗鲁的行为。比如下面是一个具有菱形继承的类D:

 

D对象内的四个“基类成份”,每一个都有不同地址。虽然指针和引用的行为不同,但编译器通常是以指针来实现引用的。因此,当对象拥有多个基类,并以引用的方式传递给函数时,编译器是否传递了正确的地址(此地址对应于被调用函数的参数类型),将是非常重要的关键。

如果你欺骗编译器,告诉它你的函数期望获得一个GameObject,而其实它真正期望获得的是个SpaceShip,当你调用那个函数,编译器就会传递错误的地址,导至执行时期可怕的大屠杀。这种问题很难找出原因。转型令人沮丧,原因有许多个,这是其中之一。

因此,只能改变hitSpaceShip这些成员函数的原型,使他们接受GameObject对象:

class SpaceShip: public GameObject {
public:
    virtual void collide(GameObject& otherObject);
    virtual void hitSpaceShip(GameObject&  spaceShip);
    virtual void hitSpaceStation(GameObject&  spaceStation);
    virtual void hitAsteroid(GameObject&  asteroid);
    ...
};

这就是为什么没有重载collide函数的原因。因为参数都一样了。

下面是剩下的代码:

SpaceShip::HitMap * SpaceShip::initializeCollisionMap(){
    HitMap *phm = new HitMap;
    (*phm)["SpaceShip"] = &hitSpaceShip;
    (*phm)["SpaceStation"] = &hitSpaceStation;
    (*phm)["Asteroid"] = &hitAsteroid;
    return phm;
}

void SpaceShip::hitSpaceShip(GameObject&  spaceShip){
    SpaceShip& otherShip=dynamic_cast<SpaceShip&>(spaceShip);
    process a SpaceShip-SpaceShip collision;
}

void SpaceShip::hitSpaceStation(GameObject&  spaceStation){
    SpaceStation& station=dynamic_cast<SpaceStation&>(spaceStation);
    process a SpaceShip-SpaceStation collision;
}

void SpaceShip::hitAsteroid(GameObject&  asteroid){
    Asteroid& theAsteroid = dynamic_cast<Asteroid&>(asteroid);
    process a SpaceShip-Asteroid collision;
}

在hitSpaceShip这样的函数中,需要将参数对象使用dynamic_cast强制转换为真正的类型。

实际上,上面这种自行实现vtbl的解法依然有类似的问题,因为针对每一个兄弟类,都需要有一个成员函数处理碰撞。一旦有新的GameObject类型加入到这个游戏中,还是需要修改类的定义。

如果关系型数组内含的指针指向的是non-member functions,重新编译的问题便可消除:

#include "SpaceShip.h"
#include "SpaceStation.h"
#include "Asteroid.h"
namespace  {
    void shipAsteroid(GameObject& spaceShip, GameObject& asteroid);
    void shipStation(GameObject& spaceShip, GameObject& spaceStation);
    void asteroidStation(GameObject& asteroid, GameObject& spaceStation);

    void asteroidShip(GameObject& asteroid, GameObject& spaceShip)
    { shipAsteroid(spaceShip, asteroid); }
    void stationShip(GameObject& spaceStation, GameObject& spaceShip)
    { shipStation(spaceShip, spaceStation); }
    void stationAsteroid(GameObject& spaceStation, GameObject& asteroid)
    { asteroidStation(asteroid, spaceStation); }

    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 UnknownCollision(object1, object2);
}

这里使用了匿名namespace,匿名namespace内的所有东西对其所在的编译单元而言都是私有的,也就是说,其效果好像是在文件中将函数声明为static一样,更推荐使用匿名namespace。

剩下的代码如下:

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 unique_ptr<HitMap> collisionMap(initializeCollisionMap());

        HitMap::iterator mapEntry = collisionMap->find(make_pair(class1, class2));
        if (mapEntry == collisionMap->end()) return 0;
        return (*mapEntry).second;
    }
}

现在,一旦有新的GameObject子类加进这个继承体系中,原有的类不再需要重新编译,也不需维护纠葛混乱的“以 RTTI 为基础的”switch 或 if-then-else。如果新的类加入到GameObject的继承体系中,只要其本身定义良好;我们的系统只需在initializeCollisionMap 内调整代码,并在“与processCollision相应的那个匿名namespace”内增加新碰撞处理函数。

还有一个问题,如果需要满足inheritance-based类型转换,比如现在宇宙飞船需要区分商业宇宙飞船和军事宇宙飞船:SpaceShip现在派生出了CommercialShip和MilitaryShip,而他们与原有对象的碰撞处理与SpaceShip完全相同,因此如果有一个MilitaryShip和一个Asteroid碰撞,我们希望调用的是void shipAsteroid(GameObject& spaceShip, GameObject& asteroid)。但是就目前的代码而言,这种情况下实际上会抛出一个UnknownCollision异常,因为lookup中会根据”MilitaryShip”和”Asteroid”寻找对应的函数,而map中没有这样的函数。

没有什么简单的办法解决这个问题,如果需要实现double-dispatching,而且需要支持  inheritance-based 参数转换,只能使用最早介绍的“双虚拟函数调用”机制。

以上的代码中,collisionMap是静态的,一旦初始化便不再改动,如果需要动态处理,也就是能增加、删除、修改collisionMap中内容,则需要重新设计一个CollisionMap类:

class CollisionMap {
public:
    typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
    void addEntry(const string& type1, const string& type2, 
                HitFunctionPtr collisionFunction, bool symmetric = true);

    void removeEntry(const string& type1, const string& type2);
    HitFunctionPtr lookup(const string& type1, const string& type2);

    static CollisionMap& theCollisionMap();
private:
    CollisionMap();
    CollisionMap(const CollisionMap&);
};

void shipAsteroid(GameObject& spaceShip, GameObject& asteroid);
CollisionMap::theCollisionMap().addEntry("SpaceShip", "Asteroid", &shipAsteroid);

void shipStation(GameObject& spaceShip, GameObject& spaceStation);
CollisionMap::theCollisionMap().addEntry("SpaceShip", "SpaceStation", &shipStation);

void asteroidStation(GameObject& asteroid, GameObject& spaceStation);
CollisionMap::theCollisionMap().addEntry("Asteroid", "SpaceStation", &asteroidStation);
...

addEntry的symmetric参数,主要是为了能对称处理条目而设,也就是增加<T1,T2>时也会增加<T2,T1>。

为了确保这些map条目在其对应的任何撞击发生之前就被加入map,可以使用RegisterCollisionFunction类:

class RegisterCollisionFunction {
public:
    RegisterCollisionFunction(const string& type1, const string& type2,
            CollisionMap::HitFunctionPtr collisionFunction, bool symmetric = true)
    {
        CollisionMap::theCollisionMap().addEntry(type1, type2, 
                    collisionFunction, symmetric);
    }
};

RegisterCollisionFunction cf1("SpaceShip", "Asteroid", &shipAsteroid);
RegisterCollisionFunction cf2("SpaceShip", "SpaceStation", &shipStation);
RegisterCollisionFunction cf3("Asteroid", "SpaceStation", &asteroidStation);
...
int main(int argc, char * argv[])
{
    ...
}

使用全局对象,保证在main函数之前,就已经将所需的条目加入到了map中。

稍后如果有新的派生类加入,无需改动原有代码,只需新增代码:

class Satellite: public GameObject { ... };

void satelliteShip(GameObject& satellite, GameObject& spaceShip);
void satelliteAsteroid(GameObject& satellite, GameObject& asteroid);

RegisterCollisionFunction cf4("Satellite", "SpaceShip", &satelliteShip);
RegisterCollisionFunction cf5("Satellite", "Asteroid", &satelliteAsteroid);

猜你喜欢

转载自www.cnblogs.com/gqtcgq/p/9625438.html