上一篇文章的最后,提到了官方实例SimpleGame
学过该实例之后,会对添加精灵,元素运动,播放音乐,切换场景都一定的了解
有了这样的基础之后
这一篇我们来讲如何使用Box2d物理引擎,并做一个例子
1.Box2d简介
Box2d是Cocos2d自带的一个物理引擎
可以模拟重力,运动,弹性,摩擦,关节等等中学物理中的东西
Box2d的意义在于可以让我们轻松的在游戏中模拟物理世界
诸如<愤怒的小鸟>之类的游戏,若不使用Box2d,我们需要自己在代码里计算各种运动轨迹,各种碰撞后的位置
而使用了Box2d之后能大大大大的减少这方面的工作,引擎已经帮你做了
如果你之前从未使用过Box2d
在开始今天的例子之前
http://box2d.org/manual.pdf
可以到这里看一下Box2d的使用手册,了解一下其中world,body,fixture,shape的关系,看一看有什么joint,什么是senior等等这些基本概念
这里就不一一赘述
2.从'头'讲起
首先像上一篇一样创建一个新工程,名为HelloBox2d
头文件我们这样写
#ifndef __HELLOWORLD_SCENE_H__ #define __HELLOWORLD_SCENE_H__ #include "cocos2d.h" #include "Box2D/Box2D.h" class HelloWorld : public cocos2d::CCLayer { public: ~HelloWorld(); HelloWorld(); // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone virtual bool init(); void update( float dt ); virtual void didAccelerate(cocos2d::CCAcceleration* pAccelerationValue); // there's no 'id' in cpp, so we recommend returning the class instance pointer static cocos2d::CCScene* scene(); // a selector callback void menuCloseCallback(CCObject* pSender); // implement the "static node()" method manually CREATE_FUNC(HelloWorld); private: b2World *_world; b2Body * ballBody; cocos2d::CCLabelTTF* pLabel; }; #endif // __HELLOWORLD_SCENE_H__
首先包含头文件Box2D.h
注意它在当前版本中的引用位置,和旧版好像不一样
private里面我们写了3个成员
b2World *_world;是Box2d中的世界对象,可以为其设置重力场,在程序中加入b2World对象是使用物理模拟的条件
b2Body * ballBody;是我们将要创建的 球对象
cocos2d::CCLabelTTF* pLabel;是cocos2d的label,等会儿会修改其文字
public方法中,声明了didAccelerate方法
这是一个在移动设备上获取重力感应的函数
接下来我们将在该函数中实现重力场方向的改变(随手机姿势改变而改变)
3.init函数初始化
在cpp文件中,init函数将初始化我们的世界
bool HelloWorld::init() { ////////////////////////////// // 1. super init first if ( !CCLayer::init() ) { return false; } CCSize visibleSize = CCDirector::sharedDirector()->getVisibleSize(); CCPoint origin = CCDirector::sharedDirector()->getVisibleOrigin(); ///////////////////////////// // 2. add a menu item with "X" image, which is clicked to quit the program // you may modify it. // add a "close" icon to exit the progress. it's an autorelease object CCMenuItemImage *pCloseItem = CCMenuItemImage::create( "CloseNormal.png", "CloseSelected.png", this, menu_selector(HelloWorld::menuCloseCallback)); pCloseItem->setPosition(ccp(origin.x + visibleSize.width - pCloseItem->getContentSize().width/2 , origin.y + pCloseItem->getContentSize().height/2)); // create menu, it's an autorelease object CCMenu* pMenu = CCMenu::create(pCloseItem, NULL); pMenu->setPosition(CCPointZero); this->addChild(pMenu, 1); ///////////////////////////// // 3. add your codes below... // add a label shows "Hello World" // create and initialize a label pLabel = CCLabelTTF::create("Hello World", "Arial", 24); // position the label on the center of the screen pLabel->setPosition(ccp(origin.x + visibleSize.width/2, origin.y + visibleSize.height - pLabel->getContentSize().height)); // add the label as a child to this layer this->addChild(pLabel, 1); // add "HelloWorld" splash screen" CCSprite* pSprite = CCSprite::create("HelloWorld.png"); // position the sprite on the center of the screen pSprite->setPosition(ccp(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y)); // add the sprite as a child to this layer this->addChild(pSprite, 0); //初始化box2d的world对象 b2Vec2 gravity; gravity.Set(0.0f,-10.0f); _world = new b2World( gravity ); //创建边界 CCSize winSize = CCDirector::sharedDirector()->getWinSize(); b2BodyDef groundBodyDef; groundBodyDef.position.Set(0, 0); b2Body * _groundBody = _world->CreateBody(&groundBodyDef); // Define the ground box shape. b2EdgeShape groundBox; // bottom groundBox.Set(b2Vec2(0,0), b2Vec2(winSize.width/PTM_RATIO,0)); _groundBody->CreateFixture(&groundBox,0); // top groundBox.Set(b2Vec2(0,winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO,winSize.height/PTM_RATIO)); _groundBody->CreateFixture(&groundBox,0); // left groundBox.Set(b2Vec2(0,winSize.height/PTM_RATIO), b2Vec2(0,0)); _groundBody->CreateFixture(&groundBox,0); // right groundBox.Set(b2Vec2(winSize.width/PTM_RATIO,winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO,0)); _groundBody->CreateFixture(&groundBox,0); //创建球 CCSprite* pBall = CCSprite::create("CloseSelected.png"); pBall->setTag(1); // Place the sprite on the center of the screen pBall->setPosition(ccp(winSize.width/2, winSize.height/2)); this->addChild(pBall, 0); //在box2d中创建这个球 b2BodyDef ballBodyDef; ballBodyDef.type = b2_dynamicBody; ballBodyDef.position.Set(winSize.width/2/PTM_RATIO, winSize.height/2/PTM_RATIO); ballBodyDef.userData = pBall; ballBody = _world->CreateBody(&ballBodyDef); // Create circle shape b2CircleShape circle; circle.m_radius = pBall->getContentSize().height/2/PTM_RATIO; // Create shape definition and add to body b2FixtureDef ballShapeDef; ballShapeDef.shape = &circle; ballShapeDef.density = 1.0f; ballShapeDef.friction = 0.1f; ballShapeDef.restitution = 0.1f; ballBody->CreateFixture(&ballShapeDef); //设置layer this->setAccelerometerEnabled(true); this->scheduleUpdate(); return true; }
前面的代码很好理解,就从Box2d这里开始解释
我们为_world设置了一个初始的(0,-10)的重力场,这和我们生活中的重力场-9.8一样
然后实例化了_world对象
接下来我们依据winSize创建一个边界
也就是沿着设备屏幕的边缘,添加了一个不可见的b2Body对象
我们称之为'透明的框'
b2Body * _groundBody = _world->CreateBody(&groundBodyDef);
就是这个'透明的框'
'透明的框'包含四条'透明的边'
以下代码中bottom top right left就分别是这四条'透明的边'
我们具体来看其中的一条
// bottom
groundBox.Set(b2Vec2(0,0), b2Vec2(winSize.width/PTM_RATIO,0));
_groundBody->CreateFixture(&groundBox,0);
[1].一条边是一个b2Fixture对象,由CreateFixture创建而来
b2Fixture可以理解为b2Body里的一个形状
一个b2Body对象可以包含多个b2Fixture形状
就好比一个人,有2个胳膊,2条腿1个脑袋
'透明的框'就拥有这样四条'透明的边'
[2].groundBox.Set(b2Vec2(0,0), b2Vec2(winSize.width/PTM_RATIO,0));
这里设置了要创建的边的两个端点
底边左侧(0,0)没有问题
右侧(winSize.width/PTM_RATIO,0)
用宽度除以PTM_RATIO
这是因为box2d中的物理世界坐标和cocos2d中的坐标需要转换!
转换的方法就是除以这个值(转换为了满足box2d处理物体的范围,0.1米~10米,详见其资料)
这样我们就搞定了框,框为何是透明的呢?
因为box2d本身是没有图像的
_world->CreateBody(&groundBodyDef);
只用在world里面create一下,就已经把框加到物理世界中了
下面我们来搞个球(听起来怪怪的)
CCSprite* pBall = CCSprite::create("CloseSelected.png");
pBall->setTag(1);
// Place the sprite on the center of the screen
pBall->setPosition(ccp(winSize.width/2, winSize.height/2));
this->addChild(pBall, 0);
这样把球加到当前层,我们就能看到了
下面我们把该球加入物理世界
//在box2d中创建这个球
b2BodyDef ballBodyDef;
ballBodyDef.type = b2_dynamicBody;
ballBodyDef.position.Set(winSize.width/2/PTM_RATIO, winSize.height/2/PTM_RATIO);
ballBodyDef.userData = pBall;
ballBody = _world->CreateBody(&ballBodyDef);
// Create circle shape
b2CircleShape circle;
circle.m_radius = pBall->getContentSize().height/2/PTM_RATIO;
// Create shape definition and add to body
b2FixtureDef ballShapeDef;
ballShapeDef.shape = &circle;
ballShapeDef.density = 1.0f;//密度
ballShapeDef.friction = 0.1f;//摩擦
ballShapeDef.restitution = 0.1f;//弹性
ballBody->CreateFixture(&ballShapeDef);
一步步来
ballBodyDef.type = b2_dynamicBody;设置该球是动态的(默认静态)
以后球就可以在受力后运动了,而静态物体是不会在物理模拟中动的
ballBodyDef.position.Set 设置了初始位置
ballBodyDef.userData = pBall;这里很关键,将我们刚刚做的'显示球'和这个'物理球'关联起来
之后我们又设置了该球的具体属性(半径,密度等)
需要注意的是,'物理球'的大小应与'显示球'一致,这一点很重要
显示球的半径是pBall->getContentSize().height/2
故转换后我们得到相应的物理球半径pBall->getContentSize().height/2/PTM_RATIO
最后我们设置该layer启用重力感应检测和帧函数
this->setAccelerometerEnabled(true);
this->scheduleUpdate();
4.帧函数里做了什么
void HelloWorld::update( float dt ){ //注意,之前2dx的版本是用ccTime的,在2.0.3版本中,用float类型 int32 velocityIterations = 8; int32 positionIteratoins = 1; _world->Step( dt, velocityIterations, positionIteratoins); //由box2d世界模拟来确定各元素的位置 for( b2Body *b = _world->GetBodyList();b;b = b->GetNext() ) { if(b->GetUserData() != NULL) { CCSprite *myActor = (CCSprite*)b->GetUserData(); myActor->setPosition(ccp((b->GetPosition().x )* PTM_RATIO,b->GetPosition().y * PTM_RATIO));//设置精灵位置 myActor->setRotation( -1 * CC_RADIANS_TO_DEGREES(b->GetAngle()) );//设置精灵旋转方向 } } }
this->scheduleUpdate();启用帧函数后
update会在每帧执行
这里我们做的事很简单:就是根据box2d物理模拟来设置每个显示元素的位置和角度
在_world对象遍历后得到所有已加入的b2Body对象
若创建时关联了显示对象,如CCSprite
通过GetUserData获取CCSprite对象,设置其位置和角度,与物理模拟结果一致
5.重力感应函数
void HelloWorld::didAccelerate(CCAcceleration* pAccelerationValue) { //x,y both range from -1 to 1 b2Vec2 gravity(10*pAccelerationValue->x, 10*pAccelerationValue->y); _world->SetGravity(gravity); //防止球休眠 ballBody->SetAwake(true); //在label中显示参数 string strx; string stry; stringstream ss; stringstream ss2; ss<<pAccelerationValue->x; ss>>strx; ss2<<pAccelerationValue->y; ss2>>stry; pLabel->setString(ccs(strx+" and "+stry)->getCString()); }
该函数会在有重力感应的设备上执行
比如电脑上就不会执行,手机上就能执行(扩平台就是好啊)
这里我们根据其传入的参数,改变重力场
并且在label中打印这两个参数
等一下我们可以看到x,y都是range from -1 to 1的,好像是个单位向量吧
顺便吐槽一下C++的字符/字符串处理太费劲了,类型转换也不方便..什么char char * ...不多提了
大家可以用一下cocos2d-x封装的CCString相对好用一些
好了,一共就这么多代码
在手机上运行,可以看到转动手机
球会向不同的方向掉落!是不是amazing!
自己亲自照了一张,如下图
红色的就是风中飞舞的球了(后来换成开关图了)
本篇到此结束
希望你对Box2d已经有了一点点理解
起码感受到了重力
自己在学习的时候,看到过一篇初探box2d的文章,里面也有个例子也挺不错的
有兴趣可以看看 传送门
本来想把源码打个包传上来
一寻思一共也就俩文件,直接贴出来得了
.h文件前面已经贴了
完整的.cpp文件如下
#include "HelloWorldScene.h" #define PTM_RATIO 32 USING_NS_CC; HelloWorld::HelloWorld() :_world(NULL) { } HelloWorld::~HelloWorld() { CC_SAFE_DELETE(_world); } CCScene* HelloWorld::scene() { // 'scene' is an autorelease object CCScene *scene = CCScene::create(); // 'layer' is an autorelease object HelloWorld *layer = HelloWorld::create(); // add layer as a child to scene scene->addChild(layer); // return the scene return scene; } // on "init" you need to initialize your instance bool HelloWorld::init() { ////////////////////////////// // 1. super init first if ( !CCLayer::init() ) { return false; } CCSize visibleSize = CCDirector::sharedDirector()->getVisibleSize(); CCPoint origin = CCDirector::sharedDirector()->getVisibleOrigin(); ///////////////////////////// // 2. add a menu item with "X" image, which is clicked to quit the program // you may modify it. // add a "close" icon to exit the progress. it's an autorelease object CCMenuItemImage *pCloseItem = CCMenuItemImage::create( "CloseNormal.png", "CloseSelected.png", this, menu_selector(HelloWorld::menuCloseCallback)); pCloseItem->setPosition(ccp(origin.x + visibleSize.width - pCloseItem->getContentSize().width/2 , origin.y + pCloseItem->getContentSize().height/2)); // create menu, it's an autorelease object CCMenu* pMenu = CCMenu::create(pCloseItem, NULL); pMenu->setPosition(CCPointZero); this->addChild(pMenu, 1); ///////////////////////////// // 3. add your codes below... // add a label shows "Hello World" // create and initialize a label pLabel = CCLabelTTF::create("Hello World", "Arial", 24); // position the label on the center of the screen pLabel->setPosition(ccp(origin.x + visibleSize.width/2, origin.y + visibleSize.height - pLabel->getContentSize().height)); // add the label as a child to this layer this->addChild(pLabel, 1); // add "HelloWorld" splash screen" CCSprite* pSprite = CCSprite::create("HelloWorld.png"); // position the sprite on the center of the screen pSprite->setPosition(ccp(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y)); // add the sprite as a child to this layer this->addChild(pSprite, 0); //初始化box2d的world对象 b2Vec2 gravity; gravity.Set(0.0f,-10.0f); _world = new b2World( gravity ); //创建边界 CCSize winSize = CCDirector::sharedDirector()->getWinSize(); b2BodyDef groundBodyDef; groundBodyDef.position.Set(0, 0); b2Body * _groundBody = _world->CreateBody(&groundBodyDef); // Define the ground box shape. b2EdgeShape groundBox; // bottom groundBox.Set(b2Vec2(0,0), b2Vec2(winSize.width/PTM_RATIO,0)); _groundBody->CreateFixture(&groundBox,0); // top groundBox.Set(b2Vec2(0,winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO,winSize.height/PTM_RATIO)); _groundBody->CreateFixture(&groundBox,0); // left groundBox.Set(b2Vec2(0,winSize.height/PTM_RATIO), b2Vec2(0,0)); _groundBody->CreateFixture(&groundBox,0); // right groundBox.Set(b2Vec2(winSize.width/PTM_RATIO,winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO,0)); _groundBody->CreateFixture(&groundBox,0); //创建球 CCSprite* pBall = CCSprite::create("CloseSelected.png"); pBall->setTag(1); // Place the sprite on the center of the screen pBall->setPosition(ccp(winSize.width/2, winSize.height/2)); this->addChild(pBall, 0); //在box2d中创建这个球 b2BodyDef ballBodyDef; ballBodyDef.type = b2_dynamicBody; ballBodyDef.position.Set(winSize.width/2/PTM_RATIO, winSize.height/2/PTM_RATIO); ballBodyDef.userData = pBall; ballBody = _world->CreateBody(&ballBodyDef); // Create circle shape b2CircleShape circle; circle.m_radius = pBall->getContentSize().height/2/PTM_RATIO; // Create shape definition and add to body b2FixtureDef ballShapeDef; ballShapeDef.shape = &circle; ballShapeDef.density = 1.0f; ballShapeDef.friction = 0.1f; ballShapeDef.restitution = 0.1f; ballBody->CreateFixture(&ballShapeDef); //设置layer this->setAccelerometerEnabled(true); this->scheduleUpdate(); return true; } void HelloWorld::update( float dt ){ //注意,之前2dx的版本是用ccTime的,在2.0.3版本中,用float类型 int32 velocityIterations = 8; int32 positionIteratoins = 1; _world->Step( dt, velocityIterations, positionIteratoins); //由box2d世界模拟来确定各元素的位置 for( b2Body *b = _world->GetBodyList();b;b = b->GetNext() ) { if(b->GetUserData() != NULL) { CCSprite *myActor = (CCSprite*)b->GetUserData(); myActor->setPosition(ccp((b->GetPosition().x )* PTM_RATIO,b->GetPosition().y * PTM_RATIO));//设置精灵位置 myActor->setRotation( -1 * CC_RADIANS_TO_DEGREES(b->GetAngle()) );//设置精灵旋转方向 } } } void HelloWorld::didAccelerate(CCAcceleration* pAccelerationValue) { //x,y both range from -1 to 1 b2Vec2 gravity(10*pAccelerationValue->x, 10*pAccelerationValue->y); _world->SetGravity(gravity); //防止球休眠 ballBody->SetAwake(true); //在label中显示参数 string strx; string stry; stringstream ss; stringstream ss2; ss<<pAccelerationValue->x; ss>>strx; ss2<<pAccelerationValue->y; ss2>>stry; pLabel->setString(ccs(strx+" and "+stry)->getCString()); } void HelloWorld::menuCloseCallback(CCObject* pSender) { #if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) || (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) CCMessageBox("You pressed the close button. Windows Store Apps do not implement a close button.","Alert"); #else CCDirector::sharedDirector()->end(); #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) exit(0); #endif #endif }