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