本教程基于子龙山人翻译的cocos2d的IPHONE教程,用cocos2d-x for XNA引擎重写,加上我一些加工制作。教程中大多数文字图片都是原作者和翻译作者子龙山人,还有不少是我自己的理解和加工。感谢原作者的教程和子龙山人的翻译。本教程仅供学习交流之用,切勿进行商业传播。
子龙山人翻译的Iphone教程地址:http://www.cnblogs.com/zilongshanren/archive/2011/05/27/2059453.html
Iphone教程原文地址:http://www.raywenderlich.com/457/intro-to-box2d-with-cocos2d-tutorial-bouncing-balls
BOX2D这个引擎,是个很好的模拟真实物理世界的引擎。愤怒的小鸟就是基于这个引擎做的。想想愤怒的小鸟,是不是很有冲动来学习下使用这个引擎了呢。
本教程目的就是让你们熟悉在cocos2d里面如何使用box2d,所采用的例子就是制作一个简单的应用。里面有一个篮球,使篮球自己碰到墙壁反弹。
本教程假定你学过前面的教程《用cocos2d-x做一个简单的windows phone 7游戏》,或者有同等的相关经验。
下载XNA版的BOX2D:
到http://box2dxna.codeplex.com/最新的BOX2D。然后解压。到其目录下的Box2D.XNA.TestBed\bin\Windows Phone\Debug这里把这个BOX2D.XNA.DLL这个DLL文件复制出来。其实下载这么个文件我们就需要这个DLL。然后估计有人会想,怎么到这里复制呢,那么不是有个Box2D.XNA工程么,其实,经过我实践,发现原来Box2D.XNA\bin\Windows Phone\Debug这个目录下的DLL有些问题。所以到人家的示例工程里面把DLL复制出来。
这里先介绍下BOX2D世界的相关理论。(其实推荐看Box2d参考手册(英文)和Box2d参考手册(中文),不是很多,才30+页,看了就了解了BOX2D的理论,再进行以下的理解就简单了)
在我们开始之前,让我们先交待一下Box2D具体是如何运作的。
你需要做的第一件事情就是,当使用cocos2d来为box2d创建一个world对象的时候。这个world对象管理物理仿真中的所有对象。
一旦我们已经创建了这个world对象,接下来需要往里面加入一些body对象。body对象可以随意移动,可以是怪物或者飞镖什么的,只要是参与碰撞的游戏对象都要为之创建一个相应的body对象。当然,也可以创建一些静态的body对象,用来表示游戏中的台阶或者墙壁等不可以移动的物体。
为了创建一个body对象,你需要做很多事情--首先,创建一个body定义结构,然后是body对象,再指定一个shap,再是fixture定义,然后再创建一个fixture对象。下面会一个一个解释刚刚这些东西。
· 你首先创建一个body定义结构体,用以指定body的初始属性,比如位置或者速度。
· 一旦创建好body结构体后,你就可以调用world对象来创建一个body对象了。
· 然后,你为body对象定义一个shape,用以指定你想要仿真的物体的几何形状。
· 接着创建一个fixture定义,同时设置之前创建好的shape为fixture的一个属性,并且设置其它的属性,比如质量或者摩擦力。
· 最后,你可以使用body对象来创建fixture对象,通过传入一个fixture的定义结构就可以了。
· 请注意,你可以往单个body对象里面添加很多个fixture对象。这个功能在你创建特别复杂的对象的时候非常有用。比如自行车,你可能要创建2个*,车身等等,这些fixture可以用关节连接起来。
只要你把所有需要创建的body对象都创建好之后,box2d接下来就会接管工作,并且高效地进行物理仿真---只要你周期性地调用world对象的step函数就可以了。
但是,请注意,box2d仅仅是更新它内部模型对象的位置--如果你想让cocos2d里面的sprite的位置也更新,并且和物理仿真中的位置相同的话,那么你也需要周期性地更新精灵的位置
在cocos2d-x for wp7上使用BOX2D:
我们新建个cocos2d-x的工程,命名为cocos2dBOX2DDemo,同样的,OpenXLive的那个勾去掉。因为我们不需要这个服务。同样的,我们往工程里面新建一个lib的文件夹。然后把需要的DLL添加到该工程的这个lib文件夹内。然后把带叹号的引用移除后并添加该工程lib文件夹内的相同的DLL。并且在cocos2dBOX2DDemo工程中添加BOX2D.XNA.DLL这个引用。
然后新建一个类添加到Classes文件夹。命名为BOX2DLayer。并使之继承于CCLayer。
下载http://dl.dbank.com/c0md7urpb3并且添加到cocos2dBOX2DDemoContent工程的images文件夹。
接下来,在BOX2DLayer类里面添加以下声明:
public static double PTM_RATIO = 32.0;
这里定义了一个“像素/米”的比率。当你在cocos2d里面指定一个body在哪个位置时,你使用的单位要是米。但是,我们之前使用的都是像素作为单位,那样的话,位置就会不正确。根据Box2d参考手册,Box2d在处理大小在0.1到10个单元的对象的时候做了一些优化。这里的0.1米大概就是一个杯子那么大,10的话,大概就是一个箱子的大小。
因此,我们并不直接传递像素,因为一个很小的对象很有60×60个像素,那已经大大超过了box2d优化时所限定的大小。因此,如果我们有一个64像素的对象,我们可以把它除以PTM_RATIO,得到2米---这个长度,box2d刚好可以很好地用来做物理仿真。
另外,为什么要用double类型呢,这个有个精度的问题,如果用int的话,我曾经尝试过,篮球直接嵌在墙壁不反弹。
好了,现在来点有意思的东西。在BOX2DLayer类里面添加以下声明:
World world;
Body body;
CCSprite ball;
然后重载Init方法并且修改为:
public override bool init() { if (!base.init()) return false; if (!base.init()) { return false; } CCSize winSize = CCDirector.sharedDirector().getWinSize(); CCLabelTTF title = CCLabelTTF.labelWithString("Boxing", "Arial", 24); //title.Color = new ccColor3B(0, 255, 255); title.position = new CCPoint(winSize.width / 2, winSize.height - 50); this.addChild(title, 1); //Create sprite and add it to the layer ball = CCSprite.spriteWithFile(@"images/Ball"); ball.position = new CCPoint(100, 300); this.addChild(ball); //Create the world Vector2 gravity = new Vector2(0.0f, -30.0f); bool doSleep = true; world = new World(gravity, doSleep); //Create edges around the entire screen BodyDef groundBodyDef = new BodyDef(); groundBodyDef.position = new Vector2(0, 0); Body groundBody = world.CreateBody(groundBodyDef); PolygonShape groundBox = new PolygonShape(); FixtureDef boxShapeDef = new FixtureDef(); boxShapeDef.shape = groundBox; groundBox.SetAsEdge(new Vector2(0, 0), new Vector2((float)(winSize.width / PTM_RATIO), 0)); groundBody.CreateFixture(boxShapeDef); groundBox.SetAsEdge(new Vector2(0, 0), new Vector2(0, (float)(winSize.height / PTM_RATIO))); groundBody.CreateFixture(boxShapeDef); groundBox.SetAsEdge(new Vector2(0, (float)(winSize.height / PTM_RATIO)), new Vector2((float)(winSize.width / PTM_RATIO), (float)(winSize.height / PTM_RATIO))); groundBody.CreateFixture(boxShapeDef); groundBox.SetAsEdge(new Vector2((float)(winSize.width / PTM_RATIO), (float)(winSize.height / PTM_RATIO)), new Vector2((float)(winSize.width / PTM_RATIO), 0)); groundBody.CreateFixture(boxShapeDef); //Create ball body and shape BodyDef ballBodyDef = new BodyDef(); ballBodyDef.type = BodyType.Dynamic; ballBodyDef.position = new Vector2((float)(100 / PTM_RATIO), (float)(300 / PTM_RATIO)); ballBodyDef.userData = ball; body = world.CreateBody(ballBodyDef); CircleShape circle = new CircleShape(); circle._radius = (float)(26.0 / PTM_RATIO); FixtureDef ballShapeDef = new FixtureDef(); ballShapeDef.shape = circle; ballShapeDef.density = 1.0f; ballShapeDef.friction = 0.0f; ballShapeDef.restitution = 1.0f; body.CreateFixture(ballShapeDef); this.schedule(tick); return true; }
呃,这里有很多陌生的代码。我们一点点来解释一下。下面,我会一段段地重复上面的代码,那样可以解释地更加清楚一些。
CCSize winSize = CCDirector.sharedDirector().getWinSize(); CCLabelTTF title = CCLabelTTF.labelWithString("Boxing", "Arial", 24); //title.Color = new ccColor3B(0, 255, 255); title.position = new CCPoint(winSize.width / 2, winSize.height - 50); this.addChild(title, 1); //Create sprite and add it to the layer ball = CCSprite.spriteWithFile(@"images/Ball"); ball.position = new CCPoint(100, 300); this.addChild(ball);
首先,我们往屏幕中间加入一个精灵。如果你看了前面的教程的话,这里应该没有什么问题。这个加入一个Label是为了让精灵能够更好的显示,这个是cocos2d-x的一些问题,如果不添加一个Label,精灵的背景是黑的不是透明的。
//Create the world Vector2 gravity = new Vector2(0.0f, -30.0f); bool doSleep = true; world = new World(gravity, doSleep);
接下来,我们创建了world对象。当我们创建这个对象的时候,需要指定一个初始的重力向量。这里,我们设置y轴方向为-30,因此,所有的body都会往屏幕下面下落。同时,我们还指定了一个值,用以指明对象不参与碰撞时,是否可以“休眠”。一个休眠的对象将不会花费处理时间,直到它与其实对象发生碰撞的时候才会“醒”过来。
//Create edges around the entire screen BodyDef groundBodyDef = new BodyDef(); groundBodyDef.position = new Vector2(0, 0); Body groundBody = world.CreateBody(groundBodyDef); PolygonShape groundBox = new PolygonShape(); FixtureDef boxShapeDef = new FixtureDef(); boxShapeDef.shape = groundBox; groundBox.SetAsEdge(new Vector2(0, 0), new Vector2((float)(winSize.width / PTM_RATIO), 0)); groundBody.CreateFixture(boxShapeDef); groundBox.SetAsEdge(new Vector2(0, 0), new Vector2(0, (float)(winSize.height / PTM_RATIO))); groundBody.CreateFixture(boxShapeDef); groundBox.SetAsEdge(new Vector2(0, (float)(winSize.height / PTM_RATIO)), new Vector2((float)(winSize.width / PTM_RATIO), (float)(winSize.height / PTM_RATIO))); groundBody.CreateFixture(boxShapeDef); groundBox.SetAsEdge(new Vector2((float)(winSize.width / PTM_RATIO), (float)(winSize.height / PTM_RATIO)), new Vector2((float)(winSize.width / PTM_RATIO), 0)); groundBody.CreateFixture(boxShapeDef);
接下来,我们为整个屏幕创建了一圈不可见的边。具体的步骤如下:
· 首先创建一个body定义结构体,并且指定它应该放在左下角。
· 然后,使用world对象来创建body对象。(注意,这里一定要使用world对象来创建,不能直接new,因为world对象会做一些内存管理操作。)
· 接着,为屏幕的每一个边界创建一个多边形shape。这些“shape”仅仅是一些线段。注意,我们把像素转换成了“meter”。通过除以之前定义的比率来实现的。
· 再创建一个fixture定义,指定shape为polygon shape。
· 再使用body对象来为每一个shape创建一个fixture对象。
· 注意:一个body对象可以包含许许多多的fixture对象。
//Create ball body and shape BodyDef ballBodyDef = new BodyDef(); ballBodyDef.type = BodyType.Dynamic; ballBodyDef.position = new Vector2((float)(100 / PTM_RATIO), (float)(300 / PTM_RATIO)); ballBodyDef.userData = ball; body = world.CreateBody(ballBodyDef); CircleShape circle = new CircleShape(); circle._radius = (float)(26.0 / PTM_RATIO); FixtureDef ballShapeDef = new FixtureDef(); ballShapeDef.shape = circle; ballShapeDef.density = 1.0f; ballShapeDef.friction = 0.0f; ballShapeDef.restitution = 1.0f; body.CreateFixture(ballShapeDef);
接下来,我们创建篮球的body。这个步骤和之前创建地面的body差不多,但是有下面一些差别需要注意一下:
· 我们指定body的类型为dynamic body。默认值是static body,那意味着那个body不能被移动也不会参与仿真。很明显,我们想让篮球参与仿真。
· 设置body的user data属性为篮球精灵。你可以设置任何东西,但是,你设置成精灵会很方便,特别是当两个body碰撞的时候,你可以通过这个参数把精灵对象取出来,然后做一些逻辑处理。
· 这里使用了一个不同的shape类型--circle shape。
· 在这里,我们需要为这个fixture指定一些参数,因此,我们没有使用便捷方法来创建fixture。后面我们会讲到这些参数的具体意义。
this.schedule(tick);
最后一件事情就是调度一个tick方法。
实现tick方法:
void tick(float dt) { world.Step(dt, 10, 10); for (Body b = world.GetBodyList(); b != null;b = b.GetNext() ) { if (b.GetUserData() != null) { CCSprite ballData = (CCSprite)b.GetUserData(); ballData.position = new CCPoint((float)(b.GetPosition().X * PTM_RATIO), (float)(b.GetPosition().Y * PTM_RATIO)); ballData.rotation = -1 * MathHelper.ToDegrees(b.GetAngle()); } } }
第一件事情就是调用world对象的step方法,这样它就可以进行物理仿真了。这里的两个参数分别是“速度迭代次数”和“位置迭代次数”--你应该设置他们的范围在8-10之间。(译者:这里的数字越小,精度越小,但是效率更高。数字越大,仿真越精确,但同时耗时更多。8一般是个折中,如果学过数值分析,应该知道迭代步数的具体作用)。
接下来,我们要使我们的精灵匹配物理仿真。因此,我们遍历world对象里面的所有body,然后看body的user data属性是否为空,如果不为空,就可以强制转换成精灵对象。接下来,就可以根据body的位置来更新精灵的位置了。
现在我们再添加一些代码作为层的初始化用。
public static new CCLayer node() { BOX2DLayer layer = new BOX2DLayer(); if (layer.init()) { return layer; } else layer = null; return layer; }
然后再修改AppDelegate.cs里面的applicationDidFinishLaunching。在这个导演类里面修改初始场景。
修改如下:
// create a scene. it's an autorelease object //CCScene pScene = cocos2dBOX2DDemoScene.scene(); CCScene pScene = CCScene.node(); pScene.addChild(Classes.BOX2DLayer.node()); //run pDirector.runWithScene(pScene); return true;
编译并运行,你应该可以看到球会往下掉,并且会从屏幕底部往上面弹起来。
关于仿真的一些注意事项
前面我们说后面会讨论density,friction和restitution参数的意义。
· Density就是单位体积的质量(密度)。因此,一个对象的密度越大,那么它就有更多的质量,当然就会越难以移动.
· Friction 就是摩擦力。它的范围是0-1.0, 0意味着没有摩擦,1代表最大摩擦,几乎移不动的摩擦。
· Restitution回复力。它的范围也是0到1.0. 0意味着对象碰撞之后不会反弹,1意味着是完全弹性碰撞,会以同样的速度反弹。
建议多去改一改这些参数,看看具体会给小球带来什么影响。一定要去试哦!
如果你想学习更多有关box2d相关的内容,请继续关注。接下来会带来更好的教程。
本次工程下载:http://dl.dbank.com/c0mx9imdgw
深入学习:<cocos2d-x for wp7>使用cocos2d-x和BOX2D来制作一个BreakOut(打砖块)游戏(一)