为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

俯视汽车物理模拟

2017-09-18 13页 doc 104KB 20阅读

用户头像

is_538355

暂无简介

举报
俯视汽车物理模拟俯视汽车物理模拟box2dc++教程出处:http://www.iforce2d.net/b2dtut/top-down-car注意:本教程适合有一定box2d基础者。关于如何用box2d来实现俯视汽车物理经常被大家讨论,因此我想我也许也来尝试一下,并且做一个专题。通常,俯视汽车是建立在一个0重力的模型下,由一个body作为底盘,四个单独的body作为轮子。如果模拟的不是十分精细的话,我们甚至可以不去管轮子,仅仅用一个body做车身就可以了。在俯视汽车物理模拟问题的核心就是阻止body垂直于轮胎方向的移动(不发生侧滑)的同时,...
俯视汽车物理模拟
俯视汽车物理模拟box2dc++教程出处:http://www.iforce2d.net/b2dtut/top-down-car注意:本教程适合有一定box2d基础者。关于如何用box2d来实现俯视汽车物理经常被大家讨论,因此我想我也许也来尝试一下,并且做一个专。通常,俯视汽车是建立在一个0重力的模型下,由一个body作为底盘,四个单独的body作为轮子。如果模拟的不是十分精细的话,我们甚至可以不去管轮子,仅仅用一个body做车身就可以了。在俯视汽车物理模拟问题的核心就是阻止body垂直于轮胎方向的移动(不发生侧滑)的同时,允许沿着轮胎方向的移动(轮子可以前后滚动)。这个问题本身倒不是什么大问题,但是给用户更好的汽车操纵感就是个技术活了。如果横向速度完全被消除,那么车子就好像走在轨道上,同时我们还想在一定情况下允许汽车漂移,同时在不同的地面材质上有不同的现,等等。在我们真正开始之前,你也许有兴趣瞧瞧DougKoellmer做的俯视汽车模拟不错的示例。qb2DemoReel.swf在第三个示例里。点nextdemo吧,看看别的也不错。最基本的工作就是我们找到body的横向速度,并且对其施加一个冲量来实现消除速度。我们从一个轮子的body开始,待会我们再把四个轮子安装在一个车身的body上来实现更复杂的模拟。由于轮子其实都是一回事,所以我们把轮子做成类。下面的代码,我们实现了把body作为轮子的一个成员变量,同时绑定一个四边形作为shape。C++代码 1234567891011121314151617181920 classTDTire{public:b2Body*m_body;TDTire(b2World*world){b2BodyDefbodyDef;bodyDef.type=b2_dynamicBody;m_body=world->CreateBody(&bodyDef);b2PolygonShapepolygonShape;polygonShape.SetAsBox(0.5f,1.25f);m_body->CreateFixture(&polygonShape,1);//shape,densitym_body->SetUserData(this);}~TDTire(){m_body->GetWorld()->DestroyBody(m_body);}};Love2d代码Tire=class(“tire”)Functiontire:initialize()Tire.body=newbody(world,0,0,”dynamic”)Tire.shape=newbox(0.5,1.25)Tire.fixture=(self.body,self.shape)Tire.fixture:setUserData(self)EndFunctiontire:destroy()end这里我用到里一个简便去创建fixture,这样就不必单独定义fixture了。注意,我们把轮子的地址记录为userdata了,这样游戏逻辑和物理逻辑就可以相互调用了。消除横向速度要去消除横向速度,我们首先需要知道它是什么。我们可以用body的当前速度分解到body的本地坐标系的横向来得到它。如果轮子的本地坐标系中向量(0,1)作为前进方向的话,那么(1,0)就是它的右侧方向。我们可以用getworldvector来把这些方向向量来转化到世界坐标系中。例如,假设一个轮子滚动了一点并且向前移动了些(如图所示)。我们希望将蓝色的向量分解到红色方向向量上,就可以知道蓝色速度在红色方向的大小了。因此,我们可以在轮子类中添加一个方法 1234 b2Vec2getLateralVelocity(){b2Vec2currentRightNormal=m_body->GetWorldVector(b2Vec2(1,0));returnb2Dot(currentRightNormal,m_body->GetLinearVelocity())*currentRightNormal;}好啦,我们现在可以施加一个冲量来消除上面提到的侧方向速度了。这个跟movingatconstantspeed教程中最后一部分描述的十分类似,我们有个预期的速度,我们在同一帧内对body的质心施加冲量,来达到那个速度。让我们在轮子类中添加一个方法,在每帧都去调这方法。 1234 voidupdateFriction(){b2Vec2impulse=m_body->GetMass()*-getLateralVelocity();m_body->ApplyLinearImpulse(impulse,m_body->GetWorldCenter());}做完了这些,如果你在模拟器上创建一个这么个轮子再推一推它,你会发现确实它的侧边速度被这个冲量消除了。如果你向前推这个轮子,它会试图走成一个圈,几乎就像是我们推一个轮子,轮子慢慢减速。当然,你也会发现轮子会无约束的自转,这个跟现实不相符。真实的轮子不会那样,所以我们要像消除侧速度那样消除自转速度。转动速度消除更加容易实现,因为我们不需要去做速度分解工作,将下面的代码也加到updatefriction方法中, 1 m_body->ApplyAngularImpulse(0.1f*m_body->GetInertia()*-m_body->GetAngularVelocity());系数0.1这个值是我有次自己转轮胎时看起来的效果,我在模拟时,这个值与那次效果最相似。如果我们我们把自转也完全消除,那么轮子像在轨道上就只能走直线了。最后,你可能发现轮子可以一直向前走,因此我们需要给它点阻力让它最终停下来。 1234 b2Vec2currentForwardNormal=getForwardVelocity();floatcurrentForwardSpeed=currentForwardNormal.Normalize();floatdragForceMagnitude=-2*currentForwardSpeed;m_body->ApplyForce(dragForceMagnitude*currentForwardNormal,m_body->GetWorldCenter());有一次,我们发现有个系数2,这个也是我们一点一点调试出来的。当然,你们这些聪明的人自然已经想到getforwardvleocity()跟getlateralvelocity()其实是一样的,只不过局部方向向量从1,0改到了0,1控制轮子在把轮子装到车上之前,我们还需要为单独的轮子稍微完善下。至少我们需要让他能够向前和向后移动,并且让它能够模拟现实的漂移,让他能够对不同的地表有所反应。在这些做完之后,我们再去组装一辆四轮的车。测试一个轮子是否运行得当,实际上我们可以假装这个轮子就是台车,并让用户去滚动它。下面是一个简单的键盘检验的方法,通过w/a/s/d来控制。 123456789101112131415161718192021222324252627282930313233 //globalscopeenum{TDC_LEFT=0x1,TDC_RIGHT=0x2,TDC_UP=0x4,TDC_DOWN=0x8};//testbedTestclassvariableintm_controlState;//testbedTestclassconstructorm_controlState=0;//testbedTestclassfunctionsvoidKeyboard(unsignedcharkey){switch(key){case'a':m_controlState|=TDC_LEFT;break;case'd':m_controlState|=TDC_RIGHT;break;case'w':m_controlState|=TDC_UP;break;case's':m_controlState|=TDC_DOWN;break;default:Test::Keyboard(key);}}voidKeyboardUp(unsignedcharkey){switch(key){case'a':m_controlState&=~TDC_LEFT;break;case'd':m_controlState&=~TDC_RIGHT;break;case'w':m_controlState&=~TDC_UP;break;case's':m_controlState&=~TDC_DOWN;break;default:Test::Keyboard(key);}}注意,keyboardup在最新的box2d模拟器上可用,如果你用2.1.2版本的话是没有这个函数的,你可以用最新的模拟器或者自己照着上面的方法做一下。现在我们向轮子类中添加个方法来适配输入状态。 1234567891011121314151617181920212223242526272829 //tireclassvariablesfloatm_maxForwardSpeed;//100;floatm_maxBackwardSpeed;//-20;floatm_maxDriveForce;//150;//tireclassfunctionvoidupdateDrive(intcontrolState){//finddesiredspeedfloatdesiredSpeed=0;switch(controlState&(TDC_UP|TDC_DOWN)){caseTDC_UP:desiredSpeed=m_maxForwardSpeed;break;caseTDC_DOWN:desiredSpeed=m_maxBackwardSpeed;break;default:return;//donothing}//findcurrentspeedinforwarddirectionb2Vec2currentForwardNormal=m_body->GetWorldVector(b2Vec2(0,1));floatcurrentSpeed=b2Dot(getForwardVelocity(),currentForwardNormal);//applynecessaryforcefloatforce=0;if(desiredSpeed>currentSpeed)force=m_maxDriveForce;elseif(desiredSpeed<currentSpeed)force=-m_maxDriveForce;elsereturn;m_body->ApplyForce(force*currentForwardNormal,m_body->GetWorldCenter());}调整下speed和force值来实现你喜欢的样子。最开始时,我还没怎么考虑尺寸的问题,所以这个轮子要是1米宽的话,就有点不自然了,这些速度也不是现实速度。现在轮子可以向前向后移动了,然后就是通过a/d键来施加扭力让轮子转向了。因为我们的目标是把轮子安装在车上,以你下面这段代码仅仅是展示一个粗略的控制方法让我们来测试后面的阶段-漂移和路面,具体实现我们在后面介绍,另一方面,如果你把车子就做成一个body的话,你可以把这个做的再精细点,比如除非车在移动,不让车子转向。 123456789 voidupdateTurn(intcontrolState){floatdesiredTorque=0;switch(controlState&(TDC_LEFT|TDC_RIGHT)){caseTDC_LEFT:desiredTorque=15;break;caseTDC_RIGHT:desiredTorque=-15;break;default:;//nothing}m_body->ApplyTorque(desiredTorque);}设置漂移目前为止,我们已经可以让我们的小车按照我们的想法通过消除横向速度来实现仿真。要是我们仅仅想实现一个有车道的模拟还可以,但是如果让车子能够滑行一点的话,会上车子更加自然。不幸的是,这实现起来真的很难,,,哈哈,开个玩笑0。0.事实上,我们已经完成了这个工作。记得我们消除侧方向速度是完全消除的,对吧,我们仅仅需要计算一下需要的冲量,然后再施加上去就好了。那样也不是很现实,因为意味着永远不会出现侧滑。因此我们需要做的就是把冲量限制到一个上限,那么当环境使得轮子做一些滑动的妥协。这个仅仅是在updatefriction中额外的一些说明而已。 12345 //inupdateFriction,lateralvelocityhandlingsectionb2Vec2impulse=m_body->GetMass()*-getLateralVelocity();//existingcodeif(impulse.Length()>maxLateralImpulse)impulse*=maxLateralImpulse/impulse.Length();m_body->ApplyLinearImpulse(impulse,m_body->GetWorldCenter());//existingcode我发现把maxlateralimpulse系数定为3可以使车子在高速运行转向时发生小的漂移,而系数2更加类似于湿滑的路面,而系数1则类似于水上的快艇。这个系数我们之后装在车子上后再调试,所以也不需要大惊小怪的。上面的代码可以下载Sourcecodeuptothispoint轮到不同的路面了为了定义场景中某区域的不同路面情况,我们需要绑定一个groundbody,并且用碰撞回调来跟踪轮子与他们接触。这个跟我之前的一个教程jumpability中我们在人物脚下设置一个感受器来判定人物是否与地面接触类似。位移的区别是,这次地面是感受器,因为我们需要在上面行驶,车辆是实体,我们需要它跟其他物体碰撞。所以我们可以把绑定fixture设置一个整数userdata标签来指定它是不同的地面,就像jumpability中提到的。然后我们可以监听beginContact/endContact回调来判定是否有轮子进入/离开了区域。然而,这种方式仅仅适用于你确定所有的fixture的userdata都用整数作为标签。在一个完整的游戏中,可定会有很多的fixture相互碰撞。更好的方法是,将更多的信息放到userdata中,而不单单是一个整数。比如表面阻力,比如我们可能需要知道车子是否跑出跑到冲到观众席等等。我们同样要能够改变这些信息而不需要每次都要通过setUserData来更新。比如,一些路面情况的改变(原版湿滑的路面渐渐变干)有很多种方法来实现用户数据的改变,但这不是我们今天要讨论的主题,既然我们之前从来都没真正细致的描述,借着这个机会,我向大家展示一个我经常做的做法。我不确定是否有某种典型的或者推荐的方式,但是我这个一般都ok。我们来建立一个通用的类去描述fixture的数据。 123456789101112131415 //typesoffixtureuserdataenumfixtureUserDataType{FUD_CAR_TIRE,FUD_GROUND_AREA};//aclasstoallowsubclassingofdifferentfixtureuserdataclassFixtureUserData{fixtureUserDataTypem_type;protected:FixtureUserData(fixtureUserDataTypetype):m_type(type){}public:virtualfixtureUserDataTypegetType(){returnm_type;}virtual~FixtureUserData(){}};然后做一个子类来存储特殊类型的fixture以及他们相关信息。 1234567891011121314151617 //classtoallowmarkingafixtureasacartireclassCarTireFUD:publicFixtureUserData{public:CarTireFUD():FixtureUserData(FUD_CAR_TIRE){}};//classtoallowmarkingafixtureasagroundareaclassGroundAreaFUD:publicFixtureUserData{public:floatfrictionModifier;booloutOfCourse;GroundAreaFUD(floatfm,boolooc):FixtureUserData(FUD_GROUND_AREA){frictionModifier=fm;outOfCourse=ooc;}};我们现在还不准备把“飞出跑道”这个设置加入到我们路面fixture中,我添加这个就是让大伙知道现在我们可以把一大堆的信息装到userdata中。下面的代码描述了如何建立几个静止的路面fixture并且将信息通过类放在userdata中。你可能注意到我们刚才通过new来创建了一个userdata的实例但是并没有引用它。诚然,box2d可以引用,但是我们用过之后,它并不管删除,这可能导致内存泄露。处理这个问题的方法是当我们删除了body和fixture时,需要从box2d获取这个指针并且删除它。这是个烦人的工作,特别是几乎但凡有fixture被删除,就有指针需要被处理。幸亏erin已经想到前面了。Box2d有一个销毁监听器,当我们销毁fixture的时候,就会搜集body一并销毁。这是我要提及的另一个事。现在,我们要做一个b2destructionlistener类的子类,移植一个销毁fixture的函数,并在world中创建一个实例。 123456789101112131415161718 //globalscopeclassMyDestructionListener:publicb2DestructionListener{voidSayGoodbye(b2Fixture*fixture){if(FixtureUserData*fud=(FixtureUserData*)fixture->GetUserData())deletefud;}//(unused,butwemustimplementallpurevirtualfunctions)voidSayGoodbye(b2Joint*joint){}};//testbedTestclassvariableMyDestructionListenerm_destructionListener;//intestbedTestclassconstructorm_world->SetDestructionListener(&m_destructionListener);现在我们可以向测试类添加一个销毁器,这样我们销毁一个fixture的时候,它就帮我们销毁相关的userdata。 234 ~iforce2d_TopdownCar(){m_world->DestroyBody(m_groundBody);}现在我们已经做完了由不同的userdata描述的路面fixture了,我们在碰撞时就需要处理这些东西。这个是另一个无官方最简单方法来处理的工作,但是我们继续使用我经常使用的一个。碰撞监听我们在其他的教程Contactlisteners已经说过了,我这里就不多说了,现在我们就要试着建立一个回调函数来处理每次在不同路面fixture的begin/end碰撞,这样我们就可以专心写游戏逻辑了。比如,在这个场景下,仅有两种路面。voidtire_vs_groundArea(b2Fixture*tireFixture,b2Fixture*groundAreaFixture,boolbegan);或者你也可以把这个做到轮子类里,把地面fixture的userdata参数传入轮子,让轮子来进行判断。你也可以把它做到路面类里(如果有),只要别双方同时告诉对方信息就行,那样就混乱了。我认为需要明确一个控制点来告诉二者信息。无论如何,让这个函数正确工作就是在碰撞回调中检验路面fixture的userdata。在这个教程中,我们只管碰撞开始和结束,因此我们可以复制下面的代码: 123456789101112131415161718 voidhandleContact(b2Contact*contact,boolbegan){b2Fixture*a=contact->GetFixtureA();b2Fixture*b=contact->GetFixtureB();FixtureUserData*fudA=(FixtureUserData*)a->GetUserData();FixtureUserData*fudB=(FixtureUserData*)b->GetUserData();if(!fudA||!fudB)return;if(fudA->getType()==FUD_CAR_TIRE&&fudB->getType()==FUD_GROUND_AREA)tire_vs_groundArea(a,b,began);elseif(fudA->getType()==FUD_GROUND_AREA&&fudB->getType()==FUD_CAR_TIRE)tire_vs_groundArea(b,a,began);}voidBeginContact(b2Contact*contact){handleContact(contact,true);}voidEndContact(b2Contact*contact){handleContact(contact,false);}很好,现在tirevsgroundArea函数需要做点什么。我们需要检验当前脚下的路面情况,所以轮子要记录一组GroundAreaFUD,同时它还需要知道当前的牵引状态,来在不同的路面中调整牵引力。 1234567891011121314151617181920212223242526 //tireclassvariablesstd::set<GroundAreaFUD*>m_groundAreas;floatm_currentTraction;//tireclassconstructorm_currentTraction=1;//tireclassfunctionsvoidaddGroundArea(GroundAreaFUD*ga){m_groundAreas.insert(ga);updateTraction();}voidremoveGroundArea(GroundAreaFUD*ga){m_groundAreas.erase(ga);updateTraction();}voidupdateTraction(){if(m_groundAreas.empty())m_currentTraction=1;else{//findareawithhighesttractionm_currentTraction=0;std::set<GroundAreaFUD*>::iteratorit=m_groundAreas.begin();while(it!=m_groundAreas.end()){GroundAreaFUD*ga=*it;if(ga->frictionModifier>m_currentTraction)m_currentTraction=ga->frictionModifier;++it;}}}我需要指出,因为轮子类也有路面fixture的userdata的引用,如果轮子再路面上删除这个路面时,会出现引用为空的情况。理想的处理方法是每当有fixture删除时,每个轮子也运行一次销毁程序。几乎快完事了,现在我们需要把逻辑加入到tire_vs_groundArea程序中让他滚蛋了。实际实现起来相当简单,因为我们已经知道了路面的类型了,就可以根据类型进行一些判断而不需要深入到userdata中的每一项。 123456789 voidtire_vs_groundArea(b2Fixture*tireFixture,b2Fixture*groundAreaFixture,boolbegan){TDTire*tire=(TDTire*)tireFixture->GetBody()->GetUserData();GroundAreaFUD*gaFud=(GroundAreaFUD*)groundAreaFixture->GetUserData();if(began)tire->addGroundArea(gaFud);elsetire->removeGroundArea(gaFud);}试一下下面的就可以知道什么情况下会产生最强的牵引力。把这个结束掉,用当前的牵引力去调整是施加在轮子上的friction,drag和power.在updatefriction和userdataDrive函数中。为了简便,我直接用牵引力乘以上述变量。同时我们也要保持牵引力处于一个0~合理的区间内。以上内容源码Sourcecodeuptothispoint整合在一起现在我们有一个漂亮的轮子可以驱动和漂移了。根据你的游戏,一些小改变,对于一些游戏已经足够了,但是我不认为你来这就到此结束了,更好玩的是你把四个轮子装起来组装成一个完整的车。幸运的是,零件都已经准备好了,我们现在剩下的就是做个body,再把控制部分调整下。让我们从一个四个轮子的车开始,然后我们来处理操作问题。我们需要一个类来表示一辆车。 1234567891011121314151617181920212223 classTDCar{b2Body*m_body;public:TDCar(b2World*world){//createcarbodyb2BodyDefbodyDef;bodyDef.type=b2_dynamicBody;m_body=world->CreateBody(&bodyDef);b2Vec2vertices[8];vertices[0].Set(1.5,0);vertices[1].Set(3,2.5);vertices[2].Set(2.8,5.5);vertices[3].Set(1,10);vertices[4].Set(-1,10);vertices[5].Set(-2.8,5.5);vertices[6].Set(-3,2.5);vertices[7].Set(-1.5,0);b2PolygonShapepolygonShape;polygonShape.Set(vertices,8);b2Fixture*fixture=m_body->CreateFixture(&polygonShape,0.1f);//shape,density}};下一步建立轮子并且安装上。因为我们需要前轮同时转向,我们需要一个wheeljoint。并且要设置要设置好转向的最低值和最高值。后轮使用简单的weltjoint就可以了。 12345678910111213141516171819 //carclassvariablesstd::vector<TDTire*>m_tires;b2RevoluteJoint*flJoint,*frJoint;//carclassconstructorb2RevoluteJointDefjointDef;jointDef.bodyA=m_body;jointDef.enableLimit=true;jointDef.lowerAngle=0;//withboththeseatzero...jointDef.upperAngle=0;//...thejointwillnotmovejointDef.localAnchorB.SetZero();//jointanchorintireisalwayscenterTDTire*tire=newTDTire(world);jointDef.bodyB=tire->m_body;jointDef.localAnchorA.Set(-3,8.5f);flJoint=(b2RevoluteJoint*)world->CreateJoint(&jointDef);m_tires.push_back(tire);//(...othertiresaresimilar...)很明显,这个漂亮的机器是从。。。法拉第山寨来的。相当老的车型,你晓得。不,它绝不像一直青蛙,也不像别的什么。不管怎样,让我们来写个输入函数来控制轮子。 1234567 //carclassfunctionvoidupdate(intcontrolState){for(inti=0;i<m_tires.size();i++)m_tires[i]->updateFriction();for(inti=0;i<m_tires.size();i++)m_tires[i]->updateDrive(controlState);}我去,我居然花了10分钟来玩这个,而且还不能转向。。。我相信这是个好的信号,我还为这个大家伙更高的加速和极速配置了更好的轮子。floatmaxForwardSpeed=250;floatmaxBackwardSpeed=-40;floatmaxDriveForce=300;前轮转向我们可以用关节马达,但是如果两个轮子没有任何关联的话会无法同步,我们要有特殊的方法来处理这个问题。对于这个教程,我决定用关节限制来控制转向,这样强制轮子转到某个角度,虽然这个方法很异类,但是工作起来还不错,而且可以让我们可以独立的控制转向速度,而不受其他因素影响(因为我们已经移出了角转速),而不去使用传统的关节马达速度和扭力。 12345678910111213141516 //carclassfunctionfloatlockAngle=40*DEGTORAD;floatturnSpeedPerSec=320*DEGTORAD;//fromlocktolockin0.25secfloatturnPerTimeStep=turnSpeedPerSec/60.0f;floatdesiredAngle=0;switch(controlState&(TDC_LEFT|TDC_RIGHT)){caseTDC_LEFT:desiredAngle=lockAngle;break;caseTDC_RIGHT:desiredAngle=-lockAngle;break;default:;//nothing}floatangleNow=flJoint->GetJointAngle();floatangleToTurn=desiredAngle-angleNow;angleToTurn=b2Clamp(angleToTurn,-turnPerTimeStep,turnPerTimeStep);floatnewAngle=angleNow+angleToTurn;flJoint->SetLimits(newAngle,newAngle);frJoint->SetLimits(newAngle,newAngle);40度可能有点太高?我们试试当我们玩这个的时候,我们发现漂移飘的很爽,感觉一直在不停的漂移,但是有点飘的像在石子路上了。我想可能是由于额外的车重造成的。后来我尝试前后轮采用不同的材质,不过感觉没想象中的那么好玩。要想更好的操作感,可能需要对在不同速度下的阻力、动力进行一点点的调整,但这个就是你们要做的了。结论。。。额,你还在么?这个教程比我预期的貌似要长点,而且内容不仅仅包括将横向速度消除这么点。把这个变成一个好玩的游戏还有很长的路要走,但希望这个教程是个好的开端。下载源文件自己编译或者玩会已经编译好的东西吧
/
本文档为【俯视汽车物理模拟】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索