关闭

Coding4Fun:开始游戏开发 II

780人阅读 评论(0) 收藏 举报

简介

欢迎阅读“开始游戏开发”的第二篇文章。本文,我们将介绍 DirectX 的基础知识。

DirectX 是一个多媒体 API,它提供标准接口来与图形卡和声卡、输入设备等进行交互。如果没有这组标准 API,您需要为图形卡和声卡的每个组合和每种类型的键盘、鼠标和游戏杆编写不同的代码。DirectX 从具体的硬件中抽象出来,并且将一组通用指令转换成硬件的具体命令。

与所有新工具一样,DirectX 有大量新术语和定义需要理解。除了这些新术语,您还必须提高数学技能。DirectX 和游戏开发一般都需要大量的数学知识,理解一些基础知识对您会有帮助。不过,不需要您拿出老式计算器,我们的思想是要了解目标和实现目标的方式,其余的就是在代码中使用大量预先准备好的数学库。

DirectX 概述

DirectX 首次出现在 1995 年,当时称为“GameSDK”。在其原始形式中,针对的目标是使用 C 和 C++ 的开发人员。只有在 2002 年 12 月该 API 的第一个托管版本 (9.0) 发布以来,才可以使用 C# 或 VB.NET 开发 DirectX(实际上,如果您希望,那么可以使用任何符合 CLR 的语言)。

虽然与非托管版本相比,关于托管 DirectX 的性能争论很多,但商业游戏已使用托管 DirectX 进行创建的事实应该能够从根本上平息这样的争论。虽然某些需要极高性能的游戏可能需要使用非托管代码,但大多数游戏可以使用托管代码或者结合使用托管和非托管代码进行创建。编写托管代码使开发人员的效率更高,从而编写出更多的代码,生成更安全的代码。

安装 DirectX SDK 之后,在 C:/WINDOWS/Microsoft.NET/Managed DirectX 应该有一个目录,在机器上安装的每个版本的 SDK 都有一个子目录。我机器上使用的已经是第四版了,因此我有四个子目录。在每个子目录中应该有九个 DLL 和九个 XML 文件。由于托管环境中的 .NET 允许同一台机器上的同一个 DLL 文件有多个版本,而不会引起以前被称为 DLL Hell 的问题,所以我们可以使用多版本的托管 DirectX 库。这就允许您在安装新版本之后轻松地回滚到以前的版本。

如果您以前有在 Windows 下处理 DLL 文件的经验,您可能会担心在同一台计算机上安装同一个文件的多个版本会产生问题。自从 .NET 引入并行版本控制,这些版本控制问题就不复存在了。这意味着当新版本的 SDK 发布时,您可以使用多个版本来检查兼容性问题,而不必强迫自己进行升级。

九个 DLL 文件大致对应于 DirectX 中的十个命名空间。在创建游戏时,我们使用其中的大量命名空间来提供对输入设备、声音、网络播放(当然还有 3D 图形)的支持。

命名空间 描述

Microsoft.DirectX

公共类和数学结构

Microsoft.DirectX.Direct3D

3D 图形和助手库

Microsoft.DirectX.DirectDraw

Direct Draw 图形 API。这是旧式命名空间,您不需要使用它。

Microsoft.DirectX.DirectPlay

用于多玩家游戏的网络 API

Microsoft.DirectX.DirectSound

声音支持

Microsoft.DirectX.DirectInput

输入设备支持(例如,鼠标和游戏杆)

Microsoft.DirectX.AudioVideoPlayback

播放视频和音频(例如,在 PC 上播放 DVD)

Microsoft.DirectX.Diagnostics

疑难解答

Microsoft.DirectX.Security

访问安全性

Microsoft.DirectX.Security.Permissions

访问安全权限

1. DirectX 9.0 命名空间列表。

在继续进行之前,我们需要完成上一篇文章未完成的一些事情。在添加 FrameworkTimer 类之后,我们不能再构建项目,因为我们缺少对 DirectX 的引用。我们现在来修复这个问题,将它们添加进来。

右键单击 Solution Explorer 中的“References”,选择“Add Reference”。

在 .NET 选项卡中,向下滚动至发现名为 Microsoft.DirectX 的组件

按住 CTRL 键,选择以下组件:Microsoft.DirectX、Microsoft.DirectX.Direct3D,并单击 OK。

最终构建解决方案之前需要完成的最后一步是,注释掉 dxmutmisc.cs 文件中我们不需要的部分。

打开 dxmutmisc.cs 文件,注释掉不在 Native Methods 和 Timer 区域中的所有代码。

现在构建解决方案(按下 F6)。如果您的所有操作都正确,现在将会构建解决方案。

我的 GPU 比您的大

在深入 DirectX API 之前,我们先退一步,思考一下我们准备做什么。要创建快速游戏,我们需要使用某种类型的处理器,它允许我们计算要在监视器上显示的实际画面。由于我们都没有图像为 2D 的 3D 监视器,因此我们需要进行一些数学运算,通过将 3D 模型转换成 2D 图像来计算每个帧。如果所有这些计算都使用计算机的 CPU,我们的游戏将运行得较慢,因为我们还必须用同一个 CPU 计算 AI、检查输入。另外,我们还需要运行操作系统和所有后台处理。如果我们能够将图形部分的计算交给独立的处理器,则可以加速处理过程。

现代的图形卡拥有自己的处理器,称为图形处理单元 (Graphics Processing Unit) 或 GPU。这些 GPU 是专用处理器,它们为执行所需的这类计算而进行了优化。另外,每张图形卡都有自己的内存,实际上相当于计算机内部的一台独立计算机。这意味着,不管您的基本计算机多大、多快,图形速度更多地取决于 GPU 和视频内存,而非其他任何硬件。

适配器和设备

大多数图形卡同一时间只允许一台监视器与之相连,但有些图形卡提供对多台监视器的支持。您在计算机中也可以同时拥有多块图形卡。不管如何安装,每块图形卡都有一个 Adapter。您可以将它看作计算机中的物理视频卡。适配器从计算机的角度看具有“名称”,第一块适配器(或默认适配器)命名为 0,第二块适配器为 1,依此类推。在 DirectX 中您不直接和适配器相交互。而是使用 Device 连接到适配器。

一个设备代表到特定适配器的一个连接,每个适配器都可以有多个设备与之关联。DirectX 支持三种类型的设备:Hardware、References 和 Software。在我们的游戏中将使用 Hardware 类型,因为它提供运行游戏需要的速度。

现在,我们开始创建将在游戏中使用的设备。将以下代码添加到 GameEngine 窗体的现有代码之后的构造函数中。我们使用该构造函数,因为我们可以保证其中的任何代码都会在其他代码之前运行。这就确保我们始终有一个有效的设备对象,随后可以引用。

将以下代码添加到紧跟在 this.SetStyle 语句后面的 GameEngine 构造函数中

// Get the ordinal for the default adapter
int adapterOrdinal = Manager.Adapters.Default.Adapter;
// Get our device capabilities so we can check them to set up the CreateFlags
Caps caps = Manager.GetDeviceCaps(adapterOrdinal, DeviceType.Hardware);
CreateFlags createFlags;
// Check the capabilities of the graphcis card is capable of
// performing the vertex-processing operations
// The HardwareVertexProcessing choice is the best
if (caps.DeviceCaps.SupportsHardwareTransformAndLight)
{
createFlags = CreateFlags.HardwareVertexProcessing;
}
else
{
createFlags = CreateFlags.SoftwareVertexProcessing;
}
// If the graphics card supports vertex processing check if the device can
// do rasterization, matrix transformations, and lighting and shading operations
// This combination provides the fastest game experience
if (caps.DeviceCaps.SupportsPureDevice && createFlags == CreateFlags.HardwareVertexProcessing)
{
createFlags |= CreateFlags.PureDevice;
}
// Set up the PresentParameters which determine how the device behaves
PresentParameters presentParams = new PresentParameters();
presentParams.SwapEffect = SwapEffect.Discard;
// Make sure we are in windowed mode when we are debugging
#if DEBUG
presentParams.Windowed = true;
#endif
// Now create the device
device = new Device(
adapterOrdinal,
DeviceType.Hardware,
this,
createFlags,
presentParams
);

在窗体的尾部,deltaTime 变量声明的后面,添加如下代码:

private Device device;

有大量代码可以获取配置的 Device,但这种设置设备的方法是最安全的,它确保我们根据图形卡的硬件而使游戏的性能最大化。理解这块代码最简单的方式是将它分成四个不同的部分。

1.

第一行代码只是获取默认适配器的名称(通常为 0)。与赌它为零不同,更安全的做法是使用 Manager 类来获取默认适配器的名称。如果由于某种原因使得默认适配器的实际名称为 2,则采用这种方法不会出现问题。

2.

接下来的一节代码用于确定传递给 Device 构造函数的 CreateFlags 枚举的设置,以及哪个设置控制设备创建后的行为。我们再次使用 Manager 来获取默认适配器的功能(简称为 Caps)列表。然后使用此功能列表来确定是在硬件中执行顶点处理(较快),还是在软件中执行(较慢,但保证始终工作)。这其实用词不当,因为 SoftwareVertexProcessing 实际指使用 CPU 而 HardwareVertexProcessing 指使用 GPU。然后执行另一个检查,查看适配器是否支持纯设备,即图形卡可以处理光栅化、矩阵转换以及打光和阴影计算。如果设备可以,而且前一个检查确定我们可以使用硬件顶点处理,则将 PureDevice 设置添加到 CreateFlags 枚举中。HardwareVertexProcessing 和 PureDevice 的组合为我们提供可能的最佳性能,所以如果可能,要尽量使用它。

3.

创建设备需要的最后一个参数是 PresentParameters 对象。这个对象确定设备向屏幕显示数据的方式,因此得名。首先我们设置 SwapEffect 枚举,它确定缓冲和设备如何相互联系。我们通过选择 Discard 选项来简单地选择放弃后台缓冲区,直接写入到前台缓冲区。在 If 语句中,我们确定应用程序是否在调试模式下运行。如果处于调试模式,我们不希望在全屏模式下运行(这是默认情况),因为它使调试非常困难。使用这种方法确定配置好于对其硬编码而在发布游戏时忘了切换回来。

4.

最后一步是真正创建设备。我们传入默认适配器的序号、想要将设备绑定到的窗口、设备类型,然后传入前面创建的 CreateFlags 和 PresentParameters 对象。

这段代码的实际结果就是使我们拥有一个有效的设备,可以用它在屏幕上进行绘制。要真正使用设备,我们需要在呈现循环中添加两行代码。

将以下代码添加到紧跟在 FrameworkTimer.Start() 语句后面的 OnPaint 方法中。

device.Clear(ClearFlags.Target, Color.DarkBlue, 1.0f, 0); device.Present();

第一行用第二个参数指定的颜色填充窗口(您可以在 Color 枚举中使用任何预定义的窗口颜色)。Clear 方法的最后两个参数描述 z-depth 和 stencil 值,此时不重要。

设备的 Present 方法使设备在屏幕中显示后台缓冲区的内容。屏幕也称为前台缓冲区,这些缓冲区如何交互是由您前面设置的 SwapEffect 枚举确定的。

现在我们运行解决方案,其结果是生成蓝色的屏幕。虽然这平淡无奇,看似使用大量代码才得到一个简单的蓝色屏幕,但我们已经成功地将 DirectX 集成到我们的 GameEngine 中。

3D 图形术语

在我们继续呈现地形和部件之前,我们需要驻留片刻,介绍一些在三维图形编程中使用的原理和定义。我们没这么做,所以大家觉得我们很厉害,仅仅因为这些原理和术语是三维图形编程的基础。

我目前玩的游戏名为 Brothers in Arms:Road to Hill 30 (www.brothersinarmsgame.com)。它是根据 1942 年法国诺曼底登陆设计的一款第一人称射击游戏。游戏中的所有地形、建筑、道路、河流等完全复制 1942 年诺曼底的原始地形。游戏创作者使用航空照片和地图来重新创建地形,并亲自到法国测绘地形。他们使用此信息来为游戏重新创建原始地形。当我们需要描述某件事物位于世界的哪个地方时,例如西雅图的太空针塔 (Space Needle),我们通常使用一个坐标系统,称为地理坐标系统 (http://en.wikipedia.org/wiki/Geographic_coordinate_system)。在此系统中,每个点都用唯一一对经纬度来表示(太空针塔位于:纬度:47.62117,经度:-122.34923)。

当游戏创作者将地形传到计算机时,他们不能只向 DirectX 提供经纬度坐标,因为计算机不知道如何表示地理坐标。要在三维世界中放置物体,我们必须有一个计算机可以理解的坐标系统,将坐标从一个系统转换到另一个系统。DirectX 中使用的坐标系统是左手的笛卡尔坐标系统 (Cartesian coordinate system)。

笛卡尔坐标系统

要在三维世界中恰当地放置物体,我们需要知道在哪放置以及如何明确地定义每个点。我们使用笛卡尔坐标系统来实现这一点。此笛卡尔坐标系统是由右角的三根轴组成的,它们的交叉点称为 origin(您可能对中学几何中使用的只有 x 轴和 y 轴的二维坐标更为熟悉)。要完整定义这个三维坐标系统还需要另外两个属性:偏手性 (handedness) 和方向

偏手性

在稍微复杂点的方式中,表示三维笛卡尔坐标系统的方式实际上有两种:左手的和右手的。如果您点击以下链接 (http://en.wikipedia.org/wiki/Cartesian_coordinate_system),您可以得到其原因的更多解释,但要记住的主要一点是 DirectX 使用左手坐标系统。使用左手坐标系统的结果是,Z 值越大,距离越大,而在右手系统中,该值随距离的增大而减小。您需要知道坐标空间的偏手性才能使用 Z 值比较两个对象,并且知道哪一个离您更远。

方向

三维笛卡尔坐标系统要注意的另外一件事是它可以根据 z 轴的绘制方式而有不同的方向。如果它按左下图那样垂直绘制,则称为世界坐标方向;如果按右边的方式绘制,则称为本地或主体坐标方向。

不管您使用什么方向来作为点的参考方向,要在屏幕上绘制,坐标都必须转换成屏幕坐标(二维屏幕空间)。


23D 笛卡尔坐标系统

矢量

三维坐标系统中的每个点都由三个值定义:X、Y 和 Z 值。为了使事情简单一些,DirectX 为我们提供了一个名为 Vector3 的结构来让我们存储这些坐标。不过这只是为了让我们使用方便,因为矢量被定义成同时表示方向和速率的对象,而不是一个位置。vector 类的方法可用于实现这个目标,不过现在只将它看作一个能在其中存储三个值的方便的结构。根据您的需要,也可以使用 Vector2 或 Vector4 结构。

顶点

在我们添加的用于连接设备的代码中,您可能已经注意到我们需要确定是否应该在软件或硬件中进行顶点处理。那什么是顶点处理,什么是顶点呢?顶点是一个点,这个点是三维物体的一部分。顶点是由一个矢量和与纹理映射相关的其他信息组成的。

纹理

纹理只是一个 2D 位图,它应用到 3D 对象,向其提供某种类型的外观(纹理),例如草地、混凝土等。

网格

本文并不准备介绍使用网格;网格的定义与顶点的定义相关,所以这里顺便提及。网格是描述三维形状的数据。此数据包括形状的顶点列表以及描述顶点如何连接的信息和覆盖它们的纹理的相关信息。

您可以看到,网格是由顶点组成的,而顶点又包含矢量。每一级都只是添加一些关于各块如何相互联系的信息。从现在起,当您听到矢量时就应该想到点,听到顶点时想到点和数据,听到网格时想到许多点和更多的数据。

三角形

既然您已经知道在 3D 空间中矢量实际上就是一个点,我们需要介绍点如何组合以形成物体。在 DirectX 中,每个物体都是由一个或多个三角形组成的。虽然一开始会觉得很难,但使用三角形表示任何 2D 或 3D 形状是完全可能的。原因很简单:三角形是最简单的共面多边形(三角形的所有点都在同一平面上)。这就简化了执行所有计算需要的数学运算。

虽然所有这些看上去是一大堆要掌握的新术语,但在继续介绍矩阵前理解这些基本概念是很重要的。我们将在下一篇文章中介绍矩阵、照相机(DirectX 意义下的词)背后的原理以及转换。我们也将解释网格的定义以及如何使用顶点的纹理映射信息。

帧速率计数器

正如我在第一篇文章中提到的,我们准备在游戏中添加一个帧速率计数器。知道游戏的帧速率是很重要的,因为它确定了游戏的速度。另外,在向呈现循环添加任何代码之前知道帧速率是什么可以让我们了解每次添加对游戏的速度产生什么样的影响。

在项目中添加一个新的类,并将它命名为 FrameRate。

在类声明中添加如下代码。

public static int CalculateFrameRate()
{
if (System.Environment.TickCount - lastTick >= 1000)
{
lastFrameRate = frameRate;
frameRate = 0;
lastTick = System.Environment.TickCount;
}
frameRate++;
return lastFrameRate;
}
private static int lastTick;
private static int lastFrameRate;
private static int frameRate;

在 GameEngine OnPaint 方法中,在 deltaTime 赋值语句的后面添加下面一行代码。

this.Text = string.Format("The framerate is {0}", FrameRate.CalculateFrameRate());

该帧速率计数器使用 System 类的 TickCount 属性,它是 WIN32 API 的 GetTickCount 方法的一个包装,精度大约为 15 毫秒。虽然 FrameworkTimer 类更准确,但这个精度对于计算帧速率来说已经足够了。

代码维护工作

我们在完成之前,对原始代码做了两处更改。第一个改动是删除 Program 类中的 Application.EnablRTLMirroring() 语句。在 .NET Framework 的 Beta2 版本中已不推荐使用这个方法。另一个改动是将 GameEngine 类的创建包装到一个 using 语句中。这确保不管 GameEngine 类中发生什么,当我们关闭应用程序时它都会被正确地处置。

小结

此时您可能想知道我们是否一直都在准备开发我们的游戏。学习游戏开发的挑战之一是您必须在开始时打好基础,这样以后才能掌握好更高级的思想。一旦您理解了 3D 图形术语和原理,您就可以游刃有余地专注于游戏创作了。

在本文中,我们介绍了 DirectX — 准备用于 3D 图形的 API,以及 BattleTank 2005 中的输入设备控制和声音。然后讨论了 GPU 是什么以及为什么它在今天的游戏中如此重要。还阐述了适配器和设备是什么以及如何在 DirectX 中配置它们。接下来,介绍了需要深入理解的术语和定义。最后,我们在游戏中添加了一个帧速率计数器以跟踪游戏性能。

在下一篇文章中,我们将探讨矩阵和转换、如何在 3D 世界中放置照相机,以及剪辑和精选是什么。在那篇文章中,我们还准备为我们的游戏添加地形。

我希望首次踏入 DirectX 世界不会让您望而却步。因为只靠阅读有些定义和原理理解起来有点困难,所以建议您编写一些代码,试试不同的设置会产生什么效果。DirectX SDK 也包含大量介绍这一话题的示例和教程,可以让您试验这些设置。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:125396次
    • 积分:1720
    • 等级:
    • 排名:千里之外
    • 原创:36篇
    • 转载:92篇
    • 译文:2篇
    • 评论:7条
    最新评论
    IT Technology's Summary And Introduction
    编程技术
    技术/官方网站