BUG?物理引擎,切换场景后每次碰撞触发2次回调函数。附代码!

首次启动的时候,两个物理碰撞时,触发一次回调函数,但是当切换到别的场景,然后再切换回来,每次碰撞触发2次,再切换一次,就变3次,以此类推,每次切换一次场景就多一次触发。

这是最终的界面:

这是第一次启动的时候的log,每触碰一次就调用一次回调函数:

当切换一次场景后再切换回来后,每次触碰就回调两次:

下面附代码,望大家帮忙看看怎么回事。



//头文件

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"

class HelloWorld : public cocos2d::Layer
{
public:
    // there's no 'id' in cpp, so we recommend returning the class instance pointer
    static cocos2d::Scene* createScene();

    // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
    virtual bool init();
    
    // a selector callback
    void menubackCallback(cocos2d::Ref* pSender);
    
    // implement the "static create()" method manually
    CREATE_FUNC(HelloWorld);

    //phy call backs
    bool contactBegin(cocos2d::PhysicsContact &contact);
};

class AnotherScene : public cocos2d::Layer {

public:

    static cocos2d::Scene* createScene();

    virtual bool init();

    CREATE_FUNC(AnotherScene);

    void menubackCallback(cocos2d::Ref* pSender);

};

#endif // __HELLOWORLD_SCENE_H__





#include "HelloWorldScene.h"

USING_NS_CC;

Scene* HelloWorld::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::createWithPhysics();
    scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
    scene->getPhysicsWorld()->setGravity(Vect(0, -10));
    
    // 'layer' is an autorelease object
    auto layer = HelloWorld::create();

    // add layer as a child to scene
    scene->addChild(layer);

    // return the scene
    return scene;
}

// on "init" you need to initialize your instance
bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Layer::init() )
    {
        return false;
    }
    
    Size visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    /////////////////////////////
    // 2. add a menu item with "X" image, which is clicked to quit the program
    //    you may modify it.

    // add a "close" icon to exit the progress. it's an autorelease object
    auto closeItem = MenuItemImage::create(
                                           "CloseNormal.png",
                                           "CloseSelected.png",
                                           CC_CALLBACK_1(HelloWorld::menubackCallback, this));
    
    closeItem->setPosition(Vec2(origin.x + visibleSize.width - closeItem->getContentSize().width/2 ,
                                origin.y + closeItem->getContentSize().height/2));

    // create menu, it's an autorelease object
    auto menu = Menu::create(closeItem, NULL);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1);

    /////////////////////////////
    // 3. add your codes below...

    // add a label shows "Hello World"
    // create and initialize a label
    
    auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);
    
    // position the label on the center of the screen
    label->setPosition(Vec2(origin.x + visibleSize.width/2,
                            origin.y + visibleSize.height - label->getContentSize().height));

    // add the label as a child to this layer
    this->addChild(label, 1);

    // add "HelloWorld" splash screen"
    auto sprite = Sprite::create("HelloWorld.png");

    // position the sprite on the center of the screen
    sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));

    // add the sprite as a child to this layer
    this->addChild(sprite, 0);



    //物理世界测试

    auto obj1 = Sprite::create("CloseNormal.png");
    addChild(obj1);
    obj1->setPosition(Vec2(200, 500));

    auto obj2 = Sprite::create("CloseNormal.png");
    addChild(obj2);
    obj2->setPosition(Vec2(200, 300));

    auto phy1 = PhysicsBody::createCircle(obj1->getBoundingBox().size.width / 2);
    phy1->setCategoryBitmask(1);
    phy1->setCollisionBitmask(1);
    phy1->setContactTestBitmask(1);

    auto phy2 = PhysicsBody::createEdgeSegment(Vec2(-obj2->getBoundingBox().size.width/2, 0), Vec2(obj2->getBoundingBox().size.width/2, 0));
    phy2->setCategoryBitmask(1);
    phy2->setCollisionBitmask(1);
    phy2->setContactTestBitmask(1);
    
    obj1->setPhysicsBody(phy1);
    obj2->setPhysicsBody(phy2);

    auto phyListener = EventListenerPhysicsContact::create();
    phyListener->onContactBegin = CC_CALLBACK_1(HelloWorld::contactBegin, this);

    _eventDispatcher->addEventListenerWithFixedPriority(phyListener, 1);

    
    return true;
}


void HelloWorld::menubackCallback(Ref* pSender)
{

    Director::getInstance()->replaceScene(AnotherScene::createScene());

}

bool HelloWorld::contactBegin(PhysicsContact &contact) {
    
    CCLOG("contact begin!");

    return true;
}







// scene 2

Scene* AnotherScene::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::create();

    // 'layer' is an autorelease object
    auto layer = AnotherScene::create();

    // add layer as a child to scene
    scene->addChild(layer);

    // return the scene
    return scene;
}

bool AnotherScene::init() {

    //////////////////////////////
    // 1. super init first
    if (!Layer::init())
    {
        return false;
    }

    Size visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    /////////////////////////////
    // 2. add a menu item with "X" image, which is clicked to quit the program
    //    you may modify it.

    // add a "close" icon to exit the progress. it's an autorelease object
    auto closeItem = MenuItemImage::create(
        "CloseNormal.png",
        "CloseSelected.png",
        CC_CALLBACK_1(AnotherScene::menubackCallback, this));

    closeItem->setPosition(Vec2(origin.x + visibleSize.width - closeItem->getContentSize().width / 2,
        origin.y + closeItem->getContentSize().height / 2));

    // create menu, it's an autorelease object
    auto menu = Menu::create(closeItem, NULL);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1);

    return true;

}

void AnotherScene::menubackCallback(Ref* pSender)
{

    Director::getInstance()->replaceScene(HelloWorld::createScene());

}


Mark,我试下

感谢版主,今早我发现问题在哪里了。因为addEventListenerWithFixedPriority不会自动销毁,需要在destructor中销毁就可以了 :)

我销毁的时候遇到另外一个问题:
如果我选择_eventDispatcher->removeAllEventListeners();然后replaceScene,然后到下一个场景后,所有的按钮响应也都失效了。在remove当前所有listener后,下一个场景的按钮事件不应该是会再次随着按钮的创建而创建的吗?

你第二个问题我刚刚试的时候也发现了,我是把_eventDispatcher->removeAllEventListeners();写在切换场景前面搞定的。

非常感谢!问题解决了。

我猜估计是放在destructor后,移除所有监听器的操作replaceScene完成之后才执行,此时第二个场景已经加载好了,所以,第二个场景加载完成后再移除所有监听,导致第二个场景监听全部遗失。

请教另外一个问题,也是在这个例子中出现的。就是BitMask的问题。

根据教程中所述:

— Begin quote from ____

属性值:

group = 0

CategoryBitmask = 0xFFFFFFFF

ContactTestBitmask = 0x00000000

CollisionBitmask = 0xFFFFFFFF

if((A.CategoryBitmask & B.ContactTestBitmask) != 0 || (A.ContactTestBitmask & B.CategoryBitmask) != 0)

{/*条件成立,执行相交通知*/}

else{/没有相交通知/}

//教程地址:http://cn.cocos2d-x.org/tutorial/show?id=2395

— End quote

所以一开始我认为,只需要将A或B任意一个物理体的ContactTestBitmask设置个1就可以了。比如将A的ContactTestBitmask设置成1,那么A的ContactTestBitmask和B的CategoryBitmask就不是0了。

但是,实际测试是No。只有在将A和B的ContactTestBitmask都设置成非零值才可以触发事件回调。

所以我想,是不是教程中的 || 应该改成 &&

此外,我想检查A和B的CategoryBitmask默认值是否是0xFFFFFFFF,可是通过getCategoryBitmask()得到的是-1,这是什么鬼:10:

那里面有这个说明
a、b为同一类物品,group都取-1。无碰撞现象,无回调,物体a b分别从同一个位置落下后,a b重叠【其他bitmask的默认值设置是有碰撞现象的,但设置group取相同的负数后没碰撞了,group优先级高!
然后,第二点你输出16进制就知道了 log("%x",phy2->getCategoryBitmask());

— Begin quote from ____

想要回调

属性值:

group = 0

CategoryBitmask = 0xFFFFFFFF

ContactTestBitmask = 0x00000001<-----change to this

CollisionBitmask = 0xFFFFFFFF

现象和分析:


a、b为同一类物品,有碰撞现象,有回调【碰撞的理由是(a.CollisionBitmask & b.CategoryBitmask) != 0非0, true;有回调的理由是((a.CategoryBitmask & b.ContactTestBitmask) != 0 || (a.ContactTestBitmask & b.CategoryBitmask) != 0) 等于true】

— End quote

我指的是是否回调函数,不是是否碰撞。
根据((a.CategoryBitmask & b.ContactTestBitmask) != 0 || (a.ContactTestBitmask & b.CategoryBitmask) != 0,意思应该是|| OR operator左右两边只要实现一边就可以回调函数。
所以,只要设置a的ContactTestBitmask为1就可以满足这个等式为true了,但是实际却得不到回调函数,必须要a和b的ContactTestBitmask都设置成1,即左右两边都要==true的时候,才会回调函数。

所以我认为是否回调函数的判断应该是:
(a.CategoryBitmask & b.ContactTestBitmask) != 0
&& (a.ContactTestBitmask & b.CategoryBitmask) != 0

可以试一下我下面的代码:

在HelloWorld init中添加代码如下:

//create physical world

auto edge = PhysicsBody::createEdgeBox(visibleSize);
edge->setContactTestBitmask(0x1);//只有在将碰撞的双方的setContactTestBitmask都设置为1以后,才会回调函数。
auto node = Node::create();
node->setPosition(origin + visibleSize / 2);
addChild(node);
node->setPhysicsBody(edge);

auto phyBody = PhysicsBody::createBox(sprite->getBoundingBox().size);
phyBody->setContactTestBitmask(0x1);//只有在将碰撞的双方的setContactTestBitmask都设置为1以后,才会回调函数。
sprite->setPhysicsBody(phyBody);//这里的sprite就是HelloWorld中默认的那张图片

auto phyListener = EventListenerPhysicsContact::create();
phyListener->onContactBegin = CC_CALLBACK_1(HelloWorld::phyContactBegin, this);

_eventDispatcher->addEventListenerWithSceneGraphPriority(phyListener, sprite);

另外再设置一个回调函数:

bool HelloWorld::phyContactBegin(PhysicsContact &contact) {

CCLOG("begin contact");

return true;

}

碰撞都没了什么回调= =!这个回调函数的触发事件是什么…

碰撞有啊,category和collision默认不都是0xFFFFFFFF吗?所以保持默认就可以了啊。实际测试也是有碰撞的。

好吧,一开始没注意到是CategoryBitmask 和 ContactTestBitmask…如果是这两个的话,确实正确的感觉应该是&&

嗯,我也觉得是。