前一阵子学习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:



