探索 SDL
SDL 是一个跨平台应用程序编程接口 (API),它允许您在多个平台上编写图形代码。它的许多工作都在幕后完成,为您(程序员)提供了一个更容易的接口来访问这些内部机制。
跨平台编程是通过动态检查用户正在运行的操作系统来实现的。这是通过使用条件宏来完成的
#ifdef _WIN32
/*Windows specific code here*/
#endif
#ifdef _APPLE_
/*Macintosh OS code here*/
#endif
#ifdef _linux_
/*Linux code here*/
#endif
这些宏检查操作系统编译器库中存储的预定义变量的存在。根据定义的变量,将执行对应于该特定系统的代码。此方法还防止操作系统特定代码相互冲突。
这样做的原因是操作系统具有不同的显示图形方式。尽管代码在每个操作系统中都不同,但大多数操作都执行类似的任务,例如创建窗口、渲染到窗口、获取用户输入等。SDL 将这些任务整合到统一的接口上,让您基本上可以在多个平台上编写、编译和运行程序。
视频表面是包含像素数据的视频内存块。每个像素都由表面存储的内存块内的内存位置表示。此内存块通常是一个简单的数组,有时称为线性内存。表面的主要属性是其以像素为单位的宽度和高度,以及其像素格式,即为每个像素分配的数据量。此像素大小决定了可以显示到表面的颜色数量。
像素格式 | 大小(颜色数量) |
---|---|
索引(调色板) | 1 字节 – 256 种颜色 |
高色 | 2 字节 – 65536 种颜色 |
真彩色 | 3-4 字节 – 16777216 种颜色 (额外字节用于 alpha 值,或未用) |
主视频表面指的是用户屏幕上的图像。与 SDL 视频表面类似,此表面的主要属性是其尺寸(宽度和高度)及其像素格式。
#include <stdio.h>
#include <stdlib.h>
#include "SDL/SDL.h"
int main(int argv, char **argc)
{
SDL_Surface *display;
SDL_Rect sDim;
/*initialize SDL video subsystem*/
if(SDL_Init(SDL_INIT_VIDEO) < 0){
/*error, quit*/
exit(-1);
}
/*retrieve 640 pixel wide by 480 pixel high 8 bit display with video memory access*/
display = SDL_SetVideoMode(640, 480, 8, SDL_HWSURFACE);
/*check that surface was retrieved*/
if(display == NULL){
/*quit SDL*/
SDL_Quit();
exit(-1);
}
/*set square dimensions and position*/
sDim.w = 200;
sDim.h = 200;
sDim.x = 640/2;
sDim.y = 480/2;
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
/*wait 5 seconds (5000ms) before exiting*/
SDL_Delay(5000);
SDL_Quit();
exit(0);
}
截至 2014 年 10 月,SDL2 的当前稳定版本为 2.0.3。设置此库类似于 SDL 1.2 设置,但有一个小错误,该错误应该在下个版本 (2.0.4) 中解决。然而,在发布之前,有一个变通方法。错误位于 SDL2 头文件包含文件夹中,特别是 SDL_platform.h 文件。此错误(及其解决方案)在 这里 有更好的解释。以下是替换文件的链接:SDL_platform.h。
SDL2 还导致上述函数发生变化。不是使用 SDL_SetVideoMode 创建表面,而是可以创建窗口,并使用该窗口获取表面
SDL_Window* window = SDL_CreateWindow(
"window title", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
if (window == NULL) {
SDL_Quit();
exit(-1)
}
SDL_Surface* display = SDL_GetWindowSurface(window);
同样,在 SDL2 中,不是调用 `SDL_UpdateRect(display, 0, 0, 0, 0);`,而是可以调用
SDL_UpdateWindowSurface(window);
将 SDL 头文件从开发包复制到编译器的包含目录中。为了更好地组织,在包含目录中创建一个名为“SDL”的新文件夹,并将所有头文件复制到该文件夹中。
要从 gcc 编译器链接 SDL 库,请使用以下语法
gcc sdl_prog.c -o sdl_out -lSDL
在 Windows 中,您还需要链接 SDLmain 库
gcc sdl_prog.c -o sdl_out -lSDL -lSDLmain
在 SDL 程序的顶部,包含标准的 C 头文件以允许输入/输出以及内存分配
#include <stdio.h>
#include <stdlib.h>
假设您将 SDL 头文件复制到包含目录中的 SDL 文件夹中,请在 C 头文件之后添加此头文件包含
#include “SDL/SDL.h”
创建主入口点函数并包含其命令行参数。SDL 在内部需要这些参数进行初始化。
int main(int argc, char **argv)
{
SDL 的初始化使用 SDL_Init 函数完成,该函数接受一个参数,即要初始化的系统。
int SDL_Init(Uint32 flags);
如果函数成功,则返回值为 0。l 如果由于任何原因初始化失败,则返回值为 -1。可用的系统包括:视频、音频、CD-ROM、计时器和操纵杆。您可以使用这些标志值指定要初始化的系统,这些标志值可以通过 OR(|)运算进行组合以允许初始化多个系统
SDL_INIT_VIDEO
SDL_INIT_AUDIO
SDL_INIT_CDROM
SDL_INIT_TIMER
SDL_INIT_JOYSTICK
现在,我们只需要初始化视频系统,因此我们只包含该标志作为 SDL_Init 函数的参数。此外,我们检查返回值是否为 -1,以确保它已成功初始化。如果初始化失败,我们立即使用 C 函数 exit 退出程序
if(SDL_Init(SDL_INIT_VIDEO) < 0){
/*error initializing*/
exit(0)
}
请注意,无论提供哪些标志,调用 SDL_Init 函数都会自动初始化事件处理、文件输入/输出和线程系统,我们将在后面讨论。
现在我们开始初始化主显示表面。主显示器表示为 SDL Surface。让我们快速浏览一下 SDL_Surface 结构
SDL Surface Structure
typedef struct SDL_Surface {
Uint32 flags; /* Read-only */
SDL_PixelFormat *format; /* Read-only */
int w, h; /* Read-only */
Uint16 pitch; /* Read-only */
void *pixels; /* Read-write */
/* clipping information */
SDL_Rect clip_rect; /* Read-only */
/* Reference count -- used when freeing surface */
int refcount; /* Read-mostly */
/* This structure also contains private fields not shown here */
} SDL_Surface;
- flags – 设置表面支持的某些功能。这将在我们设置显示表面时讨论
- format – 像素数据的描述
- w,h – 表面的尺寸(宽度和高度)
- pitch – 表面每行的长度(扫描线)。以字节为单位。请注意,pitch 并不总是与表面宽度匹配。
- pixels – 用于渲染的实际表面数据。
- clip_rect – 用于将表面的绘制限制为设置尺寸的剪切矩形。
要使用给定的位深度和尺寸检索主视频显示,可以使用 SDL_SetVideoMode 函数
SDL_Surface *SDL_SetVideoMode(int width, int height, int bpp, Uint32 flags);
宽度和高度指的是您希望设置的屏幕的像素尺寸。每像素位数可以设置为标准的 8、16、24、32,也可以设置为 0,这将与用户当前的显示位深度匹配。flags 参数指的是您希望表面支持的额外功能。在这里,我将介绍一些主要的功能
SDL_HWSURFACE | SDL_HWSURFACE 允许将表面数据(即屏幕、位图、字体)直接存储到视频内存中。SDL_SWSURFACE 将表面数据存储在主系统内存中,这往往速度较慢。 |
SDL_DOUBLEBUF | 这允许双缓冲支持以实现更流畅的动画。请注意,此标志仅在与 SDL_HWSURFACE 标志进行 OR 运算时才有效。也可以在不指定此标志的情况下实现此效果,这将在后面介绍。 |
SDL_FULLSCREEN | 允许 SDL 控制窗口系统并以给定的分辨率在整个屏幕上显示图像。退出时,SDL 会将控制权返回给操作系统及其默认分辨率。 |
SDL_HWPALETTE | 提供对调色板的访问权限。仅在 8 位索引模式下有效。 |
在这个例子中,我们唯一设置的标志是 SDL_HWSURFACE 标志,它允许将 SDL_Surface 数据存储到视频内存而不是主系统内存,这可以提供更好的性能。如果用户的机器无法支持视频内存存储,SDL 将回退到系统内存存储。
此函数在成功后将返回指向所请求的 SDL_Surface 的指针。如果由于任何原因失败,它将返回 NULL。请务必始终检查返回值以确保视频模式已设置。
if(display == NULL){
/*quit SDL*/
SDL_Quit();
exit(-1);
}
函数 SDL_Quit 关闭了您使用 SDL_Init 函数初始化的所有 SDL 子系统。由于我们只初始化了视频,SDL_Quit 将只关闭该系统。
接下来的代码片段设置了用于确定绘制位置的 SDL Rect 结构。
typedef struct{
Sint16 x, y;
Uint16 w, h;
} SDL_Rect;
* x, y – Position of upper left corner of rectangle in relation to the display surface coordinate system. * w, h – Width and height of rectangle
显示表面被设置为一个网格,原点 (x=0, y=0) 是显示屏的左上角。此网格的每个条目都是该位置像素的颜色值。在这个例子中,我们定义的矩形将它的左上角 (原点) 定位在右侧 320 像素、下方 240 像素处,宽度为 200 像素,高度为 200 像素。
/*set square dimensions and position*/
sDim.w = 200;
sDim.h = 200;
sDim.x = 640/2;
sDim.y = 480/2;
设置矩形尺寸后,我们可以使用 SDL_FillRect 函数用纯色填充它。
int SDL_FillRect(SDL_Surface *dst, SDL_Rect *dstrect, Uint32 color);
* dst – SDL_Surface pointer to draw to. Specifying the main display surface will cause drawing to the screen. * dstrect – Rectangle structure pointer containing upper-left position and dimensions of rectangle to fill with color * color – Color to fill rectangle structure with
颜色使用 SDL 函数 SDL_MapRGB 指定。
Uint32 SDL_MapRGB(SDL_PixelFormat *fmt, Uint8 r, Uint8 g, Uint8 b);
* fmt – Pixel structure defining pixel color, depending on the depth. Specifying the display surface's structure member format, matches the color to that display's pixel format. * r,g,b – Individual color components to specify final color.
此函数的返回值是与指定表面像素格式匹配的颜色值。此值可用作 SDL 绘图函数中的颜色参数。请注意,如果显示深度低于 24 位,此函数将返回与指定颜色最接近的匹配颜色,因为在如此小的位深度 (8 位或 16 位) 上像素格式的空间有限。
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
在向主显示表面绘制内容后,我们需要指示 SDL 刷新表面的内容,以确保我们获得表面的当前状态。函数 SDL_UpdateRect 更新给定表面的一部分矩形区域。如果我们想要更新整个表面,我们将位置和尺寸设置为 0,这将指示 SDL 更新整个表面。
void SDL_UpdateRect(SDL_Surface *screen, Sint32 x, Sint32 y, Sint32 w, Sint32 h);
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
在最终在屏幕上绘制内容后,我们需要防止 SDL 在我们运行它时立即关闭,方法是引入基于时间的延迟。现在,我们将指示 SDL 在绘制到显示表面并更新显示表面后,等待 5 秒钟再退出,方法是使用 SDL 时间函数 SDL_Delay,该函数接受一个参数,即毫秒数 (在 5 的情况下为 5000 毫秒),等待该时间后,程序将继续执行下一行代码。
void SDL_Delay(Uint32 ms);
/*wait 5 seconds (5000ms) before exiting*/
SDL_Delay(5000);
延迟应用程序后,我们使用 SDL_Quit 函数关闭所有 SDL 系统 (对于此示例,只是视频子系统),并使用成功代码 (0) 退出程序。
void SDL_Quit(void);
SDL_Quit();
exit(0);
循环和基本输入
[edit | edit source]在前面的示例中,程序运行的持续时间由我们想要延迟的毫秒数决定。这种方法对于让我们查看单个帧的图形是有效的,但如果我们想要查看多帧,例如动画的绘制,则不是有效的。理想的帧循环将允许我们以设定的时间间隔重复绘制和更新屏幕。让我们看一下通用 C 循环实现。请注意,其中没有计时机制,这会导致循环以由屏幕绘制和更新速度决定的间隔重复。
int loop = 1;
while(loop){
if(quit_event()){
loop = 0;
}
draw_and_update_screen();
}
这个循环会导致屏幕更新尽可能快地进行。当程序接收到用户想要退出的事件时,循环最终退出,并且循环变量设置为 0,以便在循环重新启动时评估为 false。请注意,即使在收到退出事件后,在循环退出之前也会进行一次屏幕更新。
在实现此循环之前,我们必须了解 SDL 如何处理事件,例如键盘或鼠标输入。事件使用 SDL_Event 联合体进行处理。
typedef union{
Uint8 type;
SDL_ActiveEvent active;
SDL_KeyboardEvent key;
SDL_MouseMotionEvent motion;
SDL_MouseButtonEvent button;
SDL_JoyAxisEvent jaxis;
SDL_JoyBallEvent jball;
SDL_JoyHatEvent jhat;
SDL_JoyButtonEvent jbutton;
SDL_ResizeEvent resize;
SDL_QuitEvent quit;
SDL_UserEvent user;
SDL_SywWMEvent syswm;
} SDL_Event;
要确定发生了什么类型的事件 (例如,按键、鼠标点击),我们检查类型成员的值。处理的其他一些事件包括操纵杆输入和显示大小调整 (在程序运行时更改分辨率和/或位深度)。现在,我们只介绍键盘和鼠标事件类型。
SDL_KEYDOWN | 当用户按下某个键时 |
SDL_KEYUP | 用户释放键 |
SDL_MOUSEMOTION | 鼠标移动 |
SDL_MOUSEBUTTONDOWN | 鼠标按钮点击 |
SDL_MOUSEBUTTONUP | 鼠标按钮释放 |
如果类型成员的值为 SDL_KEYDOWN 或 SDL_KEYUP,则将设置 SDL_Event 联合体的键成员,它是一个 KeyboardEvent 结构指针。
如果我们只检查按键事件,甚至不知道按下了哪个键,我们就可以实现循环的第一个版本。如果您还记得前面的示例,循环的基本任务是
将布尔变量 (int) 初始化为 true (1) 如果变量为 true,则进入循环 在循环中 如果出现退出事件 (键盘按下),则将变量设置为 false (0) 绘制和更新屏幕
重写前面的 SDL 简介程序,以实现此循环,您需要声明一个 SDL_Event 实例以及一个用于保存循环值的整数变量。
SDL_Event event;
int loop = 1;
在设置 SDL_Rect 结构的代码之后,我们使用一个 while 条件语句来检查是否需要运行循环。
while(loop){
现在,我们检查键盘事件。如果按下任何键,程序将把循环变量设置为 0,程序将继续执行 while 循环后面的代码。
现在,我们填写创建的 SDL_Event 联合体实例。SDL_PollEvent() 函数将 SDL_Event 指针 (变量引用) 作为参数,如果发生事件 (例如,键盘、鼠标),则填充事件联合体的相应成员。如果正在处理事件,此函数返回 1,否则返回 0,表示没有事件正在处理。
int SDL_PollEvent(SDL_Event *event); event – SDL_Event 指针,用于填充相应的事件数据 返回值:如果正在处理事件,则为 1;如果未处理事件,则为 0 |
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
loop = 0;
}
}
检查键盘事件后,我们可以使用前面示例中学习的函数绘制和更新屏幕。
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
}
完整的循环(包括初始化变量)如下所示
SDL_Event event;
int loop = 1;
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
loop = 0;
}
}
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
}
此循环运行良好,除了它不允许任何键盘输入,因为所有按键在按下时都会发送 SDL_KEYDOWN 事件类型。如果我们想要将退出程序与特定的键盘按键联系起来 (例如,按下 Esc 键退出),我们将按照前面列出的事件处理程序进行操作,但不是在按下某个键时将循环变量设置为 0,而是创建一个另一个 if 条件语句,检查按下了哪个键并做出相应的响应。这是一个通用的伪代码概要,用于描述我们要实现的新循环。
- 声明布尔循环变量并设置为 true
- 进入循环 IF 循环变量为 TRUE
- 如果按下了键盘键 (SDL_Event.type == SDL_KEYDOWN)
- 如果按下的键盘键是
键 (SDL_Event.key.keysym.sym == SDLK_ESCAPE) - 将循环变量设置为 FALSE
- 绘制和更新屏幕
因此,使用前面相同的代码,在事件处理程序中,我们检查是否按下了键盘键 (无论具体是哪个键)。在这个 if 条件语句中,我们没有立即将循环变量设置为 0,而是检查按下的具体键盘键的值。键盘值存储在 SDL_Event 结构体的 SDL_KeyboardEvent 成员 key 中。
typedef union{
Uint8 type;
SDL_ActiveEvent active;
SDL_KeyboardEvent key;
SDL_MouseMotionEvent motion;
…
}
SDL_KeyboardEvent 结构比包含它的 SDL_Event 联合体要简单得多。
typedef struct{
Uint8 type;
Uint8 state;
SDL_keysym keysym;
} SDL_KeyboardEvent;
- type – 与我们在循环第一个版本中检查的 SDL_Event 类型成员相同。由于这是一个键盘结构,因此此成员的唯一值为 SDL_KEYDOWN 或 SDL_KEYUP。
- state – 键盘的状态。可以是 SDL_PRESSED 或 SDL_RELEASED。请注意,此成员与类型成员的功能相似。
- keysym – SDL_keysym 结构,包含按下的或释放的实际键值。
typedef struct{
Uint8 scancode;
SDLKey sym;
SDLMod mod;
Uint16 unicode;
} SDL_keysym;
这里唯一重要的字段是 SDLKey 成员 sym,它包含键盘上每个键的预定义标识符值。scancode 成员包含有关键盘键的信息,但以硬件相关的方式 (例如,一个系统上的扫描码与另一个系统上的扫描码不同)。sym 成员包含跨平台识别的系统无关键代码。以下是 SDLKey 成员的一些值。
SDLK_ESCAPE | 按下了 |
SDLK_RIGHT | SDLK_DOWN | SDLK_LEFT | 按下了方向键 |
因此,要检查是否按下了
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
将此放在检查 SDL_KEYDOWN 事件类型的条件语句中。
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
}
}
程序中的最新循环现在应该这样实现
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
}
}
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
}
基于时间的延迟
[edit | edit source]在实现游戏循环时,必须能够以恒定的帧速率运行它。SDL 的计时机制精确到毫秒。考虑到一秒钟有 1000 毫秒,通过能够检索程序执行以来的当前毫秒数 (或“滴答数”),人们可以实现一个以恒定速率设置的基于时间的循环。
如果您希望将程序设置为每秒更新 20 帧作为它的帧速率,则可以通过将一秒钟中的毫秒数 (滴答数) (1000) 除以所需的帧速率来确定帧更新之前需要经过的滴答数。
tick_count = (1000 / frame_rate)
要获得 20 帧/秒帧率所需的滴答计数
tick_count = (1000/ 20) = 50 ticks (ms)
使用 SDL_Delay 函数,该函数接受一个参数,即延迟的毫秒数,我们可以实现基于时间的循环的第一个实现。
/*set tick interval to 20 frames/ second (1000ms / 20 = 50 ticks)*/
#define TICK_INTERVAL 50
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
…}
draw_and_update_screen();
SDL_Delay(TICK_INTERVAL);
/*end game loop*/
}
此循环的主要问题是 [b]draw_and_update_screen()[/b] 函数可能需要几毫秒才能完成,具体取决于绘制的图形的复杂性。例如,如果每帧绘制和更新屏幕需要 5 毫秒,再加上循环结束时 [b]滴答间隔[/b] 的延迟,我们的帧率将从每 50 个滴答更新一次变为每 55 个滴答更新一次,使我们的恒定速率降低了 5 个滴答。
为了解决这个问题,我们必须能够跟踪程序开始时和循环中经过的时间,并考虑处理游戏循环所需的时间。
SDL 包含一个函数,允许你检索程序开始运行以来的滴答计数。
Uint32 SDL_GetTicks(void);
Get the number of milliseconds since the SDL library initialization.
因此,当程序第一次启动时,[b]SDL_GetTicks[/b] 的返回值将为 0。如果程序执行其他需要 30 个滴答才能完成的过程,然后再次检索此函数的值,它将返回 30。
为了实现一个始终如一的帧速率,它将考虑运行循环所需的时间,我们必须使用两个变量来跟踪时间。
- 一个使用 SDL_GetTicks 获取当前滴答计数的变量。
- 一个静态变量,它保存帧将更新的下一个滴答计数,当前滴答计数需要达到该值才能更新到下一帧。此变量的值将是当前滴答计数加上滴答间隔(在本例中为 20 帧/秒速率的 50),以确定何时需要绘制下一帧。
static Uint32 next_tick = 0;
Uint32 cur_tick;
/*retrieve program tick count once per frame*/
cur_tick = SDL_GetTicks();
[i] 类型 [b]Uint32[/b] 表示一个 4 字节(32 位)整数,SDL 在其库中定义了该类型以实现跨平台兼容性。此类型类似于在 32 位机器上将变量声明为 [b]unsigned int[/b]。[/i]
声明并设置这些变量后,我们将进行比较,以查看当前滴答计数是否已达到下一个滴答计数的值,以指示帧递增(更新到下一帧)。
if(next_tick <= cur_tick){
当当前滴答计数等于或超过下一个滴答计数时,这将评估为真,允许帧更新。如果是这样,我们需要更新下一个滴答计数,以便将其用于下一次帧更新,方法是向其添加滴答间隔值(在本例中为 50)。
next_tick = cur_tick + 50;
如果上述比较评估为假,则意味着该帧尚未准备好更新,并且帧中还有剩余时间。要检索剩余时间,只需计算下一个滴答计数与当前滴答计数之间的差值即可。
int time_left = next_tick – cur_tick;
你会将此值传递给 [b]SDL_Delay[/b] 函数,以使程序以恒定速率运行。
以下是内联实现的时间循环的下一个实现(注意,稍后我们将把所有这些逻辑放入单独的函数中)。
#define TICK_INTERVAL 50
/*declare variables at beginning to keep track of current and next tick count*/
Uint32 next_tick = 0;
Uint32 cur_tick = SDL_GetTicks();
/*variable to hold number of ticks to delay after update of screen*/
Uint32 tick_delay = 0;
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
…}
draw_and_update_screen();
/*retrieve tick count up to now (after time taken to draw and update screen)*/
cur_tick = SDL_GetTicks();
if(next_tick <= cur_tick){
next_tick = cur_tick + TICK_INTERVAL;
tick_delay = 0;
}else{
tick_delay = (next_tick - cur_tick);
}
SDL_Delay(tick_delay);
/*end game loop*/
}
基于时间的动画示例
[edit | edit source]此示例将采用我们在本章开头做的程序,添加我们在本章中讨论过的事件处理和时间延迟概念。
示例代码:动画矩形
[edit | edit source] #include <stdio.h>
#include <stdlib.h>
#include "SDL/SDL.h"
#define TICK_INTERVAL 50
Uint32 TimeLeft(void)
{
Uint32 next_tick = 0;
Uint32 cur_tick;
cur_tick = SDL_GetTicks();
if(next_tick <= cur_tick){
next_tick = cur_tick + TICK_INTERVAL;
return 0;
}else{
return (next_tick - cur_tick);
}
}
int main(int argv, char **argc)
{
SDL_Surface *display;
SDL_Rect sDim;
/*direction of moving block (0 - left, 1, -right)*/
int dir = 0;
SDL_Event event;
int loop = 1;
/*initialize SDL video subsystem*/
if(SDL_Init(SDL_INIT_VIDEO) < 0){
/*error, quit*/
exit(-1);
}
/*retrieve 640 pixel wide by 480 pixel high 8 bit display with video memory access*/
display = SDL_SetVideoMode(640, 480, 8, SDL_HWSURFACE);
/*check that surface was retrieved*/
if(display == NULL){
/*quit SDL*/
SDL_Quit();
exit(-1);
}
/*set square dimensions and position*/
sDim.w = 200;
sDim.h = 200;
sDim.x = 640/2;
sDim.y = 480/2;
dir = 1;
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
}
}
/*update rectangle position*/
if(dir){
if((sDim.x + sDim.w) < 640){
sDim.x += 1;
}else{
dir = 0;
}
}
if(!dir){
if(sDim.x > 0){
sDim.x -= 1;
}else{
dir = 1;
}
}
/*clear display with black*/
SDL_FillRect(display, NULL, SDL_MapRGB(display->format, 0, 0, 0));
/*draw square with given dimensions with color blue (0, 0, 255)*/
SDL_FillRect(display, &sDim, SDL_MapRGB(display->format, 0, 0, 255));
SDL_Delay(TimeLeft());
/*inform screen to update its contents*/
SDL_UpdateRect(display, 0, 0, 0, 0);
}
SDL_Quit();
exit(0);
}
程序的第一个新增部分是 dir 变量,如果它的值为正(1),则会导致我们在本章开头定义的矩形向右移动一个像素/帧。如果 dir 为 0,当矩形移出屏幕时会发生这种情况,则矩形向左移动 1 个像素/帧。时间间隔设置为每 50 个滴答更新一次(20 帧/秒),因此矩形相对于屏幕坐标的速度为 20 像素/秒。
/*direction of moving block (0 - left, 1, -right)*/
int dir = 0;
在游戏循环中,在我们向屏幕绘制矩形之前,我们有条件语句来确定矩形应该移动的方向(1-右,0-左),具体取决于它是否到达了屏幕上的边界。
/*update rectangle position*/
if(dir){
if((sDim.x + sDim.w) < 640){
sDim.x += 1;
}else{
dir = 0;
}
}
if(!dir){
if(sDim.x > 0){
sDim.x -= 1;
}else{
dir = 1;
}
}
位图和精灵
[edit | edit source]本章将讨论使用位图图像文件以及在 SDL 程序中加载和显示位图图像文件。我们将从讨论 Windows 位图格式(在所有平台上都支持)开始。
位图分解
[edit | edit source]位图文件,除了实际的图像数据外,还包含类似于在 SDL 表面中找到的信息。这包括宽度和高度,以及图像数据的每像素位数(像素格式)。除了这些信息外,还有位图文件的大小(以字节为单位),以及帮助程序员读取位图文件以在图形程序中使用的信息。
作为 SDL 程序员,我们不必太担心位图文件的内部结构。SDL 包含函数来读取位图文件并将其显示为 SDL 表面。
SDL 位图程序
[edit | edit source]使用以下图像(保存在 .bmp 格式中),我将向你展示的代码清单将读取文件(假定名为 'clouds.bmp')并在屏幕上显示它。请注意,由于此图像的像素格式为 24 位(3 字节),因此显示表面的位深度也将设置为该值以实现兼容性。(作为一个有趣的练习,在运行以下代码后,尝试将显示表面的位深度设置为不同的值,并查看结果。它看起来正常吗?)
#include <stdio.h>
#include <stdlib.h>
#include "SDL/SDL.h"
#define TICK_INTERVAL 50
Uint32 TimeLeft(void)
{
Uint32 next_tick = 0;
Uint32 cur_tick;
cur_tick = SDL_GetTicks();
if(next_tick <= cur_tick){
next_tick = cur_tick + TICK_INTERVAL;
return 0;
}else{
return (next_tick - cur_tick);
}
}
int main(int argv, char **argc)
{
SDL_Surface *display;
SDL_Surface *image;
SDL_Event event;
int loop = 1;
/*initialize SDL video subsystem*/
if(SDL_Init(SDL_INIT_VIDEO) < 0){
/*error, quit*/
exit(-1);
}
/*retrieve 640 pixel wide by 480 pixel high 24 bit display with video memory access*/
display = SDL_SetVideoMode(640, 480, 24, SDL_HWSURFACE);
/*check that surface was retrieved*/
if(display == NULL){
/*quit SDL*/
SDL_Quit();
exit(-1);
}
image = SDL_LoadBMP("clouds.bmp");
if(image == NULL){
SDL_Quit();
exit(-1);
}
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
}
}
SDL_BlitSurface(image, NULL, display, NULL);
SDL_UpdateRect(display, 0, 0, 0, 0);
}
SDL_FreeSurface(image);
SDL_Quit();
exit(0);
}
SDL 函数 SDL_LoadBMP 接受一个参数,即位图文件的名称。如果文件加载成功,该函数将返回指向 SDL 表面的指针(类似于显示表面),其中加载了位图数据。如果由于任何原因(例如,文件名错误或数据不兼容)函数失败,它将返回 NULL。就像你检查显示表面以查看它是否为 NULL 一样,对位图表面也这样做。
image = SDL_LoadBMP("clouds.bmp");
if(image == NULL){
SDL_Quit();
exit(-1);
}
要查看图像,我们必须将图像数据复制到显示表面。为此,我们使用 SDL_BlitSurface 函数。
int SDL_BlitSurface(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect);
第一个参数是我们想要显示的位图图像表面。第二个参数是我们想要显示的位图表面的尺寸。将其设置为 NULL 允许显示整个位图。第三个参数是我们想要显示位图的 SDL 表面。我们在这里传递显示表面以在屏幕上显示位图。第四个参数是我们想要显示位图表面的显示表面的尺寸。将其设置为 NULL 允许我们显示整个显示表面的位图。
在完成位图处理后(例如,程序关闭),我们需要清除位图数据所占用的内存。这可以通过使用 SDL_FreeSurface 函数来完成,该函数接受一个指向要释放的 SDL 表面的指针作为参数。
SDL_FreeSurface(image);
位图定位和透明度
[edit | edit source]将位图相对于屏幕进行定位与我们在上一章中对蓝色矩形所做的操作基本相同。以以下位图图像(来自 Ari Feldman 的 GPL 集合)为例。
要将此位图图像的左上角定位在屏幕的中心,我们将创建一个 SDL_Rect 结构,其 x 和 y 字段设置为屏幕的中心。
SDL_Surface *plane;
SDL_Rect plane_rect;
/*load plane image as previously shown*/
plane_rect.x = 640/2;
plane_rect.y = 480/2;
这里唯一的问题是检索图像的宽度和高度以完成 SDL_Rect 结构。要设置这些值,请使用 SDL_Surface 成员 'w' 和 'h',它们会在加载位图后自动存储其尺寸。
plane_rect.w = plane->w;
plane_rect.h = plane->h;
然后,在游戏循环中,你将调用 SDL_BlitSurface 函数,并将你定义的 SDL_Rect 结构作为第四个(目标矩形)参数传递。
SDL_BlitSurface(image, NULL, display, NULL);
SDL_BlitSurface(plane, NULL, display, &plane_rect);
注意背景和飞机位图的绘制顺序。此顺序允许飞机显示在我们之前显示的云彩位图之上。
飞机位图看起来很粗糙,蓝色背景在它后面。当处理精灵(动画位图)时,您需要能够丢弃背景颜色以获得更干净的外观。SDL 允许通过使用颜色键来丢弃此颜色。颜色键是单个颜色(或颜色范围),在绘制位图时会被忽略。对于上面的图形,飞机,背景是纯蓝色(RGB 0 0 255)。要设置颜色键以忽略该颜色,请使用 SDL 函数 SDL_SetColorKey
int SDL_SetColorKey(SDL_Surface *surface, Uint32 flag, Uint32 key);
此函数将 SDL 表面的指针作为第一个参数,以设置颜色键。第二个参数是确定颜色键类型的一个标志。指定 SDL_SRCCOLORKEY 指示 SDL 包含的颜色键应为透明的。第三个参数是要设置为颜色键的颜色值。此值可以通过调用 SDL_MapRGB 来检索。此函数在出错时返回 -1,成功时返回 0。因此,在主循环之前以及加载飞机位图之后,可以设置颜色键,如下所示
if(SDL_SetColorKey(plane, SDL_SRCCOLORKEY,
SDL_MapRGB(plane->format, 0, 0, 255)) < 0){
printf("\nUnable to set colorkey");
}
您也可以在变量中预定义颜色键,然后将该变量传递给 SDL_SetColorKey
Uint32 colorkey;
...
colorkey = SDL_MapRGB(plane->format, 0, 0, 255);
if(SDL_SetColorKey(plane, SDL_SRCCOLORKEY, colorkey) < 0){
printf("\nUnable to set colorkey");
}
位图动画是通过将图像分割成相等的部分(尺寸方面)来实现的。然后,在设定的时间间隔(或速率)内切换图像的不同部分以模拟动画。例如,以下旋转飞机的位图图像
鉴于此图像的尺寸为 256 像素宽,32 像素高,将其分成八部分,每个部分的尺寸为 32x32 像素。在对该图像进行动画处理时,我们将以一定的速率在这八个部分之间进行切换,类似于投影仪在帧之间切换的方式
为了将这种胶片卷轴的概念转化为计算机图形,可以将位图精灵视为一个有序的帧数组(0-7)
要执行动画,您将切换帧 0 和 7。跟踪表示它的矩形的定位和尺寸很容易。假设您正在跟踪位图的源矩形和目标矩形(分别是图像的源矩形和显示器上的目标矩形)
/*frame (src) rectangle and destination rect*/
SDL_Rect frect;
SDL_Rect prect;
/*cur frame of animation*/
int curframe = 0;
在定义矩形后,您可以通过将当前帧的值乘以帧宽度来检索源矩形的 x 值
frect.x = curframe * FRAME_WIDTH;
frect.y = 0;
frect.w = FRAME_WIDTH;
frect.h = FRAME_HEIGHT;
目标矩形类似于我们之前在讨论位图定位时在本章中定义的,只是我们不是使用整个图像的尺寸,而是使用帧尺寸
/*position bitmap at screen coordinates (50, 50)*/
prect.x = 50;
prect.y = 50;
prect.w = FRAME_WIDTH;
prect.h = FRAME_HEIGHT;
绘制图像需要我们之前学到的有关显示和定位位图的知识。基本上,我们只需要提供之前定义的矩形
SDL_BlitSurface(plane_spr, &frect, display, &prect);
更新帧时,我们需要确保在递增帧时,它不会超出与图像帧布局相关的数组边界。以下显示了根据最大帧数(8)更新当前帧的第一种方法
if(++curframe >= NUM_FRAMES){
curframe = 0;
}
这种方法的缺点是动画精灵将在每次滴答间隔后更新,这使得动画速度过快。为了降低速度,我们引入了速率的概念。速率将是在更新精灵帧之前经过的滴答间隔数。假设我们将速率定义为每 3 个滴答间隔更新一次
int rate = 3;
int cur_rate = 0;
可以通过以下方法更新当前速率并最终更新帧
if(++cur_rate > rate){
curframe += 1;
if(curframe >= NUM_FRAMES){
curframe = 0;
}
cur_rate = 0;
}
这将检查当前速率是否达到最初设置的速率(在本例中为 3)。如果该条件为真,则当前帧将递增,当前速率将重置。
#include <stdio.h>
#include <stdlib.h>
#include "SDL/SDL.h"
#define TICK_INTERVAL 50
#define NUM_FRAMES 8
#define FRAME_WIDTH 32
#define FRAME_HEIGHT 32
Uint32 TimeLeft(void)
{
Uint32 next_tick = 0;
Uint32 cur_tick;
cur_tick = SDL_GetTicks();
if(next_tick <= cur_tick){
next_tick = cur_tick + TICK_INTERVAL;
return 0;
}else{
return (next_tick - cur_tick);
}
}
int main(int argv, char **argc)
{
SDL_Surface *display;
SDL_Surface *image;
SDL_Surface *plane;
SDL_Surface *plane_spr;
SDL_Rect frect;
SDL_Rect prect;
int curframe = 0;
int rate = 3;
int cur_rate = 0;
SDL_Rect plane_rect;
Uint32 colorkey;
SDL_Event event;
int loop = 1;
/*initialize SDL video subsystem*/
if(SDL_Init(SDL_INIT_VIDEO) < 0){
/*error, quit*/
exit(-1);
}
/*retrieve 640 pixel wide by 480 pixel high 32 bit display with video memory access*/
display = SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE);
/*check that surface was retrieved*/
if(display == NULL){
/*quit SDL*/
SDL_Quit();
exit(-1);
}
image = SDL_LoadBMP("clouds.bmp");
if(image == NULL){
SDL_Quit();
exit(-1);
}
/*load plane bitmap*/
plane = SDL_LoadBMP("plane.bmp");
if(plane == NULL){
printf("\nUnable to load plane bitmap");
}
/*set rectangle dimensions to center of screen*/
plane_rect.x = 640/2;
plane_rect.y = 480/2;
plane_rect.w = plane->w;
plane_rect.h = plane->h;
/*set colorkey (transparent) to blue*/
colorkey = SDL_MapRGB(plane->format, 0, 0, 255);
if(SDL_SetColorKey(plane, SDL_SRCCOLORKEY, colorkey) < 0){
printf("\nUnable to set colorkey");
}
/*load sprite bitmap*/
plane_spr = SDL_LoadBMP("plane_spr.bmp");
if(!plane_spr){
printf("\nUnable to open plane sprite");
}
/*update color key for sprite bitmap*/
colorkey = SDL_MapRGB(plane_spr->format, 0, 0, 255);
if(SDL_SetColorKey(plane_spr, SDL_SRCCOLORKEY, colorkey) < 0){
printf("\nUnable to set sprite color key");
}
prect.x = 50;
prect.y = 50;
prect.w = FRAME_WIDTH;
prect.h = FRAME_HEIGHT;
frect.x = curframe * FRAME_WIDTH;
frect.y = 0;
frect.w = FRAME_WIDTH;
frect.h = FRAME_HEIGHT;
while(loop){
/*check if events are being processed*/
while(SDL_PollEvent(&event)){
/*check if event type is keyboard press*/
if(event.type == SDL_KEYDOWN){
if(event.key.keysym.sym == SDLK_ESCAPE){
loop = 0;
}
}
}
/*update x position of frame*/
frect.x = curframe * FRAME_WIDTH;
SDL_BlitSurface(image, NULL, display, NULL);
SDL_BlitSurface(plane, NULL, display, &plane_rect);
/*Blit sprite based on frame dimensions (frect)*/
SDL_BlitSurface(plane_spr, &frect, display, &prect);
SDL_UpdateRect(display, 0, 0, 0, 0);
/*update frame based on rate*/
if(++cur_rate > rate){
curframe += 1;
if(curframe >= NUM_FRAMES){
curframe = 0;
}
cur_rate = 0;
}
SDL_Delay(TimeLeft());
}
SDL_FreeSurface(image);
SDL_Quit();
exit(0);
}
- 基本图形库
- 游戏设计:空中战争
- 游戏设计:图形
- 游戏设计:人工智能
- 完整的游戏代码和解释