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一同討論。