在 XNA 中创建简单的 3D 游戏引擎/第一章
XNA 4 引擎第一章 我假设您已经准备好 XNA 4 以供使用,如果没有,请转到此处以了解您需要什么。我拥有本教程所需的资源,除非您已经制作了自己的太空岩石和宇宙飞船以及弹药。您可以在这里下载所有模型的压缩文件。在每个章节的末尾,我都会提供到目前为止完成的项目,供您下载,无需帮助。帮助将在页面底部的单独压缩文件中提供,我将在适当的时候解释解压缩的位置。您可以在此视频中看到最终的运行游戏。它将是您完成我五章系列的第一部分的样子,这五章将成为亚马逊上的书籍。我们将创建和规划我们的项目。组织极其重要。除非您喜欢撞头,否则您会同意的。首先创建一个新的 XNA 4 Windows 游戏项目,将其命名为 Asteroids,如图 1 所示。
点击“确定”后,您应该看到如图 2 所示的内容。
很好,现在我们组织项目文件夹。首先,按照图 3 所示,在项目中添加 Entities 文件夹,然后添加 Engine 文件夹。
然后,将 Models 和 Textures 文件夹添加到项目的 Content 部分。您应该看到这两个文件夹,如图 4 所示。
项目文件夹应该设置为如图 5 所示的样子。
目前,我们只会在 Engine 文件夹中工作。即使我们直到后面的章节才会使用其他文件夹,但我喜欢提前计划;到目前为止,这对我来说效果很好。
现在,我们将准备 Game1 类。首先,让我们使游戏窗口的大小合理。到目前为止,每个人都应该有一个可以处理它的屏幕,我知道您一定有一个不太旧的显卡,或者旧技术,否则 XNA 4 不会编译,除非您使用 Reach 受限 API,关于这方面的内容将在后面的章节中介绍。我的引擎可以在任何一种模式下运行,Reach 模式主要启用了一些限制器。
您应该仍然打开了 Game1.cs;在构造函数中,从清单 1.1 添加以下行。
清单 1.1 game1 编辑
Window.Title = "Asteroids 3D in XNA 4"; graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 600; graphics.ApplyChanges();
现在,从清单 1.2 中删除以下行。
清单 1.2 game1
graphics = new GraphicsDeviceManager(this);
现在您应该看到如图 6 所示的内容。
这使得它成为第一件事,它调整了任何想要玩游戏的人都可以很好地看到它的屏幕大小,以便他们享受它。1024 x 600 只是略低于 WSVGA 的高度。这为任何至少拥有 SVGA 的玩家在窗口中留出空间,我们假设任何拥有能够运行使用 XNA 制作的游戏的 PC 的玩家都应该拥有 SVGA。默认的 VGA 600 x 480 对我们的目的来说太小了。在设置窗口的高度和宽度的后备缓冲区后,您必须应用更改才能使其生效。这是游戏玻璃的一个内置方法,它按其命名执行。当您设置属性时,它只会更改该类中的这些变量。因此,为了使这些更改生效,您必须将它们全部应用,即使您只更改了一些。到最后,这个游戏将是分辨率无关的。这只是暂时的。在后面的章节中,我们将看到如何查看它正在运行的计算机能够处理什么,并使用它来设置默认屏幕尺寸。现在是开始编写引擎类的时候了。首先,我们需要创建一个新类。将其命名为 Services;将其添加到 engine 文件夹中,如下面的图 7 所示,右键单击 Engine 文件夹,选择 Add,然后单击“class…”,您将看到图 8 中的内容。
现在您应该看到如图 9 所示的内容。
现在,我们需要添加另一个新类,Services 类将使用它,即 Camera 类,如下所示。我们需要接下来添加它,因为如果没有它,我们就无法构建使用它的 Services 类。因此,我们将从 Camera 类开始,然后继续处理 Services 类。它应该看起来如图 10 所示。然后,在您点击“确定”后,您应该看到如图 11 所示的内容。
首先,我们需要添加一些 using 语句,我同样喜欢对它们使用区域,如下面的清单 2.1 所示:清单 2.1 Camera.cs 添加
#region Using using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; #endregion
我删除了 System.Collections.Generic、System.Linq 和 System.Text 命名空间,因为它们不在此类中使用。我们需要使它成为一个公共类,因此在 class Camera 前面加上 public,如屏幕截图所示。为了使其能够将引擎复制到任何项目中,我们将创建命名空间 Engine,因此将其更改为清单 2.1.1 中的样式,清单 2.1.1 Camera.cs 编辑命名空间
namespace Engine
接下来,我将让您添加所有变量,或者我更喜欢称它们为字段。我同样喜欢使用区域,因此我同样让您添加它们。按照清单 2.2 中的步骤添加类级字段:清单 2.2 Camera.cs 添加
#region Fields private Matrix cameraRotation; #endregion
这是对 Matrix 类的突然介绍,它是 XNA 库的一部分,我甚至不确定它是如何工作的;对我来说,它就像一种魔法。但是,您无需了解它是如何工作的,即可使用它。就像您无需了解汽车是如何工作的,即可使用它一样。转到此处以了解更多关于 Matrix 魔法的信息。
接下来,我将让您添加该类所需的属性,如下面的清单 2.3 所示。
清单 2.3 Camera.cs 添加
#region Properties public Matrix View { get; set; } public Matrix Projection { get; protected set; } public Vector3 Target { get; set; } #endregion
您会注意到 View 和 Projection 是 Matrix 类型,而 target 是 Vector3 类型。这一点非常重要。相机的工作原理有点像投影仪,就好像它所看到的内容被投影到您的屏幕上一样。当我们回到这个类时,我会对此进行更多解释。
我们只需要添加最后一件东西,以便在清单 2.4 中完成它。
清单 2.4 Camera.cs 添加
#region Constructor #endregion
到目前为止,您应该看到清单 2.5 和图 12 中的内容。
清单 2.5 Camera.cs 到目前为止
#region Using using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; #endregion namespace Engine { public class Camera : PositionedObject { #region Fields private Matrix cameraRotation; #endregion #region Properties public Matrix View { get; set; } public Matrix Projection { get; protected set; } public Vector3 Target { get; set; } #endregion #region Constructor #endregion } }
在我们继续之前,我们需要创建一个新类,即 Positioned Object 类。因为相机也将使用这个类,它跟踪所有事物的位置,以及将要处于的位置。尽管这个类很小,但它非常强大。它使用游戏计时器来跟踪时间,因此可以应用于移动。这使得无论运行游戏的计算机速度如何,所有事物都能平滑地移动。我对此再怎么强调也不为过,绝不要使用游戏循环来影响事物的移动速度。即使这在 80 年代使用过,当时大多数计算机的速度都相同,但这是一种不好的做法。后来的游戏无法在较新的计算机上运行,因为它们运行得太快了。不仅如此,它还使游戏的移动速度取决于每帧的处理速度。您肯定喜欢平滑的事物,就像我一样。因此,我们创建 Positioned Object 类,在 engine 文件夹中名为 PositionedObject.cs。我假设您现在知道如何创建新类了。首先,我们处理 Using 命名空间,如下面的清单 3.1 所示。
清单 3.1 PositionedObject.cs 添加
#region Using using System; using Microsoft.Xna.Framework; #endregion
现在,我们需要像对 Camera 所做的那样,将其设为公共类,在 class 前面添加 public。大多数类将是游戏的公共类,并在冒号后面的末尾添加 DrawableGameComponent,这是继承的类。我们使用该类的原因是,将继承此类的游戏对象将被绘制,draw 方法不需要在该类中。如果按照这种方式进行,游戏中继承此类的每个对象都将自动调用其 update 和 draw 方法,每帧调用一次。现在,有些人认为“那么您就无法控制它们的调用顺序”。这种假设是不正确的。您实际上可以控制顺序,不仅如此,而且您拥有比平时更多的控制权。有一个用于更新顺序和绘制顺序的属性,可以随时更改它们。默认情况下,顺序是按照实例化的顺序进行的。同样,如果您不想每帧都调用 Draw 方法,请使用 Visible 属性,如果您不想每帧都调用 Update 方法,请使用 Enabled 属性。就这么简单。仅仅因为您不知道如何使用某些东西,并不意味着它无法做到。这是库中内置的原因,应该使用所有可用的工具。我打算纠正这种谬误。在我们完成之前,您会亲吻这些 Component 类,如果您像我一样的话。
我们需要在这个类上更改命名空间,就像在清单 3.1.1 中一样。
清单 3.1.1 PositionedObject.cs 编辑命名空间
namespace Engine
现在类行应该看起来像清单 3.2 中的样式。
清单 3.2 PositionedObject.cs 添加
public abstract class PositionedObject : DrawableGameComponent
这使得一个类只能被继承,而不能被实例化。这应该作为最佳实践在所有基类上进行。
现在,我们添加您在清单 3.2.5 中看到的字段。
清单 3.2.5 PositionedObject.cs 添加
#region Fields private float frameTime; // Doing these as fields is almost twice as fast as if they were properties. // Also, sense XYZ are fields they do not get data binned as a property. public Vector3 Position; public Vector3 Acceleration; public Vector3 Velocity; public Vector3 RotationInRadians; public Vector3 ScalePercent; public Vector3 RotationVelocity; public Vector3 RotationAcceleration; #endregion
现在,您可能想知道为什么我们不为所有这些使用属性,事实证明,使用属性比使用字段要慢。这是 Vector3 不使用属性的相同原因。此类每帧运行一次,因此速度极其重要,每毫秒都算,因为屏幕上的每个对象都将使用此类。
现在,我希望您添加如下面的清单 3.3 所示的构造函数。
清单 3.3 PositionedObject.cs 添加
#region Constructor /// <summary> /// This gets the Positioned Object ready for use, initializing all the fields. /// </summary> /// <param name="game">The game class</param> public PositionedObject(Game game) : base(game) { } #endregion
这里我们只需要将游戏类传递给组件类。
接下来是执行所有工作的那个方法。请注意 override,如果您已经了解它的作用,请继续;如果您不了解,请继续阅读。已经有一个相同名称的方法,它是一个虚方法,我们重写它。当该方法被调用时,它会一直向上追溯到这个类,并由组件类自动调用;然后,我们向下追溯到基类以调用它。我们将在此处停止,并稍后回来。update 方法使用一个需要在 Services 类中创建的方法。现在它应该看起来像清单 3.5 中的那样。
清单 3.5 PositionedObject.cs 到目前为止
#region Using using System; using Microsoft.Xna.Framework; #endregion namespace Engine { public class PositionedObject : DrawableGameComponent { #region Fields private float frameTime; // Doing these as fields is almost twice as fast as if they were properties. // Also, sense XYZ are fields they do not get data binned as a property. public Vector3 Position; public Vector3 Acceleration; public Vector3 Velocity; public Vector3 RotationInRadians; public Vector3 ScalePercent; public Vector3 RotationVelocity; public Vector3 RotationAcceleration; #endregion #region Constructor /// <summary> /// This gets the Positioned Object ready for use, initializing all the fields. /// </summary> /// <param name="game">The game class</param> public PositionedObject(Game game) : base(game) { game.Components.Add(this); } #endregion #region Public Methods #endregion } }
注意图 13 中我仍然打开了所有类。我们将来会用到它们,所以您可能也希望将它们保持打开状态,以便您可以轻松地在它们之间切换选项卡。
现在我们回到 Services 类,开始处理它。您可能会发现自己经常这样做,在不同的类之间切换,因为它们相互依赖,并且您应该等到创建了计划添加的部分后再添加它们。现在,我将首先让您更改类行,这是一个特殊的类,称为单例类。为了确保它无法被调用或初始化,我们将使用 sealed 关键字,如清单 4.1 中所示更改该行。
清单 4.1 Services.cs 编辑 public sealed class Services 我们还需要像清单 4.1.1 中那样更改此类中的命名空间。清单 4.1.1 Services.cs 编辑命名空间 namespace Engine 接下来,我们按照清单 4.2 中的步骤添加字段:清单 4.2 Services.cs 添加
#region Fields private static Services instance = null; private static GraphicsDevice graphics; private static Random randomNumber; #endregion
我们将确保此类在生命周期中只能存在一次,因此我们有 Services 实例行。稍后会检查它以确保它只能有一个实例。只能有一个!这里有我们的类将提供访问权限的服务,即相机、图形和随机数生成器。图形就是图形,您看到的所有内容。这里我们有一个随机数生成器,您必须确保只使用一个,还有什么比将它放在此类中更好的方法呢?
现在,我将让您添加所有属性,并且有几个。按照清单 4.3 中的步骤添加它们。
清单 4.3 Services.cs 添加
#region Properties /// <summary> /// This is used to get the Services Instance /// Instead of using the mInstance this will do the check to see if the Instance is valid /// where ever you use it. It is also private so it will only get used inside the engine services. /// </summary> private static Services Instance { get { //Make sure the Instance is valid if (instance != null) { return instance; } throw new InvalidOperationException("The Engine Services have not been started!"); } } public static Camera Camera { get { return camera; } } public static GraphicsDevice Graphics { get { return graphics; } } public static Random RandomNumber { get { return randomNumber; } } /// <summary> /// Returns elapsed seconds, in milliseconds. /// </summary> /// <returns>double</returns> /// <summary> /// Returns the window size in pixels, of the height. /// </summary> /// <returns>int</returns> public static int WindowHeight { get { return graphics.ScissorRectangle.Height; } } /// <summary> /// Returns the window size in pixels, of the width. /// </summary> /// <returns>int</returns> public static int WindowWidth { get { return graphics.ScissorRectangle.Width; } } #endregion
我希望您不会觉得一次添加所有这些内容让人不知所措。这里有很多事情要做,我们将在稍后使用它们。请记住,提前计划!如您所见,第一个是实例检查器,我在摘要中对此进行了解释。接下来是相机、图形、游戏时间和随机数生成器的访问属性。然后,我们具有用于访问窗口高度和宽度的属性。请注意,Camera 具有私有 set 访问器,这是因为我们在初始化时从 Game 类中传入相机引用,如您将在到达该方法时看到的那样。这是我们唯一允许访问以设置主相机的相机引用的时间。
接下来,我们按照清单 4.4 中的步骤添加构造函数。
清单 4.4 Services.cs 添加
#region Constructor /// <summary> /// This is the constructor for the Services /// You will note that it is private that means that only the Services can create itself. /// </summary> private Services(Game game) { } #endregion
您会注意到它是私有的;这使得您无法在自身之外对其进行实例化。您会问,为什么要这样做?这就是您创建单例类的方式。请记住,只能有一个。目前,我们将在此类上进行的所有工作就是这些。在我们进一步研究之前,我们需要再次回到 Camera 类。
这是您目前应该具有的内容,如清单 4.5 所示。
清单 4.5 Services.cs 目前
#region Using using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Content; #endregion namespace Engine { public sealed class Services { #region Fields private static Services instance = null; private static GraphicsDevice graphics; private static Random randomNumber; #endregion #region Properties /// <summary> /// This is used to get the Services Instance /// Instead of using the mInstance this will do the check to see if the Instance is valid /// where ever you use it. It is also private so it will only get used inside the engine services. /// </summary> private static Services Instance { get { //Make sure the Instance is valid if (instance != null) { return instance; } throw new InvalidOperationException("The Engine Services have not been started!"); } } public static Camera Camera { get; private set; } public static GraphicsDevice Graphics { get { return graphics; } } public static Random RandomNumber { get { return randomNumber; } } /// <summary> /// Returns elapsed seconds, in milliseconds. /// </summary> /// <returns>double</returns> /// <summary> /// Returns the window size in pixels, of the height. /// </summary> /// <returns>int</returns> public static int WindowHeight { get { return graphics.ScissorRectangle.Height; } } /// <summary> /// Returns the window size in pixels, of the width. /// </summary> /// <returns>int</returns> public static int WindowWidth { get { return graphics.ScissorRectangle.Width; } } #endregion #region Constructor /// <summary> /// This is the constructor for the Services /// You will note that it is private that means that only the Services can only create itself. /// </summary> private Services(Game game) { } #endregion #region Public Methods #endregion } }
好吧,您知道我会这样做的,回到 Camera 类!我希望在本章结束时,您将能够理解为什么我一直这样来回切换。您可能需要像我一样,喝很多咖啡。在 Camera 类准备进行编辑后,我们将添加它将继承的第一个类。因此,按照 5.1 中的步骤更改类行。
清单 5.1 Camera.cs 编辑
public class Camera : PositionedObject
我们还需要像清单 5.1.1 中那样更改此类中的命名空间。
清单 5.1.1 Camera.cs 编辑命名空间
namespace Engine
现在我们可以添加构造函数,并且此构造函数做了很多事情,因此请仔细阅读清单 5.2 中的步骤。
清单 5.2 Camera.cs 编辑
#region Constructor public Camera(Game game, Vector3 position, Vector3 target, Vector3 rotation, bool Orthographic, float near, float far) : base(game) { Position = position; RotationInRadians = rotation; Target = target; if (Orthographic) { Projection = Matrix.CreateOrthographic(Game.Window.ClientBounds.Width, Game.Window.ClientBounds.Height, near, far); } else { Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, (float)Game.Window.ClientBounds.Width / (float)Game.Window.ClientBounds.Height, near, far); } } #endregion
第一行非常重要,您会看到它调用了游戏组件集合。这就是 Game Component 如何跟踪要更新哪个类以及何时更新。默认情况下,它们按添加顺序进行更新。因此,每个属于 Positioned Object 或 Game Component 的类都需要像这样添加,否则它将不会被更新。这是您唯一需要注意的事情。我会让您轻松完成,当我们到达那里时,我会让您创建一个通用敌人类,除了玩家之外的所有敌人都会继承它,它将包含该行,因此您无需记住每次都将其放在那里。只有玩家以及任何不是独立类的对象的类需要它。
现在您可以看到矩阵是多么神奇。现在我们有了 Positioned Object 类,我们可以从它继承,因此我们可以使用它来跟踪运动及其正确位置。由于相机的运作方式,我们必须将位置转换为它理解的东西。我们使用库中内置的方法来实现这一点!我们这里还使用了一个 switch 语句,以防我们想要将其用于正交相机。这将用于菜单、HUD 或使用精灵的 2D 游戏。这是一个 3D 引擎,因此默认相机不是正交相机。当我们回到 Services 类时,您会看到这一点。其余部分只是传递给 Position 和 Rotation,并且我们之前添加了 Target Vector3。当您实例化另一个相机时,您将告诉它该怎么做,所有这些都是传入的,如您所见。此外,您会看到 near 和 far float 类型字段,它告诉它渲染对象的距离范围。所有这些都在矩阵中计算,如您所见。现在我们继续进行公共方法,第一个是 Initialize 方法,如清单 5.3 中所示。
清单 5.3 Camera.cs 编辑
#region Public Methods /// <summary> /// Allows the game component to perform any initialization it needs to before starting /// to run. This is where it can query for any required services and load content. /// </summary> public override void Initialize() { base.Initialize(); cameraRotation = Matrix.Identity; } #endregion
Matrix.Identity 属性是一个空白但非空的矩阵,就像创建一个值为 0 的新 int 一样,例如 int i = 0;因此您会看到,它并没有做太多事情,但它很方便。我希望您还记得我们在另一个类中之前执行的相同方法中的其余部分。接下来我们有 Update 方法,在清单 5.4 中看到的相同区域内添加如下内容。
清单 5.4 Camera.cs 编辑
/// <summary> /// Allows the game component to update itself via the GameComponent. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> public override void Update(GameTime gameTime) { base.Update(gameTime); // This rotates the free camera. cameraRotation = Matrix.CreateFromAxisAngle(cameraRotation.Forward, RotationInRadians.Z) * Matrix.CreateFromAxisAngle(cameraRotation.Right, RotationInRadians.X) * Matrix.CreateFromAxisAngle(cameraRotation.Up, RotationInRadians.Y); // Make sure the camera is always pointing forward. Target = Position + cameraRotation.Forward; View = Matrix.CreateLookAt(Position, Target, cameraRotation.Up); }
我希望您已准备好上手实践,因为您正在查看为相机完成工作的代码。这在每一帧都会被调用,但所有这些都必须在每一帧上完成。Matrix 具有您在 3D 空间中可能需要的每个方法。为了在 3D 空间中旋转某物,使用 Matrix 来确定您想要如何旋转。当您按该顺序将旋转的 X、Y 和 Z 相乘时,它会使之成为现实。请注意 cameraRotation.Forward、Right 和 Up,这样它就知道您希望如何计算。每个都对应于我们希望它如何显示出来,相对于投射到屏幕上的相机的向上、向右和向前方向。在 3D 空间中旋转物体可能会非常复杂,因此 XNA 使其尽可能简单。当我们回到 Positioned Object 类时,您将看到我们是如何使该方法在每一帧自动调用。接下来,我们添加 Draw 方法,这将是您第一次看到此方法。请记住,正如一本伟大的书所说,“不要惊慌”。您将在相同区域内添加此方法,紧随 Update 之后,如清单 5.5 中所示。
清单 5.5 Camera.cs 编辑
public void Draw(BasicEffect effect) { effect.View = View; effect.Projection = Projection; }
现在您会注意到没有覆盖。这是因为继承的类没有 Draw 方法。如果您还记得,Positioned Object 类使用了 Draw Component。这意味着 Draw 方法将在每一帧自动调用。这样就完成了我们的 Camera 类。一个类完成,还有两个要完成。
这是完成的 Camera 类,如清单 5.6 所示。
清单 5.6 Camera.cs 目前
#region Using using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; #endregion namespace Engine { public class Camera : PositionedObject { #region Fields private Matrix cameraRotation; #endregion #region Properties public Matrix View { get; set; } public Matrix Projection { get; protected set; } public Vector3 Target { get; set; } #endregion #region Constructor public Camera(Game game, Vector3 position, Vector3 target, Vector3 rotation, bool Orthographic, float near, float far) : base(game) { Position = position; RotationInRadians = rotation; Target = target; if (Orthographic) { Projection = Matrix.CreateOrthographic(Game.Window.ClientBounds.Width, Game.Window.ClientBounds.Height, near, far); } else { Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, (float)Game.Window.ClientBounds.Width / (float)Game.Window.ClientBounds.Height, near, far); } } #endregion #region Public Methods /// <summary> /// Allows the game component to perform any initialization it needs to before starting /// to run. This is where it can query for any required services and load content. /// </summary> public override void Initialize() { base.Initialize(); cameraRotation = Matrix.Identity; } /// <summary> /// Allows the game component to update itself via the GameComponent. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> public override void Update(GameTime gameTime) { base.Update(gameTime); // This rotates the free camera. cameraRotation = Matrix.CreateFromAxisAngle(cameraRotation.Forward, RotationInRadians.Z) * Matrix.CreateFromAxisAngle(cameraRotation.Right, RotationInRadians.X) * Matrix.CreateFromAxisAngle(cameraRotation.Up, RotationInRadians.Y); // Make sure the camera is always pointing forward. Target = Position + cameraRotation.Forward; View = Matrix.CreateLookAt(Position, Target, cameraRotation.Up); } public void Draw(BasicEffect effect) { effect.View = View; effect.Projection = Projection; } #endregion } }
我认为是时候完成 Positioned Object 类了,我们只需要添加最后两个方法,即 update 方法。因此,您现在需要将 PositionedObject.cs 文件类调回到最前面。在 public 区域内,紧接 Initialize 类下方,添加 Update 类,如清单 6.1 中所示。
清单 6.1 PositionedObject.cs 编辑
/// <summary> /// Allows the game component to be updated. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> public override void Update(GameTime gameTime) { base.Update(gameTime); frameTime = (float)gameTime.ElapsedGameTime.Seconds; Velocity += Acceleration * frameTime; Position += Velocity * frameTime; RotationVelocity += RotationAcceleration * frameTime; RotationInRadians += RotationVelocity * frameTime; }
我们这样使用 frameTime float 的原因是,这里只有一个外部调用,而不是四个。然后,我们使用总秒数内的经过游戏时间来计算速度和加速度乘以秒数。这用于通过添加速度乘以秒数来计算当前位置。RotationVelocity 和 RotationAcceleration 也是如此。请注意,这完成了所有工作,并且它将对我们屏幕上绘制的每个游戏对象执行此操作。我们不使用 Services 访问相同的东西,因为那将是另一个外部类调用,它会进行另一个外部调用,并使用比一个更多的 CPU。当游戏屏幕上的所有内容都使用此方法时,这些调用就会加起来。
这样就完成了我们的 Positioned Object 类。这是完成的类,如清单 6.3 中所示。
清单 6.3 PositionedObject.cs 目前
#region Using using System; using Microsoft.Xna.Framework; #endregion namespace Engine { public class PositionedObject : DrawableGameComponent { #region Fields private float frameTime; // Doing these as fields is almost twice as fast as if they were properties. // Also, sense XYZ are fields they do not get data binned as a property. public Vector3 Position; public Vector3 Acceleration; public Vector3 Velocity; public Vector3 RotationInRadians; public Vector3 ScalePercent; public Vector3 RotationVelocity; public Vector3 RotationAcceleration; #endregion #region Constructor /// <summary> /// This gets the Positioned Object ready for use, initializing all the fields. /// </summary> /// <param name="game">The game class</param> public PositionedObject(Game game) : base(game) { game.Components.Add(this); } #endregion #region Public Methods /// <summary> /// Allows the game component to be updated. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> public override void Update(GameTime gameTime) { base.Update(gameTime); frameTime = (float)gameTime.ElapsedGameTime.Seconds; Velocity += Acceleration * frameTime; Position += Velocity * frameTime; RotationVelocity += RotationAcceleration * frameTime; RotationInRadians += RotationVelocity * frameTime; } #endregion } }
在进入第二章之前,您只需要完成最后一个类!您应该为自己能够走到这一步而感到自豪。您离拥有一个游戏引擎已经很近了!调出 Services 类,并在 public 方法区域内添加 Initialize 类,如清单 7.1 中所示。
清单 7.1 Services.cs 编辑
/// <summary> /// This is used to start up Panther Engine Services. /// It makes sure that it has not already been started if it has been it will throw and exception /// to let the user know. /// /// You pass in the game class so you can get information needed. /// </summary> /// <param name="game">Reference to the game class.</param> /// <param name="graphicsDevice">Reference to the graphic device.</param> /// <param name="Camera">For passing the reference of the camera when instanced.</param> public static void Initialize(Game game, GraphicsDevice graphicsDevice, Camera camera) { //First make sure there is not already an instance started if (instance == null) { //Create the Engine Services instance = new Services(game); //Reference the camera to the property. Camera = camera; graphics = graphicsDevice; randomNumber = new Random(); return; } throw new Exception("The Engine Services have already been started."); }
这个方法,和其他的方法一样,也是一个静态方法。我们可以从游戏中的任何类访问它。我们会在游戏类中添加对它的调用,传入我们想要的数字。我将 Z 默认值设置为 20,这是一个很好的起始大小。Z 是指在我们的设置中,屏幕进出方向的平面。这在电子游戏中是标准的做法。X 代表屏幕的上下方向,Y 当然代表左右方向。零点是屏幕的正中心。摄像机需要向后移动一段距离,以便我们可以看到我们放置在世界中的模型。现在,我们的 Services 类就完成了。在后面的章节中,我会让你添加一些额外的辅助方法,比如计算空间中两点之间角度的方法。我会在添加时解释它们。以下是 listing 7.2 中完整的类。
Listing 7.2 Services.cs so far
#region Using using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Content; #endregion namespace Engine { public sealed class Services { #region Fields private static Services instance = null; private static GraphicsDevice graphics; private static Random randomNumber; #endregion #region Properties /// <summary> /// This is used to get the Services Instance /// Instead of using the mInstance this will do the check to see if the Instance is valid /// where ever you use it. It is also private so it will only get used inside the engine services. /// </summary> private static Services Instance { get { //Make sure the Instance is valid if (instance != null) { return instance; } throw new InvalidOperationException("The Engine Services have not been started!"); } } public static Camera Camera { get; private set; } public static GraphicsDevice Graphics { get { return graphics; } } public static Random RandomNumber { get { return randomNumber; } } /// <summary> /// Returns the window size in pixels, of the height. /// </summary> /// <returns>int</returns> public static int WindowHeight { get { return graphics.ScissorRectangle.Height; } } /// <summary> /// Returns the window size in pixels, of the width. /// </summary> /// <returns>int</returns> public static int WindowWidth { get { return graphics.ScissorRectangle.Width; } } #endregion #region Constructor /// <summary> /// This is the constructor for the Services /// You will note that it is private that means that only the Services can only create itself. /// </summary> private Services(Game game) { } #endregion #region Public Methods /// <summary> /// This is used to start up Panther Engine Services. /// It makes sure that it has not already been started if it has been it will throw and exception /// to let the user know. /// /// You pass in the game class so you can get information needed. /// </summary> /// <param name="game">Reference to the game class.</param> /// <param name="graphicsDevice">Reference to the graphic device.</param> /// <param name="Camera">For passing the reference of the camera when instanced.</param> public static void Initialize(Game game, GraphicsDevice graphicsDevice, Camera camera) { //First make sure there is not already an instance started if (instance == null) { //Create the Engine Services instance = new Services(game); //Reference the camera to the property. Camera = camera; graphics = graphicsDevice; randomNumber = new Random(); return; } throw new Exception("The Engine Services have already been started."); } #endregion } }
再次打开 Game1 类,我会让你把它更新到最新状态。首先找到 Game1 的构造函数,并在底部添加 listing 8.1.1 中的那一行。然后找到 Initialize 方法,并添加 listing 8.1.2 中的这一行:Listing 8.1.1 Game1.cs edit
Camera = new Engine.Camera(this, new Vector3(0, 0, 275), Vector3.Forward, Vector3.Zero, false, 200, 325);
Listing 8.1.2 Game1.cs edit
Engine.Services.Initialize(this, graphics.GraphicsDevice, Camera);
这将启动程序,用 200 的近平面和 325 的远平面设置摄像机。这意味着所有距离摄像机 200 到 325 个单位之间,且在摄像机指向方向上的物体都会被绘制出来。我们将编辑 Update 方法;以下是 listing 8.2 中现在应该显示的内容。我在所有游戏中都这样做,这样我就可以直接按 Esc 键退出游戏。事实上,我更改了默认的游戏类,以便我不必添加这部分代码,我不知道为什么它最初没有这样做。因为用户总是会有键盘,但可能没有 360 游戏手柄。
Listing 8.2 Game1.cs edit
/// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) this.Exit(); base.Update(gameTime); }
我让你更改了退出游戏的首行代码。我添加了对 Esc 键的键盘输入。滚动到顶部,在构造函数类中添加 listing 8.3 中的这一行
Listing 8.3 Game1.cs edit
graphics = new GraphicsDeviceManager(this);
这意味着我们完成了第一章。以下是 listing 8.4 中 Game1 类现在应该显示的内容。
Listing 8.4 Game1.cs so far
#region Using using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Media; #endregion namespace Asteroids { /// <summary> /// This is the main type for your game /// </summary> public class Game1 : Microsoft.Xna.Framework.Game { private GraphicsDeviceManager graphics; private Engine.Camera Camera; public Game1() { graphics = new GraphicsDeviceManager(this); Window.Title = "Asteroids 3D in XNA 4 Chapter One"; graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 600; graphics.ApplyChanges(); Content.RootDirectory = "Content"; // Here we instance the camera, setting its position, target, rotation, whether it is orthographic, // then finally the near and far plane distances from the camera. Camera = new Engine.Camera(this, new Vector3(0, 0, 275), Vector3.Forward, Vector3.Zero, false, 200, 325); } /// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { Engine.Services.Initialize(this, graphics.GraphicsDevice, Camera); base.Initialize(); } /// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { } /// <summary> /// UnloadContent will be called once per game and is the place to unload /// all content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here } /// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) this.Exit(); base.Update(gameTime); } /// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(new Color(5, 0, 10)); base.Draw(gameTime); } } }
你应该能够点击运行,并看到图 14 中所示的空白午夜紫色屏幕,这意味着它正在工作。我们还没有任何东西显示,但如果它没有出现错误,那么你很可能做得正确。除了 using 之外,我没有在 Game1 类中添加区域,因为它占用了太多空间,而且你不需要看到它。如果你正确地编写了你的游戏,你就不会在游戏类中做太多事情。到目前为止,我创建这个教程玩得很开心,希望你也很享受跟我一起创建它的过程。你应该有一个包含整个项目的压缩文件。 Chapter One Project File 如果你遇到任何问题,请将包含的项目压缩文件解压缩到你的 Visual Studio 文件夹中,或者解压缩到文档中的任何位置。你将找到到目前为止完成的项目,你可以打开并检查。
谢谢,祝游戏愉快!要继续 第二章