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下如果沒有,小球會在某次碰撞時候損失速度(隨機的,有時不會損失),直至停下來。
這篇博文就到這里,下面附上截圖:

