[翻译]Oreilly.Learning.XNA.3.0之four

PS:自己翻译的,转载请著明出处
                                                    第十二章   3D碰撞冲突检测和射击
                       行...我们开始吧。系紧安全带;我们来到最后阶段。在过去的第二章,您建立了一个飞行相机和讨论了不同在这个和路基相机的不同。然后,你获得这些代码,已经创建了在这本书的3D章节,创建一个自定义相机为你的游戏。在这章,你添加一些游戏逻辑,支持射击和碰撞检测。我准备了许多去这样做,所以让我们去实现吧。
                       本章结合这些代码它来自于11章。打开3D游戏工程,使用它为你的例子,在本章演练一下。
注意:在你下载本书的源代码中,你会发现11章有两个工程:Flying Camera,它有3D飞行相机的代码你创建在前面章的代码中,另一个3D游戏,它有固定的,自定义的相机代码,你创建在那章的第二部分。确保你使用这章的项目称为3D Game。
创建一个移动的敌人
                       现在,你有一个自定义摄象机,它旋转用yaw和pitch方向,同时寻找旋转的飞船。That's pretty sweet,你现在可以考验去释放你的游戏,看看是否能使一些用户远离world of warcraft(魔兽世界),使每个月都有数一百计的定单。 我想提醒您采取的一个步骤后退一分钟,但并意识到你还有一些工作要做在你这一点之前。
                       在你将创建的游戏中,在接下来的几章里,你有一个固定的相机,由它我们向朝我们飞来的敌人射击。它造成这样的感觉,然后,去添加你的BasicModel的一个新的子类,您可以向其中添加功能使它朝一个给定的方向飞行。为了添加一个真实的感觉,你同样要使敌人自由的roll.
                      首先,你需要修改SpinningEnemy类。添加下面的类别变量到SpinningEnemy类:
1 float yawAngle = 0;
2 float pitchAngle = 0;
3 float rollAngle = 0;
4 Vector3 direction;
                      接下来,修改SpinningEnemy类的结构如下面所示:
1 public SpinningEnemy(Model m, Vector3 Position,Vector3 Direction, float yaw, float pitch, float roll): base(m)
2 {
3      world = Matrix.CreateTranslation(Position);
4      yawAngle = yaw;
5      pitchAngle = pitch;
6      rollAngle = roll;
7      direction = Direction;
8 }
                     你先前有一个参数为一个Model在这个结构中,但是你现在添加几个其他参数。让我们看看每一个参数,你添加到这个结构中,它们用来做什么:
         Vector3 Position
                     这个位置参数代表,说也奇怪,对象的位置。记住,在你的BasicModel类中,你有一个 world变量,它是初始化设置到Matrix.Identity.在SpinningEnemy类的结构中,你设置基类的world矩阵到一个Matrix.Translation中,使用passed-in位置向量。这将使对象初始位置通过位置向量参数来代表。
        Vector3 Direction
                     这方向参数代表对象移动的方向,和它移动的速度。在这个结构中,你简单分配这个参数的值到一个类变量,去为以后跟踪它。在Update方法中,注意world变量(你使用它去代表对象的位置)通过一个新的Matrix.Translation被multipied,使用这个方向向量。通过这个参数移动这个对象在指定的方向在一个速度,速度等于向量的数量级每一次调用Update(译者:就是指定个速度的大小)。
        float yaw, float pitch, float roll
                     yaw,pitch,和roll角度参数代表多少度,用弧度表示,去旋转这个对象在一个yaw,pitch,或者roll每一次Update被调用时。在这个结构中,你分配这个值到类变量。在这个Update方法中,注意你multiplying一个新的称为rotation的矩阵(它是一个类别变量初始化到Matrix.Identity)通过一个Matrix.CreateFromYawPitchRoll,使用yaw,pitch,和roll角度作为参数。这将导致对象旋转逐步增加每一次Update方法被调用。
                     接下来,修改你的SpinningEnemy类的Update方法,去旋转模型使用yaw,pitch,和roll变量传递到这个结构,然后移动这个模型使用这个方向向量:
1 public override void Update( )
2 {
3    // Rotate model
4    rotation *= Matrix.CreateFromYawPitchRoll(yawAngle,pitchAngle, rollAngle);
5    // Move model
6    world *= Matrix.CreateTranslation(direction);
7 }
                     最后,修改GetWorld方法去返回旋转乘积通过world矩阵,修改你的GetWorld方法如下:
1 public override Matrix GetWorld( )
2 {
3     return rotation * world;
4 }
                    你的新的修改的SpinningEnemy类处理两个矩阵去代表模型位置和旋转。首先,旋转矩阵被用来跟踪这个对象旋转多少度。其次,world矩阵被用来跟踪对象移动了多远(使用CreateTranslation方法)。返回旋转矩阵multiplied通过world矩阵导致对象旋转在这个地方,同时在这个指定的方向上移动通过方向向量。
注意:所以,这里有一个问题:如果在GetWorld方法中你返回world*rotation代替rotation*world,会有什么样的不同?虽然rotation*world会导致这个对象去spin in place(译者:自转)同时在这个指定的方向上移动通过方向向量,world*rotation将导致对象移动在指定的方向通过方向向量,同时围绕原点公转。这个小变化到矩阵multiplication导致一些不同的功能。稍后在本章,你会有一些类的对象在屏幕周围移动,你可以调整GetWorld方法来获得不同的效果。
Adding Some Game Logic
                    美好的时光。现在,你有一个类,将创造一个移动的敌人,你需要添加一些游戏的逻辑。几乎与所有的游戏一样,在游戏中有一个自由成分,这样,你需要添加一个自由number generator(译者:数据产生器)。记住,你总是希望有唯一自由的数据产生器,你使用贯穿你整个游戏。如果你有多个自由数据产生器变量,有一个可能,一些变量可能最终有同样的自由seeds,在这种情况下,数据序列是相同的(将不再是随机的) 。添加一个Random对象到你的Game1类,一个公有自动的属性为这个对象:
1 public Random rnd { getprotected set; }
                    然后,初始化rnd变量在Game1类的构造器内:
1 rnd = new Random( );
                    当你工作在Game1类,请便,改变背景颜色游戏黑色,使其看起来更像我们在外层空间(噎,外太空)。记住去改变背景颜色,你改变这个参数发送到Clear方法中,在Game1的Draw方法中:
1 GraphicsDevice.Clear(Color.Black);
                    此外,您可能会想你的游戏在全屏模式下,让我们1280*1024(你想确保你的解决方案是标准的解决方案通过PC显示器所支持,1280*1024解决方案显然能通过测试)。为了改变屏幕大小,指定的全屏幕的模式,添加下面的代码到Game1类的构造部分的后面:
1 class:
2    graphics.PreferredBackBufferWidth = 1280;
3    graphics.PreferredBackBufferHeight = 1024;
4 #if !DEBUG
5    graphics.IsFullScreen = true;
6 #endif
注意:该解决方案你指定的只表明是首选的解决方案。如果由于什么原因PC不支持这个解决方案,你的游戏将返回另一个解决方案。同样记住,Game.Window.ClientBounds方法,你已经使用这本书去检测屏幕的边,取决于你的屏幕足够大去适应你指定的解决方案。例如,如果你运行在宽屏显示器上,在1400*900的解决方案上,你不可能合适一个1280*1024大小的窗口在你的屏幕上。确保屏幕的大小你选择的将适应你使用的显示器大小。
                     好吧,现在你将要添加一些功能,到你的ModelManager类,它将定期产生敌人。你做一些就象在这本书前面的2D游戏中的事情,但是我们准备去接受它在下一步,添加日益困难的逻辑水平到这个逻辑。首先,你需要去做的是删除代码行,它创建你当前有的自旋的飞船。这一行是调用到models.Add方法在你的ModelManager的LoadContent方法中,完全删除下面的代码:
1 models.Add(new BasicModel(Game.Content.Load<Model>(@"models\spaceship")));
Issues in Full-Screen Mode
                     在这个代码你已经添加到Game1类的结构中,注意到预处理程序指示这个游戏应该运行在全屏模式,如果它不是返回在调试构造中。为什么会是这样?在XNA中,Visual Studio有一个有着极其艰难的时刻让你的调试和通过断点,当一个游戏正在全屏模式运行。由于这个原因,你总是运行你的游戏在窗口模式,当你正在运行在调试结构。然而,当游戏是发布模式,你应该没有问题运行在全屏模式。
                     此外,您可能会注意到当运行在全屏模式下,您不能退出比赛按一下红色的X在右上角的窗口。这是因为它不存在, 你在全屏模式下!如果你有一个gamepad,你可以使用这个返回按钮去退出游戏。换句话说,使用Alt+F4去关闭游戏,当运行在全屏幕模式。你同样可以添加一些代码在你的游戏Game1类的Update方法去退出,如果玩家按下Escape键,如果你想这样做。
                     现在,运行游戏会显示黑屏,没有看见任何物体。这会没有什么趣味,你可以做的更好。在你的游戏中,你准备执行一系列的越来越难级别,敌人产生并朝着玩家飞来,并且玩家不得不把它们射下来,去进入下一个级别。首先,增加一个新类到你的游戏中调用LevelInfo和用下面的代替这段代码:
 1 namespace _3D_Game
 2 {
 3    class LevelInfo
 4    {
 5        // Spawn variables
 6        public int minSpawnTime { getset; }
 7        public int maxSpawnTime { getset; }
 8        // Enemy count variables
 9        public int numberEnemies { getset; }
10        public int minSpeed { getset; }
11        public int maxSpeed { getset; }
12        // Misses
13        public int missesAllowed { getset; }
14        public LevelInfo(int minSpawnTime, int maxSpawnTime,int numberEnemies, int minSpeed, int maxSpeed,int missesAllowed)
15       {
16         this.minSpawnTime = minSpawnTime;
17         this.maxSpawnTime = maxSpawnTime;
18         this.numberEnemies = numberEnemies;
19         this.minSpeed = minSpeed;
20         this.maxSpeed = maxSpeed;
21         this.missesAllowed = missesAllowed;
22       }
23   }
24 }
                     基本上,你将会创建一个LevelInfo类的对象为每一个水平在你的游戏中,让我们看有一下,在LevelInfo中的每一个变量如何使用:
        int[] minSpawnTimes
                     整数的数组代表最小的产生时间(用毫秒计算)为一个新的敌人产生。用数组的理由是什么?每一个敌人在这个数组中代表毫秒产生时间为一个游戏不同的级别(译者:用敌人产生的时间来决定游戏的难度)。
        int[] maxSpawnTimes
                     这个整数数组代表最大的产生时间(用毫秒计算)为一个新的敌人产生。每一个成分在这个数组中代表最大产生时间为不同的游戏水平。
        int[] numberEnemies
                     这代表多少敌人将产生在每一个水平中。一旦这个数字达到,所有敌人将离开屏幕,当前的水平结束,下个水平的游戏开始了。
        int maxSpeed and int minSpeed
                     这代表敌人的最大的速度。这是一个整型而不是一个Vector3,因为你的敌人只会在正Z轴移动。当产生一个新的敌人,你会使用一个(0,0)方向向量,和一些自由值在minSpeed和maxSpeed之间。
        int[] missesAllowed
                     这将会用来你的最终的游戏逻辑。一旦这个数量达到给定的水平,游戏结束。
                     现在,添加下面的类别变量到你的ModelManager类。这些变量都被用来帮助创建日益增加的难度水平:
 1 // Spawn variables
 2 Vector3 maxSpawnLocation = new Vector3(100100-3000);
 3 int nextSpawnTime = 0;
 4 int timeSinceLastSpawn = 0;
 5 float maxRollAngle = MathHelper.Pi / 40;
 6 // Enemy count
 7 int enemiesThisLevel = 0;
 8 // Misses variables
 9 int missedThisLevel = 0;
10 // Current level
11 int currentLevel = 0;
12 // List of LevelInfo objects
13 List<LevelInfo> levelInfoList = new List<LevelInfo>( );
                     让我们来看看所有这些变量,哪些是你会利用他们的:
          Vector3 maxSpawnLocation
                     这个向量将被使用去代表敌人飞船开始的位置。Z值是一个常量对于所有的飞船:所有的飞船将开始于-3000.X和Y的值被用来作为范围使用,分别是-X到X和-Y到Y。从本质上讲,当产生一个敌人,你放置它在一些随机的位置在-X到X之间,和-Y到Y之间,和在-3000Z的地方。
         int nextSpawnTime
                     这个变量将会被用来确定什么时候下一个敌人应该产生。它作为一个随机数被产生在minSpawnTime和maxSpawnTime之间为当前的水平。
         int timeSinceLastSpawn
                     这个变量用于追踪上一个敌人产生到现在过去了多少时间,与nextSpawnTime变量相比较去决定什么时候一个新的敌人应该产生。
         float maxRollAngle
                     这个值将代表最大的roll角度去传递到你的SpinningEnemy类中。这个值传入,将会是一些自由的值在-maxRollAngle和+maxRollAngle之间。
         int enemiesThisLevel
                     这个变量将会被用来跟踪多少敌人已经产生了在当前的游戏水平,与numberEnemies数组相比较去决定什么时候游戏水平结束。
         int missedThisLevel
                     这个变量被用来跟踪多少个敌人已经躲避开了玩家在当前的水平,与missesAllowed数组相比较去决定游戏结束的条件。
         int currentLevel
                     这个值将保持数量反映当前游戏的级别。当游戏开始,currentLevel的值是0,游戏在级别1以上。这个值是零基础,更容易进入前面列出的数组。
         levelInfoList
                     这个变量将保存一个LevelInfo对象的表单,它描述产生时间,速度,和每一关的级别。
                     一旦你添加这些变量到你的ModelManager类,你需要去初始化你的levelInfoList对象信息为每一个游戏级别,你将要执行它。添加下面的代码到ModelManager类的结构中:
 1 // Initialize game levels
 2 levelInfoList.Add(new LevelInfo(10003000202610));
 3 levelInfoList.Add(new LevelInfo(900280022269));
 4 levelInfoList.Add(new LevelInfo(800260024268));
 5 levelInfoList.Add(new LevelInfo(700240026377));
 6 levelInfoList.Add(new LevelInfo(600220028376));
 7 levelInfoList.Add(new LevelInfo(500200030375));
 8 levelInfoList.Add(new LevelInfo(400180032474));
 9 levelInfoList.Add(new LevelInfo(300160034483));
10 levelInfoList.Add(new LevelInfo(200140036582));
11 levelInfoList.Add(new LevelInfo(100120038591));
12 levelInfoList.Add(new LevelInfo(50100040690));
13 levelInfoList.Add(new LevelInfo(5080042690));
14 levelInfoList.Add(new LevelInfo(50600448100));
15 levelInfoList.Add(new LevelInfo(25400468100));
16 levelInfoList.Add(new LevelInfo(02004818200));
                      这段代码创建15个不同的级别水平,每一个通过一个LevelInfo实例来代表。在这段代码中,你已经设置了产生时间,速度为每一个敌人的创建,大量的敌人创建在每一个级别水平,多少敌人玩家能够错过在每一个级别水平。你可以看见通过这个数量,它是游戏逐渐增加难度从这个级别水平到另一个级别水平。
注意:我是如何知道使用这些值为每个级别水平的?我又是如何知道敌人的飞船在-3000Z这里开始的呢?大都是通过实验和错误中得来的。当创建一个象这样的游戏,你开始在这个概念上工作,当你玩这个游戏,你获得一个摸索东西如何工作。如果敌人看起来移动的太快,让它们减速。如果开始离相机太近,把它们远离相机。得到别人的反馈也是非常重要的,因为一般来讲,你想让你的游戏去吸引更多的玩家,不仅仅是自己的开发商。
                      接下来,你需要开始思考什么时候开始产生你第一个敌人。现在,没有任何逻辑,说明当一个新的敌人将产生。你需要创建一个方法去设置nextSpawnTime变量为了产生你的第一个敌人。添加下面的代码到ModelManager类中:
1 private void SetNextSpawnTime( )
2 {
3     nextSpawnTime = ((Game1)Game).rnd.Next(levelInfoList[currentLevel].minSpawnTime,levelInfoList[currentLevel].maxSpawnTime);
4     timeSinceLastSpawn = 0;
5 }
                      注意,在这个方法中nextSpawnTime变量设置成一个随机数使用Random对象在这个Game1类中。结果值是一些数在minSpawnTime和maxSpawnTime之间的为当前的级别水平,using the levelInfoList list offset by the current level index。然后,timeSinceLastSpawn变量设置成0,这将让你计算毫秒从这个方法调用的时间到下一个敌人产生的时候所用去的时间。
                      接下来,调用SetNextSpawnTime方法在你的ModelManager类的Initialize方法。它将设置nextSpawnTime,并且允许你倒计时这个时间,你将释放你的第一个敌人在可怜人人类玩家:
1 // Set initial spawn time
2 SetNextSpawnTime( );
                      现在,您需要的代码的方法实际上会产生一个新的敌人:
 1 private void SpawnEnemy( )
 2 {
 3     // Generate random position with random X and random Y
 4     // between -maxX and maxX and -maxY and maxY. Z is always
 5     // the same for all ships.
 6     Vector3 position = new Vector3(((Game1)Game).rnd.Next(-(int)maxSpawnLocation.X, (int)maxSpawnLocation.X),((Game1)Game).rnd.Next(-(int)maxSpawnLocation.Y, (int)maxSpawnLocation.Y),maxSpawnLocation.Z);
 7     // Direction will always be (0, 0, Z), where
 8     // Z is a random value between minSpeed and maxSpeed
 9     Vector3 direction = new Vector3(00,((Game1)Game).rnd.Next(levelInfoList[currentLevel].minSpeed,levelInfoList[currentLevel].maxSpeed));
10     // Get a random roll rotation between -maxRollAngle and maxRollAngle
11     float rollRotation = (float)((Game1)Game).rnd.NextDouble( ) *maxRollAngle - (maxRollAngle / 2);
12     // Add model to the list
13     models.Add(new SpinningEnemy(Game.Content.Load<Model>(@"models\spaceship"),position, direction, 00, rollRotation));
14     // Increment # of enemies this level and set next spawn time
15     ++enemiesThisLevel;
16     SetNextSpawnTime( );
17 }
                       这个方法创建一个新SpinningEnemy和添加它到模型的表单中在你的ModelManager.首先,位置作为一个Vector3被产生用随机值在-maxSpawnLocation.X和+maxSpawnLocation.X,-maxSpawnLocation.Y和+maxSpawnLocation.Y,和maxSpawnLocation.Z。
                       然后,计算出的方向,用0和X和Y之间的随机值在minSpeed和maxSpeed为Z轴,它移动对象朝着摄象机方面在不同的速度Z轴的方向。
                       其次,roll旋转被计算作为一个随机值在-maxRollAngle和+maxRollAngle之间。这个敌人被创建不会旋转用一个pitch或者yaw,所以,唯一的旋转发生在roll上。
                       这个模型当时创建,并且添加到模型表单中,enemiesThislevel变量被增加,下一个敌人产生时间被设置通过调用SetNextSpawnTime。
                       当下一个敌人产生时间已经达到,你实际上需要产生一个你的敌人。添加下面的方法到ModelManager类中去产生一个新的敌人当时间恰好到的时候:
 1 protected void CheckToSpawnEnemy(GameTime gameTime)
 2 {
 3        // Time to spawn a new enemy?
 4       if (enemiesThisLevel <levelInfoList[currentLevel].numberEnemies)
 5      {
 6          timeSinceLastSpawn += gameTime.ElapsedGameTime.Milliseconds;
 7          if (timeSinceLastSpawn > nextSpawnTime)
 8          {
 9              SpawnEnemy( );
10          }
11      }
12 }
                       随后,调用新的方法在Update方法的上面,在你的ModelManager类中:
1 // Check to see if it's time to spawn
2 CheckToSpawnEnemy(gameTime);
                       使用gameTime变量,你增加timeSinceLastSpawn变量。当timeSinceLastSpawn变量的值比nextSpawnTime变量的值大的时候,时机已经成熟,推出一个新的敌人。调用SpawnEnemy将创建一个新敌人和重设产生敌人的计时器,这样,在这一点上是你所有需要做的。
                       最后,由于你的游戏相机只能旋转45度,一旦一个敌人出离了相机的视阈,它死球了,需要被删除从这个游戏中。你当前有一个公有的访问为这个Vector3 cameraPosition变量在你的Camera 类中,它允许你的ModelManager去使用这个属性去检测当一个敌人出离相机的视阈。
                       Matrix类有一个Translation属性,它返回一个Vector3代表纯粹的平移应用于Matrix.你可以使用这个属性在ModelManager类在所有敌人飞船上,你创建和比较它依靠Z坐标从你的相机的cameraPosition 自动执行属性到检测是否飞船的Z值比相机位置的Z值大(说明飞船飞出了相机的视野)。如果是这种情况,飞船出了范围应该从这个游戏中删除。
                       修改代码调用Update方法在模型表单的每个模型上(在你的ModelManager类的Update方法中)去删除模型一旦它们出离了游戏。删除下面的代码从你的ModelManager类的Update方法中:
1 // Loop through all models and call Update
2 for (int i = 0; i < models.Count; ++i)
3 {
4       models[i].Update( );
5 
                       添加下面的方法,它通过你的飞船模型循环,更新它们,删除它们当它们飞出范围:
 1 protected void UpdateModels( )
 2 {
 3     // Loop through all models and call Update
 4     for (int i = 0; i < models.Count; ++i)
 5     {
 6        // Update each model
 7        models[i].Update( );
 8       // Remove models that are out of bounds
 9        if (models[i].GetWorld( ).Translation.Z >((Game1)Game).camera.cameraPosition.Z + 100)
10        {
11            models.RemoveAt(i);
12            --i;
13        }
14     }
15 }
                      接下来,调用UpdateModel方法从你的ModelManager类的update方法中,就在base.Update的前面:
1 // Update models
2 UpdateModels( );
                      哇,你刚才做了一个大的进展。在这点上,你应该能编译运行你的游戏,看看你使敌人向摄象机移动,roll随机的左或者右为了达到真实的状态,如图12-1所示。你取得一些伟大的一步在你现在游戏中。下一件事去做是添加功能把敌人打下来!
开火射击
                     许多游戏的类型是由某种形式的射子弹是对抛掷或枪杀其他玩家或敌人。你如何去增加这个能力打出一个某种弹丸在你的游戏中?想想看,在最基本的级别水平,你会认识到,射弹的飞行通过空气或者通过空间就象移动相机一样,并且在本书前面的章节你已经执行过了一个对象的移动。从本质上讲,你的设计(或者子弹)每一个由一个模型,位置,方向所组成。让我看看你如何添加它到你的游戏中。
                     你的SpinningEnemy类已经有这个子弹必须的所有组成部分,这样你可以使用这个类为子弹以及敌人的飞船。这不是很高兴能有这样一个通用的,多用途的类,可重复使用的不同的东西?考虑下你所节约的时间!你已经准备好编写你的子弹类了....不错!
                     下一步是建立一个子弹,添加子弹模型到你的工程中。如果你没有准备好,下载本书本章的源代码。这章的模型文件名称叫ammo.x在3D Game\Content\Models文件夹。在Visual Studio中,通过右击Content\Models文件夹,添加这个ammo.x文件到你的工程中选择Add-Existing Item...浏览到ammo.x文件的地方,选择它。
                     一旦你添加了这ammo.x文件到你的工程中,添加几个类别变量到你的ModelManager类去帮助你跟踪你射击的子弹:
1 List<BasicModel> shots = new List<BasicModel>( );
2 float shotMinZ = -3000;
                     第一个变量是一个新BasicModel对象的表单。这个表单将包含所有的发射的子弹。由于子弹使用和飞船同样的类,你可以保存你的子弹对象在与敌人飞船同样的表单中。虽然这个方法可以使用,增加了处理器的时间,当它达到碰撞检测是处理器需要去鉴别哪个对象是飞船,哪个对象是子弹(你不必担心子弹撞上子弹,飞船撞上飞船;在这个游戏中你所要担心的是子弹碰撞到了飞船)。所以,考虑到说明的游戏,最好的途径是有一个飞船列表和一个子弹列表。
                     第二个变量添加在前面的代码,它是子弹的Z的最小值。一旦一个子弹超出在Z方向的-2000的范围,这个子弹飞出了游戏,它被标记然后从列表中删除。
                     下一步,你需要创建一个方法去添加子弹到ModelManager。你的模型管理器负责添加敌人飞船它自己本身,因为你的敌人是自动生成在随机的间隔中。然而,子弹将会被添加是基于用户的输入,这真是你的ModelManager类职责范围以外,它应负责的管理模式,而不是处理用户的输入。
                     所以,从你的Game1类中添加子弹,需要有一个方式添加子弹到ModelManager的子弹列表。添加下面的方法到你的ModelManager去允许Game1类去添加子弹到这个表单中:
1 public void AddShot(Vector3 position, Vector3 direction)
2 {
3    shots.Add(new SpinningEnemy(Game.Content.Load<Model>(@"models\ammo"),position, direction, 000));
4 }
                     每一次你的ModelManager的Update和Draw方法被调用时,同样需要更新和绘制你的子弹。处理这个用与你的敌人飞船同样的方式。此外,当更新你的子弹时,你想去检查确保子弹在游戏中(它们没有传递shotMinZ的值说明它们飞出了范围)。任何子弹不在游戏中,应该被删除。添加下面的方法到你的ModelManager类去更新每个子弹和删除那些飞出范围的子弹:
 1 protected void UpdateShots( )
 2 {
 3     // Loop through shots
 4     for (int i = 0; i < shots.Count; ++i)
 5     {
 6         // Update each shot
 7         shots[i].Update( );
 8         // If shot is out of bounds, remove it from game
 9         if (shots[i].GetWorld( ).Translation.Z < shotMinZ)
10         {
11             shots.RemoveAt(i);
12             --i;
13         }
14     }
15 }
                      接下来,调用UpdateShots方法从你的ModelManager的Update方法中,只在调用base.Update之前:
1 // Update shots
2 UpdateShots( );
                      请注意,你可以使用你子弹的world矩阵的Translation属性去检测对象的位置,拿它和shotMinZ的值相比较。你可能已经注意到这个了,但是这是最基本的相同方式,这方式是:你比较你的敌人飞船和相机的位置看下他们是否飞出了范围。
                      为了绘制你的子弹,添加下面的代码到你的ModelManager的Draw方法,只在调用base.Draw之前:
1 // Loop through and draw each shot
2 foreach (BasicModel bm in shots)
3 {
4      bm.Draw(((Game1)Game).camera);
5 }
                      好吧,这是所有到现在的ModelManager类。让我们继续前进到相机的类中。
                      什么?等一下。我们为什么要修改相机类去添加发射功能?
                      嗯,记得我提到,一个子弹是与带有位置和方向的移动的相机或者移动的对象相同的。这是真的,子弹的最初的位置将会是子弹起源的地方(在你的游戏里,它将是相机的位置)。但是你给了每个子弹什么方向呢?子弹的方向是制造子弹的对象的方向。例如,如果你有一把枪指向某一个方向,它射出一个子弹,这个子弹将又一个方向向量,这个方向向量将与子弹的方向相同。
                      在您的游戏中,相机将检测射击的方向,因此Game1类必须能够获得相机的方向的向量。所以,你需要为你的Camera类的cameraDirection变量去添加一个public属性:
1 public Vector3 GetCameraDirection
2 {
3      get { return cameraDirection; }
4 }
注意:为什么创建一个传统属性访问为这个cameraDirecetion而不是使用一个自动执行属性,就象你对cameraPosition做的?在相机类的结构中,你调用Normalize在cameraDirection上。这将无法正常工作在自动执行(译者:这里可能是get,set都有)的属性中,因此,最好单独留下cameraDirection成员,创建一个新的,传统的属性访问器。                   
                      现在,在您的Game1类,让我们看一下你需要添加什么去生成一个子弹,首先,添加三个类别变量去帮助你的子弹逻辑:
1 float shotSpeed = 10;
2 int shotDelay = 300;
3 int shotCountdown = 0;
                      让我们来看看这些变量。首先,你增加一个子弹速度变量,它通过乘以方向向量去得到子弹补充的动力在速率部分。
                      接下来,你添加射击延时和射击倒计时变量。这些用来做什么?好,考虑下你准备去检查用户的输入为了去发射一个子弹。玩家能够发射一个子弹通过按下空格键或者鼠标的左键,你将会检测用户的输入在你的Game1的Update方法中。记住多少时间后第二次Update方法被调用?如果一切顺利的话,它被调用在每秒60次。现在,想象如果你简单射出一个子弹用Update方法每一次空格被按下,或者鼠标左键被单击。如果你按住空格键长达半秒,你已经发射了30个左右的子弹!我都喜欢速射行为,但这有点太多了。
                      为了阻止这种超负荷,一把武器共同的特点是延迟周期在你个子弹被发射之后,在此周期之间不会再有子弹被发射。
                      shotDelay变量你添加的代表你武器射击延迟的持续时间,同时shotCountdown变量将会被用来检测,当射击延迟期满,另外一个子弹可以被发射。
                      添加下面的方法到Game1类中,它将导致子弹被发射当玩家按下鼠标左键或者空格键,不仅仅是发射延迟期满:
 1 protected void FireShots(GameTime gameTime)
 2 {
 3      if (shotCountdown <= 0)
 4      {
 5          // Did player press space bar or left mouse button?
 6          if (Keyboard.GetState( ).IsKeyDown(Keys.Space) ||Mouse.GetState( ).LeftButton == ButtonState.Pressed)
 7          {
 8              // Add a shot to the model manager
 9               modelManager.AddShot(camera.cameraPosition + new Vector3(0-50),camera.GetCameraDirection * shotSpeed);
10              // Reset the shot countdown
11               shotCountdown = shotDelay;
12          }
13      }
14      else
15      shotCountdown -= gameTime.ElapsedGameTime.Milliseconds;
16 }
17                       下一步,调用FireShots方法在你的Game1的Update方法中,只在调用base.Update之前:
18 // See if the player has fired a shot
19 FireShots(gameTime);
                      从本质上讲,你可以使用武器发射只有当shotCountdown已经达到或者超过0的时候。如果在这种情况下,
你检查看空格键或者鼠标左键是否被按下。如果他们中的一个被按下,你已经准备好去发射一个子弹,噎呼....准备。。。孩子们,这是已经非常激动了!
                       首先,你调用ModeManager的AddShot方法,它是前面添加的,它添加子弹到子弹列表中。为了这个位置参数,你传入相机的位置,但是你同样添加一个向量到推动向量朝着负Y轴的位置。这是因为如果你发射了子弹从真实的相机位置,你可以看见巨大圆球在这个屏幕上,正好是模型将要远离相机的位置。为了防止这一点,并提供更切合实际的效果,你添加一些负Y到这个位置,这样你的子弹将会出现来自相机的下面。
                      子弹的方向由相机的方向乘以shotSpeed变量组成的。
                      一旦子弹被添加,你分配shotDelay变量的值到shotCountdown变量,为防止另外一次射击从开始射击到shotCountdown变量再一次达到0时。
                      如果shotCountDown不是0或者小于它在调用Update期间,在FireShots方法中声明的if语句将评估为false并且shotCountdown变量通过共用时间被消耗掉在声明的else部分。
                      编译并运行你的游戏,你应该看到子弹被发射当你按下空格键或者鼠标的左键(如图12-2)。同样,注意到一直按下空格键或者鼠标左键导致子弹每300毫秒发射一次(或者相等的发射延迟时间)。谁需要虚幻比赛,当你有这种好的游戏?
                      好了,伟大的工程做了这一点。飞船正在飞行并且子弹看上去很大。这有一个问题:当子弹和飞船相撞时不需要做任何事。是啊,是啊,是啊...小的细节,对不对?它无论如何都值得去修理。让我们看看你如何添加一些碰撞检测使用XNA的BoundingSphere类。
3D Collision Detection and Bounding Spheres
                      在本书的2D部分,我们涵盖了包围盒算法的碰撞检测。从本质上讲,该算法使用一种无形的框,周围的一个对象,被用来检测是否另外一个对象的合子与它碰撞。该包围盒算法是最快的一个碰撞检测算法。
                     一个类似的方法是使用spheres(译者:球体)包围这个对象,而不是一个盒子。同样的概念也适用-你检查一个对象的球体去检测它是否与另一个对象的圆球发生了碰撞。
                      当你使用模型在XNA中,为你生成的包围球体作为了模型的一部分。每一个模型包含一个或多个ModelMesh对象,每一个ModelMesh有一个属性称为BoundingSphere,它定义一个球体包围模型的部分。

                      这是棘手的部分,当你应用一个平移和缩放到这个模型,模型的BoundingSphere没有受到影响。所以,去使用指定的BoundingSphere在这个模型中,你不得不应用同样的平移和缩放它,你应用它到你的模型上。
                      要做到这一点,你可以添加一个碰撞检测方法在BasicModel类里,你也会同样传递到另一个BasicModel的模型,世界碰撞检测所有它的ModelMesh的BoundingSphere.
                      这种碰撞检测方法将得到一个不同的模式和世界矩阵在其参数列表。这个方法将循环通过它自己的所有ModelMeshes然后循环通过另外Model的ModelMeshes和检测它自己的ModelMesh的BoundingSphere与另外一个模型的BoundingSpheres的碰撞.在比较每一个BoundingSpheres之前,这个方法将不得不应用world矩阵到BoundingSphere使用这个Transform方法.这将移动和旋转BoundingSphere到相同的地方,你已经准备移动和旋转它的模型了.
注意:虽然它是非常重要的应用任何平移或缩放操作,它已经应用到模型的world矩阵到这BoundingSphere,任何旋转应用到模型不需要应用到BoundingSphere.这是因为BoundingSphere是一个球体:任何旋转应用到它不会影响它的形状.也就是说,它不会做任何伤害应用平移动BoundingSphere.
                      添加下面的方法到你的BasicModel类:
 1 public bool CollidesWith(Model otherModel, Matrix otherWorld)
 2 {
 3    // Loop through each ModelMesh in both objects and compare
 4    // all bounding spheres for collisions
 5    foreach (ModelMesh myModelMeshes in model.Meshes)
 6    {
 7            foreach (ModelMesh hisModelMeshes in otherModel.Meshes)
 8            {
 9               if (myModelMeshes.BoundingSphere.Transform(GetWorld( )).Intersects(hisModelMeshes.BoundingSphere.Transform(otherWorld)))
10               return true;
11            }
12    }
13     return false;
14 }
                     这真的就是这么回事。不是太寒酸,不是吗?现在,您将不得不添加一些代码到您的ModelManager处理碰撞。
                     在你的ModelManager类的UpdateShots方法,你已经准备添加一些代码去删除子弹当它们飞出了范围,使用这个shotMinZ变量作为标尺.改变你的if声明,用if/else检测飞出范围的子弹.在else声明的部分,调用子弹的CollidesWith方法,传入这个方法,每一个敌人飞船的world矩阵在一个循环中.如果一个碰撞被检测到,删除飞船和子弹并推出循环.
                     改变UpdateShots方法:
 1 protected void UpdateShots( )
 2 {
 3    // Loop through shots
 4    for (int i = 0; i < shots.Count; ++i)
 5    {
 6        // Update each shot
 7        shots[i].Update( );
 8        // If shot is out of bounds, remove it from game
 9        if (shots[i].GetWorld( ).Translation.Z < shotMinZ)
10        {
11             shots.RemoveAt(i);
12             --i;
13         }
14     }
15 }
                      到这里:
 1 protected void UpdateShots( )
 2 {
 3    // Loop through shots
 4    for (int i = 0; i < shots.Count; ++i)
 5    {
 6        // Update each shot
 7        shots[i].Update( );
 8        // If shot is out of bounds, remove it from game
 9        if (shots[i].GetWorld( ).Translation.Z < shotMinZ)
10        {
11             shots.RemoveAt(i);
12             --i;
13         }
14         else
15         {
16              // If shot is still in play, check for collisions
17              for (int j = 0; j < models.Count; ++j)
18              {
19                   if (shots[i].CollidesWith(models[j].model,models[j].GetWorld( )))
20                   {
21                       // Collision! remove the ship and the shot.
22                       models.RemoveAt(j);
23                       shots.RemoveAt(i);
24                       --i;
25                       break;
26                    }
27                }
28      }
29 }
                      嘣!你有使用BoundingSpheres的碰撞检测!在这时运行这个游戏,向你的敌人射击.你可以发现它很难去跟踪敌人在哪里产生,保持你的相机面对这个方向(译者:敌人产生的方向).我们会立刻固定它.同样,你可以找到碰撞检测,因为它的当前执行非常好,或者你可以发现它非常forgiving,或者不够forgiving.你可以调整BoundingSphere的大小通过应用一个统一标准,除此之外world矩阵去修改大小,它将使碰撞检测或多或少forgiving.
                      另外,如果你发现飞船太快,太难被击中,你可以调整飞船的速度,使用minSpeed和maxSpeed变量在LevelInfo对象的表单.这是你的游戏,你想让它按照你喜欢的方式运行,所以你可以真正调整您想要的以符合您的需求。
添加一个十字准心
                      一件事你可能注意到你的游戏在这一点上缺少东西,是一种方式能保证你瞄向正确的方向.一般来说,这个实现通过布置一个十字准心在屏幕上.即使你工作在XNA3D中,你仍然可以绘制2D精灵在屏幕上,正如你在这本书前面做的.绘制十字准心在屏幕上用2D方法,而不是用3D在world中,使它更容易工作.想象一下如果你想让它用3D绘制在世界中,你可以把它绘制在相机前面的几个单位,每一次你的相机旋转或者移动,你因此不得不调整十字准心.
                      当你绘制一个十字准心在屏幕上,你基本上是"设置它然后忘记它",但是,这并不是讨厌... 这是很酷的东西。虽然...如果您有兴趣.
                      首先,你需要添加一个十字准心的图片到你的工程中.开始,创建一个新的文件夹在你解决方案的Content节点下,右击解决方案的浏览器的Content节点,选择Add-New Folder,然后命名这个文件夹Texture.
                      这章的源代码是一个文件称为:crosshair.png在3D Game\Content\Textures文件夹.添加文件到你的项目中通过右击Content\Textures文件夹在解决方案,选择Add-Existing Item...,浏览到crosshair.png文件,然后选择它.
                      接下来,在你的Game1类中,你需要添加一个Texture2D 类别变量去保存你的图象:
1 Texture2D crosshairTexture;
                      在你的Game1类的LoadContent方法中,你需要加载crosshair.png图象到你的新crosshairTexture变量通过内容管道:
1 crosshairTexture = Content.Load<Texture2D>(@"textures\crosshair");
                     现在,所以你不得不做的是绘制你的纹理在屏幕上,使用提供的spriteBatch.你要使用的屏幕尺寸和大小的纹理中心十字正是在屏幕中间(稍微偏离中心一点效果怎么样呢?).你添加代码去绘制十字准心到Game1类的Draw方法中,在调用base.Draw的后面.
                     添加下面的代码在Game1类的Draw方法的后面:
1 spriteBatch.Begin( );
2 spriteBatch.Draw(crosshairTexture,new Vector2((Window.ClientBounds.Width / 2)- (crosshairTexture.Width / 2),(Window.ClientBounds.Height / 2)- (crosshairTexture.Height / 2)),Color.White);
3 spriteBatch.End( );
在base.Draw后面绘制?
                    为什么你需要添加代码去绘制十字准心在调用base.Draw的后面使用呢?
                    你现在混合两个不同的绘制方法(2D相对3D).记住用3D绘制就好象围绕着摄象机走,同时用2D绘制更象绘制在2D画布上.无论哪一个绘制方法被使用,第二种方法都会取代第一种.
                    因为你绘制所有你的模型用ModelManager,它是一个GameComponent,所有绘制实际上都不会发生,直到base.Draw方法被调用.base.Draw调用会抽取Draw调用通过所有的DrawableGameComponents.你不得不把你的十字准心代码放在base.Draw调用的后面,去保证你的十字准心总是在所有3D模型后被绘制,只有这样才会使它在3D模型上.

                    好,现在运行这个游戏,看看,添加一个十字准心能否帮你瞄准.你会看到十字准心在屏幕的中心,如图12-3所示.
添加声音
                    另外一件事,你游戏真正缺少的是气氛和活力,这个声音能给予.在这节,你会添加一些音效和音乐到你的游戏中.

                    在Visual Studio中,使用解决方案去添加一个新文件夹在Content节点下命名为Audio(右击这Content文件夹,选择Add-New Folder).
                     微软推出的跨平台音频创作工具( XACT )通过点击您的Windows开始按钮,选择所有程序➝微软XNAGame 工作室3.0 ➝工具➝微软跨平台音频创作工具( XACT ) 。
                     在XACT中,选择File-New Project,导航到你的工程的Content\Audio目录,保存文件称为GameAudio.xap.一旦你的工程准备好,创建一个新wave bank和一个新sound bank通过右击在Wave Banks和Sound Banks树形选项在它的左边,分别选择New WaveBank和New Sound Bank.接受wave和sound banks默认的名字.一旦都被创建,选择Window-Tile Horizontally,使你的窗口更容易使用.

                     这里有6个.wav文件在本章源代码3D Game\Content\Audio文件夹.复制这些文件到你的工程的Content\Audio文件夹在Window Explorer.下面是文件:
        1.Explosion1.wav
        2.Explosion2.wav
        3.Explosion3.wav
        4.Shot.wav
        5.Track1.wav
        6.Track2.wav
                     再次,请记住当处理音频用XACT,不需要添加一个实际的.wav文件到你的工程中;你只需要复制这个文件到工程的Content\Audio文件夹在Windows Explorer中.添加这些文件到一个XACT工程中,这个工程文件只是一个音频文件,你可以添加到工程中.
                     在Wave Bank窗口,右击在空的部分,选择Insert Wave File(s)....浏览到six.wav文件,然后添加他们中的每一个到你的wave bank.这个文件用红色显示在您的Wave Bank窗口中,表明他们目前没有使用(如图12-4)

                     拖曳影像声音从Wave Bank窗口到Sound Bank窗口的Cue Name中,这将创造一个cue和一个sound为Shot在Sound Bank 窗口中,如图12-5图所显示.
                     下一步,你准备创建一个单一的cue,将播放双方的声音文件,分配50%的机会给它们.这将允许你去调用特定的cue从代码中,使它有50%的机会去播放其中的音频.
                     要做到这一点,拖动Track1.wav和Track2.wav文件从Wave Bank窗口到Sound Bank窗口的Sound Name部分.直到现在,我总是告诉你去保证,你拖放声音到Sound Bank窗口的Cue Name部分.在这种情况下,虽然,你想拖放它们到Sound Name部分因为你不想创建cue名字为这些声音中的每一个,您将为这两个声音创建一个cue名字.

                     接下来,右击在Sound Bank窗口的Cue Name部分的某个空白地区,并且选择New Cue.一个新的cue name将会创建用这个"New Cue"名称.选择新的cue,点击F2去重命名它,命名新的cue轨道。
                     下一步,拖动Track1和Track2音频从Sound Bank窗口的Sound Name部分,并且拖拉它们到新的Tracks cue,你已经创建它在Sound Bank窗体的Cue Name的地方.这将添加两个音频到这个新Tracks cue,再给它们每个50%机会播放.基本上什么在这里发生的是当你播放Trancks cue,它联合cue将播放这些音频中的一个,选择哪一个音频播放是基于概率在这里列出.
                     你的Sound Bank窗口如果图12-6所示.
                     你现在需要对爆炸音频做同样的事情.你加载了三个爆炸音频在你的wave bank.从Wave Bank窗口拖拉所有三个爆炸音频,然后放到Sound Bank窗口的Sound Name部分.
                     右击在Sound Bank窗口的Cue Name的某个地方,选择New Cue.选择新的cue然后按F2去重命名它,命名为新的cue Explosions.
                     然后,拖拉每个爆炸的声音从Sound Bank窗口的Sound Name部分,放他们到new Explosions cue在Sound Bank窗口的Cue Name部分.注意每一个爆炸的声音会自动的指派33%的概率去播放,当Explosions cue被播放时(译者:Explosions cue是这三个音频文件的组合).
                     你的Sound Bank 窗口如图12-7所示.
                     在关闭XACT之前,你想要设置Track1和Track2音频的循环属性去无限循环.首先,选择Track1音频在Sound Bank窗口的Sound Name部分.然后,在屏幕的左下角,点击单击无限复选框中的循环,正如图12-8所示.以及为Track2重复此步骤。
                     你已经完成了XACT,所以保存你的项目,关闭XACT,回到Visual Studio.
                     在Visual Studio中,你需要添加.xap文件从XACT到你的工程中.右击Content\Audio文件夹在解决方案中,选择Add-Existing Item....,浏览到你的GameAudio.xap文件(它应该被保存在你的工程的Content\Audio文件夹中).添加文件到你的解决方案通过浏览到它并选择它.
                     一旦.xap文件是你解决方案的一部分,你需要添加一些代码到你的Game1类.添加下面的类别变量:
1 AudioEngine audioEngine;
2 WaveBank waveBank;
3 SoundBank soundBank;
4 Cue trackCue;
                     下一步,你需要初始化这些变量通过内容管道.加载这些音频从你的XACT工程,通过添加下面的行到Game1类的LoadContent方法.
1 // Load sounds and play initial sounds
2 audioEngine = new AudioEngine(@"Content\Audio\GameAudio.xgs");
3 waveBank = new WaveBank(audioEngine, @"Content\Audio\Wave Bank.xwb");
4 soundBank = new SoundBank(audioEngine, @"Content\Audio\Sound Bank.xsb");
5 trackCue = soundBank.GetCue("Tracks");
6 trackCue.Play( );
                     前面的三行加载声音和cues从XACT文件,同时第四行设置trackCue变量通过重新得到的cue为你的音轨.这样你可以暂停/恢复/停止/重新启动音轨音乐.其他音效不需要被这种方式操纵,所以你不需要保存这个cue信息为这些音频中.
                     最后,音轨cue被前面代码的最后一行播放.
                     为了允许你的ModelManager去播放音频,当碰撞发生的时候,添加下面的方法到你的Game1类:
1 public void PlayCue(string cue)
2 {
3      soundBank.PlayCue(cue);
4 }
                      在heading这个ModelManager,你需要调用这PlayCue方法从这个Game1类的FireShots方法,当一个子弹被发射时.为了实现这个,立即添加这一行在调用modelManager.AddShot的后面:
1 // Play shot audio
2 PlayCue("Shot");
                      在ModelManager,你需要添加一行代码,它将调用PlayCue方法当一个碰撞发生时.在你的ModelManager的UpdateShots方法,找到if/else声明,它检测是否有一个碰撞发生了.当前的这段代码看上去就象这样:
1 if (shots[i].CollidesWith(models[j].GetModel,models[j].GetWorld( )))
2 {
3     // Collision! Remove the ship and the shot.
4     models.RemoveAt(j);
5     shots.RemoveAt(i);
6      --i;
7      break;
8 }
                       添加一个调用到Game1类的PlayCue方法就象之前的break关键字,它跳出for的循环:
1 if (shots[i].CollidesWith(models[j].GetModel,models[j].GetWorld( )))
2 {
3      // Collision! Remove the ship and the shot.
4      models.RemoveAt(j);
5      shots.RemoveAt(i);
6      --i;
7     ((Game1)Game).PlayCue("Explosions");
8      break;
9 }
                       好的!现在开始游戏吧,看看音频制造了多少差异.你可以用不同的声音来表示不同的关卡.爆炸声音和子弹声音看上去有一点弱,我想使它变大一点点.记住从第5章,你可以通过选择声音的名字在XACT里去实现它,编辑它音量属性在窗口的左下角.以个人喜好,不过,您可以调整任何声音以你喜欢的方式。
源代码:http://shiba.hpe.cn/jiaoyanzu/WULI/soft/xna.aspx?classId=4
(完)

posted on 2009-08-21 13:12  一盘散沙  阅读(656)  评论(0编辑  收藏  举报

导航