上一篇文章的最后,提到了官方实例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
}