赞助广告

 

年份

最新评论

评论 RSS

物理学——车辆碰撞检测

clock 十月 16, 2010 23:56 by author alex
车辆碰撞检测 借助于前面的代码,我们已经知道如何检测汽车与护栏的碰撞。只需创建两个碰撞平面,每个护栏一个,然后检查四个轮子与这两个平面的碰撞。因为赛车不仅是一个球体,因此无法像小行星那样做简单的处理,四个面都必须检测,但只检测四轮的位置(或至少是赛车的最外层)仍能实现基本碰撞检测(见图13-15)。 Figure 13-15 碰撞检测的代码在CarPhysics类的ApplyGravityAndCheckForCollisions方法中。此代码遍历了赛车的四个角并检测是否与平面碰撞(见图13-15中的道路旁边的两条线)。在实际车长和宽的基础上可计算赛车的四个角: // Check all 4 corner points of our car. Vector3 carRight = Vector3.Cross(carDir, carUp); Vector3 carLeft = -carRight; // Car dimensions are 2.6m (width) x 5.6m (length) x 1.8m (height) // Note: This could be improved by using more points or using // the actual car geometry. // Note: We ignore the height, this way the collision is simpler. // We then check the height above the road to see if we are flying // above the guard rails out into the landscape. Vector3[] carCorners = new Vector3[] { // Top left pos + carDir * 5.6f/2.0f - carRight * 2.6f/2.0f, // Top right pos + carDir * 5.6f/2.0f + carRight * 2.6f/2.0f, // Bottom right pos - carDir * 5.6f/2.0f + carRight * 2.6f/2.0f, // Bottom left pos - carDir * 5.6f/2.0f - carRight * 2.6f/2.0f, }; 借助于在此方法前计算过的辅助值,碰撞检测现在相对简单了: // Check for each corner if we collide with the guard rail for (int num = 0; num < carCorners.Length; num++) { // Hit any guardrail? float leftDist = Vector3Helper.DistanceToLine( carCorners[num], guardrailLeft, nextGuardrailLeft); float rightDist = Vector3Helper.DistanceToLine( carCorners[num], guardrailRight, nextGuardrailRight); // If we are closer than 0.1f, thats a collision! if (leftDist < 0.1f || // Also include the case where we are farther away from rightDist // than the road is wide. rightDist > roadWidth) { // Handle collision with left guard rail here } // if (leftDist < 0.1f) if (rightDist < 0.1f || // Also include the case where we are farther away from leftDist // than the road is wide. leftDist > roadWidth) { // Handle collision with right guard rail here } // if (rightDist < 0.1f) } // for (num) 碰撞处理的最后部分是碰撞事件的反应。但在此之前,你需要在单元测试中设置一些字段来显示碰撞检测代码是否正常工作。或只在“if”代码块中设置断点,看看是否已进入条件。 现在,所有要做的就是播放碰撞音效并根据车的哪个角碰撞选择车辆。汽车将减速,相机摇晃告知玩家他刚刚碰撞了。如果直行时碰撞(0~45度对墙),车将完全停住并播放完全碰撞的音效: // Force car back on the road, for that calculate impulse and // collision direction (same stuff as in Rocket Commander). Vector3 collisionDir = Vector3.Reflect(carDir, guardrailRightNormal); float collisionAngle = Vector3Helper.GetAngleBetweenVectors( carLeft, guardrailRightNormal); // Flip at 180 degrees (if driving in wrong direction) if (collisionAngle > MathHelper.Pi / 2) collisionAngle -= MathHelper.Pi; // Just correct rotation if collison happened at 0-45 degrees (slowly) if (Math.Abs(collisionAngle) < MathHelper.Pi / 4.0f) { // Play crash sound Sound.PlayCrashSound(false); // For front wheels to full collision rotation, for back half! if (num < 2) { rotateCarAfterCollision = +collisionAngle / 1.5f; speed *= 0.935f;//0.85f; if (viewDistance > 0.75f) viewDistance -= 0.1f;//0.15f; } // if (num) else { rotateCarAfterCollision = +collisionAngle / 2.5f; //slowdownCarAfterCollision = 0.8f; speed *= 0.96f;//0.9f; if (viewDistance > 0.75f) viewDistance -= 0.05f;//0.1f; } // else // Shake camera ChaseCamera.SetCameraWobbel(0.00075f * speed); } // if (collisionAngle) // If 90-45 degrees (in either direction), make frontal crash // + stop car + wobble camera else if (Math.Abs(collisionAngle) < MathHelper.Pi * 3.0f / 4.0f) { // Also rotate car if less than 60 degrees if (Math.Abs(collisionAngle) < MathHelper.Pi / 3.0f) rotateCarAfterCollision = +collisionAngle / 3.0f; // Play crash sound Sound.PlayCrashSound(true); // Shake camera ChaseCamera.SetCameraWobbel(0.005f * speed); // Just stop car! speed = 0; } // if (collisionAngle) // For all collisions, kill the current car force carForce = Vector3.Zero; // Always make sure we are OUTSIDE of the collision range for // the next frame. But first find out how much we have to move. float speedDistanceToGuardrails = speed * Math.Abs(Vector3.Dot(carDir, guardrailLeftNormal)); if (rightDist > 0) { float correctCarPosValue = (rightDist + 0.01f +//0.11f + 0.1f * speedDistanceToGuardrails * moveFactor); carPos += correctCarPosValue * guardrailRightNormal; } // if (rightDist) 有了这些代码,现在你可以进行TestCarPhysicsOnPlaneWithGuardRails单元测试并调整与护栏的碰撞。该代码有点复杂,但规则相同,所有的碰撞代码和物理计算与单元测试中使用的的完全一样。 /*728*90文章内页广告*/ var cpro_id = 'u241919';

物理学——实现物理效果

clock 十月 16, 2010 23:52 by author alex
实现物理效果 现在可以深入讨论赛车游戏所需的物理学了。今天你能看到许多强大的物理引擎的一些功能,但因为它们很难被实现而且也没有用.NET编写,所以你将通过自己的方式实现物理引擎。 在本章开始你处理了汽车控制和简单的重力效果。目前缺少的是准确的碰撞检测系统,当撞上一个护栏时你的车应该停止。你还将处理一些比较复杂的情况,比如说通过环形轨道等。环形轨道不难实现。借助于上一章的赛道生成代码,你能很容易地通过添加两个采样点添加赛道,如果你的物理系统能正确处理汽车的受力,那么沿着环形轨道驾车几乎是自动处理的。 处理环形轨道 在你继续实现难点前(碰撞检测和响应,这是CarPhysics类的主要代码),请看看图13-8,它显示了当驾车通过一个环形轨道时,如何将力施加在车上的一个简化方式。 图 13-8 如果你只把车放置在环形轨道的顶部而不施加任何外力,重力(红色)将把车往下拉,这样你就无法移动或驾驶赛车,因为你将与赛道脱离。所以,即使到赛道顶端,车也必须始终压向赛道。这个力必须大于重力,否则你的车就会离开赛道并开始下落(见图13-9) 。重要的是你必须有足够的冲量使汽车保持在赛道上。冲量是由汽车的前一次运动的力在当前帧内计算而来的。 图 13-9 如果你腾空而起(首先在之前的坡道上获得冲量),重力会把你往下拉,过了一段时间后你将失去冲量,最终落下。由于环形赛道始终改变车的方向,你可以较容易地使汽车保持在赛道上。如果离心力比重力大,那么克服重力很容易。图13-10显示的例子是绳上拉着一个小球旋转。即使向心力不大,你也能够让球旋转,旋转地越快,小球的重力影响就越小,因为离心力也越强,所以也将更难拉住小球。 图 13-10 你可以看到离心力将小球拉离手,当你转得很快时,重力相对离心力来说是较小的,即使你停止移动手,冲量也将位置小球的圆周运动。 弹性物理学 游戏中的大多数公式都已简化,赛车的运动也不像真实的那么复杂。你不用关心汽车的引擎以及任何汽车内部的东西。使用功率、转速、启动和刹车加速度,更好的制动和摩擦公式等可以实现更多的驾驶参数。汽车物理学的更详细描述请查看以下链接http://www.racer.nl/reference/carphys.htm. 因为汽车处理逻辑的简化,许多效果,如车加速时的后仰或刹车时的前倾,并没有实现。但是,你仍可以一个简化的方式实现一些效果。 所有的车轮都连接到弹簧上使车能较稳定地行驶在道路上。当车轮加速推动汽车前进时需要一段时间才能使有足够的冲量移动汽车本身。这种影响在突然刹车时更加明显。车轮虽已停止,但汽车本身仍处于运动状态,车的质量使整个车向前倾斜一点(见图13-11)。 图 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 )能帮你实现这一点。它计算一个定质量的物体的位置,而这个物体与一个具有确定弹性系数的弹簧相连。这一位置上下起伏,但通过一个摩擦常量使速度变慢。这里将使用一个大质量和大摩擦常量,结果是俯仰效果变得缓慢而且效果很快变弱,你不想让车疯狂地反弹。 图 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命名空间。 图 13-13 碰撞检测 本章最后的任务完成车和护栏的碰撞检测。因为你是道路上唯一的汽车,所以汽车不与任何车辆发生碰撞,我确信道路上也没有任何其他物体可以碰撞。如果你允许与道路上的其他物体路发生碰撞,碰撞检测和反应将复杂很多。例如,与道路上的灯,指示牌,垃圾桶等发生碰撞意味着如果你碰到它们,它们会被弹开。而这些物体又要相互碰撞并与周围的世界互动。 如果你有一个良好的物理引擎这是可以实现的,但调整这些物体要做大量的工作,要调整基本物理常数,并不断测试所有不同种类的碰撞。实现这种技术并调整它可能很有趣,但XNA上没有一个成熟的物理引擎,即使实现基本的启动并使之运行可能要花比整个项目更长的时间。 碰撞检测和碰撞反应的规则仍然是相同的,但你可以根据需要简化它,只涉及汽车和护栏的碰撞。由于护栏不能被破坏,所以唯一受碰撞影响的就是你的车。 Rocket Commander游戏物理效果的最大部分是小行星间的碰撞检测和优化。借助于许多单元检测,碰撞反应并不十分复杂,基本碰撞检测也可以以一个非常简单的方法实现。 PhysicsAsteroidManager 例如, Rocket Commander游戏中TestAsteroidPhysicsSmallScene单元测试的PhysicsAsteroidManager类显示了一个好方法来进行碰撞检测。该单元测试让你可以按1-7键来测试各种不同的情形,并显示小行星碰撞事件的结果。 图13-14显示了该单元测试,使用了相同大小的两个小行星飞向对方并在碰撞后向相反方向弹回。SetupScene方法用来设置小行星的初始位置,而单元测试处理所有的碰撞检测。 图 13-14 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)); /*728*90文章内页广告*/ var cpro_id = 'u241919';

物理学——物理引擎

clock 十月 9, 2010 09:10 by author alex
物理引擎 有了基本物理学的新知识,现在你可以较容易地比较现有的物理引擎了。实现了基本的东西和确定在系统中已定义了正确的常量并感觉良好之后,你应该考虑一下游戏的需求。物理引擎往往可以做很多不同的东西,许多物理演示和教程看上去很酷,但往往不能用于大多数游戏。第一次看到一堆箱子互相碰撞很有趣,但很少有游戏允许直接控制箱子并推动它们,而且看多了箱子翻倒就会觉得无趣,这就好象再一次回到了幼儿园一样。 但是,并非所有的演示仅仅是一堆方块。有许多很好的示例向你展示物理引擎的特殊功能,但我的观点是,大多数游戏只需要某些特定功能。第一人称射击游戏可能会用到大多数的功能,这点你可以在近几年中发售的游戏中看到。几乎所有的第一人称射击游戏都会实现某些物理系统,但其他类型的游戏,如角色扮演游戏、策略游戏等等,要么没有物理系统或只有最基本的物理系统,而且往往只是用来改善视觉效果而不是游戏逻辑本身。 我个人不喜欢在策略游戏中使用物理效果,因为它们实现得并不好,往往只是添加更好的视觉效果,比如周围建筑物倒塌时你的单位也会被压死。这就像在下棋时发生地震,这破坏了游戏规则,因为一切都在摇晃,你将无法作出任何聪明的举动。也许要过一段时间后开发商才会弄清楚如何正确地使用物理效果,并使游戏仍然是可预见的,而不是靠运气。 我真正喜欢的物理效果是爆炸,特效和视觉质量的改进。有几个游戏非常好地模拟了水,爆炸,烟雾效果,而且这些效果还能与周围的环境互动。 正如我前面所说,当在Xbox 360上使用XNA时你将无法使用任何物理引擎,因为它们都是基于非托管代码的,而这在XNA中不被支持。在Windows平台上可以使用任何你喜欢的物理引擎,但即使在Windows平台上很多物理引擎也没有适当的. NET接口可让你在游戏中轻松地使用和处理物理效果。.NET的可能性将在接下去讨论。总之,快速浏览一下现今的物理引擎提供的一些功能: 诸如像重力,摩擦力,动力和接触摩擦,浮力等等的一些力。 基本的物理物体支持,用来定义并处理方块,球,圆柱,平面等等。通常,这些引擎也支持连接物体和更复杂的预定义模型,如汽车、飞机、气垫船、船舶等等。 物体间的碰撞检测,特别是物体不一定是球体时。因为碰撞计算使用得非常频繁,需要检查大量的物体,包围球体甚至是多边形为基础的检测,所以这通常也是优化的主要地方。Rocket Commander游戏展示了一个如何检测许多碰撞的解决方法,这个方法通过分区进行优化,极大地提高了性能。 碰撞反应,这是在碰撞被检测到时执行的。要么你让物体反弹(如在Rocket Commander),或者你也可以破坏它们(碰撞的力转化为动能并破坏模型)。游戏通常使用预定义的爆炸、残骸或破损的三维模型,因为实时产生这些模型很难,但如果美工很棒的话效果还是会很好。 弹丸物理学处理子弹、火箭弹和其他发射的弹丸。你上一章中你看到弹丸是如何被使用的,如果你有一个物理引擎,你只需添加弹丸,并设置物理参数,并根据环境增加特殊的行为(例如,子弹在水中会减速),物理引擎会帮你处理弹丸。 完整的粒子系统也要用到物理学,正如我前面所说,我很喜欢利用物理引擎实现的特效系统,但特效系统要做大量的工作。 物理引擎还有更多的功能,如支持刚体模拟,布娃娃系统,像衣服,旗帜,液体之类的可变形物体和其他复杂的物体。 PhysX PhysX是AGEIA公司的物理引擎和SDK的名称,以前被称为NovodeX SDK,PhysX也是Ageia开发的PPU(物理处理单元)的名称。PhysX PPU是世界上第一个专门用于计算物理的硬件,可用于任何使用PhysX SDK的游戏。目前支持PhysX物理引擎的游戏还不是很多,但许多引擎和即将发布的游戏已经获得了PhysX SDK的授权,而索尼公司也获得在Playstation 3游戏机使用PhysX的授权。 即使没有PPU,PhysX引擎也是强大的,开发商研究物理引擎已经很长一段时间了。它可能是仅次于Havok引擎排名第二的物理引擎,后者的应用更加广泛,不仅在游戏,而且在诸如3D Studio Max之类的3D建模程序中。将繁重的物理计算交于一个特殊的硬件处理,这个想法虽然不错,但PPU太贵了(250美元~300美元),有这种卡的人也不多。多数买这种物理卡的人是开发者或艺术家,他们可以从中受益,但你不能指望玩家要有物理卡才能运行你的游戏。但是PhysX在多核CPU也运行良好,只是不像在PPU上这么优异。 现在没有多少游戏使用PhysX,你只能找到屈指可数的几个。例如幽灵行动,哥特3,联合特遣部队以及最近的战争机器(Xbox 360游戏机)。但未来有虚幻竞技场2007和虚幻3引擎,细胞分裂,Monster Madness等等。 PhysX关注于游戏、游戏引擎、特效处理、利用物理学的游戏逻辑,有许多看上去很酷的例子和教程。它们的主要优势是:借助PPU硬件,运行速度比其他物理引擎快很多,PhysX还采用了统一架构以相同方式处理所有的物理计算。 你可在PhsyX官方网站http://www.ageia.com上找到更多信息。 Havok Havok是非常受欢迎的Havok Game Dymamics SDK(目前是第4版,在2006年发布)的名称,它也是开发这个SDK的公司的名称。这个公司成立于1998年,第一个SDK(v1.0)于2000年公布。今天有超过100个游戏使用Havok物理引擎,最有名的是游戏是半条命2和Valve公司的Source引擎,Source引擎也用在了许多以半条命为基础的改编游戏。Havok主要用于第一人称射击游戏,但其他一些游戏类型也已使用了Havok引擎。 Havok可用在许多平台上,包括个人电脑(Windows和Linux )和几乎每一种家用机(Xbox,Xbox 360,PS2,PS3,Wii,Game Cube)。它用C / C++编写,在允许编译C代码的任何系统上是非常精简的。但对.NET开发者来说没太多支持,我没听到过任何使用Havok的.NET游戏或引擎。 这个物理引擎不仅用于游戏,也可用于3D Studio Max,它利用了内部中间件,在模拟物理效果的3D艺术社区是很流行的。在以前的版本中通过一个叫reactor的插件实现,但最新版本已经在3D Studio Max的基本版中实现。Havok引擎甚至还用在了电影黑客帝国三部曲中。 Havok支持两个主要的物理操作。特效物理引擎可用于支持Shader Model 3.0的GPU如GeForce 7系列和8系列,特别是GPU很快时。然后是游戏物理引擎,这在CPU中计算。Havok是PhysX的主要竞争对手,它证明了无需额外的PPU你也可以干得很棒,2004年的半条命2游戏证明了实现很棒的物理效果是可行的,通过物理效果还能改进游戏。 你可以在Havok的官方网站http://www.havok.com/找到更多信息。 ODE ODE表示“开放的动力学引擎(Open Dynamics Engine)”,与PhysX和Havok不同,它不是一个商业引擎,这也许是初级开发人员或小组最感兴趣的地方,初学者往往苦于没有足够的钱去使用一个大型的商业物理引擎。 ODE是一个开源物理引擎,它使用BSD许可证(或LGPL如果你喜欢)。它开始于2001年,已经有一些游戏通过这个引擎被开发出来,但它没有像PhysX或Havok那么完善的功能,也没那么成功。 ODE支持刚体动力学和碰撞检测。要提供更加先进的物理效果你仍然有很多工作要做,但ODE为你提供了一个很好的基本框架,给了你所有的基本几何形状(如正方体、球体、圆柱体等)和物理计算,你可以用来做一些基本的物理演示。还有一些框架是建立在ODE之上或使用ODE。 下面是酷的地方:ODE有一个很好的.NET包装叫做ODE.NET,可以在下面的网址找到。上面有几个示例,很多游戏引擎使用C++代码并只提供C++示例,很高兴能见到一些.NET代码了。示例展示了基本的物理处理,如堆箱子并推动它们,布娃娃物理和控制一些如汽车或玩具之类的基本物件。 你也可以看看ODE的官方网站http://www.ode.org/。如果关闭了,可查看维基百科的有关条目。 Other 还有许多其他的物理引擎,我不可能全知道。也许还有其他引擎可用于.NET,但我尚未没有发现像ODE.NET那样有用的物理引擎。 下面是我遇到过的一些其他物理引擎: Newton Game Dynamics是一个免费的物理引擎,有很好的示例和文档。它可用于业余爱好和商业用途,并支持Windows,Mac和Linux平台。 像这样的一个免费引擎很不错了。我记得一年前看到国这个引擎的.NET包装,但源代码没有公开。这使得很难落实到.NET中,当你想改进引擎时也很难改变代码。而ODE能根据你的需求自由改编。可到网址http://www.newtondynamics.com/中查看。 Tokamak Game Physics是另外一个不错的物理引擎,而且对非商业和商业项目都是完全免费的。它也有不少示例和教程,但仍没有提供源代码。引擎有近两年没有更新了,看来开发者好像放弃了。论坛上最新的留言在2005年3月。几年前我实现了一个简单的.NET包装,但我没时间改进。今天,我可能只是利用其他的现有引擎,特别是ODE.NET,因为它已经工作在.NET中。可访问http://www.tokamakphysics.com/。 Physics and Math Library(也称为Game Physics Engine)是一个德国的引擎,最初是为一本书开发的。它着重于实现许多如Havok或PhysX之类的商业引擎的功能。它是免费使用的,但我无法找到许多截图或论坛的讨论。很难说这个引擎是否值得你花力气研究(使它运行你的项目中,甚至移植到.NET中),但看来这个项目非常有前途。该网站是http://game-physics-engine.info/。 nV Physics Engine(也称为The Physics Engine)有一个很好的网站和一些截图(对网站介绍产品而言有截图总是好的)。它看起来很有希望,但所有示例使用的是C++,你可能没法改编源代码。该网站是http://www.thephysicsengine.com/ 。 Bullet Physics Library是另一个免费的物理引擎,它包括完整的源代码,这很好。作者以前曾在Havok工作,这个引擎已经被其他几个引擎使用,尤其是开源引擎和三维建模工具Blender 3D。它也支持最新的文件格式,并有几个先进的功能。该网站是http://www.continuousphysics.com/Bullet/ 。 可能有更多的物理引擎,尤其是较小的。当你遇到某些问题时,尝试去寻找一些特定的引擎,通常无需一个成熟的物理引擎。 如你所见,有许多物理引擎可用,但在.NET环境中使用它们不是很容易。你也不能在Xbox 360上利用它们,因为它们都是用C++和非托管代码开发的,这意味着在Xbox 360上使用XNA时没有可用的。 但是,即使你最后不使用物理引擎而只是查看示例和教程,你也可以更清楚地了解物理引擎能干什么和哪些部分可能会很难重新自己实现。在赛车游戏不需要一个复杂的物理引擎。那么,这将有一个坚实的基础,但由于在.NET中实现物理引擎是如此困难,而且我想让游戏也能运行在Xbox 360上,所以你必须自己实现物理引擎。 /*728*90文章内页广告*/ var cpro_id = 'u241919';

物理学——牛顿运动定律

clock 十月 9, 2010 08:51 by author alex
牛顿运动定律 让我们快速浏览一下运动学的重要定律。图13-2显示了看到这些物理规则: 第一定律——物体总保持静止或匀速直线运动状态,直到有外来迫使它改变这种状态。这很容易,因为所有物体运动都有一个运动向量或速度向量,除非你施加了力让其减速,加速或改变方向,否则不要改变该值。 第二定律——当施加一个外力时,这意味着你可以使物体加速或减速,如果超过一个维度(3D游戏中有三维),你还可以改变物体运动的方向。在这种情况下你使用向量代替标量,所有向量部分(x、y和z)都将发生变化。由于物体质量永远不变(至少在大多数游戏中是这样),你可以使用简化的F=ma的公式,这里“a”是定义为速度的改变量除以时间改变量:a = dv/dt. 第三定律——每一个作用力都有一个与之大小相等方向相反的反作用力。这意味着如果你有一个力,如地球施加在物体上的引力,则物体也会在地球上施加一个方向相反的反作用力。由于地球比物体质量大得多,所以物体对地球的引力对地球没什么影响。但是,如果你有一个更大的物体,如太阳,地球的引力远远小于太阳的引力,幸好我们的地球总是在轨道上围绕太阳旋转,月球以同样的方式在轨道绕地球旋转。 图 13-2 牛顿在他的“数学原理”一书中提出了这些定律,首次是万有引力定律,这个定律解释了行星运动以及为什么月球绕地球转而地球绕太阳转。当时这时非常令人激动的,但今天,每所学校的孩子都可以在物理课上计算出这一点,计算机算起这些东西更强大。在Rocket Commander中你已经看到成千上万的小行星各自的相互作用。它们之间不存在引力只是相互碰撞,但添加引力效果改进真实度是一个很大的挑战。 保持简单 由于物理学是一个很大的议题,在游戏中实现物理效果要你全力以赴,所以你应该尽量保持简单。通常你甚至不知道哪个公式是正确的——但只要能工作就行。例如,你不必编写代码去遵循牛顿第一定律,第三定律在很多情况下也可忽略,因为通常你只关心改变物体运动的力。 在你更深入的了解物理世界之前,让我们来看看现有的物理引擎,你应该确保你知道赛车游戏究竟需要什么。如果你需要的只是驾驶汽车并使之停留在地面上,那么没有必要实现一个非常复杂的物理引擎。我真的很希望能有更多时间在赛车游戏中实现物理效果,但游戏必须按时完成,而当时我还有相当多的其他项目。由于这一原因,赛车游戏只使用一个简单的办法去实现物理效果,碰撞的处理也以非常简单的方式完成。即使它仍需要大量的微调,可在下一章中看到,但它运行良好,你可以让汽车在道路上行驶了。 图13-3显示了赛车游戏的基本物理需求。因为你不与在场景中其他物体发生交互,场景物体也不使用物理效果,你完全可以把你的注意力集中在车上而不关心其他东西。 图 13-3 这些规则通过几行代码实现,但调整所有的值让车与道路正确互动挺难的: // [From CarPhysics.cs] // Handle car engine force float newAccelerationForce = 0.0f; if (Input.KeyboardUpPressed || Input.Keyboard.IsKeyDown(Keys.W) || Input.MouseLeftButtonPressed || Input.GamePadAPressed) newAccelerationForce += MaxAccelerationPerSec; // [etc.] // Add acceleration force to total car force, but use the current // car direction! carForce += carDir * newAccelerationForce * (moveFactor * 85);// 70);//75); // Change speed with standard formula, use acceleration as our // force, gravity is handled below in the ApplyGravity method. float oldSpeed = speed; Vector3 speedChangeVector = carForce / CarMass; // Only use the amount important for our current direction // (slower rotation) if (speedChangeVector.Length() > 0) { float speedApplyFactor = Vector3.Dot(Vector3.Normalize(speedChangeVector), carDir); if (speedApplyFactor > 1) speedApplyFactor = 1; speed += speedChangeVector.Length() * speedApplyFactor; } // if (speedChangeVector.Length) 比起我第一次在CarPhysics类的单元测试的帮助下实现物理效果,该代码更复杂一点,但它仍实现了通过键盘使车加速的效果。为了让让汽车比实际方向有所漂移我分解了汽车方向和运动方向。速度用来更新运动矢量的长度,但我遇到了许多问题,为了简化问题我只使用一个速度标值。通过这种方式在反向速度向量的帮助下很容易实现了让驾驶方向落后一点点,而汽车仍然是指向前面。 由于你的赛道并非只是直的,所以你需要通过键盘左右或手柄旋转车。图13-4显示了旋转车的基本规则,这非常简单,通过短短的几行代码就能实现。为了让旋转更加平滑,上一次的旋转量仍要使用,但减少一点。 图 13-4 // First handle rotations (reduce last value) rotationChange *= 0.825f;// 75f; // Left/right changes rotation if (Input.KeyboardLeftPressed || Input.Keyboard.IsKeyDown(Keys.A)) rotationChange += MaxRotationPerSec * moveFactor / 2.5f; else if (Input.KeyboardRightPressed || Input.Keyboard.IsKeyDown(Keys.D) || Input.Keyboard.IsKeyDown(Keys.E)) rotationChange -= MaxRotationPerSec * moveFactor / 2.5f; else rotationChange = 0; if (Input.MouseXMovement != 0) rotationChange -= (Input.MouseXMovement / 15.0f) * MaxRotationPerSec * moveFactor; // Rotate dir around up vector // Interpolate rotatation amount. virtualRotationAmount += rotationChange; // Smooth over 200ms float interpolatedRotationChange = (rotationChange + virtualRotationAmount) * moveFactor / 0.225f;// / 0.200f; virtualRotationAmount -= interpolatedRotationChange; // We can't rotate our car ourself if it is currently not on the ground if (isCarOnGround) carDir = Vector3.TransformNormal(carDir, Matrix.CreateFromAxisAngle(carUp, interpolatedRotationChange)); 如果你有几个星期的时间来开发游戏,可以有一个更为复杂的方式来处理汽车的物理效果,如果你想要更真实的计算可以使用更多的常量和力让汽车的行为更加真实。但我的解决方案也运行良好。 如果你想查看效果可使用CarPhysics类中的TestCarPhysicsOnPlaneWithGuardRails单元测试。该单位测试开始时很简单,只有通过新的PlaneRenderer类渲染的汽车和地面。接着,添加了护栏和碰撞检测,这将在本章后面讨论。这是这个单元测试的早期版本,它已重新命名为TestCarPhysicsOnPlaneWithGuardRails,这个单元测试只是为处理汽车的基本物理效果显示环境。图13-5显示了结果。通过键盘和输入设备控制赛车(上,下,左,右等等)。 图 13-5 static public void TestCarPhysicsOnPlane() { PlaneRenderer plane = null; TestGame.Start("TestCarPhysicsOnPlane", delegate { plane = new PlaneRenderer(Vector3.Zero, new Plane(new Vector3(0, 0, 1), 0), new Material("CityGround", "CityGroundNormal"), 500.0f); // Put car on the ground and use standard direction and up vectors RacingGame.Player.SetCarPosition( new Vector3(0, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, 1)); }, delegate { // Test slow computers by slowing down the framerate with Ctrl if (Input.Keyboard.IsKeyDown(Keys.LeftControl)) Thread.Sleep(75); Matrix carMatrix = RacingGame.Player.UpdateCarMatrixAndCamera(); // Generate shadows, just the car does shadows ShaderEffect.shadowMapping.GenerateShadows( delegate { RacingGame.CarModel.GenerateShadow(carMatrix); }); // Render shadows (on both the plane and the car) ShaderEffect.shadowMapping.RenderShadows( delegate { RacingGame.CarModel.UseShadow(carMatrix); plane.UseShadow(); }); BaseGame.UI.RenderGameBackground(); // Show car and ground plane RacingGame.CarModel.RenderCar(0, Color.White, carMatrix); plane.Render(); // Just add brake tracks (we don't render the landscape here) RacingGame.Landscape.RenderBrakeTracks(); // Render all 3d objects BaseGame.MeshRenderManager.Render(); // Add shadows ShaderEffect.shadowMapping.ShowShadows(); RacingGame.Player.DisplayPhysicsValuesAndHelp(); }); } // TestCarPhysicsOnPlane() 重力 在平面上驾驶是很容易做到的,但不是非常有趣,特别是这个赛车游戏还可以让你在循环轨道,隧道,曲线等等上行驶。碰撞检测将在本章后面讨论,但你可以在TestCarPhysicsOnPlane单元测试中添加重力并调整它。 你只需在TestCarPhysicsOnPlane单元测试稍作改变,即在初始化代码中把车提升到离地10米的高处,它应该掉下来,但因为你没有实现真正的物理效果(只有汽车控制,加速,转弯),因此车只是停留在空中(见图13-6)。 图 13-6 // Put car 10m above the ground to test gravity and ground plane! RacingGame.Player.SetCarPosition( new Vector3(0, 0, 10), new Vector3(0, 1, 0), new Vector3(0, 0, 1)); Player类是从ChaseCamera类继承的,而ChaseCamera类继承自CarPhysics类。这样,所有的汽车控制,物理学,玩家逻辑等可以从同一个player类中处理和访问。ChaseCamera类有两种模式,类似与Rocket Commander的SpaceCamera类的模式: Free Camera模式用于所有单元测试。有时候你也可以在单元测试中改变它。 默认的Chase Car Game用于游戏。用于菜单或重放游戏过程,当然也用于在游戏中显示和控制汽车。 在初始化中使用以下代码以确保相机被使用,在游戏中你不使用变焦,你可以看到开始时车的拉近过程: // Make sure we are not in free camera mode and can control the car RacingGame.Player.FreeCamera = false; RacingGame.Player.ZoomInTime = 0; RacingGame.Player.SetCameraPosition(new Vector3(0, -5, 8)); 单元测试的其他部分保持不变,重力计算在CarPhysics类的Update方法中,它调用ApplyGravity方法: /// <summary> /// Apply gravity /// </summary> private void ApplyGravity() { // Fix car on ground float distFromGround = Vector3Helper.SignedDistanceToPlane( carPos, groundPlanePos, groundPlaneNormal); isCarOnGround = distFromGround > -0.1f; // Use very hard and instant gravity to fix if car is below ground! float moveFactor = BaseGame.MoveFactorPerSecond; float maxGravity = Gravity * moveFactor; // Use more smooth gravity for jumping float minGravity = -Gravity * moveFactor; if (distFromGround > maxGravity) { distFromGround = maxGravity; gravitySpeed = 0; } // if (distFromGround) if (distFromGround < minGravity) { distFromGround = minGravity; gravitySpeed -= distFromGround; } // if (distFromGround) carPos.Z += distFromGround; } // ApplyGravity() 此代码仅降低了z值知道车到达地面。当你驾驶上山时汽车可能会低于道路,或因为一些精度误差,z值也会被修正使车重回到公路上。 调整要用到许多常数,这些都是在CarPhysics类的一开始被定义的。下面是前三个常数。改变CarMass,例如,如果汽车很重则它几乎贴到地面行驶,或很轻,可在空中飞行很长一段时间: #region Constants /// <summary> /// Car is 1000 kg /// </summary> const float CarMass = 1000;//1000.0f; /// <summary> /// Gravity on earth is 9.81 m/s^2 /// </summary> const float Gravity = 9.81f; /// <summary> /// Max speed of our car is 275 mph. /// While we use mph for the display, we calculate internally with /// meters per sec since meter is the unit we use for everthing in the /// game. And it is a much nicer unit than miles or feet. /// </summary> public const float MaxSpeed = 275.0f * MphToMeterPerSec; /// <summary> /// Convert our meter per sec to mph for display. /// 1 mile = 1.609344 kilometers /// Each hour has 3600 seconds (60 min * 60 sec). /// 1 kilometer = 1000 meter. /// </summary> public const float MeterPerSecToMph = 1.609344f * ((60.0f*60.0f)/1000.0f), MphToMeterPerSec = 1.0f / MeterPerSecToMph; // [etc.] 你可能几次看到我使用米而不是英尺、英寸,码、英里或在美国使用的格式,很抱歉,世界的其他地方使用米,这样更容易测量和设定常数,我甚至不知道引力常数在其他单位系统中是多大,你可以看到,每当有其他的格式,如Mph(英里每小时)时我使用辅助常数将格式转换。你还可以使用谷歌转换格式。只需输入“1500 meters as miles”,你将获得英里的数值。 新的TestCarPhysicsOnPlane单元测试让你可以控制赛车和正确处理重力(见图13-7)。当你刹车时,它也处理在地面上显示制动轨迹。 图 13-7

物理学——概览

clock 十月 9, 2010 08:45 by author alex
概览 近些年来在游戏中使用物理学变得越来越流行。老游戏没有任何物理学,但最近许多射击游戏具有复杂的物理系统,这些系统只有通过计算能力的巨大提升和和多核系统才有可能实现。最近还出现了硬件PPU(物理处理单元),类似于GPU处理图形,这些处理器只计算物理系统。 使用物理学最多的游戏是第一人称射击游戏,特别是在那些玩家具有较大的自由度并允许自由走动并和场景发生互动的游戏中。其中最流行的游戏是2004年底发布的Half-Life 2,这个游戏使用了大量创新的物理技术,并在游戏中和任务设计中也涉及物理学。 Half-Life 2内部使用了商业的Havok物理引擎和其他一些物理框架,这马上就会说到。但这些引擎不能使用在Xbox 360与XNA框架上,因为它们都使用非托管代码,这在是Xbox 360是不允许的。在电脑上可以使用任何你喜欢的框架,本章马上就会讨论哪一个框架是最好的。基于上述理由,你现在开发的赛车游戏不使用任何先进的物理框架,而且使用并测试这么一个强大的物理框架要做大量的工作和测试,其中大部分物理引擎的许可费收费甚高。在Half-Life 2中,工程师花了数年时间调整游戏直到他们看到满意的结果。 物理总是伴随着繁重的数学计算。可以使用一个现成的物理框架或只使用一个简单的公式如著名的F=ma,而无需知道很多有关物理或数学的知识,但一旦你想开发自己的物理系统,你至少需要对牛顿运动定律有个基本的了解。手头备一本物理公式书或至少用谷歌搜索是一个好主意。图13-1显示了一个典型的物理问题,涉及了多个对象,作用力,重力等等。 图 13-1 当你谈论物理学时你无法避开艾萨克牛顿,苹果和树,你知道这个故事。自那时以来物理已经发展了不少东西,既有研究原子,夸克,电子等微观世界的理论,还有研究宇宙,太阳系,行星和恒星等宏观世界的理论。但对大多数的游戏来说,你只使用一系列简单的物理公式以帮助你加速和减速对象。由于处理能力有限,只要看起来足够好就行了,你不必让每一个原子的行为都像在现实世界中的一样。 牛顿运动定律公式将大量使用,同时你也要花费大部分的时间解决碰撞问题,就像在第八章中为Rocket Commander游戏做得那样。物理是有趣的,而且借助于单元测试你可以快速测试直到正常工作,但你要知道——如果你的游戏世界不允许太多的互动,你可能会花费大量的时间在物理效果上却很难看到的结果。一个益智游戏有物理系统固然不错,但如果你所做的只是按一下物件使之移动,那就无需一个复杂的物理引擎。

创建场景和赛道——总结

clock 十月 8, 2010 11:27 by author alex
总结 在这一章中你学到了很多关于渲染复杂3D物体的知识,如场景、赛道和在赛车游戏中创建关卡。希望也可用于其他游戏,当为游戏创建场景时你要记得下列几件事: 总是先判断玩家是如何看场景的。看上去很棒的场景其实没多大意义,这种场景只在近距离看时很棒,这种场景在策略游戏、RPG游戏或赛车游戏中只覆盖了50米2面积。而在这类游戏中你需要面积更大,这意味着你要么重复纹理或有更好的渲染技术将细节纹理整合成一个大纹理,或使用splatting“绘制”场景地面。 对于创建巨型场景,Splatting是一个很好的技术。大多数游戏使用纹理splatting,使用不同的地面纹理和额外的特殊纹理、赛道等等以构成看上去很棒的地形。如果你没有太多的开发时间,或者如果你不需要巨大的地形,你可以使用如上一章的简单解决方法,对射击游戏来说这个方法很不错。 附加层,shader和特效,特别是水面shader和阴影映射,是一个非常耗时的任务。我特意不在游戏中使用水面效果,因为在过去几个项目里,总是要花费几个星期的时间才能获得满意的水面效果,我了解我自己——我会不停地修改它直到我满意,但往往没有这么多的时间。 你可以从其他人身上学到许多技巧和窍门,互联网上的教程和诸如Game Programming Gems或ShaderX系列的书上有很多很好的资源。 生成赛道有类似的规则,首先创建单元测试是很重要的。始终测试性能,特别是当你创建了一个有很多多边形的区域时,在本章中场景使用了130000个左右的多边形,赛道多达24000个多边形,还有额外的50000多个多边形用于赛道护栏,隧道和支持赛道的水泥柱。这意味着即使没有任何额外的场景物体,每帧也要渲染超过200000个多边形,也就是说在帧速率为每秒100帧的情况下光渲染场景和赛道就需要把2000多万个多边形在一秒内传递给GPU。汽车大约有25000个多边形而场景物体有几千多个,这意味如果有1000个场景物体,每帧仍要渲染数百万个多边形。听起来很多,但如果看看TestRenderLandscape单元测试中的截图,你会发现大概有2000个物体在场景中。 你可能会想到,Xbox 360游戏机的GPU也许无法在一帧时间内处理2-3万多边形且仍保持超过每秒60帧的帧速率。在PC更糟的是,75赫兹是最常用的屏幕刷新率而大多数玩家的GPU太慢,导致无法在一帧中处理所有三维几何数据。为了确保游戏在中档或低档电脑上仍能运行良好,使用了以下技巧: 不要渲染看不见的东西。场景和赛道总是可见的,所以你不能忽略它们。但许多场景模型可能在你身后或超出你的视野,可简单地忽略它们。另一个诀窍是忽略任何太遥远的模型,特别是100米开外的较小的3D模型。这是提升游戏性能的最大技巧。看一下Model类和Render方法以了解更多关于这一技巧的知识。 在每帧开始时渲染GPU的大量数据。通过这种方式,你可以在GPU仍在处理场景和赛道多边形的同时,准备好场景模型和其他渲染数据。 总是通过shader排序渲染。在前面的一些游戏中你已看到MeshRender是如何被使用的,以及它是如何极大地提高性能的。在对赛车游戏的性能测试(见Model类)中我不断提高MeshRenderManager类的性能。特别是对同一类型的大量物体使用相同的shader,性能是令人诧异的。查看TestRenderManyModels单元测试,它将超过1亿个多边形传递给Xbox 360的GPU(也可以是你的PC,如果你的GPU足够快的话)。 使用简单的shader!在原始版本的Rocket Commander游戏中所有物体都使用ParallaxShader,相对NormalMapping shader来说只是添加了几条额外的指令,但它仍然需要花费比NormalMapping更多的时间。为此,在赛车游戏中只使用法线映射,而且视觉效果并没有多大差别。只有场景采用了更复杂的shader来添加细节映射效果(见图12-7;这确实是值得的)。 好吧,在这一切新知识让你的大脑爆炸前,先放松一会儿。第13章是关于物理学的,会涉及很多数学知识。当你阅读第14章时,你将最终能把所有东西都整合到一起。

创建场景和赛道——挑战:为赛道建立一个新的单元测试

clock 十月 8, 2010 11:25 by author alex
本章处理了不少复杂的问题,你很可能会觉得难,所以这次我布置了一个简单的任务。进入TrackLine类并为一个简单的赛道添加新的单元测试。你只需要新增一些顶点并测试它们直到赛道看起来足够好。 图12-22显示了一个这种自定义赛道的例子。请注意,这本书中为了看上去更好,我颠倒了截图。你也可以使用一个如第8章中所展示的倒置post-screen shader。 Figure 12-22 如果你还有精力,可试着通过TrackImporter类从3D Studio Max中导入赛道,然后在Track类或LandScape类测试它们。总之,享受一下场景和赛道渲染代码。但愿它对其他游戏项目也是有用的。 /*728*90文章内页广告*/ var cpro_id = 'u241919';

创建场景和赛道——赛道

clock 九月 25, 2010 09:12 by author alex
赛道 除了有HUD这个游戏并没有真正看起来像一个赛车游戏,在发光和颜色修正post-screen shaders作用下更像是一个幻想角色扮演,没有赛道和赛车使它看起来不像一个赛车游戏。单单把车放在场景中看起来挺有趣,但你不想在地面上驾驶,尤其是场景看起来不那么好(1场景纹纹理像素2×2米,使整个车放置在2个纹理像素上)。 这个游戏的构思是制作一些像赛道狂飙游戏类似的赛道,但经过研究赛道狂飙和游戏中的编辑器后,你会看到游戏中的绘制过程是如此的复杂。我把赛道作为一个整体渲染,而赛道狂飙的关卡是由许多不同的赛道块构成的,相互间配合完美,通过这种方式,你可以把三个轨道环相互连接而不需要自己绘制或创造它。这种方法的缺点是,可用的赛道块有限,从开发者的角度来看,创造数以百计的这些赛道块要做大量的工作,更别说要在所有关卡中测试它们。 因此,这一构思被放弃。我回到我原来的做法,只是创造一个简单的二维赛道并通过场景高程图增加了一点高度。我想使用一张绘制有赛道的位图,在位图上以一条白线作为赛道,然后从位图上提取位置信息和创建三维顶点并将其导入到游戏中。但尝试后发现这个方法使赛道紧贴在场景地面上,要实现轨道环,坡道,疯狂的曲线是不可能的,或者至少很难实现。 因此,这个构思再次被放弃。为了更好地了解赛道看起来如何我使用了3D Studio Max,通过spline函数建立一个仅有4个点的简单循环轨道(见图12 -12)。旋转90度到左侧,这看起来很像赛道,比位图的方法更吸引人。 图 12-12 我得把这些spline数据从3D Studio Max中导出并插到我的引擎中,这样我就可以在3D Studio Max中创建赛道并将它导入到赛车游戏引擎中。困难的部分是从spline数据中产生一条可用的赛道,因为每个spline点只是一个点,而不是一片有方向有宽度的道路。 在花更多的时间试图找出最好的方式来产生赛道,将spline数据导入到你的游戏前,你应该确保这一想法可行。 单元测试 这个游戏再次进行了一些繁重的单元测试。由新的TrackLine类中的第一个叫做TestRenderingTrack的单元测试开始,它只是创建了一个简单的如3D Studio Max中类似的曲线,并把它显示在屏幕上: public static void TestRenderingTrack() { TrackLine testTrack = new TrackLine( new Vector3[] { new Vector3(20, -20, 0), new Vector3(20, 20, 0), new Vector3(-20, 20, 0), new Vector3(-20, -20, 0), }); TestGame.Start( delegate { ShowGroundGrid(); ShowTrackLines(testTrack); ShowUpVectors(testTrack); }); } // TestRenderingTrack() ShowGroundGrid方法只是在xy平面上显示一些网格线以帮助你知道地面在哪。之后我在model类中写了这个方法,能够被重复使用。ShowTrackLines是最重要的方法,因为它显示了所有的线条和插值点,这些线条和插值点已在TrackLine类的构造函数中生成。最后ShowUpVectors方法告诉向量的向上方向供赛道每个点使用。没有向上向量,你将无法正确地生成道路的左右两侧。例如,在曲线赛道上应该倾斜、在循环赛道上你需要向上向量指向圆轨道圆心,而不只是向上。 ShowTrackLines辅助方法显示赛道的每个点,它们之间通过白色直线连接。当你执行TestRenderingTrack单元测试后就可以看到如图12-13的画面。 public static void ShowTrackLines(TrackLine track) { // Draw the line for each line part for (int num = 0; num < track.points.Count; num++) BaseGame.DrawLine( track.points[num].pos, track.points[(num + 1)%track.points.Count].pos, Color.White); } // ShowTrackLines(track) 图 12-13 借助于红色的向上向量和绿色的切线向量,这条赛道看起来有点像公路了。你现在要做的是调整赛道生成代码,测试更多的放样线条。在TrackLine类中你可以看到我的几个测试赛道,这些赛道是通过手工添加一些3D点创建的,更多的赛道可通过使用Collada文件实现,这时要将3D Studio Max中的赛道数据导入到你的引擎,这在接下去会讨论到。 在你查看构造函数中的放样线条插值代码前,你也可以创建一个简单的循环赛道,只需转换赛道顶点的x和z值(见图12-14 )。为了使样条看起来更圆我还补充四个新的点。新的TestRenderingTrack单元测试如下所示: public static void TestRenderingTrack() { TrackLine testTrack = new TrackLine( new Vector3[] { new Vector3(0, 0, 0), new Vector3(0, 7, 3), new Vector3(0, 10, 10), new Vector3(0, 7, 17), new Vector3(0, 0, 20), new Vector3(0, -7, 17), new Vector3(0, -10, 10), new Vector3(0, -7, 3), }); // [Rest stays the same] } // TestRenderingTrack() 图 12-14 插值样条 你可能会问如何只通过在4个或8个输入点获得所有这些点和这些点如何才能插值得更好。这一切都发生在TrackLine的构造函数中,或更准确地说,应该是在Load方法中,它可以让你在需要重新生成时重新载入数据。第一次看到Load方法时,你会觉得不很容易,它是加载所有赛道数据、验证赛道数据、插值和生成向上和切线向量的主要方法。隧道和场景物体也在这里生成。 Load方法做了以下事情: 它允许重新载入,这对载入和重新开始关卡是非常重要的。如果你再次调用Load方法,以往的数据会自动被清除。 验证所有数据以确保你可以产生赛道并使用所有辅助类。 检查赛道上的每一点看看它是否在场景之上。如果没有,该点会被纠正,而且周围的点也会稍微上升少许以使赛道看起来更光滑。通过这种方式,你可以轻松地在Max中生成一个三维赛道,而当将赛道放置在场景之上时,就无需担心场景的实际高度。 圆轨道被简化为上下两个取样点。加载代码会自动检测这两个点并用完整循环的九个点取代它们,这样插入更多的点以产生非常光滑和正确的圆轨道。 然后,所有赛道上的点通过Catmull-Rom插值方法被插值。你马上就会看到这种方法。 向上和切线向量会生成并插值好几次以使道路尽可能平滑。切线向量尤其不应该突然改变方向或翻转到另一边,这将使得在这条道路上开车变得非常困难。这个代码我花费了最长的时间才使之能工作正常。 然后,所有分析所有辅助类和对应赛道上的每一个点的道路宽度被储存,以便接下去使用,实际渲染发生在Track类,它是基于TrackLine类的。 道路纹理的纹理坐标也在这里生成,因为你将所有赛道点的信息存储在TrackVertex数组中,这样可以使接下去的渲染更容易。只有u纹理坐标是储存在这里的,而v纹理坐标在后来只是被设置为0或1,这取决于你是在道路的左边还是右边。 然后,分析隧道辅助类并生成隧道数据。这里的代码只是构建了一些新的点供以后使用。它们被用来在Track类中绘制带有隧道纹理的隧道盒。 最后所有的场景模型被添加。他们和赛道数据一起被保存为一个完整的关卡。附加的场景物体也在Track类中自动生成,例如,路边的路灯等。 当我开始编写TrackLine类时,构造函数只能通过Catmull-rom spline辅助方法从输入点中生成新的插值点。该代码看上去如下,在Load方法中也能找到类似代码: // Generate all points with help of catmull rom splines for (int num = 0; num < inputPoints.Length; num++) { // Get the 4 required points for the catmull rom spline Vector3 p1 = inputPoints[num-1 < 0 ? inputPoints.Length-1 : num-1]; Vector3 p2 = inputPoints[num]; Vector3 p3 = inputPoints[(num + 1) % inputPoints.Length]; Vector3 p4 = inputPoints[(num + 2) % inputPoints.Length]; // Calculate number of iterations we use here based // on the distance of the 2 points we generate new points from. float distance = Vector3.Distance(p2, p3); int numberOfIterations = (int)(NumberOfIterationsPer100Meters * (distance / 100.0f)); if (numberOfIterations <= 0) numberOfIterations = 1; Vector3 lastPos = p1; for (int iter = 0; iter < numberOfIterations; iter++) { TrackVertex newVertex = new TrackVertex( Vector3.CatmullRom(p1, p2, p3, p4, iter / (float)numberOfIterations)); points.Add(newVertex); } // for (iter) } // for (num) 更复杂的赛道 单元测试已经能让一切都启动和运行了,但赛道越复杂,通过输入每个样点的3D位置产生赛道就更难。要让事情变得简单点,可以使用从3D Max Studio中导出的spline,这可以让创建和修改spline变得更容易。 看看如图12-15所示的XNA Racing游戏中专家关卡的赛道。这条赛道仅包含约85个点,插值到2000个点使赛道约有24000个多边形。赛道围栏和额外的赛道物体在以后生成。构建这样的一条赛道并调整它,如果没有一个好的编辑器几乎是不可能的,不过幸好有3D Max。也许将来我会为这个游戏制作一个赛道编辑器,至少能让你在游戏中直接创建简单的赛道。 图 12-15 我最初认为导出这种数据并不容易。.x档案不支持spline,.fbx文件也不行。即使他们能导出spline,你仍需要做大量的工作从赛道中提取数据,因为在XNA中从导入的模型中获得顶点数据是不可能的。我决定使用目前非常流行的Collada格式,这种格式允许在不同的应用程序间互相导入导出3D数据。相比其他格式,Collada的主要优势是一切都存储为XML格式,从导出的文件上你可以很容易看出哪个数据对应哪个功能。你甚至不需要寻找任何文件,只需寻找你需要的数据并提取它(在这里,你只需寻找spline和辅助数据,其余的对你并不重要)。 对游戏来说Collada不是一个真正优秀的导出格式,因为它通常储存了太多的信息,而且XML数据仅仅是一堆文字,所以比起二进制文件,Collada文件的尺寸也大得多,出于这个理由,而且我也不能在XNA Starter Kit中使用任何外部数据格式,所有的Collada数据在TrackImporter类中被转换为内部数据格式。使用自己的数据格式加快了加载过程,并确保没有人能创建自己的赛道。嘿,等一下,你不希望别人创建自己的赛道吗?我的确希望这变得更容易,你需要3D Studio Max才能创建或改建赛道并不好。我必须在以后实现某种方法可以导入和创建赛道。 导入赛道数据 为了使装载Collada文件变得容易些使用了一些辅助类。首先,XmlHelper类(见图12-16 )能帮你加载和管理XML文件。 图 12-16 ColladaLoader类只是一个很短的类,它加载Collada文件(只是一个xml文件),让使用XmlHelper方法的派生类更容易。 ColladaTrack是用来加载赛道本身(trackPoints),其他辅助对象如widthHelpers可使赛道变宽和变窄,roadHelpers用于隧道,棕榈树,路灯等路边的物体。最后所有的场景物体在你接近他们时被显示(因为在场景中有大量的物体)。 ColladaCombiModels是一个小的辅助类,它用于一次加载并显示多个模型,只需设置一个包含多达10个模型的组合模型,这十个模型有不同的位置和旋转值。例如,如果你想放置一个具有建筑物的城市区域,只需使用Buildings.CombiModel文件,如果你需要一些棕榈树外加几块石头可使用Palms.CombiModel文件。 想对加载过程了解得更多,可以使用TrackLine和Track类中的单元测试,但更重要的查看ColladaTrack构造函数本身: public ColladaTrack(string setFilename) : base(setFilename) { // Get spline first (only use one line) XmlNode geometry = XmlHelper.GetChildNode(colladaFile, "geometry"); XmlNode visualScene = XmlHelper.GetChildNode(colladaFile, "visual_scene"); string splineId = XmlHelper.GetXmlAttribute(geometry, "id"); // Make sure this is a spline, everything else is not supported. if (splineId.EndsWith("-spline") == false) throw new Exception("The ColladaTrack file " + Filename + " does not have a spline geometry in it. Unable to load " + "track!"); // Get spline points XmlNode pointsArray = XmlHelper.GetChildNode(geometry, "float_array"); // Convert the points to a float array float[] pointsValues = StringHelper.ConvertStringToFloatArray( pointsArray.FirstChild.Value); // Skip first and third of each input point (MAX tangent data) trackPoints.Clear(); int pointNum = 0; while (pointNum < pointsValues.Length) { // Skip first point (first 3 floating point values) pointNum += 3; // Take second vector trackPoints.Add(MaxScalingFactor * new Vector3( pointsValues[pointNum++], pointsValues[pointNum++], pointsValues[pointNum++])); // And skip thrid pointNum += 3; } // while (pointNum) // Check if we can find translation or scaling values for our // spline XmlNode splineInstance = XmlHelper.GetChildNode( visualScene, "url", "#" + splineId); XmlNode splineMatrixNode = XmlHelper.GetChildNode( splineInstance.ParentNode, "matrix"); if (splineMatrixNode != null) throw new Exception("The ColladaTrack file " + Filename + " should not use baked matrices. Please export again " + "without baking matrices. Unable to load track!"); XmlNode splineTranslateNode = XmlHelper.GetChildNode( splineInstance.ParentNode, "translate"); XmlNode splineScaleNode = XmlHelper.GetChildNode( splineInstance.ParentNode, "scale"); Vector3 splineTranslate = Vector3.Zero; if (splineTranslateNode != null) { float[] translateValues = StringHelper.ConvertStringToFloatArray( splineTranslateNode.FirstChild.Value); splineTranslate = MaxScalingFactor * new Vector3( translateValues[0], translateValues[1], translateValues[2]); } // if (splineTranslateNode) Vector3 splineScale = new Vector3(1, 1, 1); if (splineScaleNode != null) { float[] scaleValues = StringHelper.ConvertStringToFloatArray( splineScaleNode.FirstChild.Value); splineScale = new Vector3( scaleValues[0], scaleValues[1], scaleValues[2]); } // if (splineTranslateNode) // Convert all points with our translation and scaling values for (int num = 0; num < trackPoints.Count; num++) { trackPoints[num] = Vector3.Transform(trackPoints[num], Matrix.CreateScale(splineScale) * Matrix.CreateTranslation(splineTranslate)); } // for (num) // [Now Helpers are loaded here, the loading code is similar] } // ColladaTrack(setFilename) 获取spline数据本身并不是很难,但获取移动,缩放,旋转值要多费些功夫(辅助类也更复杂),但在你编写和测试了此代码后(有几个单元测试和测试文件被用来实现这一构造函数),创建新的赛道并将它们导入到游戏中是很容易的。 从赛道数据生成顶点 获取赛道数据和导入辅助数据只完成了一半工作。你已经看到TrackLine类的构造函数是多么复杂了,它帮你产生插值点,并建立向上和切线向量。纹理坐标和所有辅助和场景模型也在这里处理。但是你现在仍然只有一大堆点,并没有一条真正的道路让你的车可以行使其上。为绘制一条具有纹理的真正的道路(见图12-17),你需要首先为所有的三维数据创建顶点,并最终生成道路,还要包括其他动态创建的对象,如护栏。最重要的纹理是道路本身,但没有法线贴图游戏看起来有点枯燥。法线贴图给道路添加了一个闪闪发光的结构,使道路在面向太阳时有光泽。道路两旁的纹理、背景(RoadBack.dds)和隧道(RoadTunnel.dds)也很重要,但你不会经常看到它们。 图 12-17 TrackLine类处理所有这些纹理,有道路材质,道路水泥柱、护栏、检查站等,它是基于,它从Track类继承而来的。Landscape类用来绘制赛道和所有场景物体以及场景本身,最后才能使汽车在道路上行驶。你还需要物理学处理在赛道上的运动、与护栏的碰撞,这在下一章会说到。 Track类负责所有道路材质,生成所有顶点以及索引缓冲,并最终在shader的帮助下渲染所有的赛道顶点。大多数材质使用NormalMapping中的Specular20技术产生一个有光泽的道路,但对隧道和其他非光泽道路材质,应使用Diffuse20技术。 绘制赛道的单元测试很简单,所有你想做的事就是绘制赛道。 public static void TestRenderTrack() { Track track = null; TestGame.Start( delegate { track = new Track("TrackBeginner", null); }, delegate { ShowUpVectors(track); track.Render(); }); } // TestRenderingTrack() 如你所见你仍然使用TrackLine类中的ShowUpVectors辅助方法,因为你是从Track类中继承而来的。Render方法也类似于前一章Mission类中的场景渲染的方法。 public void Render() { // We use tangent vertices for everything here BaseGame.Device.VertexDeclaration = TangentVertex.VertexDeclaration; // Restore the world matrix BaseGame.WorldMatrix = Matrix.Identity; // Render the road itself ShaderEffect.normalMapping.Render( roadMaterial, "Specular20", delegate { BaseGame.Device.Vertices[0].SetSource(roadVb, 0, TangentVertex.SizeInBytes); BaseGame.Device.Indices = roadIb; BaseGame.Device.DrawIndexedPrimitives( PrimitiveType.TriangleList, 0, 0, points.Count * 5, 0, (points.Count - 1) * 8); }); // [etc. Render rest of road materials] } // Render() 嗯,看来并不十分复杂。看一下生成的道路顶点和索引缓冲的代码。私有辅助类GenerateVerticesAndObjects执行上述操作: private void GenerateVerticesAndObjects(Landscape landscape) { #region Generate the road vertices // Each road segment gets 5 points: // left, left middle, middle, right middle, right. // The reason for this is that we would have bad triangle errors if the // road gets wider and wider. This happens because we need to render // quads, but we can only render triangles, which often have different // orientations, which makes the road very bumpy. This still happens // with 8 polygons instead of 2, but it is much better this way. // Another trick is to not do so many iterations in TrackLine, which // causes this problem. Better to have a not so round track, but at // least the road up/down itself is smooth. // The last point is duplicated (see TrackLine) because we have 2 sets // of texture coordinates for it (begin block, end block). // So for the index buffer we only use points.Count-1 blocks. roadVertices = new TangentVertex[points.Count * 5]; // Current texture coordinate for the roadway (in direction of // movement) for (int num = 0; num < points.Count; num++) { // Get vertices with the help of the properties in the TrackVertex // class. For the road itself we only need vertices for the left // and right side, which are vertex number 0 and 1. roadVertices[num * 5 + 0] = points[num].RightTangentVertex; roadVertices[num * 5 + 1] = points[num].MiddleRightTangentVertex; roadVertices[num * 5 + 2] = points[num].MiddleTangentVertex; roadVertices[num * 5 + 3] = points[num].MiddleLeftTangentVertex; roadVertices[num * 5 + 4] = points[num].LeftTangentVertex; } // for (num) roadVb = new VertexBuffer( BaseGame.Device, typeof(TangentVertex), roadVertices.Length, ResourceUsage.WriteOnly, ResourceManagementMode.Automatic); roadVb.SetData(roadVertices); // Also calculate all indices, we have 8 polygons for each segment // with 3 vertices each. We have 1 segment less than points because // the last point is duplicated (different tex coords). int[] indices = new int[(points.Count - 1) * 8 * 3]; int vertexIndex = 0; for (int num = 0; num < points.Count - 1; num++) { // We only use 3 vertices (and the next 3 vertices), // but we have to construct all 24 indices for our 4 polygons. for (int sideNum = 0; sideNum < 4; sideNum++) { // Each side needs 2 polygons. // 1. Polygon indices[num * 24 + 6 * sideNum + 0] = vertexIndex + sideNum; indices[num * 24 + 6 * sideNum + 1] = vertexIndex + 5 + 1 + sideNum; indices[num * 24 + 6 * sideNum + 2] = vertexIndex + 5 + sideNum; // 2. Polygon indices[num * 24 + 6 * sideNum + 3] = vertexIndex + 5 + 1 + sideNum; indices[num * 24 + 6 * sideNum + 4] = vertexIndex + sideNum; indices[num * 24 + 6 * sideNum + 5] = vertexIndex + 1 + sideNum; } // for (num) // Go to the next 5 vertices vertexIndex += 5; } // for (num) // Set road back index buffer roadIb = new IndexBuffer( BaseGame.Device, typeof(int), indices.Length, ResourceUsage.WriteOnly, ResourceManagementMode.Automatic); roadIb.SetData(indices); #endregion // [Then the rest of the road back, tunnel, etc. vertices are // generated here and all the landscape objects, checkpoints, palms, // etc. are generated at the end of this method] } // GenerateVerticesAndObjects(landscape) 在编写这个代码时我写了很多注释。第一部分生成一个很大的切线数组,数组大小是TrackLine类中的赛道顶点的5倍。此数据直接传递到顶点缓冲区,然后被用于构造多边形的索引缓冲区。每个道路片有8个多边形(由四部分组成,每部分两个多边形),因此该索引缓冲区大小是赛道顶点索引的24倍。为了确保仍然能够正确使用所有这些索引,必须使用int类型替代short类型,以前我使用short类型是因为这样做能节省一半内存。但在这种情况下有超过32000个索引( 专家关卡的赛道有2000个道路片,它的24倍已达到48000个索引)。因为赛道是自动生成而不是手工产生,所以你需要许多迭代点,如果你没有足够的迭代点会导致重叠错误,这样就没法使赛道足够圆滑(见图12-18)。 图 12-18 你可能会问,为什么是四个部分产生每个道路片,原因不是因为我喜欢让低档的GPU处理很多多边形。这项技术是用来改善赛道的视觉效果的,特别是在曲线的情况下。 图12-19能更好地解释这个问题。如你所见,构成不平行的方块的两个多边形并不总是大小相同的,但它们仍然使用同样数量的纹理像素。在右边你可以看到一个极端的情况下,道路的右下角部分严重扭曲,不看好了。 图 12-19 这个问题可通过将道路分成多个部分加以解决。你可以将道路片分成四个部分,这样做道路看起来好多了。 最后结果 让场景和道路正确显示要做大量的工作,但现在你已做得不错了,至少图形部分不错。你可以在Track命名空间下的类中看到许多小窍门和技巧。请查看单元测试以了解更多关于如何绘制道路的两旁、圆轨道和隧道的知识。 图12-20显示了Track类中的TestRenderTrack单元测试的最终结果。 图 12-20 和本章第一部分的场景渲染整合在一起,你就有了一个相当不错渲染引擎。加上背景的post-screen shader天空盒,场景和道路渲染看起来相当不错(见图12-21)。Post-screen glow shader也使一切都配合得更好,尤其是在场景中有很多物体的情况下。 图 12-21

创建场景和赛道——场景绘制

clock 九月 25, 2010 08:42 by author alex
场景绘制 在前一章你已经看到了在.x模型的帮助下用简化的方式来产生三维场景。你首先创建了diffuse纹理,并由此建立了一个法线贴图,最终添加了一个高程图为XNA Shooter生成了峡谷。 对于这个游戏你仍然要使用一个非常简化的办法去渲染场景,因为开发时间很短,而且制做一个高精度的场景渲染引擎往往要花费几个月的时间。如Arena Wars游戏中使用的场景引擎,支持非常大的场景,但它从来没被使用在游戏中,在不同的硬件配置中进行优化要做大量的工作,尤其是还要支持24中不同类型的地面纹理。 为了保持简单,这个赛车游戏只使用一个非常大的地面纹理,但你也需要一个法线贴图和一个额外的细节纹理用来添加一些细节,特别是当你离地面非常近时,这种情况通常发生在你停留在非常靠近场景的时候。一开始我制作了一个4096×4096的纹理,但它很难被更新,包括法线贴图也是。未压缩的diffuse纹理约有64MB,另外还要64MB用于法线贴图。想像一下在一台只有1GB内存的电脑上创建10个或更多的这种纹理,结果一定很糟。即使压缩为一个DXT5纹理,4096×4096的法线贴图仍有约12MB,单单载入过程就很恼人。另一个让我放弃使用4096×4096纹理的原因是XNA中的内容管道,当它把这么大的一个纹理转换到.xnb内容文件时耗时很长。 我缩小到2048×2048纹理,这看起来与4096×4096几乎一样好,但只占用1/4的大小和载入时间。法线贴图甚至降低到1024×1024,因为它看起来没多大差别。另一个不使用大尺寸纹理的原因是微软的Xbox 360显存大小有限(64MB),你不应载入太多的东西或太大的纹理,否则性能将大大下降。不要建立一个超大尺寸的纹理,作为替代,我添加了一个细节纹理,使镜头拉近时场景看起来更好,在下面几页会讨论到如何绘制这些纹理。 纹理 首先,你需要一个高程图,并知道你的场景有多大。最初我想让一个场景的texel(纹理像素,纹理的1个像素)是1米(3.3英尺),那么4096×4096纹理使整个场景大小为4×4公里大(约2.5×2.5英里)。在纹理减少到2048×2048时场景大小保持不变,则现在每个纹理像素是2×2米。 那么你的场景从哪里获得高度值?自己画可能不会非常好,你也没有一个好的程序帮你做这件事,也肯定没有足够的时间编写一个自定义场景高程图编辑器。一个好主意是在互联网上寻找并使用现成的高程图,有很多资源可以利用(国家地理,美国航天局NASA,其中甚至还有其他行星的高程图)。 对于这个赛车游戏我想有一些山。对测试来说足够好了,但后来我需要山围绕在场景周围,所以我必须要修改高程图,使场景中间是座大山,其他山体围绕在边界上。最后的高程图中可以看图12-5。请注意,这个游戏使用的LandscapeGridHeights.png高程图只有257× 257个像素,因为在Landscape类中只能生成257×257个(66049个)顶点的网格,即256* 256*2个多边形(约13万个多边形)。太多多边形会极大地拖慢渲染速度,但处理130000多边形对今天的显卡来说不是大问题,Xbox 360也能处理得很好(仍保持在数百帧每秒)。 图 12-5 你可能会注意到,高程图上用于城市地区的平坦地区,只是简单地把所有建筑物和物体放置在相同的高度上。中间的白色区域是大山,围绕在地图边界上的白灰色部分表明边界的山体。 这个高程图与一个有一点起伏的纹理产生法线贴图。另外,你也可以像上一章一样混合diffuse贴图,但是因为我要频繁地改变diffuse贴图导致法线贴图往往不再受到diffuse贴图的影响。图12-6显示了使用了diffuse贴图和法线贴图的游戏场景。 图 12-6 请注意,对这些纹理我尝试了很多次直到它们成为现在这个样子,我并不完全满意,但你必须适可而止,尤其是当你没有更多的时间进行改进时。例如,法线贴图从远处看很棒,但靠近时缺少变化,或许通过更好地匹配diffuse纹理可以加以改进。无论如何,在游戏中它们看起来很好,我也没有听到任何抱怨。 最后,在你靠近场景时还添加了细节贴图。你不会马上注意到细节贴图,但图12-7显示了使用与不使用细节贴图之间的令人信服的差异。在这个赛车游戏中有这么大的一个场景,并允许拉近镜头,如果没有细节纹理,画面表现是不好的。 图 12-7 渲染 如果你打开Racing Game的项目,你可以看到很多从以前章节而来的类,但还有两个新的命名空间将会在本章中讨论:Landscapes和Tracks。在Landscapes命名空间只有一个类Landscape(见图12-8),它负责渲染场景,包括所有在场景上的物体,所有赛道和赛道上的物体,基本上是除了你的车以外的所有东西。在Misson类你只是调用Landscape的Render方法执行所有的渲染。对于阴影映射有几个辅助方法可用,有关阴影映射的细节将在第14章中讨论。 图 12-8 所有场景物体都在Landscape类被创建是被建立,尤其是在Track类的构造函数中,它被用在了Landscape类内部。你马上就可以看到本游戏中使用的3D模型。 要渲染场景,你首先必须做的是在构造函数中产生它。在你查看构造函数前你应看看Landscape类中的TestRenderLandscape单元测试,在类实现前应先写单元测试。你也注意到了其他单元测试,GenerateLandscapeHeightFile会为场景高程图产生一个关卡文件,并生成一个特殊的内容文件,这个方法和在Rocket Commander中的一样,因为在Xbox 360中加载位图数据是不可能的。 /// <summary> /// Test render landscape /// </summary> public static void TestRenderLandscape() { TestGame.Start("TestRenderLandscape", delegate { RacingGame.LoadLevel(RacingGame.Level.Beginner); RacingGame.Landscape.SetCarToStartPosition(); }, delegate { if (BaseGame.AllowShadowMapping) { // Generate shadows ShaderEffect.shadowMapping.GenerateShadows( delegate { RacingGame.Landscape.GenerateShadow(); RacingGame.CarModel.GenerateShadow( RacingGame.Player.CarRenderMatrix); }); // Render shadows ShaderEffect.shadowMapping.RenderShadows( delegate { RacingGame.Landscape.UseShadow(); RacingGame.CarModel.UseShadow( RacingGame.Player.CarRenderMatrix); }); } // if (BaseGame.AllowShadowMapping) BaseGame.UI.PostScreenGlowShader.Start(); BaseGame.UI.RenderGameBackground(); RacingGame.Landscape.Render(); RacingGame.CarModel.RenderCar(0, Color.Goldenrod, RacingGame.Player.CarRenderMatrix); // And flush render manager to draw all objects BaseGame.MeshRenderManager.Render(); if (BaseGame.AllowShadowMapping) ShaderEffect.shadowMapping.ShowShadows(); BaseGame.UI.PostScreenGlowShader.Show(); TestGame.UI.WriteText(2, 50, "Number of objects: "+ RacingGame.Landscape.landscapeObjects.Count); }); } // TestRenderLandscape() 该单元测试做了很多事情,它显示了汽车和所有的场景对象。赛道、所有的阴影映射和post-screen shaders也在这里测试,以确保它们在场景中也能工作得很好。如果你只想测试场景本身,只需调用Landscape类中的Render方法就够了。 RacingGame类中的LoadLevel方法是这个游戏主要的类,它负责载入一个关卡。所有关卡使用相同场景,这意味着你不必重新加载它。但是,你应该检查生成场景顶点的代码。场景构造函数执行了以下操作: 从关卡文件载入高程图数据图并建立切线顶点 生成和平滑整个场景的法线,也从新的法线中重新生成切线 设置这些顶点的顶点缓冲 计算索引缓冲(与你在上一章中看到的场景三角形类似) 设置索引缓冲 载入和生成当前关卡的赛道数据,包括所有的场景物体 最后,添加额外的物体,如城市地面,给城市物体更好看的地面纹理。. 构造函数最重要的部分是从高程图生成切线顶点,它通过遍历高程图中所有257×257个点为你生成顶点: // Build our tangent vertices for (int x = 0; x < GridWidth; x++) for (int y = 0; y < GridHeight; y++) { // Step 1: Calculate position int index = x + y * GridWidth; Vector3 pos = CalcLandscapePos(x, y, heights); mapHeights[x, y] = pos.Z; vertices[index].pos = pos; // Step 2: Calculate all edge vectors (for normals and tangents) // This involves quite complicated optimizations and mathematics, // hard to explain with just a comment. Read my book :D Vector3 edge1 = pos - CalcLandscapePos(x, y + 1, heights); Vector3 edge2 = pos - CalcLandscapePos(x + 1, y, heights); Vector3 edge3 = pos - CalcLandscapePos(x - 1, y + 1, heights); Vector3 edge4 = pos - CalcLandscapePos(x + 1, y + 1, heights); Vector3 edge5 = pos - CalcLandscapePos(x - 1, y - 1, heights); // Step 3: Calculate normal based on the edges (interpolate // from 3 cross products we build from our edges). vertices[index].normal = Vector3.Normalize( Vector3.Cross(edge2, edge1) + Vector3.Cross(edge4, edge3) + Vector3.Cross(edge3, edge5)); // Step 4: Set tangent data, just use edge1 vertices[index].tangent = Vector3.Normalize(edge1); // Step 5: Set texture coordinates, use full 0.0f to 1.0f range! vertices[index].uv = new Vector2( y / (float)(GridHeight - 1), x / (float)(GridWidth - 1)); } // for for (int) 你可以看到此代码通过五个步骤生成顶点。首先,计算位置矢量。然后,计算所有边缘向量,从而从三个叉积中构造出法线并分配切线。最后,纹理坐标被分配,但你倒装了x和y 使以后的xy渲染更容易,但仍需正确地对齐纹理贴图使它看起来像一张位图。顶点列表已在定义时产生,因为只支持257×257高程图网格,该CalcLandscapePos辅助方法很简单,只是从高程图数据中提取高度向量: private Vector3 CalcLandscapePos(int x, int y, byte[] heights) { // Make sure we stay on the valid map data int mapX = x < 0 ? 0 : x >= GridWidth ? GridWidth - 1 : x; int mapY = y < 0 ? 0 : y >= GridHeight ? GridHeight - 1 : y; float heightPercent = heights[mapX+mapY*GridWidth] / 255.0f; return new Vector3( x * MapWidthFactor, y * MapHeightFactor, heightPercent * MapZScale); } // CalcLandscapePos(x, y, texData) 所有的顶点和索引生成后现在终于可以在LandscapeNormalMapping.fx的帮助下渲染场景了。你会不相信这有多容易。下面的代码渲染130000的多边形,同时还包括使用了diffuse贴图,法线贴图,以及额外的细节贴图的LandscapeNormalMapping shader: // Render landscape (pretty easy with all the data we got here) ShaderEffect.landscapeNormalMapping.Render( mat, "DiffuseWithDetail20", delegate { BaseGame.Device.VertexDeclaration = TangentVertex.VertexDeclaration; BaseGame.Device.Vertices[0].SetSource(vertexBuffer, 0, TangentVertex.SizeInBytes); BaseGame.Device.Indices = indexBuffer; BaseGame.Device.DrawIndexedPrimitives( PrimitiveType.TriangleList, 0, 0, GridWidth * GridHeight, 0, (GridWidth - 1) * (GridHeight - 1) * 2); }); 来自于第7章的ShaderEffect类让你可以通过使用RenderDelegate代码的特别技术渲染场景材质。 Landscape类中的Render方法还渲染赛道和所有的场景对象。赛道渲染将在本章的其余部分讨论。我们不会谈论所有游戏中用到的模型,因为太多。看一下Model类的TestRenderModels单元测试可以看到全部,可在图12-9中快速浏览一下。 图 12-9 优化技巧 处理场景引擎不容易。即使你已经制作出了一个很棒的场景引擎,支持许多不同的shader和纹理集,还是要担心性能。另一方面,如果你已经拥有一个性能很好的场景引擎,比如来自本章的赛车游戏或上一章的射击游戏,仍可能还想要改善视觉质量而又不影响它的性能。让场景引擎能适合你的游戏是一个棘手的挑战。正如我以前告诉你的,以前我试图创造一个更加复杂的场景和图形引擎,可以创造出比最终用到的大100倍的巨大场景,但在进行了优化后,从来没有利用到这些功能。 相反你应把重点放在游戏做的事情上。以赛车游戏为例,一个固定的场景很容易实现,它始终是相同的。所以测试比较容易,你可以为其他关卡重复使用现有的部分场景和物体而无需重新设计一切。对许多不同的轨道来说场景还不够大(4096×4096米),因为第一关只向你显示了一小部分场景(大概20%~30%)。 本章和下一章使用的场景渲染技术有三个缺点: 你不能随意改变场景大小,这将涉及大量的工作。如果你已经使用4096× 4096纹理,那么可能无法再进进一步提高纹理质量了,这时如果非常接近地面,即使使用了额外的细节纹理,看起来仍会非常模糊。 改变diffuse纹理贴图很难。你要自己组合不同类型的纹理,但还是很难看到结果并仍要涉及很多测试。更糟的是,纹理越大,开发时间越长。 场景引擎还不是足够强大到能处理高级效果,比如在地上添加陨石坑或增加额外的纹理(轨道,公路,树叶等等),你甚至不能动态地改变外观。这意味着场景是一成不变的。如果想建立一个关卡编辑器,你还需要一个更加灵活的解决方案,允许你更改纹理种类并自动将其组合在一起。 绕过这些问题的最好办法是使用一种叫做splatting的场景渲染技术,这种技术使用了一系列的纹理,并把渲染到一个使用与高程图相同分辨率的纹理贴图。因为把不同的纹理并排在地面上看起来不是非常好,你需要进行插值。你可以减弱纹理块,但这样做看起来会太粗糙,或者你也可以为每个纹理类型保存百分比值。 场景与地面纹理是分开渲染的,你应该让最低的一个完全不透明,以确保你不会看到场景背后,而地面纹理是带alpha混合的(见图12-10)。 图 12-10 你也可以使用一个alpha混合纹理或只是通过色彩值混合(见图12-10的例子)。由于在XNA中使用shader,你可以将四个(pixal shader 1.1)或八个(pixel shader 2.0)的纹理合并在一个shader pass中去优化性能。如果你的场景引擎需要处理更多的纹理,你将需要多个pass直到一切东西都被正确渲染。在不渲染所有的而只渲染可见的那一个纹理有时可以更快。当你生成顶点和索引缓冲时将变得更难,但如果你有很多的纹理,如20或30个不同的地面纹理,性能将大大增加,因为每个地面纹理类型只使用了10%或更低,一直渲染是无意义的。 无论你选择何种技术,我强烈建议你先从简单的着手,比如渲染只有一个纹理平铺于上的大场景,然后加以改进。你可能还希望在添加了所有场景模型、3D物体、效果后,游戏仍能达到几百帧每秒的速度。 创造良好的场景还有更多的技巧和窍门。你可以实现预先计算的照明和阴影,也有很多机会使用更棒的shader,尤其是场景中包括水面时。草地可通过fur shader实现。岩石和峭壁的外观可通过parallax映射甚至offset映射加强效果。如果你对这些专题感兴趣可参见关于shader的书籍,比如Game Programming Gems和ShaderX,或通过互联网搜索技巧、窍门和教程。 图12-11显示了在使用post-screen shaders、HUD以及天空盒映射(如果你忘了,可见第5和第6章详细了解这些类型的shaders)后,最终的场景表现。 图 12-11

XNA 3.0初步——自定义游戏循环时间

clock 九月 20, 2010 08:39 by author alex
1.6 自定义游戏循环时间 问题 你想改变调用Update和Draw方法的默认时间间隔。 解决方案 Update 方法默认是每秒钟更新60次,而Draw方法则没有限制,但最大为屏幕刷新率。通过改变Game类的TargetElapsedTime和IsFixedTimeStep静态属性和GraphicsDevice类的SynchronizeWithVerticalRetrace的属性,你就可以改变默认的行为。 工作原理 改变Update频率 默认情况下Update每秒更新60次,或者说每隔16.667毫秒更新一次。你可以通过改变TargetElapsedTime变量改变更新频率: this.TargetElapsedTime = TimeSpan.FromSeconds(1.0f / 100.0f); 当调用到这行代码时,XNA会以每秒100次的频率调用Update方法。你也可以让XNA不按固定时间间隔调用Update方法,要做到这点,需要将Is FixedTimeStep变量设置为false: this.IsFixedTimeStep = false; 使用IsRunningSlowly 你可以指定Update的更新频率,但如果频率太高,XNA将无法达到,在这种情况下,gameTime. IsRunningSlowly 变量会被设置为true: Window.Title = gameTime.IsRunningSlowly.ToString(); 注意:你应该将gameTime参数传递到Update方法而不是Draw方法去判断gameTime. IsRunningSlowly。 改变Draw频率 当运行程序时,XNA会以尽可能大的频率调用Draw方法,只由下面两个规则限制: 没有必要将调用Draw的频率大于屏幕刷新频率。如果屏幕刷新频率只有每秒100次,每秒绘制110次是无用的。在PC和Xbox360平台上,,屏幕刷新率是由PC屏幕和它的设置决定的,Zune 30每秒刷新60次,其他Zune设备每秒刷新30次。 Update方法每秒调用60次。如果游戏计算量太大,Draw方法调用次数会变少以保证Update方法可以每秒调用60次。 在某些情况中,以最大频率调用Draw方法是有用的,例如当决定你的游戏的最大帧频率时,你可以将graphics.SynchronizeWithVerticalRetrace变量设置为true: graphics.SynchronizeWithVerticalRetrace = false; 注意:你必须在Game1的构造函数顶部加入这行代码,因为XNA需要在创建GraphicsDevice 之前知道这个设置。 理解Update和Draw方法调用频率的重要性 因为你将更新逻辑放置在Update方法中,Update频率的减少会导致游戏中的所有物体变慢,这非常令人讨厌。 当Draw调用频率小于屏幕刷新频率时,只有游戏的视觉表现会暂时受影响,游戏帧频率暂时由每秒100帧降到80帧不容易察觉。 因此,如前所述,如果必要,XNA会降低Draw的频率以保证Update能以每秒60帧的频率调用。 /*760*60,文章内页广告*/ var cpro_id = 'u243521';

XNA 3.0初步——将你的XNA 3.0游戏部署到另一台PC

clock 九月 15, 2010 14:55 by author alex
问题 你已经完成了自己的游戏,想把它在另一台电脑上展示给你的朋友。但是当你双击. exe文件时,会遇到错误。 解决方案 将游戏发布到不同的PC比起2.0版本来说变得简单了。截止到本书编写的时间,你需要确保另一台机器上安装了两个独立的安装包才能安全地运行你的游戏程序。 XNA Framework Redistributable 3.0 .NET 3.5 Framework SP1 好消息是XNA现在支持Visual Studio的OnClick技术,让你可以创建一个单独的.exe文件,首先检查这些安装包是否已经安装,如果没有则首先安装这些安装包。但是,如果你想使用XNA 3.0的网络功能(见第八章),你仍需安装完整的XNA Game Studio 3.0安装包。 工作原理 安装XNA 3.0 Framework文件 第一个方法是在PC上安装这两个安装包和你的游戏的二进制文件,这可以通过下载并安装XNA 3.0 Framework Redistributable 3.0做到, 它可以在微软网站http://www.microsoft.com上搜索到。这个安装包非常小,包含了所有XNA 3.0 Framework的基本文件。 安装.NET 3.5 Framework文件 XNA是DirectX的托管代码包装。因为它使用的是一个托管.NET语言(C#),在系统上还需安装.NET Framework 3.5文件,它也可以从微软网站上搜索到。 复制编译过的文件 在编译并测试了你的游戏后,进入可执行文件夹,这可以是bin\x86\Debug和bin\x64\Release,确保将此文件夹和其子文件夹中的所有文件复制到目标PC。如果你已经安装了上述两个安装包,在双击.exe文件开始游戏时就不会遇到任何错误信息。 创建一个单独的ClickOnce安装包包含所有的先决条件 XNA 3.0新增了ClickOnce安装让你可以创建一个.exe安装文件,它可以安装这两个先决条件。创建安装文件的步骤如下: 在Solution Explorer中,右击项目选择Properties。 在打开的窗口中,选择左下方的Pubilsh。 点击下一个屏幕中的Prerequisites。 你可以看到可以被打包到你的游戏中的所有Microsoft安装包的列表,确保选择.NET Frameword 3.5(SP1)和XNA Framework Redistributable 3.0。 最重要的是,选择”Download prereueisties from the same location as my application”。这会将安装包复制到项目发布的文件夹中。 点击OK并关闭项目的属性窗口。 做完这一切后,你要是想发布你的项目,只需右击项目,选择Publish并点击弹出的对话框中的Finish即可。 注意:不幸的是,目前在Visual Studio中有一个bug,通过这种方式发布项目会导致一些问题,希望在下一次升级或补丁包中能加以解决。所以,现在在第4步中不要选择.NET Framework 3.5(SP1),而是将它作为添加的安装文件。 为游戏创建一个Setup项目 使用这个更为先进的方法,你可以创建一个有更多自定义选项的安装程序,它可以自动检测两个先决条件的安装包,如有必要,会自动安装它们。 注意:这个方法没有刚才在ClickOnce方法中的bug。 下面是创建Setup项目的步骤: 打开XNA Game Studio 3.0。 打开File菜单,选择New→Project。 在左侧的树状目录中,选择Other Project Types→Setup and Deployment。 在右方确认选择了Setup Project。 给项目起一个名称,例如[name]。在安装过程中,这个名称会显示在”Welcome to the [name] Setup Wizard.”中。 点击OK开始一个新的setup项目。 在屏幕左侧找到Application Folders。 在资源管理器中,找到XNA游戏的输出目录,它包含了游戏的.exe文件。选择所有文件及其子文件夹(例如Content)并将它们拖动到第7步中的Application Folders中。 返回XNA Game Studio 3.0,打开Project菜单选择“[name] properties”。 点击Prerequesites按钮。 确保选择Windows 3.1,.NET Frameword 3.5(SP1)和XNA Framework Redistributable 3.0。 在对话框底部,选择从“my application”相同的位置“Download Prerequesites”。 点击OK返回setup项目。 点击Application Folders。 在屏幕右下方的属性对话框中,从目标目录中移除[Manufacturer]。 现在,按下F6键生成setup项目解决方案。 这个程序会生成包含所有先决条件和setup.exe的文件夹,setup.exe会安装在另一台机器上运行XNA游戏所需的东西。

XNA 3.0初步——将你的XNA 3.0游戏部署到Zune

clock 九月 15, 2010 14:54 by author alex
问题 在PC上创建并测试了2D游戏后,你想把它上传到Zune中并运行。 解决方案 XNA 3.0一个主要的新功能就是支持Zune上的2D游戏。显然因为Zune上的显卡无法与你PC上的相比,因此在Zune无法运行XNA 3D程序。但是,完整的SpriteBatch在Zune上是可用的,让你可以创建移动设备的2D游戏。 工作原理 在Xbox360控制台上,你无需对代码进行任何调整就可以将它运行在Zune上,只需将Zune连接到PC,将它设置为激活目标并部署你的游戏即可。 将Zune连接到PC 使用USB线将Zune和PC连接,打开电源。然后点击开始菜单选择程序→Microsoft XNA Game Studio 3.0→XNA Game Studio Device Center打开XNA Game Studio Device Center,点击Add Device按钮,选择Zune。 如果你还没有进行过这个操作,可能会被要求根据下面的链接下载Zune软件的更新版本。如果你已经安装了这个软件,确保Zune的固件已经被更新。这可以通过连接Zune并打开PC上的Zune软件做到。如果PC已经连接到Interne并找到了固件的新版本,Zune软件会显示Device Update屏幕,让你可以通过点击Install按钮(当升级固件时不要关闭电源)升级Zune的固件。 安装了Zune软件、升级了Zune固件并连接了Zune后,当你点击XNA Game Studio Device Center的Zune按钮后Zune就会监听你的屏幕了。选择你的Zune,点击Next,当验证了连接后,你的Zune就会监听XNA Game Studio Device Center的主屏幕。 在XNA Game Studio 3.0中将Zune设置为目标平台 连接了Zune后,进入XNA Game Studio 3.0将2D游戏部署到Zune。在Xbox360控制台上,当你第一次部署Zune时,你需要将它设置为部署目标。这可以通过点击屏幕右上方的Solution Platforms选择Configuation Manager做到,如图1-4所示。 在接下来的对话框中,从屏幕左上方的配置列表中选择,在接下来的屏幕的顶部列表中选择你的Zune,点击了OK后,你可以看到你的Zune已经在顶部中央的窗口中被设置为了目标平台。 将你的XNA 3.0游戏部署到Zune 最后,你需要将XNA PC项目复制到XNA Zune项目中。非常简单,在解决方案资源管理器中右击你的项目选择Create Copy of Project for Zune。这个选项显示在图1-3的列表中。XNA会将这个项目添加到解决方案资源管理器中,只要两者之一有变动,XNA会同步这两个项目。 要将游戏上载到Zune,请确保Zune作为激活的目标平台,并且此时Zune软件没有运行在PC上,然后按下F5初始化部署。 一旦Zune屏幕显示部署已经完成,重启你的Zune,在Zune主菜单中选择Games,就可以开始你的游戏了!

XNA 3.0初步——将你的XNA3.0游戏部署到Xbox 360

clock 九月 15, 2010 14:08 by author alex
问题 在PC上创建和测试了代码后,你想将游戏上传并运行在Xbox 360上。 解决方案 XNA最好的功能之一是可以让代码运行在PC和Xbox 360上而无需任何改动。但在将游戏上传到Xbox 360之前需要进行一些准备工作。 首先,需要一个Xbox Live账号,这可以通过http://creators.xna.com或在Xbox 360上免费创建。然后需要一个Creators Club许可,对大多数学生是免费的,也可以通过Xbox Live Marketplace购买。这个许可四个月花费49美元或一年99美元。 然后需要下载并安装XNA Game Studio Connect, 一个前端段程序用来监听来自于PC的连接。 最后,在PC和Xbox360之间还需要一个LAN连接,Xbox 360需要连接到Internet。PC和Xbox 360还需进行配对,否则如果在网络上有多个Xbox360时会遇到麻烦。 如果满足了以上四个条件,你就可以上载并在Xbox360上运行程序了。 工作原理 设置Xbox Live账号 登录Silver Xbox Live账号是免费的,如果你想让自己的代码运行在Xbox360上账号也是必须的。如果你已经有了一台Xbox360,你可能已经有了一个Live账号了。如果没有,那么打开Xbox 360,插入一张游戏光盘,根据屏幕上的指示操作。 获取Creators Club License 如果你是一名学生,有可能从Microsoft DreamSpark program获取一个免费的授权。你可以从http://downloads.channel8.msdn.com访问到Microsoft DreamSpark。用你的学生证登录获取一个号码,这可以通过进入XBox360的Marketplace 选项卡并选择“Redeem code.”做到。 否则,你可以简单地将Xbox360连接到Xbox Live Marketplace导航到Games→All Game Downloads。在列表中,找到XNA Creators Club并选择它。 然后选择Memberships,你可以购买四个月或一年的授权,或者,你也可以输入Creators Club voucher卡上的号码。 在Xbox 360上安装XNA Game Studio Connect 这个程序让Xbox 360可以监听来自于PC的连接,你可以通过Xbox Live Marketplace,在Game Store→More→Genres→Other中免费下载,安装后就可以打开这个程序了。 连接Xbox 360和PC 在PC可以将数据传输到Xbox 360前,这两个设备需要通过LAN或Internet进行连接,如果Xbox 360和PC都连接到一个路由器/交换器/hub,就能工作正常。 近来,越来越多的家庭网络使用无线连接,这会产生一个问题,因为Xbox 360默认没有安装无线适配器。一个解决方法是拥有一台同时具有无线和有线(Ethernet)网络的PC,现在的大多数笔记本电脑都符合这个条件。在家中用PC连接无线网络,用网线连接Xbox 360和PC。最后,在PC上点击Start按钮,并导航到Settings→Network Connections。同时选择无线和有线适配器,右击选择Bridge Connections(桥接)。如图1-2所示。等几分钟,两个机器就会连接到Internet和互相连接! 图1-2 桥接PC上的两个网络适配器 配对PC和Xbox 360 如果网络中有多台Xbox 360,你需要指定在哪台Xbox 360上上传你的代码。在Xbox 360上找到Game选项卡选择Games Library→My Games→XNA Game Studio Connect,可以开始XNA Game Studio连接。如果这是你第一次打开连接,会要求填写Xbox360的序列号,一组5*5字符串。 在PC上点击Start按钮,导航到Programs→Microsoft XNA Game Studio 3.0→XNA Game Studio Device Center,点击Add Device键,给你的Xbox 360主机起一个名称。然后,要求输入Xbox360的序列号。如果Xbox 360和PC都通过网络连接,配对就会成功,主机就会显示在设备列表中。如果PC连接了多个Xbox30主机,那么绿色的符号表示当前活动的Xbox 360。 从已有的XNA 3.0项目生成Xbox 360项目 在XNA Game Studio 3.0中,很容易将PC游戏转换为Xbox 360项目。只需打开项目,找到Solution Explorer,右击项目名称选择Create Copy of Project for Xbox 360,如图1-3所示。 图1-3 生成Xbox 360项目 这会导致创建第二个项目,原始项目的所有文件都会被项目引用,而不是复制,因此在一个项目中作出的改变也会反映到另一个项目中。 某些情况中,你可能还需要添加一些向导忘记复制的引用,但无论如何,这个向导会节省你许多时间。 现在,你可以在屏幕顶部选择想让你的项目运行在哪个平台上,如果是第一次在Xbox360平台上运行项目,你需要通过选择“Configuration Manager”添加一个Xbox 360 profile,如图1-4所示。 图1-4 选择部署目标 在弹出的对话框中,点击“Active solution platform”列表并选择。然后,选择Xbox360,关闭所有对话框。 现在,图1-4中的列表中就包含了Xbox360的选项,当想部署到Xbox360平台时就可以选择它。确保Xbox正在运行XNA Game Studio Connect并在等待连接。当按下F5时,文件就会被上传并在Xbox360上执行。

XNA 3.0初步——开始第一个XNA 3.0项目

clock 九月 15, 2010 11:03 by author alex
问题 你想编写一个新的XNA 3.0游戏。此外,默认的初始代码已经包含了一些方法,你想知道这些方法是做什么用的。 解决方案 打开一个新项目和大多数Windows程序是相同的。在XNA Game Studio 3.0中,打开File菜单,选择New→Project。 工作原理 打开XNA Game Studio 3.0 通过点击Start按钮并选择Programs打开XNA Game Studio 3.0,找到Microsoft XNA Game Studio 3.0,点击,选择Microsoft Visual Studio 2008 (或Microsoft Visual C# 2008 Express Edition,如果你安装的是免费版本)。 打开一个新XNA 3.0项目在XNA Game Studio 3.0中,打开File菜单并选择New→Project。在左边的列表中,Visual C#下面的XNA Game Studio 3.0默认是高亮的,如图1-1所示。在右边,选择Windows Game (3.0),给你的项目起一个好听的名称,点击OK按键。 图1-1 打开一个新XNA 3.0项目(Visual Studio 2008版本) 预定义方法 当开始一个新项目时,你发现代码文件中已经包含了一些代码。注释(显示为绿色)占据了50%的代码帮助你开始编程。等会你就会发现这些方法非常有用,因为它们节省了你处理基本内容的时间。例如,当你运行这个程序时,会获得一个空的窗口,这意味着你无需将时间浪费在编写窗口或处理窗口信息队列上。 下面讨论预定义的方法。 Game1构造函数 Game1方法只在运行项目的一开始调用一次。这意味着当这个方法(构造函数)被调用时内部组件还没有被初始化。添加在这里的唯一代码应该是GameComponent类的实例化(见教程1-6),这里你无法访问任何资源(诸如GraphicsDevice类),因为它们还没有被初始化。 Initialize方法 Initialize方法也只被调用一次,在所有内部初始化完成之后。这个方法可以放置初始化变量,诸如游戏中的物体的初始位置和初始速度等,你可以访问游戏对象的所有资源了。 Update方法 当运行程序时,XNA会以每秒60次的频率调用Update方法(或者说每隔0.0167秒)。更多的知识可参见教程1-6。所以Update方法中可以放置更新游戏逻辑的代码,可以是更新物体的位置,碰撞检测,在一个位置开始爆炸,增加得分等。还有,处理用户输入和更新相机/模型的矩阵也应该在这个方法中进行。 Draw方法 在这个方法中,你应该放置绘制场景的代码。它绘制所有的2D图像,3D物体,爆炸和显示当前得分等。默认情况下,Draw方法的调用频率与屏幕刷新率相同,这取决于屏幕或Zune设备,更多内容可参见教程1-6。 LoadContent方法 在游戏中你总要从磁盘加载素材(诸如图像,模型和声音等) 。要加速这个过程并拥有灵活性,XNA通过内容管道管理这个素材。应该在LoadContent方法中加载素材,这个方法在项目开始只被调用一次。如何将一张2D图像加载到XNA项目中的详细例子可参见教程2-1,其他类型的素材加载方法是相同的。 UnloadContent方法 如果游戏中使用的对象需要清除,理想的放置位置是在UnloadContent方法中,这个方法在游戏退出前被调用一次。 添加一个.fx HLSL文件 如果你想更深入一步在项目中添加一个HLSL文件,只需在 Solution Explorer找到Content,右击并选择Add→New Item,选择“Effect file,”并起一个名称。你会获得一些默认代码,可以用这本书中的其他代码扩展或替换这些代码。之后需要像其他素材一样导入这个文件:通过创建一个对应的变量并将这个文件连接到这个变量。将这个变量添加到Game类的顶部: Effect myEffect; 然后在LoadContent 方法中将这个变量连接到代码文件上: protected override void LoadContent() { myEffect = Content.Load<Effect>("effectFile"); } 注意:你必须将素材的名称改成你的HLSL文件的名称,本例中是effectFile。

创建场景和赛道——游戏比较

clock 九月 14, 2010 10:59 by author alex
游戏比较 赛车游戏大致可以分成三大类。有时,游戏也被分为更多的类别。你当然可以想到更多的类别,如果你添加的各种类似的赛车游戏如卡丁车游戏,卡车游戏,摩托车比赛,未来赛车游戏等等,你可以创造更多的类别,但我想保持简单,所以只讨论经典的赛车游戏: 真实赛车:如GT赛车,Colin McRae Rally,Test Drive和Formula One都是真实赛车的例子。这些游戏都是高度仿真的,给玩家留下真的在驾驶赛车的印象。在展会上这些游戏往往和方向盘控制器一起展示,有时在舞台上还有显示器,特殊的赛车座椅,或多台显示器或其他设备以加强真实度。这些游戏的基本规则是挑战最快圈速,通过可选多种赛车或增加游戏模式使游戏规则变得更复杂。 街头赛车:如Need for Speed,Midnight Club,Street Racing或Juiced这些游戏在过去几年中变得很流行。游戏的基本规则仍然是相同的,但拟真度不再重要。它更像是在看电影,一切东西都要酷,这样的游戏需要改装过的汽车和很酷的音乐。它们很受欢迎,尤其是在游戏机平台上。 趣味赛车:如赛道狂飙,Wipeout,Carmageddon那些不再把重点放在真实度上的游戏属于这一类。它们都拥有有趣和疯狂的轨道或改装车,可以射击或做其他疯狂的事情。你甚至可以把所有疯狂的赛车游戏如Crazy Taxi或侠盗车手也归于这一类别中,这其实已不是真正的赛车游戏了,你只是驱车逛游。任务不再是光驾驶汽车,而是完成某些特定的目标。你也不能仅仅因为它是3D的而把一个冒险游戏称之为3D射击游戏,但有些射击游戏也有冒险成分。 一些如哥谭赛车的游戏可归于不只一个类别。它们比街头赛车更真实,但也没有像GT赛车或Formula One那么高的拟真度,因为你还可以改装你的赛车并驱车穿越街道。极品飞车也不再是高拟真的了,不管你撞击到墙上的速度有多快你也无法撞毁你的车,你可以超越其他车辆,逃避警察的追捕,破坏公共财产等,这个游戏变得更像街头赛车。 下面我们浏览一下这些类别的代表游戏。 GT赛车 最真实的赛车游戏是PlayStation平台上的GT赛车,Formula One,Enthusia Professional Racing,科林麦克雷拉力赛等。大多数游戏有PC平台的版本,有时也有Xbox的版本。在Xbox平台上街头赛车和趣味赛车比PlayStation平台更多。 总之,我不太了解这些游戏,我通常在完成第一圈前就睡着了。这种游戏最好去问我兄弟。我放了一张GT赛车的漂亮图片(见图12-2)。 图 12-2 Xbox 360平台上此种类别的最流行的游戏可能是哥谭赛车,归功于更多的街头赛车视角,这个游戏还是有点乐趣的,但即使这样我也没玩多久,几个任务后就有点重复了。但我必须说,游戏做得很好,我希望其他经典的赛车游戏也能包含点街头赛车的风格,显然,有一些额外的游戏模式比只是开车兜圈子更好玩。 极品飞车 极品飞车是由EA Sports出品的一个非常流行的游戏,有许多版本,多到我也无法完全搞清楚数目。最近的两个游戏是极品飞车地下狂飙(2005年,见图12-3)和极品飞车卡本峡谷(2006年)。通常所有的极品飞车游戏出现在所有最新的主机平台和PC上,这使得在每个平台销量有所下降,但所有平台的销售总和是很可观的。 图 12-3 这种游戏有很漂亮的图像和精细的背景。另外,还有一些额外的游戏模式,漂移过弯和实现特殊技巧的奖励分。你还可以改装你的车,喷上油漆或贴膜等,在车辆改装画面中可以实现这个功能。 赛道狂飙 最后是趣味赛车,我选择赛道狂飙作为代表。我没选择疯狂赛车游戏,因为它们往往不是一个单纯的赛车游戏,更像是一个射击和赛车游戏的混合物。 在赛道狂飙(见图12-4 )中你必须到达终点,途中有几个检查点,你可以看到你的当前时间,你上次通过这个检查点是否进步。不是很容易到达终点,有时完成赛道只要几秒钟,你不断地想超越自己的最好成绩或在多人游戏模式中你的对手的成绩。你经常获得铜牌,但你也想在这条赛道上获得银牌或金牌,这必须要比以前快1-2秒才行。一会儿后你就就会在同一赛道上玩上30遍只是为了快上几秒钟。这个游戏很容易上瘾,是此种类型的一个非常成功的游戏。 图 12-4 你不会看到其他游戏有这么多的循环和机会在关卡中飞跃。关卡各不相同,你可以花几分钟的时间去测试一个赛道,或者几个小时完成所有的单人赛道。 XNA赛车游戏 让我们谈谈你的赛车游戏的设计方向。我更喜欢趣味赛车,因为像在Test Drive游戏中驾驶汽车总是让我觉得枯燥。但是,许多人喜欢真实赛车游戏。我的弟弟是真实赛车和街头赛车的爱好者,否则我将完全不了解这种类型的游戏,除非是在杂志或游戏展览会上看到。 总之,我想开发趣味赛车,因为制作拟真图形真的很难。混合一点街头赛车风格不错,能使游戏的外观和声音更酷。最近我最喜欢的游戏规则是赛道狂飙,只是让你疯狂地驾车通过圆轨道,坡道,洞等等。这个游戏的规则是尽快地到达终点,但我喜欢变得越来越难。慢慢驾驶无法让你通过任何圆轨道或某些斜坡,如果你从赛道上掉下来就必须从头开始。它更像是一个驾车转圈圈的街机游戏。 最近发布的赛道狂飙仍有很漂亮的图形。它们可能不像这里提到的其他游戏那么真实,但作为一个趣味赛车这并不那么重要。有些技术仍然非常好,赛道狂飙解决了许多困难,如处理这么多的道路片断,在关卡中看得更远。在如极品飞车或哥谭赛车游戏中你很难看到现在的道路片断。 你要面临着同样的问题,因此,我要确保场景和赛道渲染尽可能地快,如果达到了帧的上限,可以忽略一些以达到优化的目的。

创建场景和赛道——概览

clock 九月 14, 2010 10:56 by author alex
概览 要制作本书的最后一个大游戏——XNA Racing,你必须首先在下一章学习一些高级议题。. 本章涵盖了场景生成和赛道的渲染技术,下一章深入研究赛车游戏需要的物理引擎,最后一章你将学习游戏屏幕,游戏逻辑、优化赛车游戏和游戏开发中的一些窍门。本书介绍的这个游戏是一个完整的赛车游戏,但它只能展示了一个赛道并简化了游戏规则。完整的XNA赛车游戏是由微软作为XNA框架的一个starter kit(入门套件)提供的。完整的游戏涵盖更多的任务,更酷的功能,更多的可选赛车、更酷的3D物体,以及高达10个视频教程,你可以下载并观看以了解更多的知识。这本书覆盖了所有的基本知识,让你创造一个很酷的游戏,但我想不仅仅涵盖一个游戏(也许你不太喜欢玩赛车游戏,为此即使你不想要做一个赛车游戏本章和下一章的内容仍很有趣),而且一本书也太短,不能覆盖到赛车游戏的方方面面。 在你继续设计你的小小的赛车游戏前,你应该想一想这个游戏应该能干什么。由于有许多赛车游戏可供参考,尤其是在游戏机平台,不是很难找出这些游戏的需求。大多数赛车游戏有看上去很棒的汽车并显示道路周围的景色。游戏本身往往非常简单,但游戏设计者会花大力气优化控制器输入并增加小功能,使赛车游戏更有趣。 在你确定你的游戏的大致走向后,你将考虑所需的三维模型,纹理,效果和诸如场景和赛道之类的其他数据,这对你的游戏非常重要。我花了一半以上的开发时间,只是让场景和赛道渲染正确。你会在接下来的一章中看到为什么要做这么多的工作,为什么场景和赛道渲染需要一些新的shader。 当我说:“很多工作,”只是相对而言。请记住,这个赛车游戏是在几星期内完成的,和Rocket Commander一样是个好游戏,当然你不能拿它与那些AAA级游戏比较,毕竟那些游戏需要几年时间和很多人开发,特别是大型游戏公司开发的游戏机平台的游戏,开发人员往往有100人以上。而我只是一个人,我想已经做得够好得了。 像在七十年代或八十年代,一个人可以在几天或几周时间内完成编写、设计和测试他的游戏,这种时代已经结束了,(见图12-1)。但别急,有了XNA,在短时间内制作出你的游戏再次变得可能。如果你想想,过去游戏的编程环境并没有现在这样友好。只为了获得屏幕上的一个像素,也需要你自己实现方法。更别提还要编写很多汇编代码只是为了能在一个简单的硬件配置上正常工作。 图 12-1

创建XNA Shooter游戏——总结

clock 九月 8, 2010 09:37 by author alex
  总结 在这一章中你学到了有关Xna Shooter的所有知识。不仅介绍了3D特效,Billboard,简单场景渲染,用新的游戏类处理游戏逻辑,也为接着要遇到的更先进的赛车游戏做好了准备。 你的图形引擎现在能够渲染纹理,三维模型,三维特效,shader,通过XACT处理声音效果,并处理用户输入,所有游戏类也更简单,你也非常熟悉游戏类了。Player类处理大部分游戏逻辑,这已在Rocket Commander和Xna Shooter中证明很有效。阴影映射在这章中第一次被提及,下一章你会更详细地学习这些高级议题。 本书的第四部分将学习一个很酷的赛车游戏。你要创建一个漂亮的三维场景,并通过曲线数据产生赛道。你还要学习物理效果,这在Rocket Commander中已经处理过一点了,但赛车游戏需要更为复杂的模型。最后,你将了解赛车游戏的游戏逻辑,以及如何调整这么大的一个项目。

创建XNA Shooter游戏——挑战:用引擎编写自己的游戏

clock 九月 8, 2010 09:36 by author alex
挑战:用引擎编写自己的游戏 本书之前已经讨论了很多游戏,在你开始最后一个游戏之前我建议你使用游戏引擎创建自己的小游戏。本章你看到了所有的重要步骤,从在Rocket Commander引擎的基础上创建Xna Shooter引擎,而且更强大(除非您想要做一个空间游戏)。或许你已经有了一些很棒的游戏创意,如果你还没有开始,不要等待,开始实现它们吧。 为Xna Shooter实现额外的辅助类花了我一半以上的时间,加入阴影映射必须不断调整才能在游戏中显示正常。游戏编码本身有趣,一遍又一遍试玩也很有趣,尤其是当所有特效和地方单位被正确实现后。 如果你有一个伟大的游戏构思,但不是一些简单的的街机游戏,你应该首先尝试建立一个原型,因为做一个像魔兽世界一样的MMORPG游戏(大型多人在线角色扮演游戏)是不可能的,你可能要花费几年才意识到你永远不可能做出这么大的游戏。给初学者和游戏程序员的第一秘诀是:从一个小游戏开始。这并不意味着如果你想创造一个RPG游戏(角色扮演游戏),你无需首先制作俄罗斯方块或Jump And Run游戏,但尝试先实现一个小部分(比如让你的角色四处走动并干掉简单的怪兽)并由此起步。 今天制作游戏并不容易,但借助于XNA和网上的教程,这已变得容易多了,但将所有东西整合在一起往往被低估。试着让你的第一个游戏正常工作(包括用户界面,输入,菜单和一些简单的游戏逻辑),然后再考虑你下一个大项目,而不是相反。这也就是一半以上的网上游戏项目被取消的原因之一,它们往往半途而废。 总之,祝你的游戏项目好运。请随时与我联系,并告诉我你的成果,尤其是它建立在我的游戏引擎之上时。

创建XNA Shooter游戏——XNA Shooter游戏

clock 九月 8, 2010 09:26 by author alex
  XNA Shooter游戏 现在可以创建XNA Shooter游戏了。你有了所有的三维模型,所有的效果文件和纹理和声音效果文件,也不必担心场景,因为它工作得很好场景本身是在表面之下的(即z值小于0),这意味着只需将物体放置在z高度为0的地方,这样能使添加特效,碰撞检查和测试变得更轻松。 现在,你可以通过Misson类的场景渲染方法添加自己的飞船并在Player类中控制它。渲染只需以下代码: Player.shipPos = new Vector3(Player.position, AllShipsZHeight) + levelVector; AddModelToRender( shipModels[(int)ShipModelTypes.OwnShip], Matrix.CreateScale(ShipModelSize[(int)ShipModelTypes.OwnShip]) * Matrix.CreateRotationZ(MathHelper.Pi) * Matrix.CreateRotationX(Player.shipRotation.Y) * Matrix.CreateRotationY(Player.shipRotation.X) * Matrix.CreateTranslation(Player.shipPos)); // Add smoke effects for our ship EffectManager.AddRocketOrShipFlareAndSmoke( Player.shipPos + new Vector3(-0.3f, -2.65f, +0.35f), 1.35f, 5 * Player.MovementSpeedPerSecond); EffectManager.AddRocketOrShipFlareAndSmoke( Player.shipPos + new Vector3(0.3f, -2.65f, +0.35f), 1.35f, 5 * Player.MovementSpeedPerSecond); 所有的缩放和旋转仅仅是让飞船有正确的大小并旋转到正确的方向。绕z轴旋转使飞船飞向前方,而绕x轴和y轴旋转让飞船在前进后退或左右平移是扭动船身。然后,在飞船引擎后部添加火焰和烟雾特效。EffectManager类马上就会讨论到。 游戏逻辑 控制飞船的代码在Player类中,大多数游戏逻辑都在那处理,包括武器射击,移动飞船,得分以及处理道具: // From Player.HandleGameLogic: // [Show victory/defeat messages if game is over] // Increase game time gameTimeMs += BaseGame.ElapsedTimeThisFrameInMs; // Control our ship position with the keyboard or gamepad. // Use keyboard cursor keys and the left thumb stick. The // right hand is used for fireing (ctrl, space, a, b). Vector2 lastPosition = position; Vector2 lastRotation = shipRotation; float moveFactor = mouseSensibility * MovementSpeedPerSecond * BaseGame.MoveFactorPerSecond; // Left/Right if (Input.Keyboard.IsKeyDown(moveLeftKey) || Input.Keyboard.IsKeyDown(Keys.Left) || Input.Keyboard.IsKeyDown(Keys.NumPad4) || Input.GamePad.DPad.Left == ButtonState.Pressed) { position.X -= moveFactor; } // if if (Input.Keyboard.IsKeyDown(moveRightKey) || Input.Keyboard.IsKeyDown(Keys.Right) || Input.Keyboard.IsKeyDown(Keys.NumPad6) || Input.GamePad.DPad.Right == ButtonState.Pressed) { position.X += moveFactor; } // if if (Input.GamePad.ThumbSticks.Left.X != 0.0f) { position.X += Input.GamePad.ThumbSticks.Left.X;// *0.75f; } // if // Keep position in bounds if (position.X < -MaxXPosition) position.X = -MaxXPosition; if (position.X > MaxXPosition) position.X = MaxXPosition; // [Same for Down/Up changes position.Y, see Player.cs] // Calculate ship rotation based on the current movement if (lastPosition.X > position.X) shipRotation.X = -0.5f; else if (lastPosition.X < position.X) shipRotation.X = +0.5f; else shipRotation.X = 0; // [Same for shipRotation.Y, see above] // Interpolate ship rotation to be more smooth shipRotation = lastRotation * 0.95f + shipRotation * 0.05f; HandleGameLogic首先检查游戏是否结束,并在屏幕上显示一条消息告诉你如果你输或赢。然后,当前游戏的时间增加。之后,处理飞船控制,然后发射武器和道具处理,代码最后处理击落敌人后的得分。 你可以使用键盘和gamepad控制飞船。当不支持鼠标,因为对射击游戏来说调试很难,我个人也不喜欢用鼠标控制射击游戏。通过常量MaxXPosition和MaxYPosition可以确保你的飞船没有移动到屏幕之外,shipRotation是根据你的移动计算出来的,如果飞船没有运动它会慢慢恢复到零。 以下代码显示了如何发射武器。更多细节在Player类中。 // Fire? if (Input.GamePadAPressed || Input.GamePad.Triggers.Right > 0.5f || Input.Keyboard.IsKeyDown(Keys.LeftControl) || Input.Keyboard.IsKeyDown(Keys.RightControl)) { switch (currentWeapon) { case WeaponTypes.MG: // Shooting cooldown passed? if (gameTimeMs - lastShootTimeMs >= 150) { // [Shooting code goes here ...] shootNum++; lastShootTimeMs = gameTimeMs; Input.GamePadRumble(0.015f, 0.125f); Player.score += 1; } // if break; case WeaponTypes.Plasma: // [etc. all other weapons are handled here] break; } // switch } // if 当你按下gamepad的A键或右板机或键盘的Ctrl键时就输入了发射指令,但武器不会开火,直到你达到了下一个降温阶段(每件武器都有不同长短的冷却时间)。接着武器被射击并处理武器碰撞检测,lastShootTimeMs被重置为当前的游戏时间等待下一次降温过程。 处理完武器后只剩一件事,就是创建胜负条件。如果你成功到达关尾你就胜利了,而在这之前生命值用完则失败。 if (Player.health <= 0) { victory = false; EffectManager.AddExplosion(shipPos, 20); for (int num = 0; num<8; num++) EffectManager.AddFlameExplosion(shipPos+ RandomHelper.GetRandomVector3(-12, +12)); Player.SetGameOverAndUploadHighscore(); } // if 游戏逻辑的其余部分是敌人,道具和弹药,这些都各自的类中被处理,这些类会在本章后面看到。 三维效果 回到3D特效。你已经学学习了很多3D特效的知识,借助于Billboard类,在3D中渲染纹理也不难。但现在特效被提升到更高层次,你关心特效长度,动画步骤,淡入淡出。所有的3D多边形生成和渲染都是由Billboard和Texture纹理类处理的,而所有特效是通过EffectManager类管理的(见图11-15),借助于很多静态Add方法让你从代码的任何地方添加特效。 图11-15 在Effect类你可以看到许多字段,这是用来创建许多不同种类的3D特效的。爆炸效果优化不同于灯光效果。例如常规特效只是混合了一个alpha值,但灯光效果这样做无法工作,因为如果Alpha值被修改会使光线变得更暗,这样看起来非常奇怪,对于灯光特效要在最后使用更小的值才行。其他枚举帮助你快速定义效果类型并通过AddEffect方法很容易地添加它们。 了解EffectManager类最好的办法就是看TestEffects单元测试,然后添加新的效果并加以实施,或为测试更多特效编写新的单元测试。 public static void TestEffects() { TestGame.Start("TestEffects", delegate { // No initialization code necessary here }, delegate { // Press 1-0 for creating effects in center of the 3D scene if (Input.Keyboard.IsKeyDown(Keys.D1) && BaseGame.EveryMs(200)) AddMgEffect(new Vector3(-10.0f, 0, -10), new Vector3((BaseGame.TotalTimeMs % 3592) / 100.0f, 25, +100), 0, 1, true, true); if (Input.Keyboard.IsKeyDown(Keys.D2)) { AddPlasmaEffect(new Vector3(-50.0f, 0.0f, 0.0f), 0.5f, 5); AddPlasmaEffect(new Vector3(0.0f, 0.0f, 0.0f), 1.5f, 5); AddPlasmaEffect(new Vector3(50.0f, 0.0f, 0.0f), 0.0f, 5); } // if (Input.Keyboard.IsKeyDown(Keys.D2]) if (Input.Keyboard.IsKeyDown(Keys.D3)) { AddFireBallEffect(new Vector3(-50.0f, +10.0f, 0.0f), 0.0f, 10); AddFireBallEffect(new Vector3(0.0f, +10.0f, 0.0f), (float)Math.PI / 8, 10); AddFireBallEffect(new Vector3(50.0f, +10.0f, 0.0f), (float)Math.PI * 3 / 8, 10); } // if (Input.Keyboard.IsKeyDown(Keys.D3]) if (Input.Keyboard.IsKeyDown(Keys.D4)) AddRocketOrShipFlareAndSmoke( new Vector3((BaseGame.TotalTimeMs % 4000) / 40.0f, 0, 0), 5.0f, 150.0f); if (Input.Keyboard.IsKeyDown(Keys.D5) && BaseGame.EveryMs(1000)) AddExplosion(Vector3.Zero, 9.0f); // etc. // Play a couple of sound effects if (Input.Keyboard.IsKeyDown(Keys.P) && BaseGame.EveryMs(500)) PlaySoundEffect(EffectSoundType.PlasmaShoot); // etc. // We have to render the effects ourselfs because // it is usually done in RocketCommanderForm (not in TestGame)! // Finally render all effects before applying post screen shaders BaseGame.effectManager.HandleAllEffects(); }); } // TestEffects() Unit类 也许这个类应该叫做EnemyUnit类,因为它是只用于敌人的船舶,你自己的船已经在Player类中被处理。起先我想整合所有单位(你自己的飞船和敌人的)在这个类中,但它们的行为方式非常不同,这样做只会使代码更加混乱和复杂。在较为复杂的游戏中也许你应该为单位建立一个基类,然后从基类敌方单位和我方友方单位(例如,多人游戏中要确保所有其他玩家的飞船和你的飞船应有同样的运动方式)。看看图11-16中的这个类。 图11-16 这个类使用了很多字段去跟踪各单位的生命值,射击时间,位置等,但从外部看此类非常简单,你每帧只需调用一个方法:Render。构造函数只是创建了单位并指定单位类型、位置、默认生命值和伤害值(这些都是从类中的常数数组中获得的——更复杂的游戏应该从xml文件或外部数据源中获得这些值)。 一如往常你应该看一下单元测试,以了解更多这个类的知识: public static void TestUnitAI() { Unit testUnit = null; Mission dummyMission = null; TestGame.Start("TestUnitAI", delegate { dummyMission = new Mission(); testUnit = new Unit(UnitTypes.Corvette, Vector2.Zero, MovementPattern.StraightDown); // Call dummyMission.RenderLandscape once to initialize everything dummyMission.RenderLevelBackground(0); // Remove the all enemy units (the start enemies) and // all neutral objects dummyMission.numOfModelsToRender = 2; }, delegate { // [Helper texts are displayed here, press 1-0 or CSFRA, etc.] ResetUnitDelegate ResetUnit = delegate(MovementPattern setPattern) { testUnit.movementPattern = setPattern; testUnit.position = new Vector2( RandomHelper.GetRandomFloat(-20, +20), Mission.SegmentLength/2); testUnit.hitpoints = testUnit.maxHitpoints; testUnit.speed = 0; testUnit.lifeTimeMs = 0; }; if (Input.KeyboardKeyJustPressed(Keys.D1)) ResetUnit(MovementPattern.StraightDown); if (Input.KeyboardKeyJustPressed(Keys.D2)) ResetUnit(MovementPattern.GetFasterAndMoveDown); // [etc.] if (Input.KeyboardKeyJustPressed(Keys.Space)) ResetUnit(testUnit.movementPattern); if (Input.KeyboardKeyJustPressed(Keys.C)) testUnit.unitType = UnitTypes.Corvette; if (Input.KeyboardKeyJustPressed(Keys.S)) testUnit.unitType = UnitTypes.SmallTransporter; if (Input.KeyboardKeyJustPressed(Keys.F)) testUnit.unitType = UnitTypes.Firebird; if (Input.KeyboardKeyJustPressed(Keys.R)) testUnit.unitType = UnitTypes.RocketFrigate; if (Input.KeyboardKeyJustPressed(Keys.A)) testUnit.unitType = UnitTypes.Asteroid; // Update and render unit if (testUnit.Render(dummyMission)) // Restart unit if it was removed because it was too far down ResetUnit(testUnit.movementPattern); // Render all models the normal way for (int num = 0; num < dummyMission.numOfModelsToRender; num++) dummyMission.modelsToRender[num].model.Render( dummyMission.modelsToRender[num].matrix); BaseGame.MeshRenderManager.Render(); // Restore number of units as before. dummyMission.numOfModelsToRender = 2; // Show all effects (unit smoke, etc.) BaseGame.effectManager.HandleAllEffects(); }); } // TestUnitAI() 该单元测试可以让你按C,S,F,R或A改变五种敌人中的一个:Corvette,小型运输舰,Firebird,Rocket-Frigate和小行星。按1-0你能够改变这个敌人的AI行为。这里谈到AI有点疯狂,因为所有的人工智能做的只是处理不同的运动形式。该敌人只遵循特定的运动模式,一点儿也不聪明。例如,GetFasterAndMoveDown代码看起来像这样: case MovementPattern.GetFasterAndMoveDown: // Out of visible area? Then keep speed slow and wait. if (position.Y - Mission.LookAtPosition.Y > 30) lifeTimeMs = 300; if (lifeTimeMs < 3000) speed = lifeTimeMs / 3000; position += new Vector2(0, -1) * speed * 1.5f * maxSpeed * moveSpeed; break; 其他运动模式更简单。每个运动模式的名称告知了行为的足够信息。在游戏中随机分配一种运动模式给每一个新的敌人。你还可以创建一个关卡,包含预定义的位置和运动AI的敌人信息,但我想保持简单。单元测试的其余部分只是渲染敌人和背景。这被用来创建整个Unit类,覆盖了所有东西除了射击和敌人的死亡,这直接在游戏中测试。 Projectile类 刚才我提到敌人的射击,所需的代码让Corvette的射击不那么复杂,因为如果你的飞船处在它正下方时会被立即击中(它只检查它和你的x和y位置并采取相应的行动)。大部分Corvette的射击都会落空。 所有其他敌人不发射即时武器而发射火箭弹,火球或等离子球。这些弹丸持续一段时间飞往目标。Projectile类(见图11-17 )帮助你管理这些对象,简化了武器的逻辑,让你发射出一颗弹丸后就无需管理,由Projectile类代劳。Projectile类将处理碰撞检测,在燃料耗尽或飞出屏幕后自动移除弹丸。 图11-17 有三种不同弹药,它们的行为有所不同: 等离子球只能被自己的飞船发射,前提是你拥有这个等离子武器。等离子球速度快,比MG威力大,但Gatling-Gun和火箭发射器威力更大,虽然它们也有各自的缺点。 火球从敌方Firebird飞船发射,它飞得较慢,也不会改变方向,但Firebird飞船的AI让它们比你早发射火球,所以你必须首先躲避他们。 你和敌方的火箭护卫舰都能发射火箭。你的火箭能造成更大伤害,但只会直行。敌人的火箭更加智能,会根据你的位置调整目标,更难躲避。 相对于Unit类的渲染方法,Projectile类的渲染方法不是很复杂。其中最有趣的部分是更新位置和绘制弹丸后的碰撞处理。 public bool Render(Mission mission) { // [Update movement ...] // [Render projectile, either the 3D model for the rocket or just the // effect for the Fireball or Plasma weapons] // Own projectile? if (ownProjectile) { // Hit enemy units, check all of them for (int num = 0; num < Mission.units.Count; num++) { Unit enemyUnit = Mission.units[num]; // Near enough to enemy ship? Vector2 distVec = new Vector2(enemyUnit.position.X, enemyUnit.position.Y) new Vector2(position.X, position.Y); if (distVec.Length() < 7 && (enemyUnit.position.Y - Player.shipPos.Y) < 60) { // Explode and do damage! EffectManager.AddFlameExplosion(position); Player.score += (int)enemyUnit.hitpoints / 10; enemyUnit.hitpoints -= damage; return true; } // if } // for } // if // Else this is an enemy projectile? else { // Near enough to our ship? Vector2 distVec = new Vector2(Player.shipPos.X, Player.shipPos.Y) new Vector2(position.X, position.Y); if (distVec.Length() < 3) { // Explode and do damage! EffectManager.AddFlameExplosion(position); Player.health -= damage / 1000.0f; return true; } // if } // else // Don't remove projectile yet return false; } // Render() 如果这是你自己的弹丸(等离子或火箭),你必须检查与正在活动的敌方飞船的碰撞。如果出现碰撞在造成伤害并添加爆炸效果,此外你还可以获得一些分数(10%的敌方剩余生命值)。然后,返回true,告知调用函数你已经处理了这个弹丸,可以从目前活动的弹丸列表中移除了,这也发生在弹丸飞出边界的情况中。 如果被敌方弹药击中,碰撞检测更简单,你只需要检查己方飞船的碰撞,并以相同方式造成伤害。你的和敌方的飞船的死亡都是由各自的Render方法处理的。前面你已看到当你的飞船生命值耗尽时就会触发死亡。敌人单位的死亡条件看起来非常相似,而且你也返回true把这个敌人从目前的敌方列表中移除。 Item类 Item类用来处理所有道具。如图11-18可见这是非常简单的,但没有道具游戏会少很多乐趣。从类中的ItemTypes枚举可看出有6中道具。其中四个是武器,一个是生命值,可完全回复你的飞船的生命值,一个是电磁脉冲炸弹,按空格键可以消灭屏幕上的所有敌人,你最多能同时拥有三枚炸弹。 图11-18 Render方法与Projectile类中的类似,只是与道具碰撞不会杀死任何东西,相反可以收集这些物品,其效果是立即处理的。这意味着你可以得到新的武器,或回复到100%的生命值,或获得另一枚电磁脉冲炸弹。 /// <summary> /// Render item, returns false if we are done with it. /// </summary> /// <returns>True if done, false otherwise</returns> public bool Render(Mission mission) { // Remove unit if it is out of visible range! float distance = Mission.LookAtPosition.Y - position.Y; const float MaxUnitDistance = 60; if (distance > MaxUnitDistance) return true; // Render float itemSize = Mission.ItemModelSize; float itemRotation = 0; Vector3 itemPos = new Vector3(position, Mission.AllShipsZHeight); mission.AddModelToRender(mission.itemModels[(int)itemType], Matrix.CreateScale(itemSize) * Matrix.CreateRotationZ(itemRotation) * Matrix.CreateTranslation(itemPos)); // Add glow effect around the item EffectManager.AddEffect(itemPos + new Vector3(0, 0, 1.01f), EffectManager.EffectType.LightInstant, 7.5f, 0, 0); EffectManager.AddEffect(itemPos + new Vector3(0, 0, 1.02f), EffectManager.EffectType.LightInstant, 5.0f, 0, 0); // Collect item and give to player if colliding! Vector2 distVec = new Vector2(Player.shipPos.X, Player.shipPos.Y) new Vector2(position.X, position.Y); if (distVec.Length() < 5.0f) { if (itemType == ItemTypes.Health) { // Refresh health Sound.Play(Sound.Sounds.Health); Player.health = 1.0f; } // if else { Sound.Play(Sound.Sounds.NewWeapon); if (itemType == ItemTypes.Mg) Player.currentWeapon = Player.WeaponTypes.MG; else if (itemType == ItemTypes.Plasma) Player.currentWeapon = Player.WeaponTypes.Plasma; else if (itemType == ItemTypes.Gattling) Player.currentWeapon = Player.WeaponTypes.Gattling; else if (itemType == ItemTypes.Rockets) Player.currentWeapon = Player.WeaponTypes.Rockets; else if (itemType == ItemTypes.Emp && Player.empBombs < 3) Player.empBombs++; } // else Player.score += 500; return true; } // else // Don't remove item yet return false; } // Render() Render代码做的只是在给定位置放置道具,并添加了两个灯光效果是道具有点发光。如果你不看道具可能无法注意到发光效果,但是没有发光效果就更难看到它们。 如果你接近道具小于5个单位时就会自动收集它。你的飞船大约有五个单位的半径,这意味着你可以用飞船的仍以部分去接触道具。然后,这个道具被处理,给你生命值或是武器或是炸弹,同时还有额外的奖励分。此道具现在可以被移除了。如果你没有与道具碰撞,它会留在原地,直到你接触它们或道具飞出边界。 最后截图 耶,终于完成了。这就是XNA Shooter游戏需要的一切,见图11-19的结果。我希望这一章比第8章更有用,因为在第8章我不想重复原始版本的Rocket Commander的教程。 图11-19 我希望你喜欢XNA Shooter,如果你想创建自己的射击游戏会发现这很有用。请记住,这个游戏只用了几天时间就做好了,或许你可以使它更好,添加更多的关卡,敌方飞船,或更好的人工智能。玩得快乐!

创建XNA Shooter游戏——游戏场景绘制

clock 九月 7, 2010 09:09 by author alex
游戏场景绘制 XNA Shooter游戏的场景只是一个具有1024×1024的纹理的简单3D模型。但是,场景渲染并不容易,在下一部分您将通过整整一章的内容为接下来的赛车游戏制作场景和赛道。 但是我不想为一个简单的射击游戏花太多时间,因为这个游戏只在背景上渲染场景而且无需交互。因此,没必要实现一个场景渲染引擎去渲染数以千计的多边形,只需把纹理贴片放置在多边形上,对不同地面的纹理类型实现alpha混合就可以了。我采取了简单的办法,就是对整个场景只使用一个做好的三维模型。这整屏足够了,它不允许您左右移动,因为这个游戏不需要。 一个关卡大约有60个场景模型,你只要简单地将它们相互连接,并始终显示当前的和下一个场景。它们连接得并不完美,因为边界上的光很难处理(纹理、法线贴图、3D模型的法线都没问题),但对这个游戏来说已是足够好了。 本节我想向你展示创建这样一个场景模型的必要步骤,我不是唯一一个使用这种简单技术的人(例如,一个Rocket Commander的改编版本Canyon Commander就使用了类似的技术来显示三维峡谷)。 基本纹理和法线贴图 在您开始处理场景的三维数据或高程图之前,你必须知道在背景上应显示什么。这里我想在中间显示沙漠,两旁显示岩石,形成小峡谷。我制作了两个纹理,将它们拼合到BackgroundRock.dds纹理(见图11-8)。说实话我一开始并不想用沙地纹理,认为有一个岩石纹理就足够了,但这样看起来太枯燥,缺乏变化。 图11-8 法线贴图以同样的方式被混合在一起,但对于这两个纹理,我起初没有法线贴图。我使用了NVIDIA的Photoshop插件从这两个纹理产生法线贴图。你必须反复调整,直到它们看起来不错,有时你必须重新绘制纹理以修正错误的地方。这里使用的diffuse纹理没有任何高度和法线的信息,所这个插件能做的只是把图片转化成灰度图,从伪高程图产生法线。 有时它看起来不错,但有时这种方式也会产生错误的法线贴图。最好的情况是,艺术家在创造出这些纹理的同时也提供法线贴图或至少高程图,但通常没这种奢侈。有时只有diffuse纹理,当你用照相机拍照时,法线和高度信息也不会被记录。那么,还是要尽量保持简单。您将在下一章的赛车游戏中创建更复杂的场景纹理和法线贴图。图11-9显示了混合在一起的法线贴图。你会发现,岩石的法线贴图比沙地更强,因为你想让岩石看起来比沙地更崎岖不平。但即使是沙地也有一点小起伏(添加在相对平缓的沙地纹理上)以获得更好的的光照效果。 图11-9 高程图 有了diffuse纹理和法线贴图,现在你可以在一个简单的多边形上显示场景了,但效果还够好。你需要的是两边真正耸立的悬崖和中间的峡谷。我使用3D Studio Max(你也可以使用任何3D创建工具,如果你不会也可请别人代劳)创建这个峡谷,我在xy平面创建了一个简单的平面物体(z轴朝上),它有64×64个交点形成63×63×2=7938个多边形。使用63是因为每个多边形需要有一个起点和一个终点,再乘以2,是因为每个四边形是由2个三角形组成的(见图11-10)。 图11-10 为了让每一个点都有一个大于默认为0的z值,你要在3D建模程序中上下拖动它们,但我并不熟练,也没有耐心以这种方法完成一个场景。一个非常容易的办法是使用一个高程图,然后根据这个高程图显示所有的点。高程图常被用于地理景观地图中,所以在互联网上不难找到一些很酷的高程图,甚至还有其他行星的高程图。 幸运的是在3D Studio Max中有一个简单的modifier叫Displace,这正是您所需要的(不容易找到,但一旦你知道它在哪,它就变得非常有用)。从这里您可以将您的新建立的高程图(我自己画的,不太棒,但能工作)加入到三维模型中(见图11-11)。 图11-11 现在没有任何反应,你可能会问这是为什么。Max有许多设置,哪个设置对应哪个并不总是很容易找到。只要探索一下设置,直到发生变化。在这里您需要调整顶部的强度设置,把它设置成30至40作用就能看到如图11-12的结果。 图11-12 最后一步是将diffuse纹理和法线贴图指派到法线映射shader的材质上。然后,你可以将这种材质指派到三维模型上(见图11-13)并导出。现在就能被用在游戏中了。 图11-13 场景现在从顶部被渲染到屏幕上,您只能看到的山谷的内部和悬崖的边界。场景渲染也支持16:9的宽屏分辨率,这意味着在4:3分辨率下某些部分可能并不总是可见的。游戏中所有的行动都在峡谷中间发生。当您添加3D建筑物和植物后,一切就绪了。 看一下Misson类中的单元测试,了解一下场景模型是如何被呈现在游戏中的。您也可以只看一下Model类中的单元测试,显示了游戏中所有使用的3D模型。 添加物体 场景看起来不错(至少比一个简单xy平面强),但有点空。为了使它看上去更好还添加了建筑物和植物。查看一下Misson类最后的TestRenderLandscapeBackground单元测试,它只是调用了RenderLandscapeBackground方法。该方法以目前的关卡位置作为参数,始终只显示当前和下一个场景,以确保即使你向前移动时仍能看到三维模型。玩家意识不到这一点,因为如果你向前移动了足够距离,当前场景就会被下一个场景所替代,新的场景在顶部生成,直到关尾。 物体生成代码有趣的地方在于使用了模型列表,随机增添新的物体。植物被随机放置和旋转,但建筑物只显示左边和右边,即只旋转90度。 // From the GenerateLandscapeSegment method: List<MatrixAndNumber> ret = new List<MatrixAndNumber>(); int numOfNewObjects = RandomHelper.GetRandomInt( MaxNumberOfObjectsGeneratedEachSegment); if (numOfNewObjects < 8) numOfNewObjects = 8; for (int num = 0; num < numOfNewObjects; num++) { int type = 1+RandomHelper.GetRandomInt(NumOfLandscapeModels-1); // Create buildings only left and right if (type <= 5) { int rotSimple = RandomHelper.GetRandomInt(4); float rot = rotSimple == 0 ? 0 : rotSimple == 1 ? MathHelper.PiOver2 : rotSimple == 1 ? MathHelper.Pi : MathHelper.PiOver2 * 3; bool side = RandomHelper.GetRandomInt(2) == 0; float yPos = segmentNumber * SegmentLength + 0.94f * RandomHelper.GetRandomFloat(-SegmentLength / 2, SegmentLength / 2); Vector3 pos = new Vector3(side ? -18 : +18, yPos, -16); // Add very little height to each object to avoid same height // if buildings collide into each other. pos += new Vector3(0, 0, 0.001f * num); ret.Add(new MatrixAndNumber( Matrix.CreateScale(LandscapeModelSize[type]) * Matrix.CreateRotationZ(rot) * Matrix.CreateTranslation(pos), type)); } // if else { ret.Add(new MatrixAndNumber( Matrix.CreateScale(LandscapeModelSize[type]) * Matrix.CreateRotationZ( RandomHelper.GetRandomFloat(0, MathHelper.Pi * 2)) * Matrix.CreateTranslation(new Vector3( RandomHelper.GetRandomFloat(-20, +20), segmentNumber * SegmentLength + RandomHelper.GetRandomFloat( -SegmentLength / 2, SegmentLength / 2), -15)), type)); } // else } // for 接着ret列表返回给调用函数,将它保存到场景片断。完整的GenerateLandscapeSegment代码还添加敌人,检查物体碰撞,并防止建筑物或植物靠得太近。 如果你执行TestRenderLandscapeBackground单元测试,您可以看到场景和地面上的物体,如图11-14所示。请注意,本章没有讨论阴影映射,请阅读本书的最后部分,详细了解阴影映射技术。如果你有兴趣也可以看一下ShadowMappingShader类。 图11-14

友情链接赞助