原文链接
http://www.iforce2d.net/b2dtut/top-down-car
最近讨论'top down' car physics如何很实现的话题很越来越多,所以我想我可以试一试并且展开一个话题。一个 td(top-down) car 被设计成在一个0重力的世界中,被一个为底盘的身体和四个分离的轮子身体。它取决于仅仅用一个带有底盘的身体
和不必担心的分离的轮子。
问题的关键的另一种情况是阻止身体沿一条轴移动,然而仍然允许他在另外一条轴上自由移动(轮子应该可以来回移动。)这本身并不是一个艰难的壮举,诀窍是使得那些用户来控制车有很好的感受。如果水平速度被简单的完全种植,车子就会感觉像是在铁路上跑一样,并且我们想允许车子可以在一定的情况下大话,并且在不同的表面有不同的表现等等。在我们开始以前你可以看一下这个完美实现的top-down赛车游戏.这种事情是我们的目标。
基础的步骤是去找到最近的身体的水平速度并且给他一个推理是的我们可以取消那个水平速度。我们开始仅仅用一个身体来代表一个轮子,一会我们用四个它来固定到另外一个身体来实现更复杂的模拟。因为所有的轮子做同样的事情,我们可以为他做一个类。这是起始点,一个带有b2Body指针自以为成员变量并且带着一个简单的盒子形状来初始化。
class TDTire {
public:
b2Body* m_body;
TDTire(b2World* world) {
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
m_body = world->CreateBody(&bodyDef);
b2PolygonShape polygonShape;
polygonShape.SetAsBox( 0.5f, 1.25f );
m_body->CreateFixture(&polygonShape, 1);//shape, density
m_body->SetUserData( this );
}
~TDTire() {
m_body->GetWorld()->DestroyBody(m_body);
}
};
这里我是用了一个CreateFixture不需要fixture定义的简单版本,注意到我们也设置了数据来创建b2body,所以这个物理身体和游戏逻辑会彼此涉及。
----------------------------------------------------------------------------------
消除水平速度。
为了取消水平速度,我们需要知道它是什么。我们可以找到在最近时间内身体的水平速度在赛道方向的法线上的投影假。设轮子的当前坐标是(0,1)将会向前走1,0将会向右走我们可以使用获取世界向量来获取最近的身体在世界坐标中的朝向。
假设我们的轮胎旋转了一点点,向上移动如下图所示。我们想要使得蓝色向量向红色向量投影来看看他有多长如果他仅仅是以红色方向移动。
我们可以增加下面这样的方法到TDTire里面:
b2Vec2 getLateralVelocity() {
b2Vec2 currentRightNormal = m_body->GetWorldVector( b2Vec2(1,0) );
return b2Dot( currentRightNormal, m_body->GetLinearVelocity() ) * currentRightNormal;
}
然后给他一个推力,使得摆脱他的水平速度。这和最后一部分“一恒定速度前进”很相似,那里我们有一个渴望的速度。我们应用一个推力乘以身体的质量,使其达到这一速度在一个时间步。我们增加一个函数使得它在一个时间步让他做这些。
void updateFriction() {
b2Vec2 impulse = m_body->GetMass() * -getLateralVelocity();
m_body->ApplyLinearImpulse( impulse, m_body->GetWorldCenter() );
}
在舞台上,如果你创建了一个轮子身体在世界中,如果使用鼠标推动他,将会注意到赛道方向的速度确实被推力取消了,如果朝前方推动轮子你会发现它更倾向于转圆圈。几乎他就像一个真的轮胎一样当你滚动轮子并且他会慢慢停下来。
你也会注意到他可以自由旋转,使得它看起来有点不真实,一个真实的车轮子不是那样的,所以让我们给用
相似的方式来消除侧面速度。。旋转更容易一些,因为我们不需要做这个向量投影的东西——添加这个到updateFriction:
m_body->ApplyAngularImpulse( 0.1f * m_body->GetInertia() * -m_body->GetAngularVelocity() );
0.1是我决定的值,他有点像我上一次旋转的轮胎。如果我们完全停掉轮子的旋转,他将会像在铁路一样,哪里都不能去,除了走直线。没有完全阻止轮胎的旋转的另一个原因是我们想让玩家来使用这个身体来驱动它。
最后你可能注意到轮子会永远朝前走,所以我们给一个强力使得它最终停止。
b2Vec2 currentForwardNormal = getForwardVelocity();
float currentForwardSpeed = currentForwardNormal.Normalize();
float dragForceMagnitude = -2 * currentForwardSpeed;
m_body->ApplyForce( dragForceMagnitude * currentForwardNormal, m_body->GetWorldCenter() );
再次进入updateFriction和值2来自一些诈骗和调整。当然你聪明的人自动知道getForwardVelocity getLateralVelocity()是一样的(),但与当地的一个向量(0,1)代替(1,0)对吗?
控制一个轮子。
在我们将要制作四轮车之前,我们需要更多的考虑下一个轮子应该做的事情。我们至少能让他往前往后走。我也要使得它能够打滑,并且处理不同的表面。我们先集中精力做好一个,剩下四个就会很容易。
要测试轮胎是受多种运动的情况,我们可以先假设这个单一的轮胎本身就是一辆汽车,让用户直接旋转它。这是一个基本的方式跟踪的关键(W/A/S/D)的用户是当前按下的按键:
//global scope
enum {
TDC_LEFT = 0x1,
TDC_RIGHT = 0x2,
TDC_UP = 0x4,
TDC_DOWN = 0x8
};
//testbed Test class variable
int m_controlState;
//testbed Test class constructor
m_controlState = 0;
//testbed Test class functions
void Keyboard(unsigned char key) {
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);
}
}
void KeyboardUp(unsigned char key) {
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 testbed中使用,但是如果你正在使用v2.1.2 从其他的教程中找不到。你可以获取最新的box2d版本,或者在testbed中增加实现,他是很简单的仅仅使用键盘函数。
让我们增加一个函数到轮子的clas中,使得他在输入的状态中做更聪明的事情。
float m_maxForwardSpeed; // 100;
float m_maxBackwardSpeed; // -20;
float m_maxDriveForce; // 150;
//tire class function
void updateDrive(int controlState) {
//find desired speed
float desiredSpeed = 0;
switch ( controlState & (TDC_UP|TDC_DOWN) ) {
case TDC_UP: desiredSpeed = m_maxForwardSpeed; break;
case TDC_DOWN: desiredSpeed = m_maxBackwardSpeed; break;
default: return;//do nothing
}
//find current speed in forward direction
b2Vec2 currentForwardNormal = m_body->GetWorldVector( b2Vec2(0,1) );
float currentSpeed = b2Dot( getForwardVelocity(), currentForwardNormal );
//apply necessary force
float force = 0;
if ( desiredSpeed > currentSpeed )
force = m_maxDriveForce;
else if ( desiredSpeed < currentSpeed )
force = -m_maxDriveForce;
else
return;
m_body->ApplyForce( force * currentForwardNormal, m_body->GetWorldCenter() );
}
使用速度和力量来到使他变成你想要的那样,话题的开始我们有太多考虑过尺寸,并且我的轮子不是个不真实的一宽,所以这些速度也不是真实世界的值。
现在轮子可以向前移动和向后移动,我们也使得它运转通过事假一些扭转力,当a/d 按键被按下。因为我们最后的目标是绑定这些轮子到一个车子的身上,程序的这部分将会被丢掉,所以他只是一个粗略的方式使得拐弯发生。所以我们可以测试下一部分--打滑和表面。另一方面,如果你真的倾向于设计汽车作为单体,你可能需要冲个这些使得它够合乎情理的。e.g.不要让车子拐弯除非他在移动。
void updateTurn(int controlState) {
float desiredTorque = 0;
switch ( controlState & (TDC_LEFT|TDC_RIGHT) ) {
case TDC_LEFT: desiredTorque = 15; break;
case TDC_RIGHT: desiredTorque = -15; break;
default: ;//nothing
}
m_body->ApplyTorque( desiredTorque );
}
Allowing skidding
这一点我们有一个可以控制的汽车身体,根据我们最初的计划来消除横向速度,他表现的良好。这是非常好的,如果你想模拟的槽车,坚持自己的轨道像胶水。但是他感觉更自然如果车子可以再打滑一点。记得当我们完全横向速度的时候,我们怎样完全消除他的?我们简单的计算一下推力并给他加上去。所以所有我们需要做的事情就是约束这个推力到最大值,并且轮子将会打滑当环境要求一个比允许值更大的修改。在updateFriction函数中这仅仅是一个另外的状态。
b2Vec2 impulse = m_body->GetMass() * -getLateralVelocity(); //existing code
if ( impulse.Length() > maxLateralImpulse )
impulse *= maxLateralImpulse / impulse.Length();
m_body->ApplyLinearImpulse( impulse, m_body->GetWorldCenter() ); //existing code
我发现maxLateralImpulse为3的时候允许很少的数量的打滑 当以告诉度拐弯的时候,值为2的时候就像一个湿撸,值为1的时候是我想起一个行驶的小船在水上拐弯。这些值无论如何将会被适应当轮子连接到车子底盘。所以现在不要大惊小怪。