RaceX - 使用 DirectDraw 的 2D 赛车游戏






4.92/5 (67投票s)
2002年8月31日
14分钟阅读

585149

25226
这是一款使用DirectX包装库的2D赛车游戏。游戏支持单人和多人模式。
可选下载
引言
这是我使用DirectX库创建的第二个完整游戏(实际上是第三个,但我在几次硬盘重格式化中丢失了第二个项目的半成品)。游戏是使用我创建的一个库(名为cMain.lib)实现的,该库作为DirectX库的包装器。库的源代码包含在游戏源代码中,因此您可以使用它来创建自己的游戏。我将首先解释这个库是如何工作的,然后解释游戏是如何工作的。
编译项目之前
在编译项目之前,请确保已安装DirectX SDK 8.0(请注意是DirectX SDK,不是运行时)。如果您已安装SDK但仍无法编译项目,请检查VC++的DX包含和库目录是否位于列表的顶部。如果这不起作用,请发帖或给我发电子邮件,我会尽快为您提供帮助。
cMain库
如果您打开项目工作区文件(.DSW),您会注意到工作区由两个项目组成。一个项目是作为DirectX库包装器的库项目,它还包含几乎所有游戏项目都需要的一些其他函数。这个库包含14个类,每个类在游戏中都有其自己的功能。我将解释每个类的作用,以便您了解它们在游戏中的角色。
cApplication类
cApplication
类基本上是一个简单Windows程序的包装器。由于我们在这里使用DirectX,因此该应用程序类还负责创建使用DirectDraw所需的基本框架。在库中,我们可以找到一个全局函数负责创建应用程序(WinMain)。在那里,您会找到对CreateApplication()
函数的调用。
CreateApplication()
函数是一个虚拟函数,需要创建在游戏项目本身中,并且需要返回应用程序类的新实例。这个新实例将是您自己的应用程序类,它派生自cApplication类。
在cApplication
类中有三个非常重要的虚拟函数,它们在游戏创建过程中至关重要,它们是AppInitialized
、ExitApp
和DoIdle
。
当所有应用程序启动过程调用完成后,将调用AppInitialize
,以便您可以开始自己的初始化过程。当退出游戏时,将调用ExitApp
,用于销毁和释放在AppInitialized
函数或游戏过程中创建的任何内容。DoIdle
函数是游戏实际发生的地方。当我们没有要处理的窗口消息时,cApplication
基类会调用这个虚拟函数,允许您处理您的游戏。
cWindow类
如果您检查cApplication
类,您会发现它有一个cWindow
类实例。这个cWindow
类负责在游戏中创建主窗口。这个类仅在库内部使用,并且在游戏中不需要更改其属性。
cInputDevice、cKeyboard和cMouse类
这三个类负责我们游戏的输入。由于鼠标和键盘输入依赖于DirectInput框架,因此我们需要一个地方来放置DirectInput主对象的初始化。这个地方就是cInputDevice
类。
在cInputDevice
类中,我们有一个指向DirectInput接口的指针和一个引用计数。引用计数用于检查有多少类当前正在使用DirectInput主接口。请注意,引用计数和接口指针是静态变量。这样做是因为cMouse
和cKeyboard
类派生自cInputDevice
类,并使用相同的DirectInput主对象。
cKeyboard
类负责键盘输入。它有一个静态变量,表示一个包含每个键盘按键状态的缓冲区。这个缓冲区是一个静态变量,允许我们在代码中的任何地方创建一个cKeyboard
实例并使用相同的键盘缓冲区(我们读取键盘状态一次,并在游戏迭代中使用它)。
cMouse
类负责鼠标输入。它的工作方式与cKeyboard
类非常相似。每次调用Process()函数时,它会更改其内部变量以反映屏幕上的当前鼠标位置以及其每个按钮的状态。
cSurface和cSprite类
Surface类是DirectX Surface对象的包装器。它是一个结构,用于在视频(或本地)内存中保存游戏图形,以便我们在每次游戏迭代中将这些图形绘制到屏幕上。如果您想了解有关此类的更多信息以及 powierzchni 绘制过程,您可以阅读我在Code Project上的另一篇文章。
cSprite
类是用于处理精灵的包装器。它基本上有一个cSurface
类的实例和一些关于精灵的信息。这里的主要区别在于您可以自动在精灵步进中移动,而无需担心需要从源曲面获取的位置和大小。
cSoundInterface、cSound和cWavFile类
这三个类负责我们游戏的音频处理。cSoundInterface
创建将用于创建游戏音频缓冲区的DirectSound主对象。建议您在cApplication
类的AppInitialize
虚拟函数中初始化此类的实例,以便在游戏开始之前初始化音频接口。
cSound
类保存游戏的音频缓冲区。它具有正常DirectSound缓冲区的全部兼容性,例如频率和声像控制、3D音频和循环。为了从资源或文件中加载音频,它使用cWavFile
类的实例。这个类基本上是一个波形文件的加载器。它的所有代码都取自Microsoft DirectX示例(我稍作修改,使其可以处理资源中的WAV文件)。
cMultiplayer和cMessageHandler类
这些类是DirectPlay接口的包装器,用于创建多人游戏。cMultiplayer
类处理所有DirectPlay函数,例如设备枚举、会话枚举和玩家连接。它还保存有关游戏会话中每个玩家的所有信息。需要注意的是,每个玩家都有一个唯一的ID,该ID存储在cMultiPlayer
类中的列表中。这些ID可用于控制多人游戏会话中的某些游戏行为。
为了处理多人网络消息,cMultiplayer
类有一个指向cMessageHandler
类的指针。cMessageHandler
类是一个简单的类,带有一个虚拟函数。每当计算机收到来自对等方的DirectPlay消息时,都会调用这个虚拟函数。建议您也将应用程序类派生自这个cMessageHandler
类,以便您可以将应用程序类本身作为消息处理程序传递给多人游戏类(如RaceX中所做的那样)。
cMatrix类
这是一个用于创建动态矩阵的简单类。它没什么特别的,它有一个Create
方法来分配必要的内存,还有一个Destroy()
函数来释放内存。它还有一个GetValue
和SetValue
函数,用于检索和设置矩阵的值。
cHitChecker类
这个类负责游戏中的碰撞检测。它创建一个GDI区域并使用GDI函数测试某物是否击中了该区域。这个类与我在关于碰撞检测的另一篇文章中使用的类相同。如果您想了解更多信息,请查看另一篇文章。
游戏类和元素
现在我们对游戏库中的每个类都有了简要描述,让我们来看看游戏项目。游戏项目依赖于游戏库项目,如果您加载.DSW文件。游戏示例中的每个类都描述了游戏的一个单元(游戏本身、赛道、赛车、比赛),因此我将解释每个类,以便您了解游戏是如何工作的。
cRaceXApp
这个类派生自cApplication
和cMessageHandler
。由于我们从cApplication
派生,我们必须为AppInitialize
、ExitApp
和DoIDle
函数创建实现。
在AppInitialize
中,我们初始化一些未在基类中初始化的对象。请注意,在cRaceXApp
类中,我们有一些成员变量来处理SoundInterface、Multiplayer以及鼠标和键盘输入。所有这些成员变量都在此虚拟函数实现中初始化,调用每个成员变量的Initialize方法。请注意,在初始化cMultiplayer
实例时,我们正在调用SetHandler()
函数,并将this
作为参数传递。我们可以将“this”作为参数传递,因为我们的应用程序类派生自cMessageHandler
。使用此实现,我们可以在应用程序类本身处理DirectPlay网络消息。您可以看到我们有一个IncomingMessage
函数的实现。当我们在DirectPlay对等方收到消息时(如cMultiplayer
类描述中所述),就会调用此函数。
我们在该函数中的另一个实现是DoIdle()
实现。DoIdle
是构建整个游戏逻辑的地方。在此函数中,我们做的第一件事是检查m_iState
成员变量,以了解我们正在处理的游戏状态。当您开始游戏时,游戏状态被赋值为GS_MAINSCREEN
值,它代表菜单屏幕。您可以在DoIdle
函数中的GS_MAINSCREEN
案例中查看菜单结构的构建方式。
DoIdle
实现中的另一个重要变量是iStart变量。此变量用于确定何时在游戏状态之间进行更改。当我们更改游戏状态时,我们将此变量设置为0,以便在下一次迭代中,当基类再次调用DoIdle
时,新游戏状态可以加载其所有曲面并执行所需的额外初始化。在游戏状态初始化其对象后,它将iStart
设置为非0值,并在再次更改游戏状态时将其重置为0。
当用户在菜单屏幕上选择游戏类型之一(单人或多人游戏、单赛道或比赛模式)时,游戏将进入赛道选择屏幕(或开始比赛屏幕)。当用户进入赛道选择屏幕时,程序将在cRaceXApp
类中初始化一些其他类实例,即cCompetition
实例和cRaceTrack
实例。我将解释这两个类,以便您了解此屏幕的工作原理。
cCompetiton类
即使名称表明此类负责处理游戏中的所有比赛相关事务,但该类甚至在单赛道模式下也使用。此类存储有关游戏中每个玩家的一些基本信息。当我们开始游戏时,我们需要调用此类中的AddPlayer
方法将玩家添加到我们的游戏中。将玩家添加到此类会使他们在比赛中出现,当我们把游戏状态改为GS_RACE
时。如果您查看GS_RACE
状态,您会发现它使用cCompetition
类实例中的玩家列表信息为比赛创建每辆赛车。此类还存储比赛模式下每个玩家的分数和位置,并且还负责通过使用NextRace
和GetNextRace
方法告知程序比赛的赛道顺序。
cRaceTrack类
cRaceTrack
类负责游戏中赛道的创建和处理。大部分游戏逻辑都在这个类里面。使用这个类时,我们需要做的第一件事是从赛道文件中加载一个赛道。该类有一个ReadFromFile
方法,用于从.rxt文件(Race X Track文件)加载赛道。
当从文件加载赛道时,它会用赛道的信息填充cRaceTrack类的内部成员变量。赛道结构为一个二维矩阵,矩阵的每个单元格代表一种道路类型。当我们在屏幕上绘制赛道时,我们使用这种道路类型在对应于矩阵位置的地方绘制一个40x40像素的图块。在赛道文件中,一个DWORD
数组描述了赛道中每个图块。由于一个DWORD可以存储4个字节,我们只使用LOWORD
来存储道路类型。此DWORD
数组的HIWORD
用于存储其他信息,即LOBYTE
中的检查点(CheckPoints)和HYBYTE
中的角度信息(Angle Information)。
存储在TrackFile中的CheckPoint用于控制赛道中的运行顺序。因此,如果赛车通过了检查点1,它就需要通过检查点2来完成赛道,依此类推,直到它到达最后一个检查点。使用这种检查点结构可以防止用户从起点线向后跑,并多次通过起点线,增加其完成圈数计数器。由于他必须通过所有检查点,因此他必须跑完整个比赛路线。
赛道圈数计数器将在我们再次到达检查点1并且我们已通过的最后一个检查点是此赛道上可用的最后一个检查点时增加。角度信息用于使计算机能够驾驶赛车。它将指向计算机控制的赛车应朝向的方向,以便它能够完成比赛。现在我们将检查cRaceCar
类,以便了解此角度信息的作用。
cRaceCar类
cRaceCar
类负责游戏中所有赛车行为的处理。这辆赛车类最重要的函数是Process
函数,该函数在每次游戏迭代中处理赛车行为。
当我们调用Process
函数时,赛车类会检查此赛车实例是如何控制的。赛车可以由计算机、用户或网络控制。
如果赛车由用户控制,赛车类会检查键盘输入,以查看用户是否试图加速、刹车或转弯。根据从键盘检索到的信息,该类会调用Accellerate
、BreakCar
、TurnCarRight
或TurnCarLeft
方法。
如果赛车由计算机控制,赛车类会检查赛车在赛道上的当前位置,并获取与该位置相关的角度信息。如果角度与赛车的当前角度不同,计算机将向顺时针或逆时针方向转动赛车。计算机在驾驶时总是加速,除非它检测到它将撞墙,这时它会保持相同的速度。
如果赛车由“网络”控制,我们需要检查我们是否是游戏的主机。如果我们是游戏的主机,我们需要根据远程计算机的键盘输入来处理赛车信息。如果游戏不是由我们主机托管,我们需要将我们的键盘信息发送给多人游戏主机。
整合 - 处理赛道
当我们开始新比赛,并且游戏状态变为GS_RACE
时,我们根据cCompetiton类中的信息创建cRaceCar
对象的实例。然后,我们调用cRaceTrack
类中的AddCar()
方法将每辆赛车添加到赛道比赛中。在将所有赛车添加到赛道比赛后,我们就可以处理赛道类,以进行游戏。
在每次游戏迭代中,我们都会调用cRaceTrack
类的Process()
方法。在此方法中,我们将循环遍历cRaceTrack类中可用的赛车数组,以调用每辆赛车的Process()方法并将它们在赛道上移动。我们还将使用cHitChecker
类来检查每辆赛车是否撞墙或撞到另一辆赛车。如果赛车撞墙,我们将其状态更改为CARSTATE_CRASHEDWALL
。赛车将继续在赛道上行驶,直到用户赛车完成赛道(圈数等于赛道上的总圈数)。
结束语
我试图在文章中解释游戏的基本功能,但游戏示例中有很多注释,这些注释将帮助您更好地理解游戏。如果您对游戏实现有任何疑问,请发帖或给我发电子邮件。
我想特别感谢我所有的测试者,特别是Colin J Davies、Isaac Sasson、James T Johnson、Nishant Sivakumar、Nnamdi Onyeyiri和Smitha(Tweety),感谢他们努力寻找游戏中的错误,以及他们所有的建议。