試用Box2D製作打磚塊

1. 目標

製作一個打磚塊類型的遊戲Demo

2. 使用技術

Cocos2dx 3.0  基於Box2D的Physics模塊

3. 具體步驟

    3.1 建立Lesson1.h及Lesson1.cpp

     Lesson.h 內容如下:

#ifndef __LESSON1_H__
#define __LESSON1_H__

#include "cocos2d.h"
#include "VisibleRect.h"

using namespace cocos2d;
class Lesson1 : public cocos2d::Layer
{
public:
	void initPhysics();
        static cocos2d::Scene* createScene();
	CREATE_FUNC(Lesson1);
        bool init() override;
	void setPhyWorld(PhysicsWorld* world){this->world = world;} 
	// Touch process
	 bool onTouchBegan(Touch* touch, Event* pEvent);
	 void onTouchMoved(Touch* touch, Event* pEvent);
	 void onTouchEnded(Touch* touch, Event* pEvent);
	 // Collide callback
	 bool onContactBegin(PhysicsContact& contact);
	 void update(float dt);

	 void applyImpulse();

	 void applyImpulse_withbrick(Point p);

private:
	PhysicsWorld* world;
	int m_state;
};

#endif // __LESSON1_H__

Lesson1 就是一個普通的Layer,其中有一個方法用來產生一個場景,這是cocos2dx常見的編程方法。 onTouchXXX 系列方法是cocos2dx 3.0觸摸回調方法。它的用法在Lesson1.cpp中可以看到。 onContactBegin是註冊的一個檢測碰撞信息的回調方法,通過它可以瞭解到具體的碰撞體信息。 update方法是我使用scheduleUpdate定製的

刷新方法,這裏可以做一些每幀都需要處理的事情。 appleImpulse 和applyImpulse_withbrick是定製的幫助方法,其實更合適放在private裏面,但是我們僅僅寫個Demo,

所以不必過分注意系統的架構,比如面向對象或者MVC的方法。如果我們要做一個商業化的正規遊戲,那麼這些都是要考慮的。


介紹完頭文件,接下來就進入實現文件的編寫。

 3.2 首先是產生場景方法的編寫:

Scene* Lesson1::createScene()
{
      auto scene = Scene::createWithPhysics();
      auto layer =  Lesson1::create();
      layer->setPhyWorld(scene->getPhysicsWorld());
      scene->addChild(layer);
      scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
      return scene;
}

可以看出,這裏和普通的遊戲產生場景的方法並不相同。我們無須面對Box2D的細節就完成了物理世界的構建。
 scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
這裏我們僅僅通過一句代碼就完成了Debug模式的構建,所謂DebugDraw,指的是Box2D本身並不支持繪圖,這裏使用的OpenGL ES來繪製出圖形的線框模型,可以幫助我們

可視化的看到物體的外觀。我們這個Demo的外觀就是利用了這點,沒有使用貼圖。


3.3 接下來是層的初始化方法

bool Lesson1::init() 
{
       if ( !Layer::init() )
       {
           return false;
        }
	m_state = STATE_INIT;
	auto dispatcher = Director::getInstance()->getEventDispatcher();
	auto touchListener = EventListenerTouchOneByOne::create();
	touchListener->onTouchBegan = CC_CALLBACK_2(Lesson1::onTouchBegan, this);
	touchListener->onTouchMoved = CC_CALLBACK_2(Lesson1::onTouchMoved, this);
	touchListener->onTouchEnded = CC_CALLBACK_2(Lesson1::onTouchEnded, this);
	dispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);
	this->initPhysics();
	auto contactListener = EventListenerPhysicsContact::create();
        contactListener->onContactBegin = CC_CALLBACK_1(Lesson1::onContactBegin, this);
        Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(contactListener, this);
	this->scheduleUpdate();
	return true;
}

這裏主要做了兩份工作,一份是完成觸摸事件的註冊,另外一件是碰撞事件的註冊。scheduleUpdate註冊了每幀刷新事件update(float dt)。 具體的工作在initPhysics中。

我們接下來就看看這個方法:

void Lesson1::initPhysics()
{
	Size visibleSize = Director::getInstance()->getVisibleSize(); 
	Size left(2, visibleSize.height);
	
        auto body = PhysicsBody::createEdgeBox(left, PHYSICSBODY_MATERIAL_DEFAULT, 1); 
        auto edgeNode = Node::create(); 
        edgeNode->setPosition(Point(1,visibleSize.height/2)); 
        edgeNode->setPhysicsBody(body); 
        this->addChild(edgeNode); 

        auto edgeNodeRight = Node::create();
	auto body_right = PhysicsBody::createEdgeBox(left, PHYSICSBODY_MATERIAL_DEFAULT, 1); 
	edgeNodeRight->setPosition(visibleSize.width-2, visibleSize.height/2);
	edgeNodeRight->setPhysicsBody(body_right);
	this->addChild(edgeNodeRight);

	Size top(visibleSize.width, 2);
	auto body_top = PhysicsBody::createEdgeBox(top, PHYSICSBODY_MATERIAL_DEFAULT, 1); 
	auto edgeNodeTop = Node::create();
	edgeNodeTop->setPosition(visibleSize.width/2, visibleSize.height-2);
	edgeNodeTop->setPhysicsBody(body_top);
	this->addChild(edgeNodeTop);

	Size size(visibleSize.width/4, PADDLE_HEIGHT);  
        body = PhysicsBody::createEdgeBox(size, PHYSICSBODY_MATERIAL_DEFAULT, 3); 
        auto edgeNode2 = Node::create(); 
        edgeNode2->setPosition(Point(visibleSize.width/2,0)); 
        edgeNode2->setPhysicsBody(body); 
	body->setTag(kTagPanel);
        this->addChild(edgeNode2); 
	edgeNode2->getPhysicsBody()->setContactTestBitmask(0x02); 
	
	int tagIndex = 0;
	for(int i = 0; i < BRICK_COL_NUM; i++)
	{
		for(int j = 0; j < BRICK_LINE_NUM; j++)
		{	
			auto body = PhysicsBody::createBox(Size(20, 20), PHYSICSBODY_MATERIAL_DEFAULT);
			body->setGravityEnable (false);
	                auto node = Node::create();
			node->setPosition(BRICK_BASE_X + BRICK_BASE_MARGIN_H*i,  BRICK_BASE_MARGIN_V*j + BRICK_BASE_Y);
			node->setPhysicsBody(body);
			this->addChild(node);
			body->setTag(BRICK_BASE_TAG + tagIndex);
			tagIndex++;
			node->getPhysicsBody()->setGroup(-1);
			node->getPhysicsBody()->setContactTestBitmask(0x02); 
		}
	}

	// create hero
	PhysicsMaterial material(HERO_DENSITY,  HERO_RESTITUTION, HERO_FRICTION);
	body = PhysicsBody::createCircle(HERO_RADIUS, material);
	body->setDynamic(true);
	auto node = Node::create();
	node->setPosition(HERO_POSX, HERO_POSY);
	node->setPhysicsBody(body);
	this->addChild(node);
	body->setTag(kTagHero);
	body->setGravityEnable (false);
        node->getPhysicsBody()->setContactTestBitmask(0x08); 
}

這裏主要完成了剛體添加。 cocos2dx爲我們封裝了一個PhysicsBody類方便使用。

auto body = PhysicsBody::createEdgeBox(left, PHYSICSBODY_MATERIAL_DEFAULT, 1);

1. 這句代碼完成了剛體的創建

PHYSICSBODY_MATERIAL_DEFAULT 是剛體的物理參數,包括密度、摩擦力和回覆力。所謂回覆力,打個比方,一個球落到地上,它要反彈,這個參數就是指它反彈的能力。

auto edgeNode = Node::create();

edgeNode->setPosition(Point(1,visibleSize.height/2));
edgeNode->setPhysicsBody(body);
this->addChild(edgeNode);

2. 創建一個節點,設好它的位置,爲它指定剛體,並添加到佈景層。 這樣我們就爲物理世界添加好了一個剛體。我們可以創建不同形狀的剛體,有圓型,方型,以及多邊形。

如果剛體的密度爲0,代表這是一個靜態剛體,碰撞後位置不會發生變動,反之則是一個動態剛體。一般來說,牆壁、地面都是靜態剛體,而落地的小球,箱子等都是動態剛體。當然,物理的世界還有Joint等其他物體。

node->getPhysicsBody()->setGroup(-1)

注意這句代碼,這裏指的是這種類型的物體相互之間不會發生碰撞,我們這麼寫的目的就是禁止磚塊間的碰撞,讓所有的磚塊都讓小球來擊打。

node->getPhysicsBody()->setContactTestBitmask(0x02);
這句代碼是設置剛體間的碰撞過濾。具體原理可以查找其他資料。這裏不再展開。

body->setGravityEnable (false);

這句代碼是讓當前剛體不考慮重力作用。類似於物體處於一種失重狀態。


3.4 觸摸事件處理

bool Lesson1::onTouchBegan(Touch* touch, Event* pEvent)
 {
	 int y = touch->getLocation().y;
	 int x = touch->getLocation().x;
	 PhysicsBody * body = world->getBody(kTagPanel);
	 if((y <= 25 && y > 0 && x >= body->getNode()->getPositionX() - VisibleRect::getVisibleRect().getMaxX()/8&& x <= body->getNode()->getPositionX() +  VisibleRect::getVisibleRect().getMaxX()/8))
	 return true;
}

void Lesson1::onTouchMoved(Touch* touch, Event* pEvent)
 {
	 PhysicsBody * body = world->getBody(kTagPanel);
	 int y = touch->getLocation().y;
	 int x = touch->getLocation().x;
         if(!(y <= 25 && y > 0 && x >= body->getNode()->getPositionX() - VisibleRect::getVisibleRect().getMaxX()/8&& x <= body->getNode()->getPositionX() +  VisibleRect::getVisibleRect().getMaxX()/8))
	     return;
	 if(body->getNode()->getPositionX() < VisibleRect::getVisibleRect().getMaxX()/8)
	    body->getNode()->setPositionX(VisibleRect::getVisibleRect().getMaxX()/8);
	 else if(body->getNode()->getPositionX() > VisibleRect::getVisibleRect().getMaxX() * 7 / 8)
	 {
	     body->getNode()->setPositionX(VisibleRect::getVisibleRect().getMaxX() * 7 / 8);
	 }
	 else
	     body->getNode()->setPosition(ccp(touch->getLocation().x, body->getPosition().y));
}

 void Lesson1::onTouchEnded(Touch* touch, Event* pEvent)
 {
	  PhysicsBody * body = world->getBody(kTagPanel);
	  int y = touch->getLocation().y;
	  int x = touch->getLocation().x;
          if(!(y <= 25 && y > 0 && x >= body->getNode()->getPositionX() - VisibleRect::getVisibleRect().getMaxX()/8&& x <= body->getNode()->getPositionX() +  VisibleRect::getVisibleRect().getMaxX()/8))
	      return;
	  
	
	  if(body->getNode()->getPositionX() <0)
	     body->getNode()->setPositionX(0);
	 else if(body->getNode()->getPositionX() > VisibleRect::getVisibleRect().getMaxX() - body->getNode()->getContentSize().width)
	 {
	     body->getNode()->setPositionX(VisibleRect::getVisibleRect().getMaxX() - body->getNode()->getContentSize().width);
	 }
	 else
	     body->getNode()->setPosition(ccp(touch->getLocation().x, body->getPosition().y));
	 if(m_state == STATE_INIT)
	 {
	     m_state = STATE_START;
	     PhysicsBody * body = world->getBody(kTagHero);
	     body->applyImpulse(Vect(0, INIT_IMPULSE));
	     body->setGravityEnable (true);
	 }
}

onTouchBegan 這裏主要處理當觸摸點處於擋板內部時候可以觸摸。

onTouchMoved 和onTouchEnded處理擋板的移動。 在onTouchEnded中在最開始時候要給小球一個衝量,讓它能夠彈跳起來去擊打磚塊。kTagHero就是小球的標記。

kTagPanel是擋板的標記。這裏的代碼處理都很明顯。


3.5 碰撞事件回調處理

 bool Lesson1::onContactBegin(PhysicsContact& contact)
{
        PhysicsBody* a = contact.getShapeA()->getBody();
        PhysicsBody* b = contact.getShapeB()->getBody();
	if(a->getTag() > kTagPanel && b->getTag() == kTagHero)
	{
		a->setGravityEnable(true);
		applyImpulse_withbrick(a->getNode()->getPosition());
		world->setGravity(Vect(0.0, -200.f));
	}
	else if(b->getTag() > kTagPanel && a->getTag() == kTagHero)
	{
		b->setGravityEnable(true);
		applyImpulse_withbrick(b->getNode()->getPosition());
		world->setGravity(Vect(0.0, -200.f));
	}
	else if(a->getTag() == kTagPanel && b->getTag() == kTagHero && m_state == STATE_START)
	{
		applyImpulse();
	}
	else if(a->getTag() == kTagHero && b->getTag() == kTagPanel && m_state == STATE_START)
	{
		applyImpulse();
	}
	return true;
 }

這裏的意思是,如果當前的碰撞一方是小球而另外一方是磚塊時候,讓磚塊掉下。小球發生碰撞時候,要給小球產生一個反彈的衝量。就是在 applyImpulse方法和applyImpulse_withbrick方法。

3.6 定時器update方法

 void Lesson1::update(float dt)
 {
	 PhysicsBody * body = world->getBody(kTagHero);
	 if(body->getVelocity().getLength() > SPEED_MAX)
	 {
		 body->setLinearDamping(DAMP_MAX);
	 }
	 else
	 {
		 body->setLinearDamping(0);
	 }
	 if(body->getNode()->getPositionX() < EDGE_OFFSET)
	 {
		 body->getNode()->setPositionX(EDGE_OFFSET);
	 }
	 if(body->getNode()->getPositionX() > VisibleRect::getVisibleRect().getMaxX()-EDGE_OFFSET)
	 {
		 body->getNode()->setPositionX(VisibleRect::getVisibleRect().getMaxX()-EDGE_OFFSET);
	 }
	  if(body->getNode()->getPositionY() < EDGE_OFFSET)
	 {
		 body->getNode()->setPositionY(EDGE_OFFSET);
	 }
	  if(body->getNode()->getPositionY() > VisibleRect::getVisibleRect().getMaxY()- EDGE_OFFSET)
	 {
		 body->getNode()->setPositionY( VisibleRect::getVisibleRect().getMaxY()- EDGE_OFFSET);
	 }
	  for(int i = 0; i < BRICK_LINE_NUM * BRICK_COL_NUM; i++)
	  {
		   int tag = BRICK_BASE_TAG + i;
		   PhysicsBody* body = world->getBody(tag);
		   if(body != NULL && body->getNode()->getPositionY() <= BRICK_REMOVE_Y)
		   {
			   body->getNode()->removeFromParent();
			   world->removeBody(tag);
			   body = NULL;
		   }
	  }
 }

這裏首先判斷小球的速度,如果速度過大,就添加阻尼。接下來是判斷小球的座標反之碰撞飛出屏幕。 最後是判斷磚塊,如果下落到屏幕下方,就移除該磚塊及對應的剛體。


好了,整個打磚塊的製作過程基本就這些了。 下面上圖。



遊戲開始界面





 擊打界面


4. 總結

這只是一個Demo. 需要完善的地方有:

1. 關卡外部導入。我們可以利用工具來完成關卡然後導入遊戲。比如Tile地圖編輯器生成xml來生成關卡。

2. 代碼的架構。一般來說,應該使用MVC模型來打造遊戲框架。這裏僅僅是個Demo,並沒有這麼做。

3. 貼圖使用,這裏僅僅使用了OpenGL ES的DebugDraw繪製圖形外觀,對於正式的遊戲,一定要給模型貼圖。

4. UI界面的添加。



暫時就想到這麼多。拋磚引玉,希望大家多多指教。歡迎大家加羣 216208142一同討論。


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