官术网_书友最值得收藏!

10.4 在Cocos2d-x中使用Box2d

在Cocos2d-x中使用Box2d有兩個要點,第一點是在Cocos2d-x中完成物理引擎的初始化和更新,第二點是將Cocos2d-x的渲染和Box2D的物理模擬結合起來。本節將由淺入深地介紹Box2d在Cocos2d-x中的應用,將物理引擎嵌入Cocos2d-x框架中,從對象物理模擬和Cocos2d-x渲染,到碰撞監聽的回調,再到安全地刪除剛體,釋放物理世界。

10.4.1 物理世界

首先需要有一個場景,將這個場景作為我們的物理世界,所以這個場景需要完成第一個任務,即在onEnter或init的時候,初始化物理世界,設置好重力。

        b2Vec2 gravity;
        gravity.Set(0.0f, -10.0f);
        m_World = new b2World(gravity);
        m_World->SetAllowSleeping(true);
        m_World->SetContinuousPhysics(true);

在每一幀的update中,都要執行物理世界的更新,這只是很簡單的幾行代碼。

        int velocityIterations = 8;
        int positionIterations = 1;
        m_World->Step(dt, velocityIterations, positionIterations);

創建World,更新World,放在場景中是沒有問題的,但是假設把這些物理相關的東西,封裝到一個物理管理器里面作為一個單例,在Cocos2d-x中使用會更好一些。這樣做的好處是,把物理框架從場景分離開,使物理框架的功能更獨立一些,清晰一些,整體的耦合性小一些。并且在很多地方,可能需要使用物理引擎來做一些東西。假如需要先找到場景節點,然后獲取它的World,那么這樣做會使整體的耦合度變得很高,而如果封裝到單例里面,這些操作就會變得“優雅”很多。

        class CPhysicsManager
        {
        private:
            CPhysicsManager(void);
            virtual ~CPhysicsManager(void);

        public:
            static CPhysicsManager* getInstance();

            //在一場游戲結束之后應該調用
            static void destory();

            //更新物理世界
            void update(float dt);

            inline bool isLocked()
            {
              if (NULL == m_World)
              {
                  return false;
              }

              return m_World->IsLocked();
            }

            //獲取世界
            inline b2World* getWorld()
            {
                return m_World;
            }

        private:
            static CPhysicsManager* m_Instance;
            b2World* m_World;
            CPhysicsListener* m_ContactListener;
        };

筆者是這樣設計這個單例的,這個單例會負責維護兩項,一個是我們的物理世界,在構造函數中初始化物理世界,另外一個是我們自定義的碰撞監聽器,在絕大部分情況下總是需要它的。在構造函數中會初始化物理世界,而update()函數將會驅動物理世界的Step進行模擬。

        CPhysicsManager::CPhysicsManager(void)
        {
            //創建碰撞監聽器
            m_ContactListener = new CPhysicsListener();

            //初始化物理世界
            b2Vec2 gravity;
            gravity.Set(0.0f, -10.0f);
            m_World = new b2World(gravity);

            //設置碰撞監聽器
            m_World->SetContactListener(m_ContactListener);

            //允許剛體睡眠
            m_World->SetAllowSleeping(true);
            //激活連續碰撞檢測
            m_World->SetContinuousPhysics(true);
        }

最后在游戲場景初始化的時候,調用單例的初始化函數,在游戲場景退出的時候,調用單例的銷毀函數,因為當玩家退回到主界面的時候,物理世界不應該繼續模擬了,而當玩家進入游戲的時候,物理世界必須是一個新的世界,不能使用上一個關卡所遺留的數據來模擬。當然,需要在游戲場景節點的update()函數中調用CPhysicsManager的update,這里沒有說釋放,但應在析構函數中,把new出來的CPhysicsListener和b2World釋放掉。

接下來還需要創建場景的邊界,用一個包圍盒把場景框住,在設置和Box2d相關的大小變量時,一般都要除以一個PTM_RATIO常量,這個常量一般是32,表示像素和物理單位米的比例,因為在設置物體大小的時候,按照現實世界的比例來設置是比較好的,假設沒有這個參數,那就會變成1像素=1米,在大多數情況下,32像素=1米的比例能夠更好地工作。當然這只是一個比例問題。讓顯示對象縮小到1/32或其他比例以適應物理對象,或者讓物理對象放大32倍來適應顯示對象,關鍵的地方在于物理對象的大小和顯示對象的大小是否相等。

        //定義包圍盒
        b2BodyDef groundBodyDef;
        groundBodyDef.position.Set(0, 0); //bottom-left corner
        //調用世界工廠的方法創建剛體
        b2Body* groundBody = m_World->CreateBody(&groundBodyDef);

        //定義包圍盒的形狀
        b2EdgeShape groundBox;

        //設置包圍盒的底部
        groundBox.Set(b2Vec2(VisibleRect::leftBottom().x/PTM_RATIO,
            VisibleRect::leftBottom().y/PTM_RATIO),
        b2Vec2(VisibleRect::rightBottom().x/PTM_RATIO,
            VisibleRect::rightBottom().y/PTM_RATIO));
        groundBody->CreateFixture(&groundBox,0);

        //設置包圍盒的頂部
        groundBox.Set(b2Vec2(VisibleRect::leftTop().x/PTM_RATIO,
            VisibleRect::leftTop().y/PTM_RATIO),
        b2Vec2(VisibleRect::rightTop().x/PTM_RATIO,
            VisibleRect::rightTop().y/PTM_RATIO));
        groundBody->CreateFixture(&groundBox,0);

        //設置包圍盒的左邊
        groundBox.Set(b2Vec2(VisibleRect::leftTop().x/PTM_RATIO,
            VisibleRect::leftTop().y/PTM_RATIO),
        b2Vec2(VisibleRect::leftBottom().x/PTM_RATIO,
            VisibleRect::leftBottom().y/PTM_RATIO));
        groundBody->CreateFixture(&groundBox,0);

        //設置包圍盒的右邊
        groundBox.Set(b2Vec2(VisibleRect::rightBottom().x/PTM_RATIO,
            VisibleRect::rightBottom().y/PTM_RATIO),
        b2Vec2(VisibleRect::rightTop().x/PTM_RATIO,
            VisibleRect::rightTop().y/PTM_RATIO));
        groundBody->CreateFixture(&groundBox,0);

我們能且只能通過World的create()方法來創建剛體,如果直接用new或者malloc來創建剛體,那么創建的剛體將不在這個世界之內,也不會和物理世界有任何交集。上面創建包圍盒的代碼,應該寫在場景中,因為這并不屬于物理框架的內容,物理框架不會知道,創建的這個場景的地形長什么樣的,有多大。

10.4.2 物理Sprite

接下來要添加場景內的東西了,主要是把顯示對象Sprite和b2Body結合起來,雙繼承也許會是一個好主意,但可能存在比較多的爭議,將b2Body作為Sprite的一個成員變量已經可以比較好地工作了,為什么是b2Body作為Sprite的成員變量,而不是反過來呢?首先,編碼的時候可能會頻繁用到Sprite里面的東西,但是b2Body可能很少問津。另外,當Body被銷毀的時候,Sprite可能需要繼續存在于場景中,對于Body,只是需要使用其物理特性而已。

首先需要有一個繼承于Sprite的類,因為需要為其添加一些成員變量,一個b2Body指針,在onEnter的時候初始化這個指針,可以在onExit或者析構函數中釋放它,繼承于Sprite的類先管其叫CPhysicsObject,可以在init中初始化物理剛體,這塊在描述不同的CPhysicsObject時,可以根據需要重寫這部分的代碼。下面的一小段代碼只是用來介紹,在Cocos2d-x中創建剛體的過程,實際上CPhysicsObject應該作為一個純粹的物理對象基類來使用,不應該在這里添加創建剛體的代碼,應該由子類來完成這個任務。

        b2BodyDef bodyDef;
        bodyDef.type = b2_dynamicBody;
        bodyDef.position.Set(getPositionX() / PTM_RATIO, getPositionY() / PTM_
        RATIO);

        m_Body = world->CreateBody(&bodyDef);

        b2PolygonShape dynamicBox;
        Size sz = getContentSize();
        //根據精靈圖片的大小以及像素和米的比例,來設置包圍盒
        dynamicBox.SetAsBox(sz.width * 0.5f / PTM_RATIO, sz.height * 0.5f / PTM_
        RATIO);

        //設置好動態剛體的屬性,然后配置給Body
        b2FixtureDef fixtureDef;
        fixtureDef.shape = &dynamicBox;
        fixtureDef.density = 1.0f;
        fixtureDef.friction = 0.3f;
        m_Body->CreateFixture(&fixtureDef);

在onExit()函數中,析構或者任何想要刪除剛體的時候,需要調用下面的代碼來釋放。

        void CPhysicsObject::onExit()
        {
            if (NULL ! = m_Body)
            {
              CPhysicsManager::getInstance()->getWorld()->DestroyBody(m_Body);
              m_Body = NULL;
            }

            Sprite::onExit();
        }

有了剛體之后,需要注意一件事情,就是不要再調用這個剛體的setPosition()方法來改變剛體的位置,如果要設置,需要同時更新m_Body的位置屬性,強制設置位置會導致物理模擬出錯。

另外還應該做一個事情,就是把m_Body的位置,旋轉等屬性同步到Sprite中。在Node中有一個nodeToParentTransform()函數,用于返回一個描述節點當前的旋轉和位置的矩陣,在Node中是根據當前節點的位置、錨點,以及旋轉來計算這個矩陣的,在這里用m_pBody的位置和旋轉來計算。

        AffineTransform CPhysicsObject::nodeToParentTransform(void)
        {
            if (NULL == m_Body)
            {
              return Sprite::nodeToParentTransform();
            }

            b2Vec2 pos  = m_Body->GetPosition();

            float x = pos.x * PTM_RATIO;
            float y = pos.y * PTM_RATIO;

            if ( isIgnoreAnchorPointForPosition() ) {
              x += m_tAnchorPointInPoints.x;
              y += m_tAnchorPointInPoints.y;
          }

          //Make matrix
          float radians = m_Body->GetAngle();
          float c = cosf(radians);
          float s = sinf(radians);

          if( ! m_tAnchorPointInPoints.equals(CCPointZero) ){
              x += ((c * -m_tAnchorPointInPoints.x * m_fScaleX) + (-s * -m_
              tAnchorPointInPoints.y * m_fScaleY));
              y += ((s * -m_tAnchorPointInPoints.x * m_fScaleX) + (c * -m_
              tAnchorPointInPoints.y * m_fScaleY));
          }

          //Rot, Translate Matrix
          m_tTransform = AffineTransformMake( c * m_fScaleX,   s * m_fScaleX,
              -s * m_fScaleY,   c * m_fScaleY,
              x,    y );

          return m_tTransform;
        }

nodeToParentTransform()函數在對象需要被重繪的時候調用,Cocos2d-x根據isDirty虛函數的返回值,來決定是否重繪。正常情況下,當玩家的位置、大小、旋轉發生改變的時候,nodeToParentTransform()函數就會返回true,而在使用了Box2d的情況下,CPhysicsObject應該在物體的運動狀態下,返回true,而在靜止狀態下,返回false,可以直接返回body的IsAwake()函數,當剛體醒著的時候更新,當剛體靜止下來的時候,停止更新。

        bool CPhysicsObject::isDirty()
        {
            if (NULL ! = m_Body)
            {
              return m_Body->IsAwake();
            }

            return CCSprite::isDirty();
        }

CPhysicsObject還需要重寫一些接口,用于設置位置和旋轉,因為在設置旋轉和位置的時候,需要同步到物理世界,而在獲取位置和旋轉的時候,也需要從物理世界中獲取。

        const CCPoint& CPhysicsObject::getPosition()
        {
            if (NULL == m_Body)
            {
              return CCSprite::getPosition();
            }

            b2Vec2 pos = m_Body->GetPosition();

            float x = pos.x * PTM_RATIO;
            float y = pos.y * PTM_RATIO;
            m_tPosition = ccp(x, y);
            return m_tPosition;
        }
        void CPhysicsObject::setPosition(const CCPoint &pos)
        {
            if (NULL == m_Body)
            {
              return Sprite::setPosition(pos);
            }
            float angle = m_Body->GetAngle();
            m_Body->SetTransform(b2Vec2(pos.x / PTM_RATIO, pos.y / PTM_RATIO),
            angle);
        }
        float CPhysicsObject::getRotation()
        {
            if (NULL == m_Body)
            {
              return Sprite::getRotation();
            }
            return CC_RADIANS_TO_DEGREES(m_Body->GetAngle());
        }
        void CPhysicsObject::setRotation(float fRotation)
        {
            if (NULL == m_Body)
            {
              return Sprite::setRotation(fRotation);
            }
            else
            {
              b2Vec2 p = m_Body->GetPosition();
              float radians = CC_DEGREES_TO_RADIANS(fRotation);
              m_Body->SetTransform(p, radians);
            }
        }

10.4.3 碰撞處理

現在我們有了一個物理場景,以及物理節點,這個帶有物理屬性的節點可以正常顯示,那么接下來還需要一個碰撞監聽器,雖然這不是必須的,但在每次碰撞發生的時候,告訴節點,被碰了一下或者說跟誰碰到一起了是非常有用的。例如,憤怒的小鳥游戲,玩家發射出去的小鳥,不同的小鳥碰到不同的障礙,效果是不一樣的,普通小鳥碰到冰塊時穿透力很低,而藍色小鳥碰到冰塊時會有非常強的穿透力。黑色小鳥碰到障礙時會直接爆炸。這些都是由碰撞觸發的,從而根據碰撞信息進行相對應的處理。

      class CPhysicsListener :
          public b2ContactListener
      {
      public:
          CPhysicsListener(void);
          virtual ~CPhysicsListener(void);

          //當兩個對象互相碰撞
          virtual void BeginContact(b2Contact* contact);
          //當兩個對象碰撞結束
          virtual void EndContact(b2Contact* contact);

          //當兩個對象準備進行物理模擬之前調用
          virtual void PreSolve(b2Contact* contact, const b2Manifold*
          oldManifold);

          //當兩個對象完成了物理模擬之后調用
          virtual void PostSolve(b2Contact* contact, const b2ContactImpulse*
          impulse);

          //處理每一幀的物理碰撞事件
          void Execute();

      private:
          std::set<CPhysicsObject*> m_PhysicsObjets;
      };

想要處理好物理碰撞,那么就需要一個碰撞監聽器,碰撞監聽器的實現很簡單,先寫一個空的碰撞監聽器,這個碰撞監聽器的關鍵在于,如何把碰撞消息傳遞給物理節點,這需要通過一個Body獲得一個PhysicsObject,那么最好的方法就是,將這個PhysicsObject放到Body的UserData中。因此在碰撞監聽器中,需要重寫4個碰撞回調函數,而在我們的物理節點基類中,也需要對應4個接口,來接收這4種碰撞消息。例如下面的代碼。

      void  CPhysicsListener::PreSolve(b2Contact*  contact,  const  b2Manifold*
      oldManifold)
      {
          //碰撞的第一個剛體如果是一個CPhysicObject(用userData判斷),那么調用它的回調
          CPhysicsObject* objA = reinterpret_cast<CPhysicsObject*>
          (contact->GetFixtureA()
            ->GetBody()->GetUserData());
          if (NULL ! = objA)
          {
            objA->beforeSimulate(contact, oldManifold);
          }

          //接下來判斷第二個剛體
          CPhysicsObject* objB = reinterpret_cast<CPhysicsObject*>
          (contact->GetFixtureB()
            ->GetBody()->GetUserData());
          if (NULL ! = objB)
          {
            objB->beforeSimulate(contact, oldManifold);
          }
      }

其他幾個接口的實現與其類似,都是簡單地轉發消息,但需要注意的一點是,不要在這些回調函數中改變剛體,因為這會對物理模擬造成影響,特別是不要刪除剛體,否則可能導致程序崩潰。假設需要在碰撞發生的時候改變剛體,那么可以在碰撞發生的時候記錄狀態,在物理模擬完成之后,再進行改變。同樣,刪除剛體,也是需要在物理模擬完成之后再進行刪除。

假設需要在碰撞的時候改變剛體的屬性,例如,讓一個物體在被碰到的時候破碎,如玻璃杯,或者是碰到某個物體之后變重,如海綿碰到水,這種情況下可以在監聽器中增加一個Execute()方法,在World的Step執行之前或之后來執行該方法,在該方法中,將調用這一幀,所有觸發碰撞的對象的一個方法,來執行這些操作,包括刪除剛體。

        void CPhysicsManager::update(float dt)
        {
            //觸發碰撞事件,交給監聽者處理
            m_ContactListener->Execute();

            int velocityIterations = 8;
            int positionIterations = 1;
            m_World->Step(dt, velocityIterations, positionIterations);
        }

在每次事件觸發的時候,對上面的代碼小小改動一下,將PhysicsObject添加到一個容器中,緩存起來。

      CPhysicsObject* objA = reinterpret_cast<CPhysicsObject*>
      (contact->GetFixtureA()
          ->GetBody()->GetUserData());
      if (NULL ! = objA)
      {
          //添加到一個Set容器中
          m_PhysicsObjets.insert(objA);
          objA->beforeSimulate(contact, oldManifold);
      }

然后在每一幀都會執行的Execute()方法中,遍歷這一幀所有觸發事件的對象,并調用它們的processOver()函數,在所有物理對象的processOver()函數中,可以根據當前的狀態改變剛體或者銷毀剛體。

      for (set<CPhysicsObject*>::iterator iter = m_PhysicsObjets.begin(); iter ! =
      m_PhysicsObjets.end(); ++iter)
      {
          CPhysicsObject* obj = *iter;
          obj->processOver();
      }

      m_PhysicsObjets.clear();

這里面有一個陷阱,可能導致一個對象被銷毀之后,仍然觸發其碰撞監聽。這是非常可怕的一件事情,意味著程序很可能因此而崩潰!那就是在銷毀一個剛體的時候,假設這個剛體正和其他對象發生了接觸,那么這個時候,會有一個在Step之外的EndContact回調被觸發,這是合理的,但很容易被忽視,并且產生BUG。正常的流程如圖10-2所示,在一個Step中完成所有的觸發,而圖10-3演示了需要多個Step才能處理完一次接觸的情況。

圖10-2 一次Step完成

圖10-3 需要多次Step

要解決這個BUG其實很簡單,就是在釋放剛體之前,先把剛體的UserData設置為NULL,這樣回調流程就無法觸發到PhysicsObject里面了。假設你的代碼期望收到這個EndContact回調,那么需要在processOver()函數里面把好關,防止因為重復的processOver()函數調用,導致重復釋放的問題,并且需要管理好UserData的引用計數。

主站蜘蛛池模板: 福清市| 巫山县| 崇信县| 炉霍县| 澄城县| 大田县| 金门县| 兖州市| 靖西县| 墨脱县| 凤冈县| 贵港市| 临桂县| 新安县| 福清市| 彭水| 九江县| 万安县| 清徐县| 天水市| 隆子县| 汉中市| 灌阳县| 海原县| 肇庆市| 怀安县| 赤壁市| 时尚| 安仁县| 皮山县| 稷山县| 麻城市| 繁峙县| 永定县| 临江市| 新郑市| 镇巴县| 延安市| 东丽区| 寿阳县| 石首市|