现在的位置: 首页 > 综合 > 正文

【cocos2D-x学习】14.code with box2D 续——重构是男人就下100层

2013年12月23日 ⁄ 综合 ⁄ 共 4865字 ⁄ 字号 评论关闭

【目标】:重构13中完成的“是男人就下100层”,更充分的利用BOX2D引擎

【参考】:

《Box2D C++教程16-碰撞剖析》

        在上一篇中,所完成的工程,虽然使用了BOX2D来进行碰撞判断,但基本思路是“控制sprite的位置,然后用sprite的位置来更新body的位置”,并没有充分利用BOX2D引擎,当时这样选择,在于对引擎了解还不充分,有好几个困难难以实现,但在项目完成后,通过一些实现过程中的思考,发现这个游戏完全可以使用BOX2D引擎来完成框架的。即修改为“用body的位置来更新sprite的位置”。废话不说,我们来看看到底是些什么问题(其实都比较基本)。

        这里假定你已经掌握了BOX2D的最基本知识,已经可以实现子龙山人教程中的几个DEMO。对这些最基本的就不再介绍了。

一、如何实现板砖

1、难点与解决方案

        第一个难点就在于这些移动的板砖(即上图中的蓝色板,当然还有弹簧之类的其他板),让我们看看这些板有些什么特性:

        1)无视场景中的重力,不停的上移

        2)能够与主人公发生碰撞

        3)与主人公发生碰撞之后,本身速度完全不受影响

        如果你刚入门BOX2D引擎,就会发现问题所在:

        1)无视重力就很麻烦,是不是应该用一个关节马达来实现?

        2)板在碰撞之后受力,速度和位置就会发生变化,如何让这些事情不发生?是不是还要再设置一个关节来限制横向的移动?那随着板的上移,这个关节的锚点是不是还要跟着变化?

        这些问题想着就头大,幸好BOX2D已经为我们提供了一个简单快捷的方式:

        将这些板设置为 b2_kinematicBody

2、三种 body 类型

        在BOX2D中,有三种body类型:

enum b2BodyType
{
    b2_staticBody = 0,
    b2_kinematicBody,
    b2_dynamicBody
};

         以我目前的使用情况来看,这三种类型区别如下:

         1)static body:没有速度,不受力,可以碰撞

         2)dynamic body:有速度,受力,可以碰撞。这前两种类型在各类教程中出现的频率更高一些。

         3)kinematic body:有速度,不受力,可以碰撞。

         这第三种正是我们想要的:一个不受重力,不受冲量,但是可以载着主人公按固定速度上移的蓝色板。只需要在完成body的初始化之后,给自己设定一个上移速度即可:

void BlockBase::initPhysics(b2World * world) {
    PhysicalSprite::initPhysics(world);
    mBody->SetLinearVelocity(b2Vec2(0, SCOLLING_SPEED));
}

二、我的主人公绝不能会打滚

        完成了板砖的设计之后,让我们将目光投向主人公。这个角色毫无疑问的是一个 dynamic body。但对于他,在这个游戏中,我们也有一些小小的限制,就是别打滚啊。不希望他发生角度的偏转。

        我最初的思路还是利用关节,毕竟这个东西就是用来限制移动的。不过BOX2D同样为我们准备了一个简易的方法:

void Hero::initPhysics(b2World * world) {
    PhysicalSprite::initPhysics(world);
    mBody->SetFixedRotation(true);
    mBody->SetBullet(true);
}

        注意这个 SetFixedRotation,设置了这个属性之后,这个body就不会发生旋转了。

三、one-side platform

       有了主人公和板砖之后,就可以发生碰撞了,我们的主角就可以堂堂正正的站在板砖上了。

       不过这个游戏还有一个特点,如果主角是从侧面或者下方冲向板砖,是不会发生碰撞的,这个特性和 BOX2D TESTBED中的 ONE-SIDE PLATFORM是一样的,我们这里可以研究一下这段代码(虽然简单的很)

        首先,控制碰撞的时机在 preSolve,这个时候可以通过设置

    contact->SetEnabled(enable);

        来允许或者禁止一次碰撞。需要注意的是,这个presolve是一直调用的,所以你要是禁止,就要在这一次碰撞流程中一直禁止。

        至于如何判断是否要禁止,则是通过这样来实现的:

//碰撞管理,如果从侧面或者下面碰撞,则返回false
bool BlockBase::onPreSolve(PhysicalSprite * other, const b2Manifold * oldManifold) {
    if ( other == NULL )
        return true;

    static float HEIGHT_IN_PTM = HEIGHT_PX / PTM_RATIO;
    float myTop = mBody->GetPosition().y + HEIGHT_IN_PTM / 2.0f;
    float otherBottom = other->mBody->GetPosition().y - HEIGHT(other) / (PTM_RATIO * 2);
    return otherBottom >= myTop - HEIGHT_PX/2 * b2_linearSlop;
}

        这里是板砖在检查,通过比较自己的顶点和主人公的底部,来确定主角是否是从上方接近,如果是,则允许碰撞,否则禁止。

        需要注意的是这里还有一个裕量 HEIGHT_PX/2 * b2_linearSlop,其中 HEIGHT_PX 是板砖的高度,b2_linearSlop是BOX2D提供的一个比较小,但是又有一定量的值(就是0.0005)。

        这个裕量的作用在于,如果主人公从上方落下的速度很快,那么在一次step中,主人公就有可能会陷入板砖中一点,这样的话,其底部就比板砖的顶部还要低一点了。在实际使用过程中,即使将主人公设置为 bullet,这个现象也会发生。所以需要有这样一个裕量。具体设置为多少,我感觉用我这里的公式基本就可以满足目标,即使是非常变态的速度值,也不会发生问题。具体公式就是:

       板砖高度 * b2_linearSlop / 2

       当然这个值到底该设多少,还是要靠你手动来调节。

四、碰撞筛选

       在完成了主人公和板砖之后,我们在屋顶上又安放了一排刺,当主人公碰到刺的时候,就会被扎落下平台,而板砖是不需要和这排刺发生碰撞的。如果用上面的presolve来进行过滤的话,代价还是太高了,我们可以通过设置filter群组的形式来完成这个特性。

        这个功能很简单,主要由两个部分组成:

        1)shapeDef.filter.categoryBits : 这个用来设定自己

        2)shapeDef.filter.maskBits:这个用来过滤别人。

        举例来说,如果两个shape,一个A的category是0x01,mask是0x02。那么如果B的category不包含0x02,mask不包含0x01,那么就不会发生碰撞。

        在BOX2D中对应的代码:

bool b2ContactFilter::ShouldCollide(b2Fixture* fixtureA, b2Fixture* fixtureB)
{
    const b2Filter& filterA = fixtureA->GetFilterData();
    const b2Filter& filterB = fixtureB->GetFilterData();

    if (filterA.groupIndex == filterB.groupIndex && filterA.groupIndex != 0)
    {
        return filterA.groupIndex > 0;
    }

    bool collide = (filterA.maskBits & filterB.categoryBits) != 0 && (filterA.categoryBits & filterB.maskBits) != 0;
    return collide;
}

       碰撞筛选和上面的ONE-SIDE相比,功能类似,略有差异。碰撞筛选是静态的,基本是一劳永逸,只要设定好,那么不能碰撞的物体就是不会碰撞,从 beginContact到preSolve一律没有,而ONE-SIDE是动态的,可以根据具体情况来进行变化,选择哪个方式还是取决于具体场景。

五、begin contact的陷阱

     这个部分其实应该接着第三节来讲。在我的这个项目中,如果主角和板砖发生了碰撞,那么主角状态会发生一些变化,例如跳起,例如伤血,例如从落下状态变成站立状态。这个功能我是在 beginContact的时候做的,但是这里有个问题:

      如果主人公是从侧面接近一个弹簧板的,理论上不会发生碰撞,实际上也在 preSolove的时候将contact设置为false了,为什么还是被弹了起来?

      这就涉及到一个问题:碰撞时的回调时序。【这一段强烈建议阅读《Box2D C++教程16-碰撞剖析》一文】

      其时序实际上是这样的:

      beginContact ->  preSolve -> postSolve ->preSolve -> postSolve -> ………… -> preSolve -> postSolve -> endContact

      也就是说,虽然我们在preSolve的时候将 contact设置为false了,但是这对于 beginContact 的发生毫无影响。实际上,beginContact发生的时机,在两个物体的 AABB框(就是那个在主角周围的紫色方框)发生碰撞的时候,其实两个物体还没有发生真实的碰撞。

       在我这里,是这样规避的:

void BlockBase::onBeginContact(PhysicalSprite * other) {
    Hero * hero = CAST_INSTANCE(other, Hero);
    if ( hero == NULL || !onPreSolve(other, NULL) || !hero->onPreSolve(this, NULL) )
        return;

    if ( hero != NULL )
        onContactWithHero(hero);
}

       思路就是再调用一边 preSolve来进行判断,如果不构成发生碰撞的条件,就直接退出不进行处理。

六、冲量与速度

      在这个游戏中,关于物理引擎使用的最后一个问题在于如何实现弹簧板的跳跃。

      有两种选择,一种是给主人公一个力或者冲量,让他向上冲起,另外一种是直接设置它的Y方向初速度。我这里最后选择的是后一种,主要在于如果使用前一种,如果从不同的高度落下,那么如果使用同样的冲量,弹起的高度是不同的(其实后来想想,只要根据物理公式算一下就可以了,也是可行的。)

      其实更推荐使用冲量的方法,要使用冲量,可以如下写:

mBody->ApplyLinearImpulse(b2Vec2(0, 80), mBody->GetWorldCenter());

      这里有两个参数,第一个是冲量的大小,第二个是施力点,一般选在重心点就可以。我这里把冲量大小是写死的,如果需要让主人公跳跃高度保持一致,则需要计算一下(我这里偷懒就不算了)。

      如果使用速度的话,则在任意正向重力条件下,其速度设定公式如下:

    //这里的算式如下:初速度 v0,加速度g,则飞行时间为 v0/g,所以高度为 0.5 * g * (v0/g)^2
    static float JUMP_HEIGHT = 40.0f;
    static float speed = sqrt( - mWorld->GetGravity().y * 2 * JUMP_HEIGHT / PTM_RATIO );
    setYSpeed( speed );

七、代码

       最后附上源码:http://download.csdn.net/detail/ronintao/6548169

       同样是JNI部分。毕竟其他部分也没有东西

抱歉!评论已关闭.