SDL 速成教程

作者:John Hall

Linux 游戏正在蓬勃发展,部分原因是极客们喜欢游戏这个简单的事实,部分原因是 Linux 多媒体的最新发展。在过去的几年中,涌现了许多优秀的面向 Linux 的多媒体工具包,例如 GGI 图形界面和 ALSA 声音系统。SDL 库最近也引起了不小的轰动。SDL 是一个通用的多媒体编程库,它提供对图形、声音、输入设备、线程和 OpenGL 渲染的快速且可移植的访问。核心 SDL 库可移植到多种 UNIX 版本以及 BeOS、MacOS 和 Win32。这使其成为开发跨平台游戏而不影响性能的绝佳选择。

与许多多媒体工具包不同,SDL 实际上并不与系统的硬件对话。相反,它充当应用程序和底层系统之间的层。例如,SDL 的图形系统可以在 Linux 下使用帧缓冲控制台或 X11,但在 Windows 下使用 DirectDraw。无论哪种情况,SDL 的 API 都不变,应用程序无需担心底层发生了什么,在某些情况下,精心编写的 SDL 应用程序可以通过快速重新编译移植到新平台。

在本文中,我们将从头开始浏览 SDL 的视频 API。我们还将演示如何从键盘收集输入。本文的大部分内容摘自作者即将出版的关于 Linux 游戏开发的书中的一章(No Starch Press 和 Loki Entertainment Software,计划于 2001 年初出版)。

获取 SDL

SDL 是免费软件(在 LGPL 许可下),可从其网站 (http://www.libsdl.org/) 下载。除了实际的 SDL 库之外,SDL 主页还包含大量示例源代码、演示、游戏和扩展。SDL 很容易从源代码安装,但 SDL 主页也为几个更常见的平台提供了二进制文件。

SDL 的设计理念

如果您曾经使用过 Microsoft 的 DirectX 工具包,您会注意到 SDL 相比之下是一个很小的库。核心库的源代码不到 6 兆字节,其中包含许多永远不会链接到 Linux 应用程序的额外代码。不过,不要被愚弄了——这 6 兆字节得到了充分利用,核心 SDL 库几乎提供了开发高质量 Linux 游戏和媒体播放器所需的一切。此外,该网站还提供了许多附加库,这些库提供了额外的功能,例如图像加载和高级音频混合。通过将这些功能与核心库分开,SDL 仍然保持小巧且易于学习。

SDL 库由几个子 API 组成,为视频、音频、输入处理、多线程、OpenGL 渲染上下文以及游戏程序员欣赏的其他内容提供跨平台支持。不幸的是,我们没有足够的空间来涵盖所有这些内容,因此我们将坚持视频编程和输入处理,这是您真正需要了解 SDL 的两件事。

SDL 视频 API

SDL 视频 API 的唯一目的是找到合适的视频设备并将其设置为供您的应用程序使用。在它初始化显示器(创建窗口或将显卡切换到特定模式)之后,SDL 就不会妨碍您,仅提供最少量的函数来推送像素块。SDL 不是绘图工具包;您在初始化视频设备后对其执行的操作不是 SDL 的职责范围。

SDL 使用称为表面(类型为 SDL_Surface)的结构来表示图形数据。表面只是用于存储像素矩形区域(单个彩色点)的内存块。每个表面都有宽度、高度和特定的像素格式(稍后详细介绍)。SDL 将图像文件直接加载到表面结构中,屏幕也是一个表面(尽管是特殊的表面)。

表面的最重要的属性是它们可以非常快速地相互复制;也就是说,一个表面的像素可以传输到另一个表面的相同大小的矩形区域。此操作称为位块传输 (blit)。位块传输是游戏编程的基本组成部分,因为它们允许使用预先绘制的图形(通常由艺术家使用图像处理软件创建)来组合完整的图像。由于屏幕像任何其他表面一样也是一个表面,因此可以使用单个位块传输操作将整个图像发送到屏幕。SDL 提供了一个通用函数,用于在表面之间执行快速位块传输,它甚至可以动态地在不同像素格式的表面之间进行转换。

设置显示器

在我们可以开始将表面位块传输到屏幕之前,我们需要初始化 SDL 库并将显示器切换到适当的模式。请看清单 1,它相当于 SDL 中的“Hello, world!”。

清单 1. 设置显示器

此程序包含 SDL.h 头文件,它是 SDL 的主头文件。每个 SDL 应用程序都应包含此文件。该程序还包含两个标准头文件,用于 printfatexit 函数。

我们首先调用 SDL_Init 来初始化 SDL。此函数接受一个 ORed 参数列表,以指示应初始化哪些子系统;我们只对视频子系统感兴趣,因此我们传递 SDL_INIT_VIDEO(例如,如果我们想要音频,我们将使用 SDL_INIT_VIDEO | SDL_INIT_AUDIO 调用此函数)。除非发生致命错误,否则此函数应返回零。我们还使用 C 的 atexit 工具来请求在程序退出之前调用 SDL_Quit。此函数确保 SDL 有机会正确关闭(如果全屏应用程序崩溃,这一点尤其重要)。

接下来,我们使用 SDL_SetVideoMode 函数来通知显示器我们期望的分辨率(在本例中为 640 像素宽 x 480 像素高)和颜色深度(16 位压缩像素)。这里有一个陷阱:SDL 将尝试按请求设置显示器,但可能会失败。如果发生这种情况,SDL 不会告诉我们,而是会在内部模拟请求的模式。这通常是可以接受的,因为模拟代码相对较快,而且我们通常宁愿不在内部处理多种模式。SDL_SetVideoMode 返回指向表示显示器的表面的指针。如果出现问题,此函数返回 NULL。

最后,我们报告成功并退出。C 库自动调用 SDL_Quit(因为我们已使用 atexit 注册它),并且 SDL 将视频显示器返回到其原始模式。(如果我们想在退出应用程序之前关闭系统,我们也可以显式调用 SDL_Quit;多次调用它没有坏处。)

现在我们已经创建了一个 SDL 应用程序,我们需要编译它。SDL 应用程序很容易构建;假设正确安装了 SDL,它们只需要一些标志和库。标准 SDL 发行版包含一个名为 sdl-config 的程序(类似于 gtk-configglib-config 程序,它们随 GTK+ 工具包一起提供),用于向 gcc 提供适当的命令行参数。命令 sdl-config --cflags 生成应传递给编译器的选项列表,sdl-config --libs 生成应链接的库列表。我们可以使用反引号替换将其放入 gcc 命令行中。如果您的系统上安装了 SDL,您可以使用以下命令编译此示例

$ gcc sdltest.c -o sdltest `sdl-config --cflags --libs`
直接绘制像素

将数据放入 SDL 表面很简单。每个 SDL_Surface 结构都包含一个像素成员。这是一个指向原始图形图像的 void 指针,如果我们知道表面设置为哪种像素类型,我们可以直接写入它。我们必须在访问此数据之前调用 SDL_LockSurface 函数(因为某些表面驻留在特殊内存区域中,需要特殊处理)。当我们完成表面操作后,我们必须调用 SDL_UnlockSurface 来释放它。图像的宽度和高度由结构的 wh 成员给出,像素格式由 format 成员(类型为 SDL_PixelFormat)指定。SDL 通常使用更高的分辨率来模拟非标准屏幕分辨率,像素格式结构的 pitch 成员指示帧缓冲区的实际宽度。您应始终使用 pitch 而不是 w 来计算像素缓冲区中的偏移量,否则您的应用程序可能无法在某些显示设备上运行。

清单 2 中显示的示例将使用 SDL 像素格式信息在屏幕上绘制单个像素。我们选择使用 16 位(高彩色)模式进行演示,但其他模式同样易于编程。

清单 2. 在屏幕上绘制单个像素

代码的注释给出了逐步说明,但有些事情可能并不明显。此程序采用了一种非常通用的例程来构造高彩色像素值;此例程适用于 SDL 识别的任何高彩色(16 位)像素格式。虽然我们可以为每种可能的高彩色数据布局编写单独的(更快的)例程,但这将需要大量工作,并且只会略微提高性能。高彩色 565(5 位红色、6 位绿色和 5 位蓝色)像素格式可能是使用最广泛的,并且可以合理地进行优化,但 556 和 555 也很常见。此外,不能保证位字段按红-绿-蓝顺序排列。我们的 CreateHicolorPixel 例程通过引用 SDL_PixelFormat 结构中的数据来解决此问题。例如,该例程使用结构的 Rloss 成员来确定从 8 位红色分量中丢弃多少位,然后使用 Rshift 成员来确定红色位应位于 16 位像素值中的哪个位置。

另一个重要问题涉及 SDL_UpdateRect 函数。正如我们之前提到的,如果显卡无法提供某种模式,SDL 有时会模拟视频模式。例如,如果显卡不支持请求的 24 位模式,SDL 可能会选择 16 位模式并返回为 24 位像素设置的伪造帧缓冲区。这将允许您的程序继续正常运行,SDL 将动态处理从 24 位到 16 位的转换(性能略有损失)。SDL_UpdateRect 函数通知 SDL 屏幕的一部分已更新,它应执行适当的转换以显示该区域。如果程序不使用此函数,则它仍有可能工作。最好谨慎一点,并在帧缓冲区表面已更改时调用此函数。

最后,如果您运行该程序,您可能会注意到它在一个窗口中运行,而不是接管整个屏幕。要更改此设置,请将 SDL_SetVideoMode 调用中的零替换为常量 SDL_FULLSCREEN。但请注意;全屏应用程序更难调试,并且它们在崩溃时往往会严重搞砸事情。

使用位块传输绘图

我们已经了解了如何直接将像素绘制到表面,并且没有理由不能仅凭此创建一个完整的游戏。但是,有一种更好的方法可以将大量数据绘制到屏幕上。我们的下一个示例将从文件加载整个表面,并使用单个 SDL 表面复制函数绘制它。代码可以在清单 3 中看到。

清单 3. 将大量数据绘制到屏幕上

如您所见,位图文件使用 SDL_LoadBMP 函数加载到内存中。此函数返回指向包含图像的 SDL_Surface 结构的指针,或者如果无法加载图像,则返回 NULL 指针。成功加载此文件后,位图表示为普通的 SDL 表面,程序可以将其绘制到屏幕或任何其他表面上。位图使用动态分配的内存,当不再需要它们时应释放它们。SDL_FreeSurface 函数释放分配给位图的内存。

SDL_BlitSurface 函数执行从一个表面到另一个表面的位块传输,并在必要时在像素格式之间进行转换。此函数接受四个参数:源表面(要从中复制图像的图像)、定义要复制的源表面矩形区域的 SDL_Rect 结构、目标表面(要复制到的图像)和另一个 SDL_Rect 结构,指示应在目标上绘制图像的坐标。这两个矩形必须具有相同的宽度和高度(SDL 目前不执行拉伸),但区域的 xy 起始坐标可能不同。

颜色键和透明度

游戏通常需要模拟透明度。例如,假设我们有一个游戏角色位图,背景为纯色,我们想在游戏关卡中绘制该角色。按原样绘制角色看起来会很傻;背景也会被绘制出来,角色周围会围绕着一块纯色块。最好只绘制实际构成角色一部分的像素,而不是其纯色背景。我们可以使用颜色键位块传输来做到这一点。SDL 提供了对此的支持,它甚至提供了对游程长度颜色键加速的支持(加速绘图的一个不错的技巧)。RLE 加速为位块传输颜色键图像提供了巨大的性能提升,但这仅适用于在程序运行过程中不会被修改的位图(因为修改 RLE 图像需要解包和重新打包图像)。

颜色键是程序声明为透明的特定像素值(在 SDL 中,这是通过 SDL_SetColorKey 函数完成的)。当位块传输图像时,与图像的颜色键匹配的像素不会被复制。在我们的游戏角色示例中,我们可以将颜色键设置为纯色背景的颜色,并且它不会被绘制。颜色键使组合非矩形对象的矩形图像变得简单。

在下一个示例中,我们将使用颜色键位块传输来绘制 Tux 的图像,背景为另一张图像[参见清单 4,可在 ftp.linuxjournal.com.pub/lj/listings/issue81/ 获取]。Tux 存储在纯蓝色背景上,因此我们将使用蓝色 (RGB 0, 0, 255) 作为我们的颜色键。为了进行比较,我们还将绘制相同的企鹅图像,但不使用颜色键。

A Crash Course in SDL

图 1. Tux.bmp

A Crash Course in SDL

图 2. 颜色键输出

简单的键盘处理

SDL 为键盘上的每个键分配一个“虚拟键符号”。这些代码(整数)在某种程度上映射到操作系统的键盘扫描码(键盘扫描码又映射到键盘硬件生成的代码),但 SDL 在幕后处理映射。SDL 为每个虚拟键符号提供了一个预处理器符号;例如,Escape 键对应于符号 SDLK_ESCAPE。(您可以在 SDL 的文档中找到有效键符号的列表。)当我们需要直接检查特定键的状态(按下或松开)时,我们会使用这些代码,并且 SDL 在报告按键事件时会使用它们。虚拟键符号由 SDLKey 数据类型表示。

由于我们现在不会接触事件接口(实际上,我们甚至没有真正提到它),因此每当我们需要了解某个键时,都需要明确地向键盘询问其当前状态。程序可以以数组的形式获取整个键盘的快照。SDL_GetKeyState 函数返回指向 SDL 内部键盘状态数组的指针,该数组使用 SDLK_ keysym 常量进行索引。您只需要调用此函数一次;指针在程序的持续时间内保持有效。数组中的每个条目都是一个简单的 Uint8 标志,指示该键当前是否被按下。您应定期调用 SDL_PumpEvents 以更新数组中的数据。

更多、更多、更多!

这些应该足以让您开始使用 SDL。我们跳过了很多内容,特别是动画、Alpha 混合和音频播放。如果您想了解有关使用此库进行编程的更多信息,最好的起点是 SDL 文档项目,网址为 http://www.libsdl.org/。您可能还想访问 irc.openprojects.net 上的 #sdl 频道,在那里您可能会找到许多具有不同经验的 SDL 爱好者。祝您玩得开心,祝您编程愉快!

A Crash Course in SDL
John Hall 是佐治亚理工学院的计算机科学专业学生,对 Linux 游戏感兴趣。当他不沉迷于键盘前时,他经常在校园里滑旱冰或照顾他的宠物蜘蛛。可以通过 overcode@lokigames.com 联系 John。
加载 Disqus 评论