Cocos2dx 3.2 横版过关游戏Brave学习笔记(六)

作者: douxt

使用物理引擎

可以使用Box2D或者Chipmunk物理引擎,Box2D功能较强,Chipmunk被内置到引擎里,使用方便。
我们就先用这个内置的。主要用物理引擎来检测是否发生接触事件。当然,如果只是检测碰撞,其实不一定非要用物理引擎。

把MainScene的createScene函数修改一下:

Scene* MainScene::createScene()
{
    // init with physics
    auto scene = Scene::createWithPhysics();
    auto layer = MainScene::create();
    //set physics world
    layer->setPhysicsWorld(scene->getPhysicsWorld());
    scene->addChild(layer);
    return scene;
}

即创建场景时就可以指定附带物理引擎效果。将物理世界传给MainScene备用。
MainScene增加setPhysicsWorld函数,用于设置一个PhysicsWorld*类型的私有变量。

然后Player类在初始化的时候设置一下body以及碰撞和接触的条件,不应发生碰撞,但需要检测到接触事件。原文里用到了Sensor,可能是版本不一样我没有发现怎么设置sensor,所以用监听接触事件代替碰撞。

auto size = this->getContentSize();
    auto body = PhysicsBody::createBox(Size(size.width/2, size.height));
    body->setCollisionBitmask(0);
    body->setContactTestBitmask(1);
    this->setPhysicsBody(body);

因为图片帧中会包含大量空白,getContentSize()获得的矩形框覆盖了过多的区域,需要根据精灵实际显示大小进行适当裁剪。

这个时候运行一下,发现角色们直接开始往下掉了……原来默认重力不为0.
可以在MainScene::onEnter里将其设置为0:

void MainScene::onEnter()
{
    Layer::onEnter();
    // set gravity to zero
    _world->setGravity(Vec2(0, 0));
}

另外再额外增加按钮来切换是否显示调试用的框框。

    auto debugItem = MenuItemImage::create(
                                        "CloseNormal.png",
                                        "CloseSelected.png",
                                        CC_CALLBACK_1(MainScene::toggleDebug, this));
    debugItem->setScale(2.0);
    debugItem->setPosition(Vec2(VisibleRect::right().x - debugItem->getContentSize().width - pauseItem->getContentSize().width ,
        VisibleRect::top().y - debugItem->getContentSize().height));
 
    _menu = Menu::create(pauseItem, debugItem, NULL);
    _menu->setPosition(0,0);
    this->addChild(_menu);

然后在MainScene中增加对物理接触的监听。

    _listener_contact = EventListenerPhysicsContact::create();
    _listener_contact->onContactBegin = CC_CALLBACK_1(MainScene::onContactBegin,this);
    _listener_contact->onContactSeperate = CC_CALLBACK_1(MainScene::onContactSeperate,this);
    _eventDispatcher->addEventListenerWithFixedPriority(_listener_contact, 10);

对应的监听函数为:

bool MainScene::onContactBegin(const PhysicsContact& contact)
{
    auto playerA = (Player*)contact.getShapeA()->getBody()->getNode();
    auto playerB = (Player*)contact.getShapeB()->getBody()->getNode();
    auto typeA = playerA->getPlayerType();
    auto typeB = playerB->getPlayerType(); 
    if(typeA == Player::PlayerType::PLAYER)
    {
        // only one player so ShapeB must belong to an enemy        
        log("contact enemy!");
        playerB->setCanAttack(true);
    }
    if(typeB == Player::PlayerType::PLAYER)
    {
        // only one player so ShapeA must belong to an enemy        
        log("contact enemy!");
        playerA->setCanAttack(true);
    }
    return true;
}
 
void MainScene::onContactSeperate(const PhysicsContact& contact)
{
    auto playerA = (Player*)contact.getShapeA()->getBody()->getNode();
    auto playerB = (Player*)contact.getShapeB()->getBody()->getNode();
    auto typeA = playerA->getPlayerType();
    auto typeB = playerB->getPlayerType(); 
    if(typeA == Player::PlayerType::PLAYER)
    {
        // only one player so ShapeB must belong to an enemy        
        log("leave enemy!");
        playerB->setCanAttack(false);
    }
 
    if(typeB == Player::PlayerType::PLAYER)
    {
        // only one player so ShapeA must belong to an enemy        
        log("leave enemy!");
        playerA->setCanAttack(false);
    }
}

这里的重复代码过多,应该有办法优化一下。contact发生时,回调用onContactBegin,参数为 contact,里面包含了两个参加接触的Shape,ShapeA和ShapeB。
如果ShapeA是玩家,那B一定是敌人,设置一下让敌人可被攻击。如果ShapeB是玩家,那A就是敌人。

这时我发现教程好像省略了些东西。现在应该补充一下了。
敌人应该会响应触摸,然后发出一个clickEnemy消息,MainScene收到消息后,会判断敌人是否可被攻击,如果可被攻击,玩家执行attack Event,敌人执行beHit Event。
玩家会播放攻击帧动画,敌人会播放被击中帧动画。

这就需要把状态机的状态完善一下,在状态中加入"beingHit"状态, 并添加相应的用于转换的Event。在FSM::init()中

bool FSM::init()
{
    this->addState("walking",[](){cocos2d::log("Enter walking");})
        ->addState("attacking",[](){cocos2d::log("Enter attacking");})
        ->addState("dead",[](){cocos2d::log("Enter dead");})
        ->addState("beingHit",[](){cocos2d::log("Enter beingHit");});
 
    this->addEvent("walk","idle","walking")
        ->addEvent("walk","attacking","walking")
        ->addEvent("attack","idle","attacking")
        ->addEvent("attack","walking", "attacking")
        ->addEvent("die","idle","dead")
        ->addEvent("die","walking","dead")
        ->addEvent("die","attacking","dead")
        ->addEvent("stop","walking","idle")
        ->addEvent("stop","attacking","idle")
        ->addEvent("walk","walking","walking")
        ->addEvent("beHit","idle","beingHit")
        ->addEvent("beHit","walking","beingHit")
//        ->addEvent("beHit","attacking","beingHit") can attacking be stoped by beHit?
        ->addEvent("die","beingHit","dead")
        ->addEvent("stop","beingHit","idle")
        ->addEvent("stop","idle","idle");
 
    return true;
}

状态机如何设置也是个问题。 例如是否应该允许从attacking转移到walking? 我们允许从walking转移到attacking,这样就有可能在attacking时瞬间转移到walking,然后又可以进入attacking了。这似乎是个bug,除非你想让玩家拼手速。

另外每个态的回调函数也要写好,在Player::initFSM()中

void Player::initFSM()
{
    _fsm = FSM::create("idle");
    _fsm->retain();
    auto onIdle =[&]()
    {
        log("onIdle: Enter idle");
        this->stopActionByTag(WALKING);
        auto sfName = String::createWithFormat("%s-1-1.png", _name.c_str());
        auto spriteFrame = SpriteFrameCache::getInstance()->getSpriteFrameByName(sfName->getCString());
        this->setSpriteFrame(spriteFrame);
    };
    _fsm->setOnEnter("idle",onIdle);
 
    auto onAttacking =[&]()
    {
        log("onAttacking: Enter Attacking");
        auto animate = getAnimateByType(ATTACKING);
        auto func = [&]()
        {
            this->_fsm->doEvent("stop");
        };
        auto callback = CallFunc::create(func);
        auto seq = Sequence::create(animate, callback, nullptr);
        this->runAction(seq);
    };
    _fsm->setOnEnter("attacking",onAttacking);
     
    auto onBeingHit = [&]()
    {
        log("onBeingHit: Enter BeingHit");
        auto animate = getAnimateByType(BEINGHIT);
        auto func = [&]()
        {
            this->_fsm->doEvent("stop");
        };
        auto wait = DelayTime::create(0.6f);
        auto callback = CallFunc::create(func);
        auto seq = Sequence::create(wait,animate, callback, nullptr);
        this->runAction(seq);
    };
    _fsm->setOnEnter("beingHit",onBeingHit);
 
    auto onDead = [&]()
    {
        log("onDead: Enter Dead");
        auto animate = getAnimateByType(DEAD);
        auto func = [&]()
        {
            log("A charactor died!");
            NotificationCenter::getInstance()->postNotification("ENEMY_DEAD",nullptr);
            this->removeFromParentAndCleanup(true);
        };
        auto blink = Blink::create(3,5);
        auto callback = CallFunc::create(func);
        auto seq = Sequence::create(animate, blink, callback, nullptr);
        this->runAction(seq);
        _progress->setVisible(false);
    };
    _fsm->setOnEnter("dead",onDead);
}

onIdle函数除了停止行走帧动画外,还需将精灵帧设置到初始帧。
beingHit里可以延迟一点时间,等到刀砍下来在播放被击中帧动画。
死亡之后尸体可以闪烁几下在消失不迟。

然后我们让敌人响应触摸事件并发送消息,Player中:

    _listener = EventListenerTouchOneByOne::create();
    _listener->setSwallowTouches(true);
    _listener->onTouchBegan = CC_CALLBACK_2(Player::onTouch,this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(_listener,this);

响应函数为:

bool Player::onTouch(Touch* touch, Event* event)
{
    if(_type == PLAYER)
        return false;
 
    log("Player: touch detected!");
    auto pos = this->convertToNodeSpace(touch->getLocation());
    auto size = this->getContentSize();
    auto rect = Rect(size.width/2, 0, size.width, size.height);
    if(rect.containsPoint(pos))
    {
        NotificationCenter::getInstance()->postNotification("clickEnemy",this);
        log("enemy touched!");
        return true;
    }
    log("enemy not touched!");
    return false;
}

其中判断了触摸是否触碰到敌人,这里检测的比较随意,可能触摸敌人附近也会被认为触摸到了敌人。 此处应该进一步改良。

在MainScene中,接收上面发出的消息:

NotificationCenter::getInstance()->addObserver(this, callfuncO_selector(MainScene::clickEnemy),"clickEnemy",nullptr);

接受消息的回调函数:

void MainScene::clickEnemy(Ref* obj)
{
    log("click enemy message received!");
    auto enemy = (Player*)obj;
    if(enemy == nullptr)
    {
        log("enemy null");
        return;
    }
    if(enemy->isCanAttack())
    {
        _player->attack();
        enemy->beHit(_player->getAttack());
    }    
    else
    {
        _player->walkTo(enemy->getPosition());
    }
}

当时忽视了一个问题,如果玩家正在攻击,那么再次点击,敌人还是会瞬间扣血。所以应该加个判断,只有玩家不处于攻击状态时才允许攻击。

给Player增加生命值,最大生命值,攻击力属性,并可以用函数获取/设定。
然后上面的函数:

void Player::attack()
{
    _fsm->doEvent("attack");
}
 
void Player::beHit(int attack)
{
    _health -= attack;
    if(_health <= 0)     {         _health = 0;         this->_progress->setProgress((float)_health/_maxHealth*100);
        _fsm->doEvent("die");
        return;
    }
    else
    {
        this->_progress->setProgress((float)_health/_maxHealth*100);
        _fsm->doEvent("beHit");
    }
}

可以看出把状态机设置好,然后在适当时候触发事件即可。

这一次主要是关于物理引擎碰撞检测,但同时也涉及了触摸事件捕捉,事件的发放与接收,状态机的使用,等等。
现在终于可以把怪杀死了。


有个明显问题,刀没碰到怪就把怪打死了。这说明碰撞检测的盒子设置还需要进一步的精细化。

然后我提交了一个新版本,名为"Note 6 Physics contact, State Machine, etc."

标签: none

?>