游戏中一种常见的需求是让物体做匀速运动。例如横屏游戏中的玩家角色,太空飞船或者汽车,等等。根据游戏的不同,有时候物体应该逐渐改变速度,其他情况又希望能够瞬间开始和停止运动。使用SetLinearVelocity方法精确的设置物体速度,这种方法看起来非常诱人,而且这么做也可以达到目标,但是这种方法有其自身的缺点。虽然这么做在屏幕上看起来很好,但是直接设置物体的速度不是参与模拟物理世界的正确方法。让我们看看如何使用实际的力和冲量来使物体达到特定速度。
我们将会看到两种情况,一种是物体立即开始移动到特定速度,另一种是物体缓慢移动直到达到特定速度。一开始,我们需要一个有动态物体的场景,然后再放一些静态物体的篱笆墙来防止这个动态物体到处跑。篱笆墙的场景在后面的话题中还会经常遇到。为了持续跟踪用户所做的操作,我们会创建一个类成员变量来记录上一次用户所做的输入状态。
//enumeration of possible input states enum _moveState { MS_STOP, MS_LEFT, MS_RIGHT, }; //class member variables b2Body* body; _moveState moveState; FooTest() { //body definition b2BodyDef myBodyDef; myBodyDef.type = b2_dynamicBody; //shape definition b2PolygonShape polygonShape; polygonShape.SetAsBox(1, 1); //a 2x2 rectangle //fixture definition b2FixtureDef myFixtureDef; myFixtureDef.shape = &polygonShape; myFixtureDef.density = 1; //create dynamic body myBodyDef.position.Set(0, 10); body = m_world->CreateBody(&myBodyDef); body->CreateFixture(&myFixtureDef); //a static body myBodyDef.type = b2_staticBody; myBodyDef.position.Set(0, 0); b2Body* staticBody = m_world->CreateBody(&myBodyDef); //add four walls to the static body polygonShape.SetAsBox( 20, 1, b2Vec2(0, 0), 0);//ground staticBody->CreateFixture(&myFixtureDef); polygonShape.SetAsBox( 20, 1, b2Vec2(0, 40), 0);//ceiling staticBody->CreateFixture(&myFixtureDef); polygonShape.SetAsBox( 1, 20, b2Vec2(-20, 20), 0);//left wall staticBody->CreateFixture(&myFixtureDef); polygonShape.SetAsBox( 1, 20, b2Vec2(20, 20), 0);//right wall staticBody->CreateFixture(&myFixtureDef); moveState = MS_STOP; }
下面我们需要Keyboard()方法实现键盘输入:
void Keyboard(unsigned char key) { switch (key) { case 'q': //move left moveState = MS_LEFT; break; case 'w': //stop moveState = MS_STOP; break; case 'e': //move right moveState = MS_RIGHT; break; default: //run default behaviour Test::Keyboard(key); } }
从现在开始,所有需要改变的特性都会在Step()方法中实现,物体具体的行为取决于用户的输入。
之前我们是以力/冲量方法作为开始的,让我们看看如何使用SetLinearVelocity方法来直接指定物体的速度。对于许多应用来说这么做已经足够好了。在Step()方法内部,完成一些每帧都需要更新的操作:
//inside Step() b2Vec2 vel = body->GetLinearVelocity(); switch ( moveState ) { case MS_LEFT: vel.x = -5; break; case MS_STOP: vel.x = 0; break; case MS_RIGHT: vel.x = 5; break; } body->SetLinearVelocity( vel );
这里,我们获取当前速度并且保持垂直方向的速度不变,相反只改变横向速度,因为我们只想影响物体水平运动的速度。
试着在testbed场景中加入上述代码,可以看到物体的速度会瞬间改变。为了让物体速度缓慢的变化至最大特定速度,使用下面代码替换上述代码:
switch ( moveState ) { case MS_LEFT: vel.x = b2Max( vel.x - 0.1f, -5.0f ); break; case MS_STOP: vel.x *= 0.98; break; case MS_RIGHT: vel.x = b2Min( vel.x + 0.1f, 5.0f ); break; }
这会在每帧计算的时候线性增加0.1,直到在该方向上增加到最大速度5为止,在testbed框架中,默认为每秒60帧,只要50帧或者一秒钟就达到最大速度了。当按下按下stop按键,速度就会减小到前一帧速度的98%,一秒钟算下来就是0.98 ^ 60=每秒大概0.3。这个方法的一个优点是可以很容易的针对加速特性进行调整。
使用力更适合使物体缓慢加速到指定速度,首先让我们尝试一下:
b2Vec2 vel = body->GetLinearVelocity(); float force = 0; switch ( moveState ) { case MS_LEFT: if ( vel.x > -5 ) force = -50; break; case MS_STOP: force = vel.x * -10; break; case MS_RIGHT: if ( vel.x < 5 ) force = 50; break; } body->ApplyForce( b2Vec2(force,0), body->GetWorldCenter() );
和上面加速方式类似,也是线性的,但是刹车是非线性的。针对这个例子我们得到了一个在每帧更新的时候对物体施加最大力并让其缓慢加速的简单的基本逻辑。你多半会希望把当前正在做的例子做一些调整,例如一辆汽车以低速为起点进行快速加速,但是随着速度越来越接近最大速度,加速度也会随之减小。因此,你要查看当前速度和最大速度之间的差距并且适当的减小力。
从之前的话题中我们得知力扮演了一个缓慢加速的角色,使用力实现瞬间加速的效果乍看起来貌似是不太可能。不过,如果我们让时间非常短,力非常大,作为冲量我们还是可以得到相同的效果的。首先我们需要做一点点数学运算…
力和加速度之间的关系是f = ma
,其中m
是我们需要移动的物体的质量,a是‘每秒钟单位变化率’物体移动的速度也就是加速度,f是我们想要进行计算的力。加速度也可以称为“每秒钟的速率”,既然这里的加速度和“每秒钟”是一回事。那么我们就可以写成f=mv/t,其中t是力作用的时间长度。
我们可以通过使用GetMass()方法获取物体的质量m
。v
是我们想要在最大速度和当前速度之间进行改变的变量。为了达到瞬间改变速度的效果,如果在默认的testbed框架内,我们需要在每一帧或者说1/60秒的时间内不断的施加力。现在我们知道了除f
以外的所有条件,具体实现可以像下面这样:
b2Vec2 vel = body->GetLinearVelocity(); float desiredVel = 0; switch ( moveState ) { case MS_LEFT: desiredVel = -5; break; case MS_STOP: desiredVel = 0; break; case MS_RIGHT: desiredVel = 5; break; } float velChange = desiredVel - vel.x; float force = body->GetMass() * velChange / (1/60.0); //f = mv/t body->ApplyForce( b2Vec2(force,0), body->GetWorldCenter() );
这和使用SetLinearVelocity方法具有同样的效果,而且还符合真实的物理场景应用。
聪明的读者或许已经发现上面的代码中可以简单的使用冲量来代替。既然冲量本身已经模拟了每帧中力的累加计算,我们只要去除时间部分然后使用ApplyLinearImpulse方法就可以达到相同的效果:
b2Vec2 vel = body->GetLinearVelocity(); float desiredVel = 0; switch ( moveState ) { case MS_LEFT: desiredVel = -5; break; case MS_STOP: desiredVel = 0; break; case MS_RIGHT: desiredVel = 5; break; } float velChange = desiredVel - vel.x; float impulse = body->GetMass() * velChange; //disregard time factor body->ApplyLinearImpulse( b2Vec2(impulse,0), body->GetWorldCenter() );
为了达到逐步加速的效果,我们只需要对速度做适当的变化就可以了:
case MS_LEFT: desiredVel = b2Max( vel.x - 0.1f, -5.0f ); break; case MS_STOP: desiredVel = vel.x * 0.98f; break; case MS_RIGHT: desiredVel = b2Min( vel.x + 0.1f, 5.0f ); break;