cocos2dx 3.2中的物理引擎初探(一)


  cocos2dx在設計之初就集成了兩套物理引擎,它們是box2d和chipmunk。我目前使用的是最新版的cocos2dx 3.2。引擎中默認使用的是chipmunk,如果想要改使用box2d的話,需要修改對應的android工程或者是ios工程的配置文件。

  在2.x版本的cocos中,使用物理引擎的步驟十分繁瑣。但在3.x版本中變得非常方便了。我這次的學習目標是制作一個打磚塊的小游戲。

  首先,現在的Scene類提供了一個靜態工廠方法,用以創造一個集成物理引擎的場景。  

 Scene::initWithPhysics()

  這個方法能讓你的場景具備創建物理世界的基本條件,接下來我們設置這個物理世界的重力條件,因為打磚塊游戲中不需要重力的影響,所以我們把該場景里的重力設置為0:

PhysicsWorld* world = getPhysicsWorld();
world->setGravity(Vec2(0,0));

  然后我們要給這個物理世界創造一個邊界,便於我們觀察效果,我的做法是把物理世界的scene和游戲邏輯的實現分開,新建一個繼承自layer的類來寫游戲邏輯:

  這是頭文件:

 1 #include "Util.h"
 2 
 3 #ifndef __FristGame__GameLayer__
 4 #define __FristGame__GameLayer__
 5 
 6 class GameLayer:public Layer
 7 {
 8 public:
 9     Sprite* ball;
10     Sprite* paddle;
11     Sprite* edgeSp;
12     Sprite* prop;
13     PhysicsBody* ballBody;
14     TMXTiledMap* map;
15     
16     CREATE_FUNC(GameLayer);
17     bool init();
18     
19     void loadPhysicsBody();//加載物理世界的邊界
20     void loadTileMap();//加載使用tiledmap地圖編輯器制作的地圖
21     void loadProp();//加載碰撞特定磚塊會掉落的道具
22     void update1(float dt);//打開一個定時器
23     void contact();//碰撞事件注冊
24 };
25 #endif /* defined(__FristGame__GameLayer__) */

  小提示:在3.2版本的物理世界中我們不能使用scheduleupdate()函數,似乎body(剛體)的運動是在update里處理的,一旦我們重寫了這個函數,物理世界中的小球就不再運動了。所以我們另外設置一個定時器update1來使用。

  這是cpp文件:

  1 #include "GameLayer.h"
  2 bool GameLayer::init()
  3 {
  4     loadTileMap();
  5     loadPhysicsBody();
  6     return true;
  7 }
  8 void GameLayer::loadPhysicsBody()
  9 {
 10     auto visibleSize = Director::getInstance()->getVisibleSize();//取得當前屏幕的尺寸size
 11     auto origin = Director::getInstance()->getVisibleOrigin();
 12     
 13     edgeSp = Sprite::create();//創建一個精靈
 14     auto boundBody = PhysicsBody::createEdgeBox(visibleSize,PhysicsMaterial(0.0f,1.0f,0.0f),3);//edgebox是不受剛體碰撞影響的一種剛體,我們用它來設置物理世界的邊界
 15     edgeSp->setPosition(visibleSize.width/2, visibleSize.height/2);//位置設置在屏幕中央
 16     edgeSp->setPhysicsBody(boundBody);//將精靈容納的剛體設置為boundbody。注意這里不能確定剛體和精靈是不是父子節點的關系。有興趣的朋友請自行研究。
 17     addChild(edgeSp);//加入渲染樹
 18     
 19     ball = Sprite::create("game_ball_a.png");//創建小球的精靈
 20     ball->setPosition(100,100);//設定位置在屏幕中下部
     //PhysicsMaterial是設置剛體屬性的類,三個參數分別對應三個屬性:1、density(密度)2、restiution(彈性)3、friction(摩擦力),在這個游戲中我們需要小球無限碰撞,因此摩擦力和密度都設為1,彈力設為1。
 21     ballBody = PhysicsBody::createCircle(ball->getContentSize().width/2,PhysicsMaterial(0.0f,1.0f,0.0f));
 22     ballBody->setContactTestBitmask(0xFFFFFFFF);//接觸掩碼值---------標注1------------(見代碼后)
 23     Vect force = Vect(1000.0f,1000.0f);
 24     ballBody->applyImpulse(force);//這個方法不會產生力,但是會讓一個速度與body的速度疊加 產生新的速度(通過這個方法我們讓小球勻速運動)
 25     ballBody->setVelocity(Vec2(150,150));//設置小球速度
 26     ball->setPhysicsBody(ballBody);
 27     addChild(ball);
 28     
 29     Sprite* batSprite = Sprite::create("game_av_d.png");//創建打磚塊游戲中的磚塊
 30     PhysicsBody* batBody = PhysicsBody::createEdgeBox(batSprite->getContentSize(),PhysicsMaterial(0.0f,1.0f,0.0f));//創建相應的剛體並設置材質 32     batSprite->setPhysicsBody(batBody);
 33     batSprite->setPosition(winSize.width/2,50);
 34     addChild(batSprite);
 35     batBody->setContactTestBitmask(0xFFFFFFFF);//設置接觸掩碼值
 36     EventListenerTouchOneByOne* ev1 = EventListenerTouchOneByOne::create();//3.x版本之后對觸摸事件做了全盤的修改,這里不作詳細描述。這是創建一個單點觸摸事件。
 37     ev1->onTouchBegan = [](Touch* touch,Event* ev){return true;};//touchbegin不作任何處理,跳過
 38     ev1->onTouchMoved = [=](Touch* touch,Event* ev){
 39         float x = touch->getDelta().x;
 40         batSprite->setPositionX(batSprite->getPositionX()+x);
 41     };//在touchmove中移動擋板,按照觸摸滑動的距離來移動擋板。
 42     _eventDispatcher->addEventListenerWithSceneGraphPriority(ev1, this);//將觸摸事件加入監聽器
 43     contact();//調用注冊碰撞事件的函數
 44     schedule(schedule_selector(GameLayer::update1));//打開定時器
 45 
 46 }
 47 void GameLayer::contact()
 48 {
 49     EventListenerPhysicsContact* evContact = EventListenerPhysicsContact::create();//創建一個物理世界的碰撞事件
 50     evContact->onContactBegin = [](PhysicsContact& contact){return true;};
 51     evContact->onContactSeperate = [=](PhysicsContact& contact)//該函數在兩個碰撞的剛體分離后調用
 52     {
 53         auto bodyA = (Sprite*)(contact.getShapeA()->getBody()->getNode());//兩個碰撞剛體相對應的節點之A
 54         auto bodyB = (Sprite*)(contact.getShapeB()->getBody()->getNode());//兩個相碰撞剛體對應節點之B
 55         if(!bodyA||!bodyB)//按理說碰撞發生之后不會發生有一個剛體的節點不存在的情況,但是實際測試時發現bodyA或bodyB有為NULL的情況,因此我們在這里做一個判斷排除節點為空的情況
 56             return;
 57         int tagA = bodyA->getTag();
 58         int tagB = bodyB->getTag();
 59         if(tagA == 3)//如果碰撞雙方剛體有一個是磚塊,則把這個磚塊連同節點一同刪掉
 60         {
 61             bodyA->removeFromParentAndCleanup(true);
 62         }
 63         if(tagB == 3)
 64         {
 65             bodyB->removeFromParentAndCleanup(true);
 66         }
 67         prop->setVisible(true);
 68     };
 69     _eventDispatcher->addEventListenerWithSceneGraphPriority(evContact,this);//注冊碰撞事件
 70 }
 71 void GameLayer::loadTileMap()
 72 {
 73     map = TMXTiledMap::create("textmap.tmx");//從tmx創建一個TMXTileMap類
 74     map->setPositionX(getPositionX() + 34);//設置位置 76     addChild(map);
 77     TMXLayer* layer = map->getLayer("bricks");//從map中取出“bricks”圖層
     //這個循環嵌套是為了給每個磚塊精靈設置一個剛體
 78     for(int x=0;x<13;x++)
 79     {
 80         for(int y=0;y<18;y++)
 81         {
 82             int gid = layer->getTileGIDAt(Vec2(x,y));//為了提高點效率,我們沒必要給每個tile加上剛體,在創建地圖時我們設定了空白處的gid值為12,因此我們只對非12的tile加上剛體
 83             if(gid != 12)
 84             {
 85                 Sprite* sprite = layer->getTileAt(Vec2(x,y));//從tile的坐標取出對應的精靈
 86                 if(!sprite)//防止sprite為NULL
 87                     continue;
 88                 PhysicsBody* body = PhysicsBody::createEdgeBox(sprite->getContentSize(),PhysicsMaterial(1.0f,1.0f,0.0f));//給精靈設置一個剛體
 89                 sprite->setTag(3);//加入tag,方便碰撞時的判斷
 90                 body->setContactTestBitmask(0xFFFFFFFF);//設置接觸掩碼值
 91                 sprite->setPhysicsBody(body);
 92             }
 93         }
 94     }
 95     loadProp();
 96 }
 97 void GameLayer::loadProp()
 98 {
 99     TMXObjectGroup* objects = map->getObjectGroup("prop");//從地圖中取出對象層prop
100     CCASSERT(NULL != objects, "'Objects' object group not found");//防止為空
101     auto spawnPoint = objects->getObject("pop");//從對象層中取出對象pop,在創建地圖時設置好的
102     CCASSERT(!spawnPoint.empty(), "spawnPoint object not found");
103     int x = spawnPoint["x"].asInt();//spwanPoint應該是一個map型容器,這方面我理解不深,不多描述了。
104     int y = spawnPoint["y"].asInt();
105     prop = Sprite::create("game_energy_b.png");//按照從地圖中取出的坐標創建一個精靈
106     prop->setPosition(x,y);
107     prop->setVisible(false);//一開始設置為不可見,當碰撞發生時設置為可見,並開始向下運動
108     prop->setZOrder(10);//防止被地圖掩蓋
109     map->addChild(prop);
110 }
111 void GameLayer::update1(float dt)
112 {
     //-----------標注2----------------
113     float x = ballBody->getVelocity().x;
114     float y = ballBody->getVelocity().y;
115     if(x!=150&&x!=-150)
116     {
117         if(x<0)
118             x = -150;
119         else
120             x = 150;
121     }
122     if(y!=150&&y!=-150)
123     {
124         if(y<0)
125             y = -150;
126         else
127             y = 150;
128     }
129     ballBody->setVelocity(Vec2(x,y));
130 
131 }

  這里對代碼中的標注進行一些解釋:

  標注1:關於接觸掩碼值。在3.0中的事件分發機制都由事件派發器管理,所以物理引擎的碰撞事件也不例外。 下面代碼注冊碰撞響應事件和回調函數

1 auto contactListener = EventListenerPhysicsContact::create();
2 contactListener->onContactBegin = CC_CALLBACK_1(HelloWorld::onContactBegin, this);
3 _eventDispatcher->addEventListenerWithSceneGraphPriority(contactListener, this);

  每一次碰撞檢測事件是有EventListenerPhysicsContact來進行監聽的。監聽到碰撞事件時,會回調響應事件onContactBegin()來進行碰撞事件的處理。_eventDispatcher是事件派發器,由它管理所有的注冊事件。 EventListenerPhysicsContact是碰撞檢測中的一種,也可以運用EventListenerPhysicsContactWithBodies,EventListenerPhysicsContactWithShapes,EventListenerPhysicsContactWithGroup 來進行碰撞事件的注冊,對你你感興趣的bodys,shape和group事件進行監聽。   

   在上面說了這么多的東西,最重要的東西就是下面的,沒有下面的東西,碰撞事件根本不起作用,這就是我第一次運用碰撞時遇到的問題。也就是設置物理接觸相關的位掩碼值,默認的接觸事件不會被接受,需要設置一定的掩碼值來使接觸事件響應。 接觸掩碼值有三個值,分別是:

  1、CategoryBitmask,默認值為0xFFFFFFFF

  2、ContactTestBitmask,默認值為 0x00000000

  3、CollisionBitmask,默認值為0xFFFFFFFF 這三個掩碼值都有對應的set/get方法來設置和獲取。

  這三個掩碼值由邏輯與來進行操作測試。
  一個body的CategoryBitmask和另一個body的ContactTestBitmask的邏輯與的結果不等於0時,接觸事件將被發出,否則不發送。
  一個body的CategoryBitmask和另一個body的CollisionBitmask的邏輯與結果不等於0時,他們將碰撞,否則不碰撞
  默認情況下的body屬性會進行物理碰撞,但不會發送碰撞檢測的信號,也就不會響應碰撞回調函數,這個可以看下默認情況下的掩碼值的邏輯與


  CategoryBitmask = 0xFFFFFFFF;
  ContactTestBitmask = 0x00000000;
  CategoryBitmask & ContactTestBitmask = 0,所以不會發送碰撞信號
 
  CollisionBitmask = 0xFFFFFFFF;
  CategoryBitmask & CollisionBitmask = 0xFFFFFFFF,所以物體會碰撞,但是不會響應碰撞回調函數。


  上面介紹的掩碼值是碰撞檢測回調中最重要的,沒有上面的掩碼值,所有的碰撞回調函數都不會發生。 EventListenerPhysicsContact有四個接觸回調函數:

  1、onContactBegin,在接觸開始時被調用,僅調用一次,通過放回true或者false來決定兩個物體是否有碰撞。同時可以使用PhysicsContact::setData()來設置接觸操作的用戶數據。當返回false時,onContactPreSolve和onContactPostSolve將不會被調用,但是onContactSeperate將被調用一次。

  2、onContactPreSlove ,會在每一次被調用,通過放回true或者false來決定兩個物體是否有碰撞,同樣可以用ignore()來跳過后續的onContactPreSolve和onContactPostSolve回調函數。(默認返回true)

  3、onContactPostSolve,在兩個物體碰撞反應中的每個步驟中被處理調用。可以在里面做一些后續的接觸操作。如銷毀body

  4、onContactSeperate,在兩個物體分開時被調用,在每次接觸時只調用一次,和onContactBegin配對使用。 上述中最重要的就是碰撞檢測事件的講解,這是游戲中用到碰撞經常要用到的。

  這里附上一篇博文,詳細的講解了3.x的碰撞機制:http://www.tuicool.com/articles/2eI7Nv

  標注2:據我這兩天的實驗來看,在mac下和windows下使用物理引擎產生的效果有巨大的差別。很多博客上的代碼都是在windows上能夠流暢運行,但是在mac上跑就會有很多問題。

  比如,25行的ballBody->applyImpulse(force)。在windows下只要使用applyForce就可以了,但是在mac下使用applyForce函數會讓球的運動越來越快,沒多久便會因為速度超過幀數飛出我們設置的邊界。

  再比如,update1中的代碼,這是為了保證能一直保持勻速運動而寫的,在windows下完全不需要這些代碼,但是在mac下如果沒有,小球會在某次碰撞時候損失速度(隨機的,有時不會損失),直至停下來。

  這篇博文就到這里,下面附上截圖:

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM