DirectX 游戏:Quadrino






4.89/5 (39投票s)
一个流行落块游戏的DirectX实现版本,尝试避免任何版权侵犯。
引言
我开发并向您展示了一个完整的2D游戏,它利用了DirectX 8.0的功能。该游戏类似于一个经典游戏,其名称可以用字母“T”“E”“R”“I”和“S”拼写。我最初在2002年实现了我的这个游戏版本。由于版权侵犯以及一个名称同样可以用字母“T”“E”“R”“I”和“S”拼写的公司的律师施压,Code Project在2007年12月被迫将其原始文章从其网站上删除。然而,由于美国版权法,我能够将我的游戏更名为“Quadrino”,现在它可以回到Code Project了。
我更新了前两段,但大部分原始文章将保持不变。大部分更改都是表面上的,包括将所有对之前暗示名称的引用更改为“Quadrino”。我已检查了源代码,并创建了一个使用Visual Studio 2005的新项目,同时我也删除了源代码中所有对禁用名称的引用。在进行这些更改时,我重新投入到我的游戏中,并修复了一些错误。我希望在不久的将来更新图形,甚至添加更多功能。
对于那些记得原始文章的人,这是原始屏幕截图。为了避免对我的努力造成进一步骚扰,我将图像像素化了。
原始文章
我对计算机图形学感兴趣已经有一段时间了,我觉得是时候学习DirectX了。我已经为我使用的DirectDraw7部分(DirectX 8.0不再支持DirectDraw接口,所以我需要使用DirectDraw7作为2D图形引擎)开发了我自己的一套包装类。这些类大致基于DirectX SDK示例以及冯渊的《Windows图形编程》一书中关于DirectX的章节。我选择创建自己的框架有几个原因:
- 我喜欢或想要的一些功能在这些类中不存在。
- 我发现这些类中的一些设计、布局或可用性存在某些弱点。
- 我认为通过重写和扩展这些类而不是简单地重用它们,我将学到更多。
DirectDraw图形只是游戏的一部分,游戏支持窗口模式和全屏模式。窗口系统使用了WTL。为此,我需要开发一个定制的CMessageLoop版本,我称之为CGameLoop,这是我在CodeProject上写的另一篇文章。我修改了SDK中包含的DirectSound示例类,并创建了我自己的类来管理音效。最后,我想要背景音乐,但通过互联网发送30 MB的音频文件并不可行,而且我没有足够的钱给大家寄送CD原声带。因此,我创建了一个利用DirectShow的简单类,它将加载和播放MP3。这会降低游戏的性能,但在速度超过1 GHz的机器上,这种差异可以忽略不计。
要求
您的显卡必须支持800x600分辨率下的24位或32位颜色模式。我最近将游戏配置为检测不同的模式。如果游戏无法将您的显卡设置为32位或24位颜色,它将通知您此错误并退出。
要编译源代码,您需要WTL和DirectX 8.0 SDK。
历史
下面我列出了自本文和代码首次发布以来执行的所有更新以及我进行更改的日期。如果CP的成员向我报告了错误或给了我改进代码的建议,我也会在更新项旁边用括号包含他们的名字。
2008年10月
文章重新发布,名称更改并修复了一些错误。
- 源项目已转换为Visual Studio 2005
- 应用程序现在默认使用UNICODE字符宽度,代码已更新以适应多字节和UNICODE编译。
- 时间变量在代码中全程使用double类型,而不是向下转换为FLOAT。这增加了动画的平滑度。
2007年12月
文章因法律原因从CP移除。
2002年11月1日
源代码已更改,并且有一个新的编译可执行文件。不需要重新下载资源文件。
- 移除了代码中所有硬编码的路径。这主要是硬编码的音频文件。(Tim Smith)
- 如果函数未能创建声音对象,DSSoundManager::CreateSound现在会初始化输出pSound数据成员。(Tim Smith)
- 修复了本文中命令列表的格式。
- 游戏现在会自动尝试将显卡设置为32位模式,如果失败则尝试24位颜色模式。以前这需要两个构建才能运行两种不同的颜色模式。
- 重新采样了一些声音,因此总下载量现在更小,约为500 KB。
- 从项目设置中移除了绝对路径定义。(Tim Smith)
游戏
我意识到许多人只想尝试示例,看看是否值得花时间查看源代码。所以,我将首先介绍游戏、规则和控制,然后我将在后面的部分解释它是如何工作的。您至少需要下载演示/资源文件。这个zip文件包含所有图形、声音文件、MP3音轨以及游戏的编译版本。然后,下载预编译的exe或源代码并自行编译exe。所有zip文件都应该解压到同一个目录中才能使项目工作。这些zip文件将为游戏创建独立的目录结构。
有三个预览方块,将按顺序显示接下来将投入使用的三个方块。还有一个称为“存储区”的单一位置。存储区允许您保留任何一个即将投入使用的方块,直到您想使用它。当您决定使用它时,您可以将当前的活动方块与存储区中的方块进行交换。
当您使用一个方块清除四行时,您将获得5分而不是4分,这被称为Tetr,呃,Quadrino。Quadrino只有直线方块才可能实现。在这个版本的Quadrino中,由于下一个功能,这个动作被大大弱化了,而下一个功能是我非常喜欢的。
Quadrino的最后一个功能是“超级方块”的创建。超级方块是通过将4个方块组合在一起形成一个4x4的正方形来创建的。如果您使用7种方块类型的组合来创建超级方块,那么将创建一个银色超级方块。每清除一行包含银色超级方块的行将价值5分而不是1分。如果您能够使用4个相同类型的方块,那么将创建一个金色超级方块,其中每清除一行将价值10分。
有许多不同的方法可以创建超级方块,这里有一些示例供您参考。
这些编队将创建金块
这些阵型将创建银块
您将能够按照标题和选项屏幕上的命令来开始游戏。对于游戏玩法,只有几个简单的命令:
命令 | 描述 |
---|---|
A | 将当前方块逆时针旋转90度。 |
S | 将当前方块顺时针旋转90度。 |
空格键 | 将当前方块与存储区中的方块交换。每个新方块只能交换一次。 |
左箭头 | 将当前方块向左移动一列。 |
右箭头 | 将当前方块向右移动一列。 |
向下箭头 | 将当前方块向下移动一行。 |
向上箭头 | 自动将方块落到阴影方块指示的位置。 |
P | 暂停游戏。 |
Alt + F11 | 在全屏模式和窗口模式之间切换游戏。全屏模式是默认设置。 |
设计
游戏由许多较小的子系统构成。每个系统都提供了一个明确的目的。我在下面列出了这些系统。
- 游戏引擎
- 主窗口
- Quadrino 视图
- DirectDraw 包装器
- 声音管理器
- 音乐播放器
在我们开始讨论独立的子系统之前,我想简单谈谈我的实现。所有图像都使用ini文件中的外部设置加载。我这样做是因为我想要一种为游戏制作多个关卡的方法。这个版本我只包含了一个,但也许以后我会扩展它并提供更多关卡。此外,许多设置只是作为常量记录在头文件中。屏幕尺寸、板尺寸和其他常量的设置也在那里定义。
游戏引擎
在编写任何显示或声音代码之前,我先从游戏逻辑开始。游戏的核心位于一个名为CQuadrinoGame
的类中。这个类包含了管理Quadrino游戏的所有逻辑。您应该能够使用这个类,并利用它创建您自己的游戏图形表示。
在内部,CQuadrinoGame
创建了另一个名为CQuadrinoBoard
的类。Quadrino棋盘负责管理与棋盘相关的所有数据。这包括方块当前放置的位置以及已完成线的状态。活动方块移动的碰撞检测也由CQuadrinoBoard类执行。
方块的数据在Pieces.h和Pieces.cpp中定义。方块定义在一组预定义结构中。每个方块可以定向的四个方向都有一个方块定义。以下是该结构的定义:
struct Quadrino_PIECE
{
BYTE cells[4][4];
BYTE bottom[4];
POINT offset;
SIZE size;
};
- cells: cells 是一个4x4的数组,表示方块在当前方向上的放置位置。
- bottom: 指示方块底部在cell数组中的位置。此字段提供碰撞检测信息。
- offset: 指示排列中方块边界框的左上角。这有助于计算方块的移动和进行碰撞检测。
- size: 指示当前方块边界框的大小。
这些方块存储在一个称为 g_Pieces 的大型多维数组中。定义了常量以在该数组中定位每个方块。下面列出了定义的常量及其含义:
- PT_EMPTY: 无方块
- PT_LINE: 直线方块,或者许多人称之为 Quadrino 方块。
- PT_LEFT: 看起来像反向“L”的方块。底部指向左侧。
- PT_RIGHT: 看起来像“L”的方块。底部指向右侧。
- PT_T: 看起来像“T”的方块。
- PT_BLOCK: 方形方块,这是一个2x2的方块。
- PT_Z: 看起来像“z”的方块。
- PT_S: 看起来像“s”的方块。
- DIR_0: 指示方块在其默认位置0度。
- DIR_90: 指示方块旋转90度。
- DIR_180: 指示方块旋转180度。
- DIR_270: 指示方块旋转270度。
以下是“T”形方块在0度方向的示例定义:
g_Pieces[PT_T][DIR_0] =
{
{
0, 0, 0, 0,
0, 0, 1, 0,
0, 1, 1, 1,
0, 0, 0, 0
},
{4, 2, 2, 2},
{1,1},
{3,2},
};
主窗口
主窗口管理游戏。这包括管理帧(在窗口模式下)、用户输入和显示。我为WTL创建了一个自定义消息处理程序类,它与我创建的CGameLoop
类协同工作。我的主窗口派生自游戏处理程序类,以便获取通知并向游戏循环提供反馈。当游戏循环没有消息要处理时,它会在主窗口中调用OnUpdateFrame
。
OnUpdateFrame
这个函数基本上是游戏处理的地方。这个函数有些简单,它做两件事:首先,它借助DirectInput处理任何输入;然后,它更新图形。这两个任务都委托给其他函数来处理更详细的方面。
游戏的当前模式取决于我用来处理输入的消息处理程序。如果用户在标题或游戏结束屏幕上,我使用主屏幕输入。如果用户暂停了游戏,则使用暂停屏幕处理程序。最后,如果游戏当前处于活动状态,则使用游戏输入处理程序。当特定输入事件触发时,也会播放声音。声音管理器将在后面部分介绍。
所有图形屏幕更新都在Quadrino视图类中完成。Quadrino视图类是主窗口的子窗口。QuadrinoView有一个名为UpdateFrame的成员函数。此函数将根据游戏的当前状态更新显示。
Quadrino 视图
CQuadrinoView
类是一个管理本游戏所有图形输出的窗口。此窗口派生自一个模拟DirectX SDK游戏窗口类的类。然而,我的版本与WTL协同工作。我创建的类名为DDrawWindow
。这个类提供了创建DirectDraw主缓冲区、初始化剪辑表面以及在窗口模式和全屏模式之间切换所需的所有基本功能。
CQuadrinoView 是游戏大部分工作所在(除了 DirectDraw 包装类)。该类的基本组织涉及一组绘图表面,这些表面缓存了显示器的不同部分。例如,背景、游戏板和方块。这些事物都被缓存,并且其显示状态被记住。当请求更新时,会检查这些缓存表面的状态,如果表面需要更新,则仅在此时进行绘制。
双缓冲
已实现双缓冲系统来管理显示。我没有创建真正的精灵动画引擎,而是选择简单地使用三个表面。一个表面是静态的后缓冲区,称为m_pUpdateSurface
,它很少改变。我在此表面上绘制背景、棋盘、分数和其他内容。每一帧,此表面都会被位图到后缓冲区,称为m_pBackSurface
。此表面是我绘制所有准备显示当前动画帧的地方。最终的表面是主显示表面。
我从不直接写入主表面(尽管DDrawWindow
基类在内部执行此操作)。为了将后台缓冲区数据显示在此表面上,我调用了CQuadrinoView
类的Flip函数。Flip函数确定游戏当前所处的模式,并适当地翻转缓冲区。在窗口模式下,最好的方法是将后台缓冲区直接位图到主表面。然而,在全屏模式下,可以使用页面翻转,这是对显存的直接操作。在全屏模式下,我通常会看到显示性能提高一倍。在调试模式下,我显示游戏的当前帧刷新率。在我的机器上,窗口模式下是25帧/秒,全屏模式下是60帧/秒。
绘制棋盘
我创建了棋盘图像的缓存。棋盘宽10格,高20格。每个格是20x20像素,因此我的棋盘表面是200x400像素。然而,这些定义都是声明为常量,所以这些尺寸可以很容易地改变。我只在Quadrino游戏类指示其中一行发生变化时才重新绘制棋盘。一行只有在新方块放置到位,或者由于行被清除而导致一排方块塌陷时才会发生变化。
目前,我只通过填充线条所占据的区域来绘制棋盘的线条,然后围绕方块的边缘绘制边框。当线条被清除时,我需要重新绘制被移除的线条,以及被清除线条上方和下方的线条。这是为了给被移除线条边缘的方块添加边框。
我想做但没想到要做的一件事是显示纹理作为设置的方块,而不是简单地使用颜色填充来绘制方块。这将需要一些我尚未设置的状态信息。我希望这在未来会成为我添加到游戏中的一项增强功能。
动画
我决定在这个游戏中使用关键帧动画。关键帧动画是指您为动画的生命周期设置关键位置,并内插所有中间帧。我选择这种方式是因为不同机器上帧率可能存在差异。我希望动画发生时声音和图形能够同步。
动画是作为独立类实现的。每个类都负责生成自己的显示。每次实例化一个类时,它都会被放入动画队列中,并且每次显示更新时都会渲染动画的帧。目前,动画只在特殊事件发生时生成,例如交换存储方块或清除一行,但我也设想为这个游戏添加背景动画。
每个动画都派生自名为Animation
的抽象基类。Animation
类很简单。它提供了初始化动画、设置动画开始延迟、在动画开始或结束时播放声音的能力,以及根据动画时间渲染当前帧的能力。以下是基类Animation
函数的摘要:
IsComplete
: 指示动画是否完成。GetStartTime
: 返回动画应该开始的时间。SetStartTime
: 设置此动画的开始时间。在此时间过去之前,它将保持非活动状态。GetTriggerID
: 返回与此动画关联的触发器ID。SetTriggerID
: 设置此动画的触发器ID。触发器ID是一个值,它指示在动画完成时应发生的视图事件。例如,再次开始方块下降,或刷新显示。SetStartSound
: 设置动画开始时播放的声音。如果未设置声音,则不会播放任何声音。SetEndSound
: 设置动画结束时播放的声音。如果未设置声音,则不会播放任何声音。RenderFrame
: 渲染动画的当前帧。计算和渲染当前帧时会考虑当前时间。
阴影块
我想快速评论一下用于显示活动方块如果任其落下将落在何处的阴影方块。我使用AlphaBlending
实现了它。然而,DirectDraw中没有alpha混合功能。因此,我手动编写了一个alpha混合函数。这工作得很好,但是它是一个资源消耗大户,当我尝试混合大量像素时,帧率开始下降。因此,我将alpha混合保留用于特殊场合,例如动画。
如果您了解Direct3D,那么您就会知道Direct3D提供了让显卡直接在显卡上进行alpha混合的功能。那是我想要的功能,但在开发这款游戏时,我还没有准备好学习Direct3D并为Direct3D表面创建我的包装类。这肯定是我未来会实现的功能,但这就是为什么它没有出现在这个游戏中的原因。
DirectDraw 包装器
一年多以前,我开始编写一个3D像素着色器。我想重温我在大学学到的一些旧的3D图形原理,并且我想学习DirectX。在编写这个程序的同时,我也开始开发我的基本DirectDraw包装类。这些类的初始设计主要基于冯远图形程序设计书中的类。然而,随着项目深入,我开始需要冯远没有提供或我想以不同方式实现的功能。
DDSurface
我创建的包装类基本上将一部分显存抽象成一个供您操作的类。这个类称为DDSurface
。这基本上是DirectDraw
类IDirectDrawSurface7
的包装器。IDirectDrawSurface7
提供的功能非常少。它允许您将表面数据位图到不同的表面并获取有关表面的信息,以及最重要的功能,即直接操作表面位的能力。这个类支持DirectDraw接口中的Blt和BltFast函数。我还尝试封装一些常见的任务,例如颜色填充。
当您处理窗口模式时,这个类负责维护许多繁琐的细节。如果您在窗口模式下绘制到表面,您需要考虑窗口客户区的位置,这与您在GDI中使用DC绘制时非常相似。因此,如果您将DDSurface与特定窗口关联,则表面会自动将所有绘图命令偏移到您指定窗口内的请求位置。将来,我还想添加支持视口缩放的功能。
DDBuffer
在直接编辑这些位之前,您需要锁定表面,然后会获得一个内存缓冲区进行操作。DirectDraw也没有能力绘制线条、矩形、椭圆、多边形等基本图形。您要么自己动手,要么使用GDI。我创建了一个名为DDBuffer
的类来封装这个过程。DDBuffer
将允许您直接操作位,以及绘制基本图形和其他基本图形函数。我模仿了GDI中的函数来设计这个类的函数,以便使系统熟悉已充分记录的API。
以下是DDBuffer支持的函数简短列表。我不会详细介绍这些函数,因为它们的行为与Win32 API中的GDI函数非常相似。
- GetPenColor
- SetPenColor
- GetFillColor
- SetFillColor
- GetPolyFillMode
- SetPolyFillMode
- GetPixel
- SetPixel
- MoveTo
- LineTo
- 矩形
- Polyline
- PolylineTo
- PolyPolyline
- SimplePolygon
- Polygon
- GradientFill
- TriangleGradientFill
- AlphaBlend
我没有将所有这些函数用于 Quadrino,但我在之前编写的 3D 像素着色器中使用了并测试了它们中的大部分。所有这些函数尚未在所有颜色深度下进行测试。我通常使用 24 位或 32 位颜色模式,因此这些类可能仍然适用于 8 位颜色模式。然而,没有理由不能扩展这些类。
DDOffScreenSurface
屏幕外表面是一种允许您从文件加载位图、创建任意大小的表面或直接从DIB部分创建表面的表面。除此之外,它的行为与DDSurface对象完全相同。
DDFontSurface
DDFontSurface
类是我从冯渊的书中学到的一个很棒的想法。DirectDraw也不支持字体。因此,如果您想要字体支持,您要么需要创建基于位图的字符块集,要么诉诸于GDI来绘制您的字体。我选择创建这个类来管理我的字体。在构建这个类时,您指定一个logfont结构,就像您在GDI中创建字体一样,这个类会将您指定的每个字符预渲染到DDSurface。然后它可以像普通位图块字体一样简单地从位图进行位图传输。
当您准备绘制字符串时,只需调用DrawText并指定目标表面、字符串和目标矩形。该函数将处理其余部分。将来,我希望扩展这个类,使其能够导出新的位图字体,以便可以对其进行外部修改和导入。我将在未来根据需要扩展这个类的功能。
声音管理器
这个类非常基础,并且密切基于DirectX 8.0 SDK中找到的代码。我改变了一些函数的工作方式,因为我认为会更高效,并且我根据自己的偏好重新格式化了代码。基本设计包括一个初始化特定窗口的DirectSound系统的类,以及一个管理单个声音的子类。
DSSoundManager
声音管理器类包含三个值得关注的函数。
Initialize
: 初始化 DirectSound 系统并将此实例与特定窗口关联。SetPrimaryBufferFormat
: 初始化将播放声音的主声音缓冲区的格式。CreateSound
: 唯一用于加载并创建新声音对象的机制。
DSSound
管理单个声音。支持的声音是波形文件。使用此类,您可以加载和管理单个波形文件声音。
- Initialize: 加载并初始化一个新的声音缓冲区。如果您希望同时播放当前声音的多个实例,那么在初始化时只需为该声音指定多个缓冲区即可。指定的缓冲区数量就是该声音可以同时播放的次数。
- Play: 播放当前声音。
- Stop: 停止播放声音
- Reset: 将声音缓冲区重置到开头。
- GetVolume: 返回此声音的当前音量级别。
- SetVolume: 设置当前声音的音量级别。DirectSound中声音的默认模型范围从-10000到0。我通过将范围设置为0-100%来简化了这一点。
音乐播放器
我曾认为我的游戏的一个巨大改进是能够播放音轨。我能想到的唯一通过互联网实现这一点的方法是使用MP3文件。令我惊喜的是,使用DirectShow中的接口播放MP3文件竟然如此简单。
我开发了一个简单的类,可以加载和播放MP3文件。这个类叫做MusicPlayer,它提供了大约10个函数供您使用。以下是其中最重要的函数的描述:
- SetCurrentFile: 设置要播放的当前文件的路径。基本上,只要安装了适当的编解码器,您就可以使用任何文件,包括MP3和波形文件。
- Play: 播放您之前加载的音乐文件。
- Pause: 暂停音乐。如果音乐再次开始播放,它将从相同的位置开始。
- Stop: 停止音乐。
- Rewind: 将音乐倒回到开头。
- IsRepeat: 指示您是否选择了重复模式。
- Repeat: 允许您设置重复模式。如果此功能被激活,音乐将在播放完成后循环。您需要通过SetNotifyWindow将此音乐播放器与一个窗口关联,以便在音乐播放完成后获取事件,从而使该类能够正确地重复播放音乐。
- SetVolume: 设置当前声音的音量级别。DirectSound中声音的默认模型范围从-10000到0。我通过将范围设置为0-100%来简化了这一点。
- SetNotifyWindow: 允许您指定一个窗口,该窗口将接收此音乐播放器的通知事件。
未来我计划扩展该类,允许您从音乐类外部处理事件。我还计划支持DirectShow中可访问的更多接口函数,这些函数将允许您在音轨中搜索特定位置和类似功能。最后,我想将其扩展以支持播放列表。当我扩展此功能时,它可能会作为CodeProject上的另一篇文章出现。
错误
不幸的是,我没有广泛的硬件来测试这个游戏,也没有大量的员工来执行我所做过的所有测试。我发现至少有一个与显卡相关的bug,我不确定问题出在哪里。我已经尽力规避这个问题。我遇到这个问题的显卡是ATI Rage 128。游戏在全屏模式下似乎运行良好,但在窗口模式下在这张显卡上运行时似乎会出现问题。
当您的显卡以低至16位颜色运行时,会出现问题。我尚未在此颜色深度下测试所有功能,但这是我计划未来改进的地方。
偶尔,当您按下“s”键开始游戏时,它会卡住,方块会持续旋转。您可以再按一次“s”键来停止。我将通过将我使用的DirectInput代码更新到DirectX 8.0来修复此问题。当我最初开始时,我使用的是所有DirectX 7接口。我还没有时间更新它。
结论
完成这个游戏我花了大约3个月的全职工作。我开始这个项目是为了学习DirectX,学习如何编写游戏,以及享受乐趣。这三点我都做到了!我希望您也能从我创建这个游戏的经验中学习。如果所有这些都没有,我希望您玩得开心。此外,我还学到了一些版权法知识。
鸣谢
我衷心感谢我的兄弟特洛伊,他为游戏创作了所有图形图像、大部分音效以及完全原创的配乐。谢谢你,特洛伊。