UML软件工程组织

Coding4Fun:开始游戏开发
转自:www.microsoft.com 作者:Derek Pierson

简介:

欢迎阅读有关利用 Microsoft .NET Framework 和托管 DirectX .0 进行游戏编程的入门系列文章中的第一篇文章。

该系列文章的目标读者是有兴趣利用 .NET Framework 和 DirectX 为他们自己开发游戏的初级编程人员。该系列文章的目标是体验创建游戏的乐趣,并在该过程中学习游戏开发和 DirectX。游戏编程和 DirectX 有其自身难以理解的术语和定义,但过一会儿您将破解代码并能够探究一个新世界中可能发生的事情。出现术语时,我将对它的解译尽量做到简单易懂。学习曲线的另一部分来自处理 DirectX 所需的数学知识。在这个过程中,我将提供一些资源,帮助您温习或学习那些继续了解 DirectX 所需的数学技能。

在本系列文章中,我们将构建一个简单的游戏来阐述商业游戏的各种组件。我们将探讨如何在 3D 空间中创建非常好看的图形,如何处理用户输入,如何向游戏中添加声音,如何利用人工智能创建计算机对手以及如何对真实世界的物理过程建模。另外,我们还将探讨如何在网络上玩游戏以及如何优化游戏性能。期间,我将向您展示如何应用面向对象开发的原理,另外,我将分享我的一些创建组织良好的优雅代码的经验。我不会探讨 C# 的基础知识,但有许多介绍这些知识的非常不错的免费资源。最佳入门点是位于 http://msdn.microsoft.com/vcsharp/ 的 C# 开发人员中心。

工具:

着手编写第一个游戏之前,需要讲讲我们将使用的工具。

对任何开发人员而言,最重要的工具就是集成开发环境 (IDE)。在该环境中,您花费大量的时间编写和调试代码,因此它必需是强大且快捷的。

Visual Studio 2005(也以代号“Whidbey”而知名)是适合基于 .NET Framework 应用程序的第三个标准 Microsoft IDE 版本。Visual Studio 2005 引入许多速成版本,它们可提供其更高版本的大多数功能,但针对初学者、计算机业余爱好者和学生开发人员则进行了简化且成本低得多(现有 VB、C#、C++、J# 的速成版本,以及使用 ASP.NET 的 Web Developer 的速成版本)。我将在本系列文章中使用 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 模型和声音的探讨,您可以发现需要使用其他程序来处理图像或声音文件。在探讨这些主题时,我会为您指出在 Web 上的免费或便宜的程序和资源。

最后,您需要知道去哪里寻求帮助。最好的地方之一就是公共新闻组。在这里,您可以向与您有同样兴趣的人提出问题并得到答案。Microsoft MVP 和员工也监控这些新闻组,并在此期间为您提供帮助。对游戏编程最感兴趣的新闻组是:

托管 DirectX: microsoft.public.win32.programmer.directx.managed

C#: microsoft.dotnet.languages.csharp

VB: Microsoft.dotnet.languages.vb

对游戏有启发和帮助的其他场所是:

The ZBuffer:http://www.thezbuffer.com/

GameDev.net:http://www.gamedev.net/

flipCode:http://www.flipcode.com/

Gamasutra:http://www.gamasutra.com/[专业读者,但很多文章来自 Game Developer,包括教程]

什么造就了成功的游戏?

我第一次使用计算机的经历是 1981 年使用 Sinclair ZX Spectrum。我的头五年计算机生涯除了编写和修改 Sinclair 游戏以及后面的 Commodore 64 游戏之外什么都没做。但是,作为一个十几岁的孩子您还想做什么呢?虽然硬件功能和可用的 API 变化很大,但卓越游戏的特性并没有改变。

现在的游戏已变得非常复杂,因此开发它们需要大量的开发人员、图形设计人员、测试人员和管理费用。它们的复杂性可以比得上大型的商业企业应用程序,要花费上百万美元来开发和销售。但回报是巨大的,并且可以与耗费巨资拍摄的好莱坞电影相匹敌 — Halo 2 在上市的第一天就共计赚取了 1 亿美元。

所有成功的游戏都有一些使它们能脱颖而出的共性。

成功游戏的主要要素是游戏的构思。无论图形多么漂亮,无论音乐多么动听,如果没有令人满意的构思,就没人想玩这个游戏。

第二个最重要的特性是游戏的可玩性。如果游戏太难,那么玩家会很快感到灰心而不玩了。相反,如果游戏太简单,玩家也会感到厌烦而不玩了。好的游戏提供多个难度级别,可以不断地挑战玩家,既不会令他们感到灰心也不会让他们厌烦。

综合起来,游戏构思和可玩性是“游戏设计”(不要与“级别设计”混淆,它是将整个游戏设计应用到游戏的特定片断)。有些游戏设计人员可以“点石成金”。Shigeru Miyamoto(Donkey Kong、Zelda 和 Mario 的创作者)和 Will Wright (Sim-everything) 是两个突出的例子。在 http://www.gamasutra.com/features/20030502/miyamoto_01.shtml,可获得 Miyamoto 在 1999 游戏开发人员会议上的发言,Wright 的有关 Spore 设计理念的最新讨论 (http://www.gamespy.com/articles/595/595975p1.html?fromint=1) 对各种设计人员而言都有很好的启示,而 Raph Koster 的书“Theory of Fun for Game Design”在游戏界则得到了良好的评论。

成功游戏的第三个要素是图形的安排。它们要能足以表达游戏构思和游戏玩法,但不要过于资源密集或华而不实,这样会分散他们的注意力。

最后一个要素是性能。没有人愿意玩速度缓慢的游戏。还记得在我的 Commodore64 上运行的一款冒险游戏,要花 10 分钟来呈现一个场面。告诉您,我还在玩这个游戏,因为它的构思非常棒,并且在没有其他选项的前提下它很刺激。图形和性能是密切相关的。您加到游戏中的奇特图形越多,它的性能就越慢。下一个最大的性能问题就是 AI。现在很多游戏开发都专注于如何使其更快,而没有提出新的思想。然而,当您学习游戏编程这样的复杂编程技巧时,不要过早地进行优化,这一点极其重要。对性能递进的理解,以及编写整洁的代码,对代码进行扼要描述并进行代码改善的技巧 比任何一种优化功能都更为重要。

如果按照该顺序应用您的设计工作,那么您也能够创作出一款不错的游戏。Tetris 可能不是一款像 Battlefield 1942 那样精制的第一人称射击游戏,但它是一款有争议的最为流行的游戏,既没有奇特的 3D 图形也没有 Dolby 数字音响。即使在今天,像 Gish 这样的游戏(http://www.chroniclogic.com/index.htm?gish.htm)依旧证明了有创意的独立开发人员可以给我们带来什么。如果您可以编写足够的游戏来展示自己游戏的构思,那么或许您可以让大型游戏公司对您的游戏感兴趣。Independent Games Festival 是游戏界的“圣丹斯电影节”,与专业的 Game Developers Conference 同时举办,相信我,它是展会上最引人关注的活动。

我们的游戏构思:

既然了解了制作优秀游戏的特性,下一步就是为我们的游戏制订游戏建议。

构思:因为提出独特的、有创意的游戏构思是任何一款游戏的核心,所以我要回避这一点而使用曾见过的第一款 3D 游戏的游戏构思:Atari 的 Battlezone。(如果我有不错的构思,我为何要让你们知道呢?)Battlezone 是一款基础的第一人称射击游戏,通过坦克的取景器来查看 3D 地形。目标是在自己被摧毁之前破坏尽可能多的敌人坦克。地形包括随机物体,您可以隐藏在它后面。游戏屏幕包括一个为您展示敌人位置的雷达和当前得分。

可玩性:最初游戏的启动很慢,但在不断添加更多的敌人坦克和其他的随机敌人。该游戏还提高了速度和敌人的智能。所有这些都使游戏很富有挑战性但却很好玩。

图形:最初游戏使用的图形已被证明非常迷人了,让您感觉好像置身于 3D 环境中,但因为 1980 年可用硬件(一个以 1.5 赫兹运行的 8 位处理器)的原因,它以线框形式呈现 3D 物体。当游戏刚出来时,这些图形非常先进,但我们将利用 DirectX 的魔力(以及摩尔定律的 25 年的魔力!)来改进它们。


 

Atari 的 Battlezone 游戏的屏幕快照 — 用线框山脉、坦克,甚至还有线框月亮来完成,以增加真实性!

性能:该游戏将当时可用的硬件发挥到了极限。这在使用线框物体时非常明显。如果游戏编写成填充这些物体,那么该游戏就不能玩。有了当今的先进硬件,除了我们编写粗糙代码所带来的性能问题,我们不应当有任何性能问题。

既然我们已选定了一种游戏,下一步就是要写下游戏的目标。这不需要任何正式的内容,只要简单地写下趋向于使构思更加清楚的内容即可。您最少需要确定的游戏目标是,玩家可以做什么、不可以做什么,以及玩家如何与游戏进行交互。我们还应当定义记分系统的外观以及获胜的条件是什么。

对于我们的游戏,这些就是我提出来的简单要求。

一种 3D 第一人称射击游戏

目标是消灭尽可能多的敌人

玩家可以在地面上穿过地形,就像一个正规坦克那样。坦克不能飞,也不能更改速度。

游戏的进行是通过键盘来控制移动和射击的。鼠标用来与菜单交互以及启动/停止游戏。我们不支持游戏杆。

玩家根据距离被摧毁敌人坦克的远近获得分数。坦克越远,分数越高。每一轮射击都会减少一定的分数,除非该轮击中了目标。

游戏将分成几级。每级都有预先确定数量的敌人。一旦消灭了所有的敌人,玩家就进升到下一级。对级别没有限制。

现在我们准备做一些编码工作。一般最好写下应用程序的全部构思。在各个微小细节的设计方面花费很多精力就是在浪费时间。随着给游戏添加更多的功能,我们将不断完成一些小型设计以表达我们的构思。这种开发软件的反复方法是创建优秀软件的最佳方法,同时也更为有趣。

创建游戏项目:

现在我们准备编写一些代码。第一步是在 Visual Studio 2005 中创建一个新解决方案。

选择 File | New | Project 并从模板列表中选定 WindowsApplication。在对话框底部的 Name 字段中,将默认的 WindowsApplication1 替换为 BattleTank2005 并单击 OK

现在 Visual Studio 为我们创建了一个名为 BattleTank2005 的新解决方案,它包含一个同名的项目。

首先,我们需要将类重命名为更具有描述性的名称。命名是保持代码有条理、易理解的最有效的方法之一。永远选择能够清楚说明所做项目的名称,避免无意义的名称,像 Class1 和 Form1。

从“Edit”菜单中,选择“Find”和“Replace”,然后选择“Quick Replace”。将 Find What 字段设置为‘Form1’,将 Replace What 字段设置为‘GameEngine’,并将 Look in 字段设置为‘Current Project’(参见下图)。单击“Replace All”,完成这种更改(应当做 5 处更改)

接着在 Solution Explorer右健单击Form1 并选择 Rename。将 Form1.cs 更改为 GameEngine.cs。


 

用 GameEngine 替换 Form1 文本。您以后会体会到这样做的好处。

保持事情有条理的另一种技巧是确保“Solution Explorer”中文件的名称和它们包含的类的名称完全相同,并且总是为每个不同的元素(例如每个类或枚举)创建单独的文件。

现在我们就拥有了一个可以运行的 Windows 应用程序,但是它不能做任何事情。窗体上没有其他控件,例如可以单击的按钮或显示任何信息的文本框。在常规的 Windows 窗体应用程序中,我们现在要向窗体添加这样的控件以创建我们最后的应用程序,但是对于我们的游戏而言,我们要利用 DirectX API 而不用 Windows 窗体 API 来画出一切。

我们真正需要的只是该窗体的 Windows 句柄,这主要是因为在屏幕上的所有其他窗口中,它的名称很独特。我们要使用该句柄来建立我们的 DirectX 画面。该窗体的另一个用处是它包含一个我们将要用来创建呈现循环的事件。

添加呈现循环

游戏与常规的应用程序(例如 Microsoft Word)有很大的区别。Word 为用户呈现的屏幕模仿打字机中的页面,然后等待用户来做一些事情。这些事情可能是通过键盘上按键或用鼠标从菜单中选择一个菜单项。在等待用户与应用程序进行交互时,Word 不做任何事情。(其实我在说谎:它确实 在做一些事情,如在后台进行拼写检查以及自动保存,但作为用户的您看不到任何内容)。一般情况下,用 Windows 窗体库编写的程序通常都有相同的行为 — 它们不消耗 CPU 时间,除非用户在做一些事情(当然,利用 Timer 控件或 System.Threading 命名空间的功能来做一些不受用户控制的事情是可能的)。

游戏各不相同。您知道,游戏中平滑的动作要求屏幕每秒钟更新多次。静态图像开始停闪的“停闪阈值”一般取为 1/16 秒,但实际上根据光照(较亮的光线,如计算机显示器,需要更高的帧速率)和图像在视网膜上停留的位置(外围的视力比视网膜中央凹视力要求更高的速率)不同,它是变化的。虽然电影每秒显示 24 帧 (FPS),但对于视频游戏,通常认为 30 FPS 是可以接受的最低速率,而大多数动作游戏玩家将他们的图形调整为不低于 60 FPS。

因为每秒要多次调用呈现循环并不停运行,所以游戏编程几乎总是利用呈现循环作为游戏的“停表”,来计算循环内部的一切,不只是图形,还有物理学、AI、检查用户输入和分数。(此外,您可以 利用 Timer 类或线程来编写多线程游戏,虽然 .NET Framework 的公共语言运行库中的多线程非常有效,但是如果没有明显的好处,这样做会带来相当大的复杂性,少量的开销就会将游戏每秒降低两个帧。)

因此,我们如何让计算机运行这个循环?我们以前添加的窗体有一个名为 Paint 的事件。重画窗体时,要调用 Windows 窗体对象的 Paint 事件。只有在您将窗体最大化或者当窗体被另一个移动窗体掩盖时,这通常才会发生。

由于所有的 Windows 窗体编程,甚至游戏编程都是基于事件的,因此理解事件的原理和事件处理程序至关重要。虽然是自动触发事件的,但我们需要创建一个名为事件处理程序的特殊方法,以便能够截获该事件并做出回应。

在 GameEngine 类的构造函数之后添加下面的代码。

protected overridedeltaTimeOnPaint(PaintEventArgs e) { 
}

这是我们的事件处理程序。何时调用它?“OnPaint” — 当 Paint 事件发生时。还缺一样东西。即使 Windows 和 Windows 窗体库自动引发事件,我们希望触发一些 Paint 事件没有触发的操作。例如,将窗体最小化不会触发 Paint 事件,因为 Windows 没有领会重画整个窗体(由于我们缩小窗体时实际是显示的更小了,因此 Windows 还是非常高效的)的需要。因此我们不能依赖这些自动创建的事件来管理游戏所需要的这种循环。

幸运的是,我们可以通过调用窗体的 Invalidate 方法来编程触发 Paint 事件。这会触发 Paint 事件并使 Windows 返回我们的 OnPaint 事件处理程序。然后我们执行希望运行每帧时的代码,并通过调用 Invalidate 方法整个重新开始。

您可能要问,“我们为什么不能只在 OnPaint() 内部直接添加一个 while(true) 循环并永远停留其中?”答案是,即使我们是游戏编程人员,我们仍希望与其他程序协作。在 OnPaint() 内部创建循环将会停止系统上运行的其他程序。虽然我们的游戏可以每秒获得几帧,但系统其余部分最好的情况是变得很难控制,最差时会变得不稳定。因此,我们必须尽可能快地“要求”再次被调用,而不是直接循环。

在 OnPaint 方法中添加下面一行代码:

this.Invalidate();

即我们已经创建了呈现循环。但还有一个问题。结果是,在 OnPaint 方法中不是所有的绘画都得以完成,当在后台进行清除时,Windows 窗体触发了另一个事件并在默认情况下进行一些绘画(还有清除)作为回应。为了迫使应用程序确实只在方法处理程序中绘画,我们需要给应用程序再添加一行代码。由于我们需要确保在应用程序启动时这些代码得以运行,因此我们将它放置到窗体的构造函数中。这意味着,我们调用该类的任何方法之前保证先运行这些代码。

在 GameEngine 类中,将下面一行代码添加到紧跟在 InitializeComponent 方法调用之后的构造函数中。

this.SetStyle ( ControlStyles.AllPaintingInWmPaint|ControlStyles.Opaque, true );

将 ControlStyles 设置为 AllPaintingInWmPaint,确保 Windows 只使用 OnPaint 事件处理程序来重画屏幕。第二个参数只是通知 Windows 我们的窗口不是透明的。

现在我们有了游戏的基本框架。至此,我们需要创建的就是呈现循环内部发生的操作。

计时器的方方面面

这类循环的一个问题是这样的一种事实:计算机完成呈现循环中任务的速度随着计算机的不同而有所不同。随着给定时刻游戏可用内存和 CPU 时间的不同,即使在同一台计算机上它也不同。我们需要拥有一些解决这些差别的方法,以确保我们制作动画的一致性。因此我们将计算两帧之间过去的时间并将该值应用到计算中,而不是将每帧都做相同的处理。

在 Windows 中跟踪时间有几种不同的方法:

1.

System.Windows.Forms.Timer:这是 Windows 编程中最常用的计时器。虽然它很容易使用,但它只有 1/18 秒的分辨率。由于我们每秒最多可以有一千帧,因此这样的分辨率对于游戏程序而言不是太令人满意。

2.

timeGetTime:这种 Windows DLL 在有些版本的 Windows 上提供 1 微秒的分辨率,在 Windows NT 上为 5 微秒。这种变化太大,而且我们确实不想通过检查游戏运行的操作系统来查看我们是否需要调整计时器的值。

3.

System.TickCount:这种托管调用返回滴答数(表示毫秒数)。这接近于我们的期望,但我们可以做得更好。

4.

QueryPerformanceCounter:这是首选使用的计时器,其分辨率小于 1 微秒。这是在游戏开发中最常用的计时器。

编写最后一种计时器需要一定的技巧,因为它要求调用 Windows 中的一个低级 DLL(kernel32,如果您需要知道)。我们幸运的是,需要的高分辨率计时器是通用的,并且计时器类包含在 DirectX SDK 中。您可以在 SDK 安装目录下的 \Samples\Managed\Common 目录中找到这个计时器类。我们所感兴趣的文件称为 dxmutmisc.cs,但随着我们向项目中添加更多的功能,我们将使用该目录中的其他大多数文件。

添加 dxmutmisc.cs 之前我们将创建一个单独的文件夹。用文件夹来组织解决方案使得将相关项分组在一起变得很容易,并且保持项目更有条理。

选择 Add | New Folder。命名新文件夹:DirectXSupport。这是我们在整个项目中使用各种支持类时添加它们的地方。

现在我们将现有文件添加到项目中。这将文件复制到我们的目录结构中。

右键单击 DirectXSupport 文件夹并选择 Add | Add Existing Item,浏览 C:\Program Files\Microsoft DirectX 9.0 SDK (February 2005)\Samples\Managed\Common 并选择 dxmutmisc.cs 文件。

如果您希望,那么可以浏览该文件的内容,它包含各种其他的助手类,除了 FrameworkTimer 类外,这些类可以使您省去自己编写许多代码的麻烦。

由于计时器类包含在不同的命名空间中,我们将添加一条 using 语句,这样我们可以使用 FrameworkTimer,而不必每次都书写“Microsoft.Samples.DirectX.UtilityToolkit.FrameworkTimer”。

在该类顶部的 using 指令区中添加下面一行代码:

Microsoft.Samples.DirectX.UtilityToolkit;

下面我们需要一种存储消逝的时间值的方法。我们将该值存储在 deltaTime 变量中。注意,我们将该变量声明为双精度型。如果将它声明为整型,将会丢失高性能计时器提供的所有分辨率,因为每个值都将取整为整数。

在该类尾部,最后的两个大括号上面,添加下面的一行代码。

private double deltaTime;

我们希望在呈现循环中最后的可能时刻启动计时器,这样我们就可以获得所能达到的最精确时间。

在 OnPaint 方法的末尾,this.Invalidate 调用之前,添加下面的代码。

FrameworkTimer.Start();

每次启动循环时我们需要计算流逝的时间(或 delta),因为我们要将它传递给我们随后要进行的大多数调用。

在 OnPaint 方法的最顶端,任何其他代码之前,添加下面一行代码。

deltaTime = FrameworkTimer.GetElapsedTime();

就是它了。现在我们有了跟踪时间的方法。作为附加的好处,在下一篇文章中,我们将使用该计时器来计算帧速率。您将会注意到现在分辨率将不再增大。这是因为我们添加在文件中的类需要引用 DirectX。在下篇文章中我们将探讨我们需要 DirectX 的哪些部分。

小结

在这第一篇文章中我们已经完成了许多工作。首先,我们探讨了创建托管 DirectX 游戏所需要的工具,然后我们讨论了造就优秀游戏的特征。接着,我们定义了游戏的构思并在 Visual C# Express 中创建了游戏项目。之后,我们创建了在整个游戏中将要使用的呈现循环,最后给项目添加了一个高分辨率计时器。

 

 

版权所有:UML软件工程组织