使用box2d來做碰撞檢測(且僅用來做碰撞檢測)



本教程基於子龍山人翻譯的cocos2d的IPHONE教程,用cocos2d-x for XNA引擎重寫,加上我一些加工製作。教程中大多數文字圖片都是原作者和翻譯作者子龍山人,還有不少是我自己的理解和加工。感謝原作者的教程和子龍山人的翻譯。本教程僅供學習交流之用,切勿進行商業傳播。

子龍山人翻譯的Iphone教程地址:http://www.cnblogs.com/zilongshanren/archive/2011/06/08/2074582.html

Iphone教程原文地址:http://www.raywenderlich.com/606/how-to-use-box2d-for-just-collision-detection-with-cocos2d-iphone

當你使用cocos2d-x來製作一個遊戲的時候,有時,你可能想使用cocos2d-x的action來移動遊戲中的對象,而不是直接使用Box2d物理引擎來做。然而,這並不是說你不能使用box2d提供的強大的碰撞檢測功能!

  這個教程的目的,就是一步一步地向你展示如何僅使用box2d來做碰撞檢測---沒有物理效果。我們將創建一個簡單的demo,裏面有一輛車在屏幕上奔馳,當它撞到一隻貓後就會放聲大笑。是的,我明白,這樣做太殘忍了。

這個簡單假設你已經閱讀過前面的一系列的cocos2d-xbox2d的教程了(如果沒有,最好把前面的教程先看一遍),或者你有相關經驗也可以。

開始吧

現在打開VS2010,新建一個cocos2d-x項目,並且命名爲cocos2dBox2DCollisionDemo,因爲我們不需要社區支持,所以新建時去掉那個Live社區的勾。接着乾和以前教程一樣的事情,修復DLL,添加DLL。並且添加BOX2D.XNA.DLL這個引用。

現在下載我們需要的兩個圖片,貓(http://dl.dbank.com/c0zpgznny1)和車(http://dl.dbank.com/c0cd6ax2x5),並且添加到images文件夾。

這次,我們直接在HelloWorld這個層做修改。打開HelloWorldScene.cs這個文件。先把init()清理下,把那些label,圖片精靈等代碼刪除。

現在往這個類裏面添加一個聲明:

public static double PTM_RATIO = 32.0;

如果你不明白我在這裏講的是些什麼,你可能需要看看以前的BOX2D的教程來獲取更多的信息。

接着修改Init的代碼爲:

        public override bool init()
        {
            CCDirector.sharedDirector().deviceOrientation = ccDeviceOrientation.CCDeviceOrientationLandscapeLeft;
            if (!base.init())
            {
                return false;
            }

            CCLabelTTF title = CCLabelTTF.labelWithString("Collision", "Arial", 24);
            title.position = new CCPoint(400, 400);
            this.addChild(title);

            spawnCar();

            this.schedule(secondUpdate, 1.0f);
            return true;
        }

我們先添加一個Label,作用就是爲了能夠使精靈透明化,不添加的話精靈的背景是黑色的。在這之後,我們調用一個函數在場景中顯示一輛車。同時,還設置一個計時器,每隔一秒調用一次secondUpdate函數。

  接下來,讓我們實現spawnCar方法。我們的做法是讓車子永遠地在屏幕中間做路徑爲三角形的運動。在init函數的後面添加下面函數代碼:

        void spawnCar()
        {
            CCSprite car = CCSprite.spriteWithFile(@"images/car");
            car.position = new CCPoint(100, 100);
            car.tag = 2;

            car.runAction(CCRepeatForever.actionWithAction(
                (CCActionInterval)CCSequence.actions(
                CCMoveTo.actionWithDuration(1.0f, new CCPoint(300, 100)),
                CCMoveTo.actionWithDuration(1.0f, new CCPoint(200, 200)),
                CCMoveTo.actionWithDuration(1.0f, new CCPoint(100, 100))
                )));
            this.addChild(car);
        }

這個函數你應該比較熟悉了。那麼,讓我們添加一些貓吧!在上面的spawnCar方法後面添加下面的方法:

         void spawnCat()
        {
            CCSize winSize = CCDirector.sharedDirector().getWinSize();
            CCSprite cat = CCSprite.spriteWithFile(@"images/cat");
            float minY = cat.contentSize.height / 2;
            float maxY = winSize.height - cat.contentSize.height / 2;
            float rangeY = maxY - minY;
            Random random = new Random();
            float acturalY = random.Next() % rangeY;


            float startX = winSize.width + cat.contentSize.width / 2;
            float endX = -cat.contentSize.width / 2;
            CCPoint startPos = new CCPoint(startX, acturalY);
            CCPoint endPos = new CCPoint(endX, acturalY);


            cat.position = startPos;
            cat.tag = 1;


            cat.runAction(CCSequence.actions(
                CCMoveTo.actionWithDuration(1.0f, endPos), CCCallFuncN.actionWithTarget(this, spriteDone)));
            this.addChild(cat);
        }


        void spriteDone(object sender)
        {
            CCSprite sprite = sender as CCSprite;
            this.removeChild(sprite, true);
        }


        void secondUpdate(float dt)
        {
            this.spawnCat();
        }

你應該對上面的代碼線路熟悉了,如果不熟悉,建議看相關的教程後再繼續。編譯並運行代碼,如果一切ok,你將會看到一輛車在屏幕上來回動,同時有一隻貓從右至左穿過屏幕。接下來,我們將添加一些碰撞檢測的代碼。



爲這些精靈創建Box2d的body

  接下來的一步就是爲每個精靈創建一個body,這樣box2d就能知道它們的位置了,這樣的話,當碰撞發生的時候,我們就可以被告知了。下面所做的事情和之前的教程做法差不多。

  然後,這一次,我們不是更新box2d的body,然後再更新sprite。這裏,我們是先更新sprite(使用action或者別的),然後再更新box2d的body。

現在我們先在HelloWorldScene裏面添加命名空間的引用。

using Box2D.XNA;

然後往類中添加變量聲明;

World world;

然後在init方法中加入下列代碼:

            Vector2 gravity = new Vector2(0, 0);
            bool doSleep = false;
            world = new World(gravity, doSleep);

注意,這裏有兩件事情非常重要!首先,我們把重力向量設置成(0,0)。因爲我們並不想讓這些對象自動運動,而是人爲控制其運動。其次,我們告訴box2d不能讓這些對象sleep。這一點非常重要,因爲我們是人爲地移動對象,所以必須設置。

然後,在spawnCat方法上面添加下列代碼:

        void addBoxBodyForSprite(CCSprite sprite)
        {
            BodyDef spriteBodyDef = new BodyDef();
            spriteBodyDef.type = BodyType.Dynamic;
            spriteBodyDef.position = new Vector2((float)(sprite.position.x / PTM_RATIO), (float)(sprite.position.y / PTM_RATIO));
            spriteBodyDef.userData = sprite;
            Body spriteBody = world.CreateBody(spriteBodyDef);

            PolygonShape spriteShape = new PolygonShape();
            spriteShape.SetAsBox((float)(sprite.contentSize.width / PTM_RATIO / 2), (float)(sprite.contentSize.height / PTM_RATIO / 2));
            FixtureDef spriteShapeDef = new FixtureDef();
            spriteShapeDef.shape = spriteShape;
            spriteShapeDef.density = 10.0f;
            spriteShapeDef.isSensor = true;
            spriteBody.CreateFixture(spriteShapeDef);
        }

這段代碼對你來說應該比較熟悉了---它和我們在breakout教程中是一樣的。然後,這裏有一點區別,我們把shape定義的isSensor成員設置成了true。

  根據Box參考手冊,如果你想讓對象之間有碰撞檢測但是又不想讓它們有碰撞反應,那麼你就需要把isSensor設置成true。這正是我們想要的效果!

接下來,在spawnCat的最後一行addChild之前添加下列代碼:

this.addBoxBodyForSprite(cat);
同樣的,你需要在spawnCar的相應位置添加下列代碼:

this.addBoxBodyForSprite(car);

我們的sprites被銷燬的時候,我們需要銷燬Box2d的body。因此,把你的spriteDone方法改寫成下面的形式:

        void spriteDone(object sender)
        {
            CCSprite sprite = (CCSprite)sender;

            Body spriteBody = null;
            for (Body b = world.GetBodyList(); b != null; b = b.GetNext())
            {
                if (b.GetUserData() != null)
                {
                    CCSprite curSprite = (CCSprite)b.GetUserData();
                    if (sprite == curSprite)
                    {
                        spriteBody = b;
                    }
                }
            }

            if (spriteBody != null)
            {
                world.DestroyBody(spriteBody);
            }
            this.removeChild(sprite, true);
        }

現在,最重要的一部分,我們需要根據sprite的位置改變來更新box2d的body。因此,在init方法裏面添加下面一行代碼:

            this.schedule(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 sprite = (CCSprite)b.GetUserData();

                    Vector2 position = new Vector2((float)(sprite.position.x / PTM_RATIO), (float)(sprite.position.y / PTM_RATIO));
                    float angle = -1 * MathHelper.ToRadians(sprite.rotation);
                    b.SetTransform(position, angle);
                }
            }
        }

這個方法和我們之前在breakout中寫的tick方法差不多,唯一的差別就是,我們現在是基於cocos2d的精靈位置來更新box2d的body位置。


碰撞檢測

  現在,是時候壓死幾隻貓了!

像之前的breakout遊戲一樣,我們將往world對象裏面註冊一個contact listener對象。寫法和之前的一樣,如果你沒寫過,請直接到<cocos2d-x for wp7>使用cocos2d-x和BOX2D來製作一個BreakOut(打磚塊)遊戲(二)裏面複製代碼即可。把MyContactListener類添加到Classes文件夾。

同時,你還可以下載音效文件http://dl.dbank.com/c09t65htr4,直接把resource文件夾添加到Content工程。另外添加對CocosDenshion.dll的引用

往HelloWorldScene類中添加一個變量聲明:

        MyContactListener contactListener;

然後,在init方法中加入下面的代碼:
            //Create contact listener
            contactListener = new MyContactListener();
            world.ContactListener = contactListener;
            //Preload effect
            SimpleAudioEngine.sharedEngine().preloadEffect(@"resource/hahaha");

最後,在tick方法的底部加入下面的代碼:

List<Body> toDestroy = new List<Body>();
            foreach (var item in contactListener.contacts)
            {
                Body bodyA = item.fixtureA.GetBody();
                Body bodyB = item.fixtureB.GetBody();
                if (bodyA.GetUserData() != null && bodyB.GetUserData() != null)
                {
                    CCSprite spriteA = (CCSprite)bodyA.GetUserData();
                    CCSprite spriteB = (CCSprite)bodyB.GetUserData();
                    if (spriteA.tag == 1 && spriteB.tag == 2)
                    {
                        toDestroy.Add(bodyA);
                    }
                    else if (spriteA.tag == 2 && spriteB.tag == 1)
                        toDestroy.Add(bodyB);
                }
            }
            foreach (var item in toDestroy)
            {
                if (item.GetUserData() != null)
                {
                    CCSprite sprite = (CCSprite)item.GetUserData();
                    this.removeChild(sprite,true);
                }
                world.DestroyBody(item);
            }
            if (toDestroy.Count > 0)
            {
                SimpleAudioEngine.sharedEngine().playEffect(@"resource/hahaha");
            }
這段代碼和我們之前的breakout教程中的代碼基本上差不多,因此你應該比較熟悉了。
再運行一下工程看看!變態的貓被壓死還會叫。。。。


調整shape的邊界

  你可能已經注意到了,我們的box2d shape的邊界並不是和sprite的邊界完全吻合。對於有些碰撞檢測要求不是特別精確的遊戲,這也許夠了,但是,有些遊戲確不行!我們需要嚴格地定義shape的邊界和精靈的邊界重合在一起。

  在Box2d裏面,你可以通過指定shape的頂點來指定shape的形狀。然後,硬編碼這些頂點數據會很耗時,而且容易出錯。MAC上有個工具叫 VertexHelper,它可以用來非常方便地定義頂點,而且可以導出box2d所需要的格式的數據。但是window上沒有,,這個有點糾結了。不過你如果看過這個軟件,其實也沒什麼大不了的,也就一個點的座標讀取轉換工具。有興趣的朋友可以做這麼一個軟件。軟件原理可以從下面的教程基本看出。

我們先看下這個叫VertexHelper軟件生成的數據:


看上圖,這個右下角的框生成的數據是相對那隻貓而言的。仔細看的朋友就會發現。其實沒什麼大不了的數據。也就在貓上點了6個點,然後再貓圖上把這6個點連線,然後生成代碼。那麼這些點怎麼定義的呢。仔細的朋友應該也發現了。其實座標就是以圖片中心爲原點的做的一個座標軸。然後就讀取相關的點的數據。但是我們在沒有這類軟件情況下,怎麼才能快速得到這些點的座標呢。

那麼我們來用window強大的畫圖工具來獲取吧:

用畫圖工具打開貓的圖片:


看上圖,就會發現,我在貓的左耳上面把鼠標放置在上面,在右下角能看到,當前像素是(8,1),然而,我們這張圖的像素是64*64.中心點的座標應該爲(32,32)。如果用這點作爲原點的座標軸,我們用筆算下上面(8,1)的座標的相對座標就爲(-24,31)。哈哈,大家是否沒想到強大的畫圖軟件也有這麼用的。

這樣我們就能確定貓的shape的頂點,同樣的,車的也能夠獲取。

後修改 addBoxBodyForSprite方法。首先註釋掉一些代碼,如下所示:

            //spriteShape.SetAsBox((float)(sprite.contentSize.width / PTM_RATIO / 2), (float)(sprite.contentSize.height / PTM_RATIO / 2));
            if (sprite.tag == 1)
            {
                //for the cat 
                int num = 6;
                Vector2[] verts = { new Vector2((float)(4.5f/PTM_RATIO), (float)(-17.7f/PTM_RATIO)),
                                   new Vector2((float)(20.5f/PTM_RATIO), (float)(7.2f/PTM_RATIO)),
                                  new Vector2((float)(22.8f/PTM_RATIO), (float)(29.5f/PTM_RATIO)),
                                  new Vector2((float)(-24.7f/PTM_RATIO), (float)(31.0f/PTM_RATIO)),
                                  new Vector2((float)(-20.2f/PTM_RATIO), (float)(4.7f/PTM_RATIO)),
                                  new Vector2((float)(-11.7f/PTM_RATIO), (float)(-17.5f/PTM_RATIO))
                                  };
                spriteShape.Set(verts, num);

            }
            else
                spriteShape.SetAsBox((float)(sprite.contentSize.width / PTM_RATIO / 2), (float)(sprite.contentSize.height / PTM_RATIO / 2));
一旦做完之後,編譯並運行工程。當然碰撞檢測就會更加真實了!
恩,你已經看到了使用box2d來做碰撞檢測的強大了!

本次工程下載:http://dl.dbank.com/c0knp7jx0j



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章