在cocos2d-x里面使用BOX2D

时间:2023-02-06 20:22:25

 

本教程基于子龙山人翻译的cocos2dIPHONE教程,用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定义,同时设置之前创建好的shapefixture的一个属性,并且设置其它的属性,比如质量或者摩擦力。

· 最后,你可以使用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.110个单元的对象的时候做了一些优化。这里的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定义,指定shapepolygon 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不能被移动也不会参与仿真。很明显,我们想让篮球参与仿真。

· 设置bodyuser 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,然后看bodyuser 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;

 

 

编译并运行,你应该可以看到球会往下掉,并且会从屏幕底部往上面弹起来。

 在cocos2d-x里面使用BOX2D

关于仿真的一些注意事项


  前面我们说后面会讨论densityfrictionrestitution参数的意义。

· Density就是单位体积的质量(密度)。因此,一个对象的密度越大,那么它就有更多的质量,当然就会越难以移动.

· Friction 就是摩擦力。它的范围是0-1.0, 0意味着没有摩擦,1代表最大摩擦,几乎移不动的摩擦。

· Restitution回复力。它的范围也是01.0. 0意味着对象碰撞之后不会反弹,1意味着是完全弹性碰撞,会以同样的速度反弹。


  建议多去改一改这些参数,看看具体会给小球带来什么影响。一定要去试哦!
如果你想学习更多有关box2d相关的内容,请继续关注。接下来会带来更好的教程。




本次工程下载:http://dl.dbank.com/c0mx9imdgw


 深入学习:<cocos2d-x for wp7>使用cocos2d-x和BOX2D来制作一个BreakOut(打砖块)游戏(一)