现在可以深入讨论赛车游戏所需的物理学了。今天你能看到许多强大的物理引擎的一些功能,但因为它们很难被实现而且也没有用.NET编写,所以你将通过自己的方式实现物理引擎。
在本章开始你处理了汽车控制和简单的重力效果。目前缺少的是准确的碰撞检测系统,当撞上一个护栏时你的车应该停止。你还将处理一些比较复杂的情况,比如说通过环形轨道等。环形轨道不难实现。借助于上一章的赛道生成代码,你能很容易地通过添加两个采样点添加赛道,如果你的物理系统能正确处理汽车的受力,那么沿着环形轨道驾车几乎是自动处理的。
在你继续实现难点前(碰撞检测和响应,这是CarPhysics类的主要代码),请看看图13-8,它显示了当驾车通过一个环形轨道时,如何将力施加在车上的一个简化方式。
如果你只把车放置在环形轨道的顶部而不施加任何外力,重力(红色)将把车往下拉,这样你就无法移动或驾驶赛车,因为你将与赛道脱离。所以,即使到赛道顶端,车也必须始终压向赛道。这个力必须大于重力,否则你的车就会离开赛道并开始下落(见图13-9) 。重要的是你必须有足够的冲量使汽车保持在赛道上。冲量是由汽车的前一次运动的力在当前帧内计算而来的。
如果你腾空而起(首先在之前的坡道上获得冲量),重力会把你往下拉,过了一段时间后你将失去冲量,最终落下。由于环形赛道始终改变车的方向,你可以较容易地使汽车保持在赛道上。如果离心力比重力大,那么克服重力很容易。图13-10显示的例子是绳上拉着一个小球旋转。即使向心力不大,你也能够让球旋转,旋转地越快,小球的重力影响就越小,因为离心力也越强,所以也将更难拉住小球。
你可以看到离心力将小球拉离手,当你转得很快时,重力相对离心力来说是较小的,即使你停止移动手,冲量也将位置小球的圆周运动。
游戏中的大多数公式都已简化,赛车的运动也不像真实的那么复杂。你不用关心汽车的引擎以及任何汽车内部的东西。使用功率、转速、启动和刹车加速度,更好的制动和摩擦公式等可以实现更多的驾驶参数。汽车物理学的更详细描述请查看以下链接http://www.racer.nl/reference/carphys.htm.
因为汽车处理逻辑的简化,许多效果,如车加速时的后仰或刹车时的前倾,并没有实现。但是,你仍可以一个简化的方式实现一些效果。
所有的车轮都连接到弹簧上使车能较稳定地行驶在道路上。当车轮加速推动汽车前进时需要一段时间才能使有足够的冲量移动汽车本身。这种影响在突然刹车时更加明显。车轮虽已停止,但汽车本身仍处于运动状态,车的质量使整个车向前倾斜一点(见图13-11)。
重力总是把你往下拉,但汽车的不同部分,特别是车轮和车的其他部位,行为方式是不同的。车轮连接弹簧上让车可以前倾和向倾,这发生在加速或减速时,即使你只是停留在道路上也不一定是平的(如果你开车上山,汽车将后倾)。
你无需有一个弹簧,也不必以图13-11中的方式绘制车轮。如果车前后倾时,车轮会陷到道路中去,但因为你不从侧面观察汽车,所以你不会注意到这种情况。相机始终在车的后面,你只能从车的顶部和后部看到大多数倾斜效果。
由于效果相对简单,只要汽车速度发生变化,你就可以前后倾汽车:
加速时,车向后倾斜——加速度越大,仰角越大。
制动时,汽车前倾——效果通常强于加速,因为制动加速度远远超过加速时。
在上一个游戏XNA Shooter中你已经看到这个效果,当你向任意方向运动时飞船会水平和垂直翻转。同样的公式也可用于汽车。为了解决车轮和车的其他部分会消失在道路中的问题,你要限制这个效果:
// Calculate pitch depending on the force float speedChange = speed - oldSpeed; // Limit speed change, never apply more than 8 mph change per sec. if (speedChange < -8 * moveFactor) speedChange = -8 * moveFactor; if (speedChange > 8 * moveFactor) speedChange = 8 * moveFactor; carPitch = speedChange;
当汽车速度没有变化时它的俯仰将保持为0,如果正在加速或减速时赛车的行为也应表现正确。但即使采用更好的平滑公式,这个效果看起来也并不十分令人信服。在现实世界中赛车将前后摆动直到弹簧失去了所有的力量。
你现在需要的使用一个简单的公式让汽车前后摆动。SpringPhysicsObject类(见图13-12 )能帮你实现这一点。它计算一个定质量的物体的位置,而这个物体与一个具有确定弹性系数的弹簧相连。这一位置上下起伏,但通过一个摩擦常量使速度变慢。这里将使用一个大质量和大摩擦常量,结果是俯仰效果变得缓慢而且效果很快变弱,你不想让车疯狂地反弹。
这个辅助类直接用于CarPhysics类,初始化CarMass常量,摩擦力为1.5,弹性系数为120(两者的值都相对较高)。虚拟弹簧的最初位置设置为0(车在正常状态):
/// <summary> /// Car pitch physics helper for a simple spring effect for /// accelerating, decelerating and crashing. /// </summary> SpringPhysicsObject carPitchPhysics = new SpringPhysicsObject( CarMass, 1.5f, 120, 0);
当汽车的速度变化时要更新虚拟弹簧的位置,你可以使用ChangePos辅助方法:
carPitchPhysics.ChangePos(speedChange);
最后,你根据当前帧的时间量调用Simulate方法来模拟俯仰效果,然后在渲染时,你使用这个类的位置值获取当前的俯仰值:
// Handle pitch spring carPitchPhysics.Simulate(moveFactor);
SpringPhysicsObject类的有趣部分显然是Simulate方法,所有其他方法只是设置一些值。要理解弹力公式,你必须知道弹簧的回复力,而这个力是用胡克定律代入F=ma中得到的。
胡克定律揭示了形变量与压力成正比。这样,弹性,弹簧,应力,应变物理都可以加以描述。
如果你想阅读更多关于胡克定律的知识,请阅读一本好的公式书,或查阅因特网。维基百科和Wolfram Research(http://scienceworld.wolfram.com)上有很好的关于物理问题的文章。
你只是使用简单弹力公式F=-kx,这个公式中的F是连接在弹簧上的物体受到的回复力,k是弹性系数。图13-13描述这个公式以及随着时间变化弹性物体的效果,这个效果你也将用在汽车的俯仰上。
如果没有力施加在物体上,物体会保持在初始的位置(0)。但如果你向下拉弹簧,弹簧将伸长,弹力使物体恢复到初始位置。当它再次回到初始位置0,弹力也回到零,但物体的速度仍很大,物体仍向上运动。回复力反向阻止物体向上运动,最终达到其最高位置1。现在回复力为-1并再次将物体往下推,当物体到达最低点时又开始往复运动了。
使之减速的力是摩擦力,要确保汽车不会反弹太多,摩擦力要很大。另请注意,这里忽略重力,因为汽车本身处理重力,忽略重力对汽车的俯仰效果影响不大,而且因为从同一方向拉动物体并不影响计算,所以弹力效果你也能放心地忽略重力。
Simulate方法中的代码使之工作,借助于你刚刚获得的知识,也应该能够迅速地理解此代码:
/// <summary> /// Simulate spring formular using the timeChange. /// The velocity is increased by the timeChange * force / mass, /// the position is then timeChange * velocity. /// </summary> public void Simulate(float timeChange) { // Calculate force again force += -pos * springConstant; // Calculate velocity velocity = force / mass; // And apply it to the current position pos += timeChange * velocity; // Apply friction force *= 1.0f - (timeChange * friction); } // Simulate(timeChange)
SpringPhysicsObject类可在Game Physics命名空间中找到(见图13-13)。如果你想添加更多的物理类并实现更多的物理行为,你应该在这个命名空间中实现。当你尝试在今后的项目中复用这些物理效果后会更容易,只需复制和使用physics命名空间。
本章最后的任务完成车和护栏的碰撞检测。因为你是道路上唯一的汽车,所以汽车不与任何车辆发生碰撞,我确信道路上也没有任何其他物体可以碰撞。如果你允许与道路上的其他物体路发生碰撞,碰撞检测和反应将复杂很多。例如,与道路上的灯,指示牌,垃圾桶等发生碰撞意味着如果你碰到它们,它们会被弹开。而这些物体又要相互碰撞并与周围的世界互动。
如果你有一个良好的物理引擎这是可以实现的,但调整这些物体要做大量的工作,要调整基本物理常数,并不断测试所有不同种类的碰撞。实现这种技术并调整它可能很有趣,但XNA上没有一个成熟的物理引擎,即使实现基本的启动并使之运行可能要花比整个项目更长的时间。
碰撞检测和碰撞反应的规则仍然是相同的,但你可以根据需要简化它,只涉及汽车和护栏的碰撞。由于护栏不能被破坏,所以唯一受碰撞影响的就是你的车。
Rocket Commander游戏物理效果的最大部分是小行星间的碰撞检测和优化。借助于许多单元检测,碰撞反应并不十分复杂,基本碰撞检测也可以以一个非常简单的方法实现。
例如, Rocket Commander游戏中TestAsteroidPhysicsSmallScene单元测试的PhysicsAsteroidManager类显示了一个好方法来进行碰撞检测。该单元测试让你可以按1-7键来测试各种不同的情形,并显示小行星碰撞事件的结果。
图13-14显示了该单元测试,使用了相同大小的两个小行星飞向对方并在碰撞后向相反方向弹回。SetupScene方法用来设置小行星的初始位置,而单元测试处理所有的碰撞检测。
case 2: // Collide with crazy movement vectors AddAsteroid(new Vector3(-35, +19, 8) * (-2), 12.5f, asteroidModel.ObjectSize, new Vector3(-35, +19, 8)); AddAsteroid(new Vector3(+15, 40, 14) * (-2), 12.5f, asteroidModel.ObjectSize, new Vector3(+15, 40, 14)); break;
TestAsteroidPhysicsSmallScene单元测试调用HandleSectorPhysics方法,检查在特定区域中的所有小行星,如果它们互相太接近则进行简单的包围球测试并处理碰撞事件。
该方法不仅检查本区域的小行星,还检查周围区域的所有小行星。即使有非常好的区域优化,在Rocket Commander游戏中每帧仍然要进行几千次碰撞检测,如果小行星更多则变得更慢。基于这个理由,Rocket Commander XNA使用多线程技术在两个不同的线程中处理物理和渲染代码。
在碰撞检测中添加了小行星的半径,检查它们是否比物理大小更小。如果是这样,则发生碰撞。下面的代码显示了单一区域的碰撞检测。测试周围区域的代码是类似的,但长得多。
// Only check this sector! Crosscheck with any other asteroid in // this sector. foreach (Asteroid otherAsteroid in thisSectorAsteroids) if (asteroid != otherAsteroid) { float maxAllowedDistance = otherAsteroid.collisionRadius + asteroid.collisionRadius; // Distance smaller than max. allowed distance? if ((otherAsteroid.position asteroid.position).LengthSq() < maxAllowedDistance * maxAllowedDistance) { HandleAsteroidCollision(asteroid, otherAsteroid); } // if (otherAsteroid.position) } // foreach if (asteroid)
现在HandleAsteroidCollision方法处理碰撞并使小行星相互远离对方:
// Put both circles outside of the collision // Add 1% to add a little distance between collided objects! otherAsteroid.position = middle + otherPositionRel * otherAsteroid.collisionRadius * 1.015f; asteroid.position = middle + positionRel * asteroid.collisionRadius * 1.015f;
然后,该方法使用总的碰撞力应用到小行星质量上,它颠倒了两个小行星的运动向量使之远离碰撞平面(图13-14中的紫色线条)。通过这种方式,一个较小的小行星被一个更大的小行星以更大的力量推离。看一下单元测试,该方法很长并做了很多额外的检查,计算出行星质量和方向,并添加了小行星的旋转速度,但处理碰撞的基本代码如下:
// Normalize movement Vector3 asteroidDirection = asteroid.movement; asteroidDirection.Normalize(); Vector3 otherAsteroidDirection = otherAsteroid.movement; otherAsteroidDirection.Normalize(); // Get collision strength (1 if pointing in same direction, // 0 if 90 degrees) for both asteroids. float asteroidCollisionStrength = Math.Abs(Vector3.Dot( asteroidDirection, asteroidNormal)); float otherAsteroidCollisionStrength = Math.Abs(Vector3.Dot( otherAsteroidDirection, otherAsteroidNormal)); // Calculate reflection vectors from the asteroid direction and the // normal towards the reflection plane. Vector3 asteroidReflection = ReflectVector(asteroidDirection, asteroidNormal); Vector3 otherAsteroidReflection = ReflectVector(otherAsteroidDirection, otherAsteroidNormal); // Make sure the strength is calculated correctly // We have also to correct the reflection vector if the length was 0, // use the normal vector instead. if (asteroidDirection.Length() <= 0.01f) { asteroidCollisionStrength = otherAsteroidCollisionStrength; asteroidReflection = asteroidNormal; } // if (asteroidDirection.Length) if (otherAsteroidDirection.Length() <= 0.01f) { otherAsteroidCollisionStrength = asteroidCollisionStrength; otherAsteroidReflection = otherAsteroidNormal; } // if (otherAsteroidDirection.Length) // Ok, now the complicated part, everything above was really easy! asteroid.movement = asteroidReflection * // So, first we have to reflect our current movement speed. // This will be scaled to 1-strength to only account the reflection // amount (imagine a collision with a static wall). In most cases // Strength is close to 1 and this reflection will be very small. ((1 - asteroidCollisionStrength) * asteroidSpeed + // And finally we have to add the impuls, which is calculated // by the formula ((m1-m2)*v1 + 2*m2*v2)/(m1+m2), see // http://de.wikipedia.org/wiki/Sto%C3%9F_%28Physik%29 for more help. (asteroidCollisionStrength * (Math.Abs(asteroidMass - otherAsteroidMass) * asteroidSpeed + (2 * otherAsteroidMass * otherAsteroidSpeed)) / bothMasses)); // Same for other asteroid, just with asteroid and otherAsteroid // inverted. otherAsteroid.movement = otherAsteroidReflection * // Same as above. ((1 - otherAsteroidCollisionStrength) * otherAsteroidSpeed + (otherAsteroidCollisionStrength * (Math.Abs(otherAsteroidMass - asteroidMass) * otherAsteroidSpeed + (2 * asteroidMass * asteroidSpeed)) / bothMasses));