物理游乐场——玩转Box2D物理引擎

前一阵子学习Box2D物理引擎,做了个小Demo,名字就叫物理游乐场(PhysicsPlayground)好了,现在和大家分享下源码,以及其基本功能要点。玩转Box2D是本程序的目标,现在和这个目标还有一定差距。o(∩_∩)o

开发环境:
Cocos2d-x 3.2 + VS2012
源码地址:
https://github.com/douxt/PhysicsPlayground

运行源码:
如果想运行此代码,请新建工程,然后把代码和资源拷贝进去,并自行添加依赖项目及引用。这样可以避免一些问题。
程序的依赖项目:

另外还需要添加一个包含路径,在项目->属性->C/C+±>常规里的 附加包含目录里需要添加:

以上是笔者重新建立项目并运行时,需要的操作。

程序介绍:

物理世界的显示用的方法和cpp-tests中的一样,在Layer的draw函数中增加绘图指令,调用物理世界的DrawDebugData()函数实现绘制。

1.增加刚体
往世界里增加刚体,是最基本的功能。程序目前可以用以下两种方法增加刚体:
1.点击"Add Regular" 会弹出两个菜单, 其中上面的可以设置大小,密度等参数,下面的选择形状,可以选择的形状为圆形以及几种正多边形。 通过点击选择形状,然后在点击空白处,刚体就会被增加到物理世界。
2.点击"Add Custom"会进入添加自定义多边形的状态,这时也可以设置弹性、摩擦等属性,然后通过点击空白处标记多边形的顶点,点击"Confirm"实现添加。
程序运行示例:

增加规则形状刚体的代码:(暂时不考虑界面,标记点等细节)

void PhysicsManager::addRegularPolygon(Point pos)
{
    if(_sideNum == 0)
    {
        addCircle(pos, _size);
        return;
    }
    if(_sideNum < 3)
    {
        return;
    }
    auto body = createBody();
    int num = _sideNum;
    auto radias =  _size/PTM_RATIO;
    auto points = new b2Vec2;
    // calculate the position of the vertices
    for(int ii=0; iiCreateFixture(&fixtureDef);

    //move body to the specified position.
    if(body){
        body->SetTransform(b2Vec2(pos.x / PTM_RATIO,pos.y / PTM_RATIO),
            body->GetAngle());
    }
    delete ] points;
}

```

其中_sideNum为多边形的边数,边数为0规定为圆形。这个数是在选择形状时被设定的。
添加自定义形状的刚体也是类似,只不过需要将标记的点坐标传入。



2.增加关节
关节有若干种类,增加的方法会根据关节的不同而有所不同。
首先创建需要用关节连接的刚体。
点击"Add Joint",会弹出一个菜单,用这个菜单选择关节的种类。在新弹出的菜单中选择参数。
然后在点击空白处,会出现标记点。 0号标记点的目的是标记关节的BodyA,1号是为了标记BodyB。
有些关节需要2号甚至3号标记点。如果ToGround选项被选择,则0号点有时会没有用,BodyA会成为预设的静态地板Body。
标记完毕之后,往下拉,最下面有个Create按钮,点了之后如果条件满足关节就会被创建。


WheelJoint (轮子关节)演示:
  
其中轮子关节会在update时根据左下角的Controller进行速度调节。


DistanceJoint(距离关节)演示: 所有关节都是距离关节。
  


RevoluteJoint演示:
  
可以看出2号点的作用是标记旋转轴。


PrismaticJoint演示:
  
其中3号点和2号点的相对位置为关节提供了方向信息。


PulleyJoint演示:
  
2号点与BodyA相连,3号点与BodyB相连。


GearJoint演示:
  
需要两个刚体,以及这两个刚体上的关节。必须为revolute或者prismatic。


FrictionJoint演示:
 
这个关节会使2个刚体的平动和转动有接近的趋势,但力和力矩有限制。图中的力上限为0, 所以二者只能传递转动能。

增加关节代码:(片段)
void PhysicsManager::addJoint(b2Body* body1, b2Body* body2,const b2Vec2& p1, const b2Vec2& p2, const b2Vec2& p3 , const b2Vec2& p4, const b2Vec2& p5 )
{
    if((body1 || (_toGround && _jointType != b2JointType::e_gearJoint))&& body2)
    {
        if(_toGround &&_jointType != b2JointType::e_gearJoint)
            body1 = _groundBody;
        if(_jointType == b2JointType::e_wheelJoint)
        {
            b2WheelJointDef wjd;
            b2Vec2 axis(0.0f, 1.0f);
            wjd.Initialize(body1, body2, body2->GetPosition(), axis);
            // use the info set by UI.
            wjd.enableMotor = _enableMotor;  
            wjd.motorSpeed = _motorSpeed;
            wjd.maxMotorTorque = _maxMotorTorque;
            wjd.frequencyHz = _frequencyHz;
            wjd.dampingRatio = _dampingRatio;
            wjd.collideConnected = _collideConnected;
            auto wj = _world->CreateJoint(&wjd);
            _wheelJoints.push_back(wj);
        }
        // ignored for clarity....

```

其他关节也是用类似的方法添加。带下画线的变量由UI直接设定。


3.界面相关
设计了一个PopMenu类,包装了ListView,用于容纳Button,Slider和CheckBox控件,并在需要的时候弹入或弹出。
下面是使用范例,在MenuLayer中:
    auto pop = PopMenu::create();
    pop->addButton("Move",nullptr);
    pop->addButton("Add Regular",nullptr);
    pop->addButton("Add Custom",nullptr);
    pop->addButton("Add Joint",nullptr);
    pop->addButton("No Collide",nullptr);
    pop->addButton("Gadgets",nullptr);
    pop->setPosition(origin.x + visibleSize.width - pop->getListViewContentSize().width, origin.y + visibleSize.height);
    pop->setName("popMain");
    this->addChild(pop);
    pop->setLocalZOrder(100);
    pop->popEnter();

    pop->setCallback("Move",&](){
        PhysicsManager::getInstance()->setTouchType(PhysicsManager::MOVE_TYPE);
        _label->setString("Mode:Move");
        if(_popCurrent)
            _popCurrent->popExit();
        _popCurrent = nullptr;
        if(_popCurrent2)
            _popCurrent2->popExit();
        _popCurrent2 = nullptr;
    });
    // ignored for clarity

```

addButton会往ListView中增加一个指定名称的Button,并且可以设置回调函数。
可以看出Move,Add Regular Button的作用仅仅是改变PhysicsManager的状态,以及弹出或收回相应的菜单。
根据不同的状态,触摸函数会做出不同的响应,例如在Move状态时移动视角或者刚体,在Add Regular时增加规则刚体,在Add Custom时增加标记点,等等。
触摸响应函数如下:
bool MainScene::onTouchBegan(Touch* touch, Event* event)
{
    if(PhysicsManager::getInstance()->getTouchType() == PhysicsManager::MOVE_TYPE)
    {
        // do move stuff
    }
    if(PhysicsManager::getInstance()->getTouchType() == PhysicsManager::ADD_TYPE)
        return true;
    if(PhysicsManager::getInstance()->getTouchType() == PhysicsManager::ADD_CUSTOM_TYPE)
    {
        auto pos = this->convertToNodeSpace(touch->getLocation());
        return doMark(pos);
    }
    // etc...

```

其他状态也是类似情况。


4.数据持久化
好不容易制造的东西,一关程序就没了,非常可惜,应该具有存档/载入功能。
使用Sqlite3实现,但不知为何,写入速度很慢。
下面是加载不同关卡展示:
  
下面是一点示例代码。
在PhysicsManager::init()中初始化数据库:
    std::string path = FileUtils::getInstance()->getWritablePath() + "save.db";
    int result = sqlite3_open(path.c_str(), &_db);
    if(result!=SQLITE_OK)
    {
        log("open db failed, number %d", result);
    }
    createTables(_db);

```

建表:
void PhysicsManager::createTables(sqlite3* pdb)
{
    std::string sql="create table if not exists body(ID integer primary key autoincrement,save integer,num integer,type integer,x float, y float,angle float)";
    int result=sqlite3_exec(pdb,sql.c_str(),NULL,NULL,NULL);
    if(result!=SQLITE_OK)
        log("create table failed");
    // etc...

```

写入,saveBody()中:
        if(hasFixture)
        {
            auto sql = String::createWithFormat("insert into body(save,num,type,x,y,angle) values(%d,%d,%d,%f,%f,%f)",
                _saveNum,bodyNum, type, pos.x, pos.y, angle);
            int result=sqlite3_exec(_db,sql->getCString(),NULL,NULL,NULL);
            if(result!=SQLITE_OK)
            {
                log("insert body data failed!==>%s",sql->getCString());
            }
        }

```

读取,loadBody()中:
void PhysicsManager::loadBody()
{
    auto sql = String::createWithFormat("select num,type,x,y,angle from body where save=%d", _saveNum);
    sqlite3_stmt* statement;
    int result=sqlite3_prepare_v2(_db, sql->getCString(), -1, &statement, NULL);
    if(result!=SQLITE_OK)
    {
        log("select body data failed!");
        return;
    }
    while(sqlite3_step(statement) == SQLITE_ROW)
    {
        int bodyNum = sqlite3_column_int(statement, 0);
        int type = sqlite3_column_int(statement, 1);
        float x = sqlite3_column_double(statement, 2);
        float y = sqlite3_column_double(statement, 3);
        float angle = sqlite3_column_double(statement, 4);
        log("body:%d,%d,%f,%f,%f",bodyNum, type, x, y, angle);
        loadOneBody(_db, _saveNum, bodyNum, type, x, y, angle);
    }
}

```


其他细节可以参考代码。代码目前还不是特别完备,敬请见谅。
凌晨1点写完,希望大家多支持……
:964:
1赞

看着不错,赞一个

不错!赞个

不错哦 支持楼主!!!

顶一个:13::13:

楼主, 我爱你~ 最近想弄这块来着

赞一个!!!!!!

:14::14::14:

很给力。。。:2::2::2::2:

不错哦:2::2::2::2:

:2: :2: ,不错

:7::7:很好次的样子!

谢谢妹子的支持!!!
:964:

这个确实是不错,加油

谢谢支持!!
:14:

没跑成功, 总感觉缺各种~

这个太吊了。:801::801::801:

哪里哪里……
不过能被收进教程,我很高兴,哈哈。

:14:

支持下 ~~:14::14::14::14:

万能的,伟大的楼主,貌似缺少一点资源,比如"PhysicsPlayground":2::14: