0?wx_fmt=jpeg



内容简介

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里:

http://wiki.libsdl.org/


:小编会在Mac OS下的XCode上用SDL2来编写演示下面的课程。其他平台(Windows,Linux等)类似,就是环境配置略有不同,SDL具有可移植性。



SDL的加载和停止


可以说大部分的C语言第三方库在使用时,都需要初始化,使用完毕都要停止。SDL库的使用也不例外。


SDL库在使用之初,需要加载一些信息到内存中,以便正常运行。这些信息是被动态地加载到内存中的,用到了malloc函数,那么释放这些信息,就要用到与malloc对应的函数free了。


大家每次用malloc函数申请了一块内存,使用完毕不再需要时,一定要记得用free函数释放。不然内存会泄露太多,最终导致没有空间可以分配。


SDL库里面为我们提供了完成加载和释放信息的两个配对函数:


  1. SDL_Init:将SDL加载到内存中(调用一些malloc函数)

  2. 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:如果有错误


这个返回值很有用,我们可以根据这个值来做对应操作。比如,出错时打印一句话,然后退出程序,因为如果初始化错误,那就没有必要再继续;一切顺利,那么继续往下执行。


虽然这个操作并不是必须,但是这是好习惯,可以让我们的程序更易调试,出了错会容易找到原因。


“如果有错误,那么我们如何打印出错误呢?”


好问题。我们有两种选择:


  1. printf:用printf函数在终端上打印错误信息。

  2. 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;  // 如果顺利结束
}


这下的程序比之前多了什么呢?


  1. 我们把错误信息写入到了标准错误stderr中。%s使得SDL可以把最近的一次错误信息写入,这个错误信息就是SDL_GetError函数的返回值。

  2. 我们在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;  // 顺利退出程序
}


现在运行之后,可以看到我们的第一个窗口了吗?是不是很激动?


0?wx_fmt=jpeg


当然,比较优雅的方式也可以不使用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;  // 顺利退出程序
}


0?wx_fmt=png


当然了,SDL2中,我们可以创建多个窗口。


事实上,每次调用SDL_CreateWindow函数都会返回一个指向窗口的结构体指针(SDL_Window*)。然后我们可以操纵每个指针来对每个窗口进行改动和操作。


暂时我们只需要一个窗口就够了。


关于游戏中一般的窗口坐标,我们需要讲一下:


一般游戏中的窗口的坐标原点是在左上角,也就是说,此处的x和y都是0。从左往右横坐标的值(x)逐渐增大;从上往下纵坐标的值(y)逐渐增大。如下图所示:


0?wx_fmt=jpeg



操纵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 种之多),所以我们的大千世界才是如此色彩斑斓啊。


上述程序中,我们的红,绿,蓝的取值分别为17206112,所以整体的颜色就是这样一种蓝绿色。


运行以上的程序,我们的窗口就有很好看的颜色了。


0?wx_fmt=jpeg




总结


  1. SDL程序在开始处需要使用SDL_Init函数来加载,在结尾处要使用SDL_Quit函数来卸载。

  2. flag(标记)是一些常量,这些常量可以用按位或操作符“|”来连接,就好像相加一般,使多个特性可以同时具有。

  3. SDL的基础元素之一是“表面”(Surface),是SDL_Surface结构体类型,形状是矩形。我们可以在这些表面上“作画”。

  4. 总是至少有一个“表面”,就是我们创建的窗口的那个表面。

  5. 填充“表面”可以使用函数SDL_FillRect。

  6. 颜色是由红,绿,蓝这三原色组成的。每一组分的取值范围都是0~255。



第三部分第三课预告:


今天的课就到这里,一起加油吧。

下一次我们学习:  SDL开发游戏之显示图像