内容简介
1、第三部分第二课: SDL开发游戏之创建窗口和画布
2、第三部分第三课预告: SDL开发游戏之显示图像
第三部分第二课:SDL开发游戏之创建窗口和画布
在上一课中,我们对SDL这个开源库做了介绍,也带大家配置了SDL的开发环境。请大家按照上一课的步骤创建一个SDL工程,能够初步运行。
如果遇到问题,可以百度,Google相关平台SDL的配置。或者联系小编。
当然了,有些朋友可能会说开发C语言游戏还可以用GTK+这个库,但是个人认为GTK+没有SDL那么适合开发游戏,其创建图形界面的能力倒是很强的。
SDL使得你可以用很少的资源占用,开发出很强大的游戏。而且我发现SDl很容易就能帮助我们开发出一些画风比较复古的游戏,如果你掌握好了SDL,那么开发怀旧的街机游戏是很简单的。
老的SDL版本一般是1.2版,但是目前已经不再怎么更新了吧,SDL官网最新的稳定版本(截止2015年6月23日)是SDL2.0.3,有人说2.0.4版也快发布了。
SDL1.2到SDL2.0,是一个很大的飞跃,增加了很多新的元素,性能也增强了很多。
不过可惜的是SDL2的API不向后兼容,不过一般来说SDL1.2编写的程序要迁移到SDL2.0,改动是不太大的。官网也有migration(迁移)的教程贴(全是英语,又一次“论学好英语对编程的帮助”,虽不是必须,但做编程不会英语是很可惜的):
http://wiki.libsdl.org/MigrationGuide
推荐一个不错的百度贴吧:SDL吧
http://tieba.baidu.com/f?kw=sdl&ie=utf-8
如果你英语还不错,那么SDL官网的WiKi毫无疑问是最好的老师了,所有你想知道的SDL的知识几乎都在WiKi里:
注:小编会在Mac OS下的XCode上用SDL2来编写演示下面的课程。其他平台(Windows,Linux等)类似,就是环境配置略有不同,SDL具有可移植性。
SDL的加载和停止
可以说大部分的C语言第三方库在使用时,都需要初始化,使用完毕都要停止。SDL库的使用也不例外。
SDL库在使用之初,需要加载一些信息到内存中,以便正常运行。这些信息是被动态地加载到内存中的,用到了malloc函数,那么释放这些信息,就要用到与malloc对应的函数free了。
大家每次用malloc函数申请了一块内存,使用完毕不再需要时,一定要记得用free函数释放。不然内存会泄露太多,最终导致没有空间可以分配。
SDL库里面为我们提供了完成加载和释放信息的两个配对函数:
SDL_Init:将SDL加载到内存中(调用一些malloc函数)
SDL_Quit:将SDL从内存中释放(调用一些free函数)
所以,我们在构建SDL程序时,一开始需要调用SDL_Init,最后结束时需要调用SDL_Quit函数。
SDL_Init:将SDL加载到内存中
函数原型:
int SDL_Init(Uint32 flags)
SDL_Init函数必须在最开始调用,也就是使用任何其他SDL函数之前。它只有一个参数,这个参数是一个Uint32类型的(其实是系统定义的,就是int类型。不过是32位的无符号整型,也就是长度为4个字节的unsigned int型)。这个参数flags可以取下表中的一个值,代表我们下面程序中要用到SDL的哪一个子系统:
SDL_INIT_TIMER | 计时器子系统 |
SDL_INIT_AUDIO | 音频子系统 |
SDL_INIT_VIDEO | 视频子系统 |
SDL_INIT_JOYSTICK | 操纵杆子系统 |
SDL_INIT_HAPTIC | 触屏反馈子系统 |
SDL_INIT_GAMECONTROLLER | 控制器子系统 |
SDL_INIT_EVENTS | 事件子系统 |
SDL_INIT_EVERYTHING | 上面所有的子系统 |
以上表格中所列出的其实都是一些用#define定义的预处理常量。不太记得预处理常量的读者可以回去复习我们的《【C语言探索之旅】 第二部分第五课:预处理》。
给出在SDL.h头文件中,以上一些常量的定义:
#define SDL_INIT_TIMER 0x00000001
#define SDL_INIT_AUDIO 0x00000010
#define SDL_INIT_VIDEO 0x00000020
那你要问了:“SDL库还有子系统吗?”
是的。SDL有的子系统(部分的库)是负责屏幕显示的,有的是负责视频处理的,有的是负责操纵杆(小时候玩的那种游戏的操纵杆、摇杆)输入,等。这么多不同的子系统组成了SDL这个强大的库。
所以,如果我们这样调用:
SDL_Init(SDL_INIT_VIDEO);
就是告诉程序,我接下来要使用SDL库中视频处理那一部分的子系统。这样我们就可以创建一个窗口,在里面绘制各种图形,写文字,等等。
这样的方法在C语言编程中是很常用的,就是用#define来定义一些预处理常量(有时也叫“宏常量”),特别在嵌入式编程中非常有用。
这样做的好处是我们并不需要记住各种复杂的数值,而只需要记住几个英文的常量名,而且这些常量名是很容易见名知意的。例如:SDL_INIT_VIDEO中,SDL当然是指代SDL库,init是英语“初始化”的意思,video是“视频”的意思,那么这个常量就告诉SDL_Init函数,我们需要初始化SDL的视频子系统,这样视频子系统的各种信息就会被预先加载到内存中了。
因此,SDL_Init函数只要识别传给它的唯一参数的数值,就知道到底要加载哪些SDL子系统了。
如果我们调用:
SDL_Init(SDL_INIT_EVERYTHING);
那么就会加载所有SDL的子系统。一般不需要用到所有的(everything是英语“每样东西”,“所有”的意思)子系统,因为我们没有必要让电脑加载那些用不到的子系统,浪费内存。
你的问题又来了:“如果我同时需要加载两个或以上的子系统,该怎么做呢?”
这就要用到“按位或”运算符了。我们需要讲一下位运算的知识。
位运算
还记得我们以前的课程《【c语言探索之旅】第一部分第六课:条件表达式》里讲过的“逻辑运算符”吗?
那时候我们介绍了几个逻辑运算符:
逻辑运算符 | 意义 |
---|---|
&& | 逻辑与 |
|| | 逻辑或 |
! | 逻辑非 |
逻辑与 &&
if (age > 18 && age < 25)
两个 & 号连在一起表示逻辑与,就是说当两边的表达式都为真时,括号中的整个表达式才为真,所以这里只有当age大于18并且小于25的情况下,括号里的表达式才为真。
逻辑或 ||
为了做逻辑或判断,我们则要用到两个 | 符号。逻辑或只要其两边的两个表达式有一个为真,整个表达式就为真。
if (age > 20 || money > 15000)
只有当age大于20或者money大于15000的情况下,括号里的表达式才为真。
逻辑非 !
逻辑非符号是感叹号,表示“取反”,加在表达式之前。如果表达式为真,那么加上感叹号则为假;如果表达式为假,那么加上感叹号则为真。
if (!(age < 18))
上面的表达式只有当age大于或等于18时才为真)。
今天来介绍一下与逻辑运算符有点“类似”但不同的“按位”运算符:
位运算,顾名思义是对“位”的运算。那么什么是“位”呢?
这里的“位”是比特位的意思,英文是“bit”。也就是我们以前说过的一个二进制位。
我们电脑最小的可读取单元就是一个bit位了,只能取0或1值(因为电脑里各样大大小小的半导体只有通和不通两种状态)。
我们平时所说的一个字节是由8个bit位组成的,例如:
01101110
C语言提供了6种位运算符:
按位运算符 | 意义 |
---|---|
& | 按位与 |
| | 按位或 |
^ | 按位异或 |
~ | 取反 |
<< | 左移 |
>> | 右移 |
按位与运算符 &
只有参与&运算的两个位都为1时,结果才为1,否则为0。例如1&1为1,0&0为0,1&0为0。
数值在内存中以二进制的形式存在,9&5可写算式如下:
00001001 (9的二进制)
&00000101 (5的二进制)
00000001 (1的二进制)
所以9&5=1。
按位与运算通常用来对某些位清0或保留某些位。例如把 a 的高16位清 0 ,保留低16位,可作a&65535运算(65536占用4个字节。65535的二进制表示为00000000000000001111111111111111)。
注:
严格来说,在计算机系统中,数值在内存中一律以补码形式存在。正数的补码与它的二进制形式相同(与其原码一致),负数则不一样。
负数的补码:符号位为 1,其余位为该数绝对值的原码按位取反,然后整个数加 1。
按照负数补码的规则,可以知道-1的补码为 0xff(对应的二进制为11111111),-2 的补码为 0xfe(对应的二进制是11111110)。
使用补码,可以将符号位和其它位统一处理;同时,减法也可按加法来处理。另外,两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃。
按位或运算符 |
参与或运算|的两个二进制位有一个为1时,结果就为1,两个都为0时结果才为0。例如1|1为1,0|0为0,1|0为1。
9|5可写算式如下:
00001001 (9的二进制)
|00000101 (5的二进制)
00001101 (13的二进制)
所以9|5=13。
按位或运算可以用来将某些二进制位置1,而保留某些位。
按位异或运算符 ^
参与异或运算^的两个二进制位不同时,结果为1,相同时结果为0。也就是说,0^1为1,0^0为0,1^1为0。
9^5可写成算式如下:
00001001 (9的二进制)
^00000101 (5的二进制)
00001100 (12的二进制)
所以9^5=12。
按位异或运算可以用来反转某些二进制位。
取反运算符 ~
取反运算符~为单目运算符,右结合性。作用是对参与运算的数的各二进位按位取反。例如 ~1为0,~0为1。
~9的运算为:
~0000000000001001
1111111111110110
所以~9=65526。
左移运算符 <<
左移运算符<<用来把操作数的各二进位全部左移若干位,高位丢弃,低位补0。例如:
a=9; a<<3;
<<左边是要移位的操作数,右边是要移动的位数。
上面的代码表示把a的各二进位向左移动3位。a=00001001(9的二进制),左移3位后为01001000(十进制72)。
右移运算符 >>
右移运算符>>用来把操作数的各二进位全部右移若干位,低位丢弃,高位补0(或1)。例如:
a=9; a>>3;
表示把a的各二进位向右移动3位。a=00001001(9的二进制),右移3位后为00000001(十进制1)。
位运算是C语言的一个难点,在嵌入式编程中经常要用到。特别是这些运算符的结合混搭使用,是非常令人头痛的。
所以最好自己经常拿纸笔来做运算,使自己对二进制,十六进制很熟悉,特别是二进制和十六进制的转换。
介绍了位运算,我们回到SDL中。
我们在SDL的函数中要用到的位运算是 “按位或 |”,因为这可以把各个不同的选项“相加”,例如:
// 加载视频和音频子系统
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
// 加载视频,音频和计时器子系统
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER);
用这样的方式,我们就可以做到“需要哪些子系统就加载哪些”,不必要用SDL_INIT_EVERYTHING。
这些我们传给SDL_Init函数的参数称为flag,也就是“标记,标志”,这在编程中是很常见的一种用法。为了同时拥有多个标记,我们用按位或符号“|”来连接各个标记,有点类似加法。
SDL_Quit:退出SDL
SDL_Quit函数的调用非常简单,因为它没有参数:
SDL_Quit();
一旦程序运行这句命令,那么之前加载入内存的所有SDL子系统都将被停止并从内存释放。
这是优雅地结束SDL程序的方式,记得在SDL程序结尾处调用这个函数。
了解了SDL_Init和SDL_Quit这两个函数,我们就可以给出SDL程序的大致框架了:
#include <stdlib.h>
#include <stdio.h>
#include <SDL2/SDL.h>
int main(int argc, char *argv[])
{
SDL_Init(SDL_INIT_VIDEO); // 启动SDL系统 (这里加载了视频子系统)
/*
SDL已被加载入内存。
在这里可以放置你的SDL程序的主体内容了
*/
SDL_Quit(); // 停止SDL (释放内存).
return 0;
}
暂时我们的main函数中还没填入实质性的内容,只是演示了一下如何初始化和结束SDL库。
错误处理
SDL_Init函数的返回值有两种情况:
0:如果一切顺利
-1:如果有错误
这个返回值很有用,我们可以根据这个值来做对应操作。比如,出错时打印一句话,然后退出程序,因为如果初始化错误,那就没有必要再继续;一切顺利,那么继续往下执行。
虽然这个操作并不是必须,但是这是好习惯,可以让我们的程序更易调试,出了错会容易找到原因。
“如果有错误,那么我们如何打印出错误呢?”
好问题。我们有两种选择:
printf:用printf函数在终端上打印错误信息。
fprintf:把错误信息写进文件里。用fprintf函数。
我们选择第二种方式,也就是写入文件的方式。当然了,这种方式比printf麻烦一些。
不过,我们却有一种更简便的写入文件的方式:使用标准错误输出。
什么是标准错误输出呢?
C语言中,有标准输入,标准输出和标准错误输出。
在C语言中,在程序开始运行时,系统自动打开3个标准文件:标准输入、 标准输出、标准错误输出。通常这3个文件都与终端相联系。因此,以前我们所用到的从终端输入或输出都不需要打开终端文件。系统自定义了3个文件指针(FILE *类型)stdin、stdout、stderr,分别指向终端输入、终端输出和标准错误输出(也从终端输出),其值通常是0,1和2。
这些变量都定义在stdio.h这个标准库的头文件里。
而我们的stderr变量就指向了一个地方,我们可以把错误信息写入到这个位置。这个地方在Windows中通常是一个叫做stderr.txt的文件;在Linux中,这个地方通常是在终端里。
这个stderr的变量在程序开始被自动创建,在程序结束会自动销毁。所以我们不需要用到文件操作相关的fopen和fclose函数了。
所以我们就可以用fprintf来写入到stderr上:
#include <stdlib.h>
#include <stdio.h>
#include <SDL2/SDL.h>
int main(int argc, char *argv[])
{
if (SDL_Init(SDL_INIT_VIDEO) == -1) // 加载SDL;如果出错
{
fprintf(stderr, "SDL 初始化错误 : %s\n", SDL_GetError()); // 打印错误信息
exit(EXIT_FAILURE); // 退出程序
}
SDL_Quit();
return EXIT_SUCCESS; // 如果顺利结束
}
这下的程序比之前多了什么呢?
我们把错误信息写入到了标准错误stderr中。%s使得SDL可以把最近的一次错误信息写入,这个错误信息就是SDL_GetError函数的返回值。
我们在SDL_Init函数出错时调用exit函数来退出我们的程序。exit函数并没有什么新鲜的,之前的课程中我们已经学习过了。不过你可以发现,我们在exit函数的括号中用的是一个常量:EXIT_FAILURE。对应的,如果程序顺利运行,在最后我们用return EXIT_SUCCESS来结束程序。
EXIT_SUCCESS:程序顺利结束的返回值。
EXIT_FAILURE:程序发生错误的返回值。
还记得我们以前怎么做的吗?如果程序出错,我们用的返回值是-1;如果程序顺利结束,我们用的返回值是0。
那为什么我们现在如此“任性”,要用EXIT_SUCCESS和EXIT_FAILURE来分别代替正确和出错时的返回值呢?
那是因为不同的操作系统下,代表正确和错误的返回值的数值可能不尽相同,并不一定总是-1和0,stdio.h的内容在不同的操作系统上也是不尽相同的。使用这两个系统常量的优势就是在不同的操作系统下可以被替换为stdio.h定义的对应数值,我们的程序就具备很好的移植性了。
打开一个窗口
好了,我们现在已经学会如何开启和关闭SDL库了。那么我们要开始做一些有趣的事咯:打开一个窗口。
从SDL1.2到SDL2,API发生了一些变化。特别是VIDEO(视频)子系统,几乎重写了全部API。因为SDL1.x版本是在20世纪90年代后期设计的,年代相距甚远,硬件设备和操作系统等发生了翻天覆地的进步,所以须要相应的改进。
英语比较好的朋友可以直接看官方给出的SDL1.2版本到SDL2版本的一些变化:
http://wiki.libsdl.org/MigrationGuide
以前创建一个窗口需要调用SDL_SetVideoMode函数。现在创建一个窗口的函数换成了SDL_CreateWindow。SDL1.2只支持单窗口模式,现在的SDL2已经支持多窗口了。
SDL_CreateWindow函数的原型如下:
SDL_Window* SDL_CreateWindow(const char* title, int x, int y, int w, int h, Uint32 flags);
SDL_CreateWindow函数的参数如下:
title | 窗口的标题,UTF-8编码 |
x | 窗口的x坐标位置(左上角), SDL_WINDOWPOS_CENTERED(在屏幕正中)或者SDL_WINDOWPOS_UNDEFINED(未定义) |
y | 窗口的y坐标位置(左上角), SDL_WINDOWPOS_CENTERED(在屏幕正中)或者SDL_WINDOWPOS_UNDEFINED(未定义) |
w | 窗口的宽 |
h | 窗口的高 |
flags | 0个, 1个或更多的 SDL_WindowFlags ,用按位或连接 |
以上表格中的flag可以是0,1或多个SDL_WindowFlags的组合,用按位或符号“|”连接。
而SDL_WindowFlags的取值可以是以下这些的组合:
SDL_WINDOW_FULLSCREEN | 窗口全屏幕 |
SDL_WINDOW_FULLSCREEN_DESKTOP | 窗口全屏幕,取当前桌面的分辨率 |
SDL_WINDOW_OPENGL | 窗口可以和OpenGL配合使用 |
SDL_WINDOW_SHOWN | 窗口被显示 |
SDL_WINDOW_HIDDEN | 窗口被隐藏 |
SDL_WINDOW_BORDERLESS | 窗口无边框 |
SDL_WINDOW_RESIZABLE | 窗口可以调节大小 |
SDL_WINDOW_MINIMIZED | 窗口最小化 |
SDL_WINDOW_MAXIMIZED | 窗口最大化 |
SDL_WINDOW_INPUT_GRABBED | 窗口得到键盘输入焦点,而且被束缚在窗口内 |
SDL_WINDOW_INPUT_FOCUS | 窗口得到键盘输入焦点 |
SDL_WINDOW_MOUSE_FOCUS | 窗口得到鼠标焦点 |
所以我们就来创建一个窗口,我们在之前的代码中增加一些内容:
#include <stdlib.h>
#include <stdio.h>
#include <SDL2/SDL.h>
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
int main(int argc, char *argv[])
{
if (SDL_Init(SDL_INIT_VIDEO) == -1) // 加载SDL;如果出错
{
fprintf(stderr, "SDL 初始化错误 : %s\n", SDL_GetError()); // 打印错误信息
exit(EXIT_FAILURE); // 出错退出程序
}
// 创建一个窗口,宽640像素,高480像素
SDL_Window *screen = SDL_CreateWindow("游戏窗口",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
SCREEN_WIDTH, SCREEN_HEIGHT,
SDL_WINDOW_SHOWN);
SDL_Quit();
return EXIT_SUCCESS; // 顺利退出程序
}
我们主要增加了这句命令:
SDL_Window *screen = SDL_CreateWindow("My Game Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
640, 480,
SDL_WINDOW_SHOWN);
将程序编译运行,可以看到窗口几乎转瞬即逝。这也不稀奇,因为在创建了窗口之后,我们立马调用了SDL_Quit函数,所以“整个世界都清净了”。
那么,如何让我们创建的窗口保持住而不消失呢?我们可以用pause函数来完成,虽然不是太优雅。
===========================
pause函数是C语言的一个函数:
头文件:#include <unistd.h>
定义函数:int pause(void);
函数说明:pause()会令目前的进程暂停(进入睡眠状态), 直到被信号(signal)所中断.
返回值:只返回-1.
错误代码:EINTR 有信号到达中断了此函数.
===========================
因此,我们修改我们的代码,加入:#include <unistd.h>和pause函数。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <SDL2/SDL.h>
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
int main(int argc, char *argv[])
{
if (SDL_Init(SDL_INIT_VIDEO) == -1) // 加载SDL;如果出错
{
fprintf(stderr, "SDL 初始化错误 : %s\n", SDL_GetError()); // 打印错误信息
exit(EXIT_FAILURE); // 出错退出程序
}
// 创建一个窗口,宽640像素,高480像素
SDL_Window *screen = SDL_CreateWindow("游戏窗口",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
SCREEN_WIDTH, SCREEN_HEIGHT,
SDL_WINDOW_SHOWN);
if(screen) // 如果创建窗口成功
{
pause(); // 暂停当前进程,使得窗口一直显示
}
else
{
fprintf(stderr, "创建窗口错误: %s\n", SDL_GetError()); // 打印错误信息
}
SDL_Quit();
return EXIT_SUCCESS; // 顺利退出程序
}
现在运行之后,可以看到我们的第一个窗口了吗?是不是很激动?
当然,比较优雅的方式也可以不使用pause函数,而让窗口显示几秒后消失,代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <SDL2/SDL.h>
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
int main(int argc, char *argv[])
{
if (SDL_Init(SDL_INIT_VIDEO) == -1) // 加载SDL;如果出错
{
fprintf(stderr, "SDL 初始化错误 : %s\n", SDL_GetError()); // 打印错误信息
exit(EXIT_FAILURE); // 出错退出程序
}
// 创建一个窗口,宽640像素,高480像素
SDL_Window *screen = SDL_CreateWindow("游戏窗口",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
SCREEN_WIDTH, SCREEN_HEIGHT,
SDL_WINDOW_SHOWN);
if(screen) // 如果创建窗口成功
{
SDL_Delay(10000); // 使窗口保持10秒
SDL_DestroyWindow(screen); // 销毁窗口
}
else
{
fprintf(stderr, "创建窗口错误: %s\n", SDL_GetError()); // 打印错误信息
}
SDL_Quit();
return EXIT_SUCCESS; // 顺利退出程序
}
当然了,SDL2中,我们可以创建多个窗口。
事实上,每次调用SDL_CreateWindow函数都会返回一个指向窗口的结构体指针(SDL_Window*)。然后我们可以操纵每个指针来对每个窗口进行改动和操作。
暂时我们只需要一个窗口就够了。
关于游戏中一般的窗口坐标,我们需要讲一下:
一般游戏中的窗口的坐标原点是在左上角,也就是说,此处的x和y都是0。从左往右横坐标的值(x)逐渐增大;从上往下纵坐标的值(y)逐渐增大。如下图所示:
操纵Surface
既然我们已经学会了如何创建一个窗口。那么我们总要在窗口中显示些东西才有意思嘛对吧,不然黑不溜秋的窗口实在提不起劲。
我们先来看一下SDL_Window这个SDL2定义的结构体的内容是什么:
我们可以在SDL2的代码中的头文件 发现这样一句话:
typedef struct SDL_Window SDL_Window;
这只是一个typedef罢了,就是一个别名而已。所以真实的SDL_Window结构体的定义一定在其他地方。
我们继续寻找,发现原来它的定义在SDL2库的源代码中,在 src/video/SDL_sysvideo.h这个头文件里(藏得这么深。这样做的好处是可以防止使用SDL的程序员修改底层代码,实际上很多大型项目都是这么用的。在用户可见的地方只用一个typedef,而真实的定义在源代码里):
/* Define the SDL window structure, corresponding to toplevel windows */
struct SDL_Window
{
const void *magic;
Uint32 id;
char *title;
int x, y;
int w, h;
Uint32 flags;
SDL_DisplayMode fullscreen_mode;
SDL_Surface *surface;
SDL_bool surface_valid;
SDL_WindowShaper *shaper;
SDL_WindowUserData *data;
void *driverdata;
SDL_Window *prev;
SDL_Window *next;
};
我们可以看到,在SDL_Window结构体中有一个成员:SDL_Surface *
这是一个SDL_Surface类型的指针。SDL_Surface又是什么呢?surface在英语中是“表面”的意思。所以说我们用SDL_CreateWindow函数创建了一个窗口(SDL_Window),它里面其实就有一个表面(SDL_Surface),可以把它看成一个画板,只不过暂时没有填充颜色,是黑色的而已。SDL2中,我们可以在表面上“作画”,也可以将一个表面“黏贴”到另一个表面上(下一课会学到)。
在SDL中,表面是很基础的元素。表面的形状都是矩形。当然,在SDL中我们也可以自己绘制其他图形,如圆形,三角形等,但是这些没有系统提供的函数,你需要自己来绘制或者使用别的开发者的插件。
当然了,SDL2在SDL1.x的基础上又增加了Texture(纹理)和Renderer(渲染器)这两个元素,我们之后的课会介绍。
我们如果要在这个表面上操作,需要首先取得这个表面才行。怎么获得这个窗口中的表面呢?可以使用SDL_GetWindowSurface函数:
SDL_Surface* SDL_GetWindowSurface(SDL_Window* window)
可以看到SDL_GetWindowSurface只有一个参数,就是窗口的结构体指针。返回值就是与此窗口绑定的表面的指针。
所以我们可以这样来写我们的代码,使得窗口的表面颜色变成一个我们指定的颜色:
#include <stdlib.h>
#include <stdio.h>
#include <SDL2/SDL.h>
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
int main(int argc, char *argv[])
{
SDL_Surface* screenSurface = NULL;
if (SDL_Init(SDL_INIT_VIDEO) == -1) // 加载SDL;如果出错
{
fprintf(stderr, "SDL 初始化错误 : %s\n", SDL_GetError()); // 打印错误信息
exit(EXIT_FAILURE); // 出错退出程序
}
// 创建一个窗口,宽640像素,高480像素
SDL_Window *screen = SDL_CreateWindow("游戏窗口",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
SCREEN_WIDTH, SCREEN_HEIGHT,
SDL_WINDOW_SHOWN);
if(screen) // 如果创建窗口成功
{
screenSurface = SDL_GetWindowSurface(screen); // 获得窗口的表面
SDL_FillRect(screenSurface, NULL, SDL_MapRGB(screenSurface->format, 17, 206, 112)); // 用指定颜色填充此表面
SDL_UpdateWindowSurface(screen); // 刷新窗口表面
SDL_Delay(10000); // 使窗口保持10秒
SDL_DestroyWindow(screen); // 销毁窗口
}
else
{
fprintf(stderr, "创建窗口错误: %s\n", SDL_GetError()); // 打印错误信息
}
SDL_Quit(); // 卸载SDL
return EXIT_SUCCESS; // 顺利退出程序
}
其中的SDL_FillRect函数用于以指定颜色填充指定的表面,其原型如下:
int SDL_FillRect(SDL_Surface* dst, const SDL_Rect* rect, Uint32 color)
dst | 目标SDL_Surface结构体 |
rect | SDL_Rect 结构体,是矩形。代表了填充的矩形区域,如果是NULL那么填充整个dst表面 |
color | 填充的颜色(由红,绿,蓝三原色组成) |
SDL_UpdateWindowSurface函数则用于刷新窗口的表面,其原型如下:
int SDL_UpdateWindowSurface(SDL_Window* window)
唯一的参数window就是要刷新的窗口。
颜色组成
颜色是由红,绿,蓝这三原色混合而成,每种颜色的取值范围都是0~255。如果三种原色的取值都是0,那么整体的颜色就是黑色;如果三种原色的取值都是255,那么整体的颜色就是白色。其他不同的取值会组合成很多种不同的颜色(一共有256 * 256 * 256 = 16777216 种之多),所以我们的大千世界才是如此色彩斑斓啊。
上述程序中,我们的红,绿,蓝的取值分别为17, 206, 112,所以整体的颜色就是这样一种蓝绿色。
运行以上的程序,我们的窗口就有很好看的颜色了。
总结
SDL程序在开始处需要使用SDL_Init函数来加载,在结尾处要使用SDL_Quit函数来卸载。
flag(标记)是一些常量,这些常量可以用按位或操作符“|”来连接,就好像相加一般,使多个特性可以同时具有。
SDL的基础元素之一是“表面”(Surface),是SDL_Surface结构体类型,形状是矩形。我们可以在这些表面上“作画”。
总是至少有一个“表面”,就是我们创建的窗口的那个表面。
填充“表面”可以使用函数SDL_FillRect。
颜色是由红,绿,蓝这三原色组成的。每一组分的取值范围都是0~255。
第三部分第三课预告:
今天的课就到这里,一起加油吧。
下一次我们学习: SDL开发游戏之显示图像
转载于:https://blog.51cto.com/4526621/1676373