【目标】:重构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部分。毕竟其他部分也没有东西