原文地址:http://wwk.iteye.com/blog/1722124
一些Box2d的基本概念,一些cocos中使用box2d需要注意的地方
1. cocos2d 自带了两套物理引擎:Box2D 和Chipmunk。
两套引擎都是为2D游戏设计的,可以和cocos2d 完美整合。 Box2D 是用 C++写的,而 Chipmunk 用的是 C。 Box2D中的变量和方法名都是用全称命名的,Chipmunk中很多地方用的是只有一个字母的简写。有一些功能只有Box2D提供,Chipmunk是没有的。比如,Box2D有针对快速移动 物体(例如子弹)直接穿透物体而不进行碰撞测试的解决方法。 ...
2. 刚体:
物理物体叫做“刚体”(Rigid Bodies),这是因为物理引擎在驱动这些物体生成动画时,把它们当作硬的,不会变形的物体。将物体这样简化以后,物理引擎就可以同时计算大量的刚体了。 物理引擎中存在两种刚体:动态移动的(dynamic )和静态的(static)刚体。
- 静态刚体不会移动,也不应该移动- 因为物理引擎可以依赖静态刚体不会互相碰撞这个特性来做出一些优化。静态刚体的密度可以被设为0。
-
动态刚体,它们会相互碰撞,而且也会和静态刚体碰撞。动态刚体除了拥有位置 (position)和旋转(rotation )参数,还有至少3个用于定义动态刚体的参数。 它们分别是:
- 密度或者质量(density or mass)- 用于衡量刚体有多重。
- 摩擦力(friction )- 用于表示刚体在平面上移动时遇到阻力的大小或者有多滑。
- 是复原(restitution )- 用于定义刚体的弹性。
3. 碰撞
现实世界里的物体在运动时都会丢失能量,但是你可以在物理引擎中生成移动碰撞后不会丢失任何能量的动态刚体,甚至让刚体在与别的刚体发生碰撞弹回来以后,以更快的速度进行移动(设置弹性系数restitution)。
动态和静态刚体外面都有一个或者多个形体。这个形体用于判断刚体之间的碰撞的。每一次碰撞会生成一些碰撞点(contact points ) - 两个相碰撞刚体之间的交叉点。这些碰撞点可被用于播放粒子效果或者在刚体的碰撞处动态地添加刮痕。
物理引擎通过使用力量,脉冲和扭矩生成动态刚体的动画。非必要情况下不能直接设置刚体的位置和旋转。如果手动修改物体位置信息,物理引擎先前所作的某些假设就会失效。
4. 关节
可以使用关节(joints )把多个刚体连接起来,这样可以用不同的方式限制相互连接着的刚体的活动。某些关节可能配备有马达,比如它们可被用于驱动汽车的*或者给关节产生摩擦力,这样关节在向某个方向移动以后,会试着回到原来的位置。
关节通过使用b2World类的CreateJoint方法来生成。使用刚体的 GetWorld 方法就可以得到b2World。
// 生成旋转关节 b2RevoluteJointDef jointDef; jointDef.Initialize(bodyA, bodyB, bodyB->GetWorldCenter()); bodyA->GetWorld()->CreateJoint(&jointDef);
柱状关节:只允许朝一个方向移动,单筒望远镜是一个应用柱状关节的很好的例子。
b2PrismaticJointDef jointDef; b2Vec2 worldAxis(0.0f, 1.0f); jointDef.Initialize(bodyA, bodyB, body->GetWorldCenter(), worldAxis); jointDef.lowerTranslation = 0.0f; jointDef.upperTranslation = 0.75f; jointDef.enableLimit = true; jointDef.maxMoto rForce = 60.0f; jointDef.motorSpeed = 20.0f; jointDef.enableMotor = false; joint = (b2PrismaticJoint*)body- >GetWorld()->CreateJoint(&jointDef);
把活塞的摩擦力和密度设置为极限值,这样弹球就不会在碰到活塞的时候弹跳出去了,这可以保证平滑的发射动作。
fixtureDef.friction = 0.99f; fixtureDef.restitution = 0.01f;
5. 物理引擎的局限性
真实世界过于复杂,完全放到物理引擎中进行模拟是不可能的。这就是为什么要使用刚体的原因。 在某些极端情况下,物理引擎有可能会捕捉不到某些已经发生的碰撞 – 例如,当刚体以很快的速度移动时,一个刚体可能直接穿透另一个刚体。虽然在量子物理学中这样的穿透情况会发生。
刚体有时候会相互穿透卡在一起,特别是在使用了关节将它们连接在一起以后。卡在一起的刚体会努力要分开,但是为了满足关节的连接要求,它们又不得不卡在一起,结果是卡在一起的刚体会产生颤动。
我们也可能碰到游戏运行的问题。如果我们在游戏里使用了很多刚体,你永远不会知道这些刚体相互作用后的最终结果。最终,有些玩家会把自己卡死在刚体中,或者他们也可能会发现如何利用物理模拟的漏洞,跑到游戏中他们本来不应该去的区域。
6. Box2D
因为Box2D使用C++写的,所以必须使用.mm作为所有项目的实现文件的后缀名(cocos2d中,也可以直接使用纯c++来编写,后缀cpp),而不是通常的.m后缀名。.mm后缀用于告知Xcode把有此后缀名的文件作为Objective-C++或者C++代码来处理。如果你使用了.m后缀,Xcode就会把代码以Objective-C和C来处理,Xcode就不能正确处理Box2D的C++代码了。因此,如果碰到很多编译错误,首先检查一下是不是所有的实现文件都是以.mm作为后缀的。
Box2D手册:http://www.box2d.org/manual.html
Box2D的API参考下载:http://code.google.com/p/box2d
7. b2World
b2World初始化时使用了一个初始的重力矢量值和用于决定是否允许动态刚体“睡眠”的标记变量。 例:对Box2D世界进行初始化
//一个重力为10的世界 b2Vec2 gravity = b2Vec2(0.0f, -10.0f); bool allowBodiesToSleep = true; world = new b2World(gravity, allowBodiesToSleep);
会“睡眠”的动态刚体:当施加到某个刚体上的力量小于临界值一段时间以后,这个刚体将会进入“睡眠”状态。换句话说,如果某个刚体移动或者旋转的很慢或者根本不在动的话,物理引擎将会把它标记为“睡眠”状态,不再对其施加力量,直到新的力量施加到刚体上让其再次移动或者旋转。通过把一些刚体标记为“睡眠”状态,物理引擎可 以省下很多时间。除非你游戏中的所有动态刚体处于持续的运动中,否则应该把allowBodiesToSleep变量设置为true。
传入Box2D世界的重力是一个b2Vec2的struct类型。它和CGPoint在本质上是 一样的,都储存着x 轴和y 轴的浮点值。和真实世界一样重力都是一个常量(0,-10)。
8. 刚体b2Body
把活动范围限制在屏幕之内: 创建一个静态刚体:
b2BodyDef containerBodyDef; b2Body* containerBody = world->CreateBody(&containerBodyDef);
刚体通常都是使用world的CreateBody方法来生成的,可以确保正确地分配和释放刚体所占用的内存。默认情况下,一个空的刚体定义会生成一个位于(0,0)位置的静态刚体。
填充4条边,以构成空心刚体,屏幕的四个边需要单独创建,以便只有四个边是实心的,中间则是空心的,用于放入其它刚体:
b2PolygonShape screenBoxShape; int density = 0; // 底部 screenBoxShape.SetAsEdge(lowerLeftCorner, lowerRightCorner); containerBody->CreateFixture(&screenBoxShape, density); // 顶部 screenBoxShape.SetAsEdge(upperLe ftCorner, upperRightCorner); containerBody->CreateFixture(&screenBoxShape, density); // 左边 screenBoxShape.SetAsEdge(upperLeftCorner, lowerLeftCorner); containerBody->CreateFixture(&screenBoxShape, density); // 右边 screenBoxShape.SetAsEdge(upperRi ghtCorner, lowerRightCorner); containerBody->CreateFixture(&screenBoxShape, density);
b2PolygonShape类还有SetAsBox方法,调用它会生成的是一个实心的刚体。
使用SetAsBox方法生成的盒子是其接受的参数大小的两倍,所以提供给这个方法的参数坐标值要除以2,或者乘以0.5f。
在cocos3.9中,已经删除掉SetAsEdge函数。可以使用新的b2EdgeShape。
9. 单位
Box2D中距离以米为单位,质量以公斤为单位,时间以秒为单位。
Box2D在0.1米到10米的范围内工作是最优化的,因为它针对这个范围做过专门优化,把创建的Box2D世界中的刚体的大小限定在越接近1米越好,太小或者太大的刚体很可能会在游戏运行过程中产生错误和奇怪的行为。所以在cocos中,我们会使用PTM_RATIO宏来进行转化,否则cocos默认1像素=1米。
PTM_RATIO的定义如下:
#define PTM_RATIO 32
PTM_RATIO用于定义32个像素在Box2D世界中等同于1米。一个有32像素宽和高的盒子形状的刚体等同于1米宽和高的物体。2x32像素大小的瓷砖的尺寸刚好是1x1米,大小4x4像素的大小是0.125米x0.125米。
Box2d在处理大小在0.1到10个单元的对象的时候做了一些优化。这里的0.1米大概就是一个杯子那么大,10的话,大概就是一个箱子的大小。
屏幕的宽度和高度值除以一个名为 PTM_RATIO的常量,把像素值转换成了以米为单位来 计算长度:
Size screenSize = [Director getInstance].winSize; float widthInMeters = screenSize.width / PTM_RATIO; float heightInMeters = screenSize.height / PTM_RATIO; //四个角的坐标, 以米为单位: b2Vec2 lowerLeftCorner = b2Vec2(0, 0); b2Vec2 lowerRightCorner = b2Vec2(widthInMeters, 0); b2Vec2 upperLeftCorner = b2Vec2(0, heightInMeters); b2Vec2 upperRightCorner = b2Vec2(widthInMeters, heightInMeters) ;
//在b2Vec2和Point之间转换: -(b2Vec2) toMeters:(Point)point { return b2Vec2(point.x / PTM_RATIO, point.y / PTM_RATIO); } -(Point) toPixels:(b2Vec2)vec { return Point(vec.x, vec.y) * PTM_RATIO; }
10. 生成一个由精灵作为表现的动态刚体示例
-(void) addNewSpriteAt:(CGPoint)pos { CCSpriteBatchNode* batch = (CCSpriteBatchNode*)[self getChildByTag:kTagBatchNode]; int idx = CCRANDOM_0_1() * TILESET_COLUMNS; int idy = CCRANDOM_0_1 () * TILESET_ROWS; CGRect tileRect = CGRectMake(TILESIZE * idx, TILESIZE * idy, TILESIZE, TILESIZE); CCSprite* sprite = [CCSprite sp riteWithBatchNod e:batch rect:tileRect]; sprite.position = pos; [batch addChild:sprite]; //创建一个刚体的定义,并将其设置为动态刚体 b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; bodyDef.position = [self toMeters:pos]; bodyDef.userData = sprite; b2Body* body = world->CreateBody(&bodyDef); // 定义一个盒子形状,并将其复制给body fixture b2PolygonShape dynamicBox; float tileInMeters = TILESIZE / PTM_RATIO; dynamicBox.SetAsBox(tileInMeters * 0.5f, tileInMe ters * 0.5f); b2FixtureDef fixtureDef; fixtureDef.shape = &dynamicBox; fixtureDef.density = 0.3f; fixtureDef.friction = 0.5f; fixtureDef.restitution = 0.6f; body->CreateFixture(&fixtureDef); }世界创建刚体,刚体再创建它的附着物。 刚体和精灵没有直接关系,它们只是位置被维持相同罢了。
可以把b2FixtureDef 理解成包含着刚体需要用到的所有数据的容器。这些数据包括:刚体的形状(最重要的一项),密度,摩擦力和复原(这项会影响刚体在world中移动和弹跳的方式)。
11. box2d的动画
刚体看不见但摸的着,精灵看的见但摸不着。
在update方法中遍历每个刚体,把刚体的userData返回,并且转换成Sprite指针。把刚体的位置信息转换成像素值,赋值给精灵的位置属性,让精灵可以随着刚体一起移动。同样地,设置精灵的角度值。
Box2D的world是通过定期地调用Step方法来实现动画的。
Step方法需要三个参数。
第一个是timeStep,它会告诉Box2D自从上次更新以后已经过去多长时间了,直接影响着刚体会在这一步移动多长距离。不建议使用delta time来作为timeStep的值,因为delta time会上下浮动,刚体就不能以相同的速度移动了。
第二和第三个参数是迭代次数。它们被用于决定物理模拟的精确程度,也决定着计算刚体移动所需要的时间。
示例:更新每个刚体相关联的精灵的位置和旋转信息
-(void) update:(ccTime)delta { //使用固定的时间间隔将物理模拟向前推进一步 float timeStep = 0.03f; int32 velocityIterations = 8; int32 positionIterations = 1; world->Step(timeStep, velocityIterati ons, positionIterations); for (b2Body* body = world->GetBodyList(); body != nil; body = body->GetNext()) { CCSprite* sprite = (CCSprite*)body->GetUserData(); if (sprite!= NULL) { sprite.position = [self toPixels:body->GetPosition()]; float angle = body ->GetAngle(); sprite.rotation = CC_RADIAN S_TO_DEGREES(angle) * -1; } } }12. 碰撞测试
创建一个继承自b2ContactListener的新类,重写BeginContact和EndContact这两个方法,任何时候两个刚体发生碰撞时都会调用这两个方法。
将它的实例设为world的contact listener:
contactListener = new ContactListener(); world->SetContactListener(contactListener);
在b2ContactListener实例的回调中不能进行任何更改游戏物理世界的操作,所以我们可以保存两个碰撞物体的引用,然后待会在update中做其它处理。
13. 凸面体
凸面体特点是:在凸面体里面找任意两个点,这两个点连成的线的任何部分都不会落在凸面体外面。
凹面体(Concave)里任意两个点连成的线可能有一部分会落在外面。
组成多边形的顶点在定义是要以反时针方向来进行。
使用VertexHelper工具,通过把顶点一个接一个画出来的方式来 生成碰撞测试用的多边形。 VertexHelper的源代码是通过GitHub来共享的: http://github.com/jfahrenkrug/VertexHelper.,下载下来是一个xcode mac工程,
VertexHelper只是一个帮助寻找凸面体顶点数组的工具,我们需要的是它生成的多边形顶点数组,对于一个物体,可以把它当作一个凸面体,而对于一个地图,要在其中找到多个凸面体,在VertexHelper中生成的顶点数组代码,可以一段一段的拷贝,当作多个多边形,只要认为某一段的顶点若连起来后是凸面体就好了。
顶点的位置是相对于body中心的,最大顶点数量不能超过8,数量越大,越费内存,性能也越差。
14. 步骤
加载场景,场景中有一个Layer
- 在Layer中初始化世界:设置边界,设置监听
- 启用渲染调试
- 加载纹理贴图,*.plist
- 在Layer中加载背景颜色层 LayerColor
- 在Layer中添加精灵批处理,SpriteBatchNode,(可以在每个Node中添加同一个纹理贴图生成的精灵批处理,以方便此node中快速生成sprite)
- 添加静态元素层Node,此node中可以添加一些Sprite。
- 使用world创建刚体,然后创建和刚体对应的sprite添加到精灵批处理,并关联给刚体的userdata,此处代码也可以封装一下。
- 在预约的update回调方法中,遍历世界中的所有刚体,把精灵的位置和刚体的位置及角度同步。
- 示例中的BodyNode被添加到了TableSetup中,只是起到了保存实例引用的作用。
15. 形状
b2CircleShape shape; float radiusInMeters = (tempSprite.contentS ize.width / PTM_RATIO) * 0.5f; shape.m_radius = radiusInMeters;
将b2BodyDef的angularDamping的域设为0.9f,它会让球对转弯的动作产生更大的阻力。这样弹球在滑过弹球桌面的时候不会产生很多的自身旋转,这对于用金属制作的有一定重量的弹球来说是标准的移动方式。
16. Box2D具体是如何运作的。
创建了world对象,接下来需要往里面加入一些body对象。body对象可以随意移动,可以是怪物或者飞镖什么的,只要是参与碰撞的游戏对象都要为之创建一个相应的body对象。当然,也可以创建一些静态的body对象,用来表示游戏中的台阶或者墙壁等不可以移动的物体。
为了创建一个body对象,首先,创建一个body定义结构,然后是body对象,再指定一个shape,再是fixture定义,然后再创建一个fixture对象。下面详细介绍这个过程:
- 首先创建一个body定义结构体,用以指定body的初始属性,比如位置或者速度。
- 然后调用world对象来创建一个body对象。
- 然后为body对象定义一个shape,用以指定想要仿真的物体的几何形状。
- 接着创建一个fixture定义,同时设置之前创建好的shape为fixture的一个属性,并且设置其它的属性,比如质量或者摩擦力。
- 最后,可以使用body对象来创建fixture对象,通过传入一个fixture的定义结构就可以了。
请注意,可以往单个body对象里面添加很多个fixture对象。这个功能在创建特别复杂的对象的时候非常有用。比如自行车,可能要创建2个*,车身等等,这些fixture可以用关节连接起来。
只要把所有需要创建的body对象都创建好之后,box2d接下来就会接管工作,并且高效地进行物理仿真—只要你周期性地调用world对象的step函数就可以了。
但是,请注意,box2d仅仅是更新它内部模型对象的位置–如果想让cocos2d里面的sprite的位置也更新,并且和物理仿真中的位置相同的话,那么你也需要周期性地更新精灵的位置。
17. density,friction和restitution参数的意义。
- Density 就是单位体积的质量(密度)。因此,一个对象的密度越大,那么它就有更多的质量,当然就会越难以移动.
- Friction 就是摩擦力。它的范围是0-1.0, 0意味着没有摩擦,1代表最大摩擦,几乎移不动的摩擦。
- Restitution 回复力。它的范围也是0到1.0. 0意味着对象碰撞之后不会反弹,1意味着是完全弹性碰撞,会以同样的速度反弹。