声明
本文来自《msdn开发精选》杂志2005年第4期“技术专题”栏目,本文版权归杂志编辑部所有,未经许可,禁止转载!
作者:Derek Pierson (3Leaf Development)
本期技术专题面向对使用.NET Framework和DirectX开发游戏感兴趣的编程新手。本文的目标是开开心心创建游戏,学习游戏开发并在其中使用DirectX。游戏编程和DirectX有自己的相关术语和定义,可能理解起来较为困难,但您很快就将掌握这些代码,并且能够开始探索充满了各种可能性的新世界。我将尽力使文中的内容显得直白简单,并对文中出现的术语一一进行说明。学习过程的另一个难点在于处理DirectX时所需要的数学技能。在文中我将提供一些可以帮助您“抱抱佛脚”的资源,学习DirectX中所需要的数学技能。
在本专题中,我们将生成一个简单的游戏,以演示商业游戏的各种组件。我们将讨论如何创建精美的3D图形,如何处理用户输入,如何向游戏添加音频效果,如何使用人工智能创建计算机组件,以及如何对现实世界环境建模。此外,我们还将讨论如何使游戏实现网络游戏功能以及如何优化游戏性能。我还将在文章中介绍如何应用面向对象开发的原则,和您分享一些在创建组织良好的一流代码方面的经验。
在开始编写第一个游戏之前,需要了解一下将使用的工具。
对所有开发人员而言,最重要的工具就是集成开发环境(IDE)。您的大部分时间都将花在其中,在其中编写和调试代码,因此集成开发环境需要功能强大、运行速度快。
Visual Studio 2005(代号“Whidbey”)是面向基于.NET Framework的应用程序的第三代标准Microsoft IDE。Visual Studio 2005引入了很多Express版本,可提供其对应的高级版本的大部分功能,但对新手、业余爱好者和实习开发人员而言更为简单,开销要少很多(目前VB、C#、C++、J#以及用于Web开发的ASP.NET均有Express版可用)。在本系列中,我将使用Visual C# Express版,但同时会提供C#和VB的源代码,以便使用VB Express版。如果尚未准备好此工具,请从以下位置下载C#或Visual Basic Visual Studio Express版IDE:http://msdn.microsoft.com/express。
我们创建界面华丽的游戏所需要的第二个重要工具是应用程序编程接口(API)。如果没有这样的API,要访问PC的图形功能将十分困难。我们将使用的API是DirectX API。该API允许我们在Windows平台上创建强大的多媒体应用程序。为了开发文中提到的游戏,需要在以下位置下载最新的DirectX SDK:http://www.microsoft.com/windows/directx/default.aspx。请确保下载的是SDK,而不是运行库。SDK中包含示例和其他实用程序,这些内容在使用DirectX进行开发时将会非常有用。
在游戏开发过程中,将必须创建或修改图形。每个Microsoft Windows版本都带有Microsoft Paint(画图)。虽然这个工具并非最强大的程序,但它是现成的,而且已经能够满足我们的大部分需求了。
随着进一步深入了解DirectX、学习3D模型和音频效果,您可能会发现需要其他程序才能对图像和声音文件进行操作。在讨论这些主题时,我将给出网络上相关的免费或价格便宜的程序和资源。
最后,您需要知道从何处得到帮助。最好的地方之一就是公共新闻组。您可以在其中向那些有相同兴趣的人提出问题、得到答案。Microsoft MVP和员工也在关注这些新闻组,他们也会为您提供帮助。与游戏编程最相关的新闻组有:
还可以到以下新闻组中寻求帮助和灵感:
我第一次使用计算机是1981年,那是一台Sinclair ZX Spectrum。我计算机技术生涯的头5年都化在了为Sinclair(以及随后的Commodore 64)编写和修改游戏,但作为一个小伙子,别的能做什么呢?尽管就硬件功能和可用的API而言,发生了很大的改变,但一个出色的游戏的构成因素却没有改变。
现在的游戏变得非常复杂,进行开发需要大量的开发人员、图形设计师、测试人员和管理开销。其复杂性可与大型商业企业应用程序匹敌,进行开发和市场营销花销可达数百万美元。不过其回报可能会非常可观,销售方面与好莱坞大片有一拼——Halo 2推出的第一天销售额就达1亿美元。
所有成功的游戏一般都具有几个特别的元素,使其能够鹤立鸡群。
一款成功的游戏,其主要的元素就是游戏创意。无论图形有多么酷,音乐有多么好,如果创意不令人满意,就会没有人玩。
第二个重要的因素就是游戏的可玩性。如果游戏太难了,玩家很快就会有挫败感,而不再继续。而反过来,如果游戏太容易了,玩家又会感到索然无味,也会停下来。一款好的游戏,应提供多个级别的难度,从而不断向玩家提出挑战,而不会让玩家感到挫败或厌烦。
游戏创意和可玩性一起构成了“游戏设计”(请勿与“层级设计”混淆,层级设计指的是将总体游戏设计应用到游戏的具体部分)。有些游戏设计者具有点石成金的天赋。Shigeru Miyamoto(大金刚、萨尔达传说及玛丽的作者)和Will Wright (Sim-everything)就是两个著名的例子。Raph Koster的《Theory of Fun for Game Design》一书对这一领域的情况做了很好的概述。
成功的游戏的第三个因素是图形集。这些图形需要非常好,能够与游戏创意和游戏可玩性完美匹配,但不应为资源密集型材料,也不应太过浮华。
最后一个因素就是性能。没有人原意玩速度慢的游戏。我现在还记得曾经在我的Commodore64上有一款冒险游戏,每呈现一个场景就需要10分钟时间。不过,我仍然在玩这个游戏,因为游戏创意非常好,又没有别的选择,但玩得很让人恼火。图形与性能紧密相关。向游戏中添加的漂亮图形越多,游戏速度越慢。下一个最大的性能问题就是AI。目前的很多游戏开发都集中于如何将运行速度提高,而不是提出新的观点。不过,在学习游戏编程一类的复杂编程技术时,非常重要的是不要过早进行优化。了解性能瓶颈,具备编写干净代码并能对其进行调整和改进的技能,这比任何单独的优化功能都要重要得多。
如果努力进行这方面的培养,您也可以制作出不错的游戏。Tetris可能不是像“战地1942”一样精致的第一人称射击游戏,也没有漂亮的3D图形和杜比数字音效,但可以算得上是最流行的游戏。Gish一样的游戏每天都在证明着富有创意的独立开发人员能带给我们什么样的好东西。如果能够将游戏编写得足够好,反映出您的游戏创意,就可能引起游戏领域的大型游戏公司的注意。独立游戏节是游戏领域的“Sundance”,与专业游戏开发者论坛同时存在,我认为独立游戏节是最受关注的事件之一。
现在已经知道了优秀游戏的构成因素,下一步就是要为我们的游戏给出游戏草案了。
创意 由于独特而极富创造性的游戏创意是任何游戏的核心,所以在此,我将抄袭一下,使用我所见过的第一款3D游戏的创意:Atari公司的“Battlezone”。(呵呵,如果我自己有好的创意,又为什么要您知道呢?)Battlezone是一款基本的第一人称射击游戏,游戏者通过坦克的取景镜观察3D场景。目标是在自己被消灭之前尽可能多地消灭敌人的坦克。布景中包括很多随机的物体,可以藏身于其后。游戏屏幕中包括显示敌人位置和当前得分的雷达。
可玩性 原来的游戏开始的时候相当慢,但会不断添加更多的敌对坦克和其他随机敌人。该游戏还会提高敌人的速度和人工智能。所有这些使得该游戏既富有挑战性,又保持了起可玩性。
图形 原来的游戏提供了足够的图形来营造3D环境,但由于1980年的硬件限制(运行频率为1.5Mhz的8位处理器),它将3D物体呈现为线框。当这款游戏首次推出时,这些图形已经非常先进了,但我们将使用DirectX的魔力(以及经过了25年考验的摩尔定律)对其进行改进。

图1 Atari公司的Battlezone游戏屏幕抓图——使用线框构成山、坦克,为了增加真实性,甚至用线框构成了一个月亮!
性能 这款游戏最大限制地利用了当时的可用硬件。这在线框对象的使用上非常明显。如果当时编写游戏时对这些对象进行了填充,则可能没有办法玩。利用今天先进的硬件,除了由于编写臃肿的代码所带来的问题之外,不应该有任何性能问题。
既然已经确定了游戏,下一步就是写下您自己的游戏目标。没有必要把这个弄得很正式,把想法写在纸上就可能使其更加清晰。您至少需要确定游戏的目标,玩家能够做哪些事,又不能做哪些事,以及玩家如何与游戏交互。我们还应该定义计分系统和胜利的条件。
以下是我对于我们这款游戏的简单规格的描述。
根据被摧毁坦克的距离给玩家计分。坦克离得越远,得到的分数越高。每次开火都会根据设定值减少分数,除非击中了目标。
游戏将会进行分级。每个级别都将有预定义数量的敌人出现。一旦所有的敌人被消灭后,玩家就进入下一个级别。级别数目没有限制。
现在我们已经准备好,可以进行编码了。通常,将应用程序的总体思想记录下来是最好的办法。在前面花费很多时间对很小的细节进行设计是浪费时间。随着更多的功能被添加到游戏中,我们将不断地进行一些小的设计过程,以整理我们的想法。开发软件的这种重复方法是制作优秀软件的最好办法,同时也更为有趣。
现在我们已经准备好,可以开始编写某些代码了。首先在Visual Studio 2005中创建新的解决方案。
Visual Studio现在将为我们创建一个名为“BattleTank2005”的解决方案,其中包含一个具有相同名字的项目。
我们首先需要将类重命名为更加具有描述性的名称。使保持代码良好组织且容易理解的最有效方法之一就是命名。要始终选择清楚描述项的功能的名称,而避免采用Class1和Form1一类无意义的名称。

图2 将“Form1”文本替换为“GameEngine”。这一点的好处将在后面得到体现。
另一个组织内容的技巧就是确保“解决方案资源管理器”中的文件的名称与其所包含的类的名称完全相同,并始终为每个不同的元素(如每个类或枚举)创建独立的文件。
现在我们就拥有了一个可以运行的Windows应用程序,但这个应用程序不进行任何操作。窗体上没有其他控件,没有可以单击的按钮,也没有可以显示信息的文本框。在普通Windows窗体应用程序中,此时我们将向窗体中添加此类控件以创建最终的应用程序,但对于我们的游戏,我们将使用DirectX API(而不是Windows窗体API)绘制所有的内容。
我们真正需要的只是窗体的Windows句柄,此句柄基本上就是该窗体在屏幕上所有其他窗口中的唯一名称。我们将使用此句柄设置DirectX绘制图面。窗体的另一个用处就是其包含我们将用于创建呈现循环的事件。
游戏与普通应用程序(如Microsoft Word)非常不一样。Word向用户呈现一个模拟打字机页面的屏幕,然后等待用户进行操作。此类操作可以为按下键盘上的键,或使用鼠标从菜单中选择菜单项。在等待用户与应用程序进行交互的过程中,Word不进行任何工作。(实际上,我说得并不准确:它的确 在后台进行拼写检查和自动保存等工作,但却不进行用户可以看到的工作)。通常,使用Windows窗体库编写的应用程序行为都一样——除非用户进行操作,否则不会占用CPU时间(当然,也可以使用计时器控件或System.Threading命名空间的功能独立于用户进行某些操作)。
游戏则不同。正如您所知道的,游戏中的流畅移动需要每秒钟对屏幕进行多次更新。尽管静态图像开始停闪的“闪烁停闪阈值”实际上会因光源情况(计算机显示器一类较亮的光源要求有更高的帧速率)和图像落入视网膜的位置(视网膜边缘的景像需要比视网膜中间的景像更高的帧速率)发生变化,但其值通常为1/16秒。尽管电影以24帧/秒(FPS)的速度放映,但对于视频游戏而言,通常将30FPS作为可以接受的最低速率,而大多数动作游戏玩家都将其图形设置为不低于60FPS。
由于每秒钟将调用呈现循环数十次,而且会不停地运行,因而在游戏编程中几乎始终将呈现循环作为游戏的“stopwatch”使用,所有的计算操作均在循环中进行,不只是图形,还包括物理方面的操作、AI、用户输入检查以及计分。(同样,也可以使用计时器类或线程编写多线程游戏,但这样做会导致复杂性大幅度增加,却没有任何明显的好处,尽管在.NET Framework的通用语言运行库中的多线程技术非常高效,但很小的开销都会让游戏的每秒帧数有所损失。)
那么我们如何让计算机运行这个循环呢?我们前面添加的窗体有一个名为Paint的事件。只要重新绘制窗体,就会调用Windows窗体对象的Paint事件。通常仅在将窗体最大化或窗体被另一个移动过的窗体遮住时才会发生这种情况。
由于所有Windows窗体编程(甚至游戏编程)都是基于事件的,因而了解事件和事件处理程序的原理非常关键。尽管事件是自动触发的,但我们需要创建名为事件处理程序的特殊方法,以能够截获事件,并进行适当的操作以对其进行响应。
protected override void OnPaint(PaintEventArgs e)
{
}
这就是我们的事件处理程序。什么时候调用它呢?“OnPaint”——发生Paint事件时。仍然缺少一个东西。尽管Windows和Windows窗体库将自动引发事件,某些我们希望触发Paint事件的操作却不会触发该事件。例如,由于Windows不会认为需要重新绘制整个窗体(Windows这样做是为了提高效率,因为缩小窗体时实际上显示更少的东西),所以最小化窗口就不会触发Paint事件。因此我们不能依赖这些自动创建的事件管理游戏所需的循环。
幸运的是,我们可以通过调用窗体的Invalidate方法,以编程的方式触发Paint事件。这将导致触发Paint事件,并使Windows重新进入我们的OnPaint事件处理程序。然后就可以执行每一帧要运行的所有代码,随后再次调用Invalidate方法重复该过程。
您可能会问:“为什么不能在OnPaint()内直接添加一个while(true)循环,而永远不离开呢?”答案是,尽管我们是游戏程序员,我们仍然需要和其他内容良好相处。在OnPaint()内创建循环可能让系统上运行的其他程序永久等待。尽管我们的游戏的每秒帧数可以得到提高,但系统的其他部分在最好的情况下会影响其画面质量,而在最坏的情况下,则会变得不稳定。因此,我们没有利用直接循环,而是“请求”能够尽快地被调用。
this.Invalidate();
这样,我们就创建了自己的呈现循环。但还有一个问题。最后,所有的绘图操作都在OnPaint方法中完成,当背景被擦除时,Windows窗体会触发另一个事件,将默认执行一些绘图(擦除)操作以进行响应。为了强制应用程序在我们的方法处理程序内进行绘制,我们需要再向应用程序添加一行代码。由于需要确保应用程序启动时此代码会运行,因此将其放入到窗体的构造函数中。这意味着我们保证该代码将在对该类调用任何方法前运行。
this.SetStyle(ControlStyles.AllPaintingInWmPaint|ControlStyles.Opaque, true);
将ControlStyles设置为AllPaintingInWmPaint,以确保Windows只使用OnPaint事件处理程序重新绘制屏幕。第二个参数仅仅告知Windows我们的窗口不会是透明的。
现在游戏的基本框架已经成形。以后创建的都将是出现在呈现循环中的操作。
此类型循环的问题之一就是计算机完成呈现循环中的任务的速度因计算机不同而会发生变化。甚至在同一台计算机上,也会因给定时间游戏可用的内存量和CPU时间不同而发生变化。我们需要采用某种办法对这种差别进行统计,以确保能够以一致的方式显示动画。因此,我们不对每个帧都采用同样的处理,而要计算帧之间已用的时间,然后对计算应用该值。
在Windows中有几种不同的方法可以用于跟踪时间:
最后一个计时器的使用有些麻烦,因为它要求调用Windows中的低级别DLL(就是kernel32)。幸好对于高分辨率的计时器的需求非常普遍,因而在DirectX SDK中包括了一个计时器类。可以在SDK的安装目录下的“\Samples\Managed\Common”目录中找到这个计时器类。我们所感兴趣的文件名为dxmutmisc.cs,但随着向项目中添加更多的功能,我们将会需要该目录中的其他文件。
添加dxmutmisc.cs之前,我们将创建一个独立的文件夹。通过文件夹组织解决方案,可以方便地对相关项进行分组,使得项目显得更有组织。
现在将要把现有的文件添加到我们的项目中。这将要把文件复制到我们的目录结构中。
如果需要,则可以浏览此文件的内容,除了FrameworkTimer类之外,其中还包含了各种其他Helper类,可以让您省去自己编写很多代码的麻烦。
由于计时器类包含在不同的命名空间中,我们将添加一个using语句,以便可以不用每次都写出“Microsoft.Samples.DirectX.UtilityToolkit.FrameworkTimer”而使用FrameworkTimer。
Microsoft.Samples.DirectX.UtilityToolkit;
接下来我们需要想办法存储已用时间的值。我们将要把此值存储在deltaTime变量中。请注意,我们将此变量声明为双精度数。如果将其声明为整数,由于会对所有的结果进行舍入,将会丧失功能强大的计时器提供的所有分辨率。
在类的最后两个大括号上面的位置添加以下的代码行。
private double deltaTime;
我们希望尽可能在呈现循环的最后时间启动计时器,从而得到尽可能准确的时间。
FrameworkTimer.Start();
我们需要在每次循环开始时计算已用(增量)时间,因为我们将会把它传递给大部分后续调用。
deltaTime = FrameworkTimer.GetElapsedTime();
这就好了。我们现在可以跟踪时间了。作为带来的额外好处,我们将在后面的文章中使用此计时器计算帧速率。您会注意到现在解决方案将不会生成。这是因为我们添加的文件中的类要求引用DirectX。在下一篇文章中,我们将讨论所需要的DirectX部分。
在本文中,我们完成了很多工作。首先,我们讨论了创建托管DirectX游戏所需的工具,然后讨论了构成出色游戏程序的因素。接着,我们定义了游戏创意,并用Visual C#速成版创建了我们的游戏项目。之后,我们创建了在整个游戏中要使用的呈现循环,最后向项目中添加了一个高分辨率的计时器。
接下来的步骤都需要DirectX,我们将在下一篇文章中对此进行讨论。我希望本文能够给您以激励,让您开始着手您一直希望创作的游戏。在计算机编程领域,游戏编程可以算得上是最能让人产生满足感的体验之一,随着本系列文章的出台,我会将完成此目标所需的基础知识逐步传授给您。
发表于 @ 2005年08月18日 13:48:00 | 评论( loading... ) | 举报| 收藏