• 深入探索SDL游戏开发


    前言

    欢迎来到小KSDL专栏第二小节,本节将为大家带来基本窗口构成、渲染器、基本图形绘制、贴图、事件处理等的详细讲解,看完以后,希望对你有帮助

    一、简单窗口

    ✨第一步,我们先包含SDL图形库的头文件

    #include
    
    • 1

    ✨第二步,我们需要初始化SDL2库

    int main(int argc, char* argv[])  //主函数必须这样写
    {
    	//初始化SDL库
    	if (SDL_Init(SDL_INIT_VIDEO) != 0)
    	{
    		SDL_Log("Init failed!%s\n", SDL_GetError());
    		return -1;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注意主函数的形参,必须是一个整型,后跟上一个char*数组(参数分别代表命令行参数个数和命令行参数数组),不能是其他形式的main函!

    在主函数中我们先调用SDL_init初始化函数,如果不先初始化 SDL,就不能调用任何 SDL 函数。暂时我们只需要SDL的视频子系统,所以我们先只将 SDL_INIT_VIDEO 标志传递给它。其它的SDL系统大家可以转到定义看一看,其中有一个是全部都包含了,还是很有意思的!!!

    当发生错误时,SDL_Init 返回 负数。当出现错误时,我们可以将具体的错误原因打印到控制台。在SDL中有一个和printf函数功能相同的函数,即SDL_Log。然后用SDL_GetError获取错误字符串并打印出来。

    第三步,我们就到了紧张刺激的创建窗口的截断了,开干!!!

    //SDL_WINDOWPOS_UNDEFINED
    SDL_Window* window = SDL_CreateWindow(u8"king word", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_SHOWN);
    if (!window)
    {
    	SDL_Log("create window failed!%s\n", SDL_GetError());
    	return -1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果SDL成功初始化,我们将使用SDL_CreateWindow创建一个窗口。

    • 第一个参数设置窗口的标题;前面的u8是如果我们的窗口名字中有汉字,它会使汉字正常显示,反之会出现乱码
    • 接下来两个参数分别是窗口的x和y,即窗口在屏幕上的位置,我们不关心位置在那里,所以直接传SDL_WINDOWPOS_UNDEFINED即可,我这里使用的是SDL_WINDOWPOS_CENTERED,表示屏幕居中的意思;
    • 接下来的两个参数分别表示窗口的宽度和高度;
    • 最后一个参数表示窗口创建成功之后显示出来。
      如果有错误,SDL_CreateWindow 返回 NULL。我们将错误打印到控制台。

    最后一步就是设置延迟和销毁窗口了

    //暂停一会
    SDL_Delay(2000);
    //销毁窗口
    SDL_DestroyWindow(window);
    //退出并清理SDL库
    SDL_Quit();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果窗口被成功创建,则会显示到桌面。
    为了防止它消失,我们将调用SDL_Delay。 SDL_Delay将等待给定的毫秒数。 一毫秒是千分之一秒。 这意味着上面的代码将使窗口等待2000 /1000秒或2秒。
    需要注意的重要一点是,当SDL延迟时,它不能接受来自键盘或鼠标的输入。当您运行这个程序时,如果它没有响应,请不要惊慌。我们没有给它处理鼠标和键盘的代码 ;

    退出之前需要调用SDL_DestroyWindow手动销毁窗口和调用SDL_Quit清理所有初始化的子系统。

    !!!简单的窗口我们已经可以熟练的完成了,接下来我们就要更深入的了解SDL了,激动吧!激动就对了!!!

    二、渲染器

    在讲基本图形绘制之前,先给大家讲一个重要的点,那就是渲染器,什么是渲染器呐?打个比方,建模就是用摄像机拍照,渲染就是打印出照片。渲染器的话就是实现建模到出图的一个必经桥梁。所以我们只有窗口是不够的我们还需要渲染器,把我们绘制的东西打印出来,渲染到屏幕上

    第一步,创建渲染器

    //创建渲染器
    SDL_Renderer* render = SDL_CreateRenderer(window, -1, 0);
    if (!render)
    {
    	SDL_Log("create Renderer failed%s\n", SDL_GetError());
    	goto done;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里用SDL_CreateRendererAPI来创建渲染器

    • 第一个参数表示你要在那个窗口上显示渲染器
    • 第二个和第三个参数这里先不讲,一般填-1和0就可以了
    • 返回的SDL_Renderer*,需要接收一下,判断下渲染器是否创建成功

    这里由于C语言的代码重用度不高,自己又懒得写,所以用了go语句,渲染器创建失败,直接跳到最后的销毁窗口

    第二步,设置渲染器背景颜色

    //设置渲染器颜色
    SDL_SetRenderDrawColor(render, 255, 255, 255, SDL_ALPHA_OPAQUE);
    
    • 1
    • 2
    • 第一个参数就是你要设置颜色的渲染器
    • 后面三个参数就是RGB
    • 最后一个参数是RGBA中的A,设置不透明度的,一般默认设置255就好了,或者用它提供的宏SDL_ALPHA_OPAQUE,它的值也是255

    第三步,清屏

    //清屏----------->背景颜色为上边设置的渲染器颜色
    SDL_RenderClear(render);
    
    • 1
    • 2

    也就是让上面设置的颜色显示出来


    然后在这之后你就可以放绘图代码了


    最后一步就是让你上面的绘图全部显示出来,并把渲染器销毁

    //渲染器出场----->让前面的渲染器操作显示出来
    SDL_RenderPresent(render);	
    //销毁渲染器
    SDL_DestroyRenderer(render);
    
    • 1
    • 2
    • 3
    • 4

    好了,渲染器的简单介绍就到这里了,下面我们来介绍基本的图形绘制

    三、基本图形绘制

    1、点

    在这里插入图片描述

    SDL_Point point[10];
    for (int i = 0; i < 10; i++)
    {
    	point[i] = (SDL_Point){ i,i };
    }
    SDL_RenderDrawPoints(render, point, 10);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里直接绘制了十个点,值得注意的是,数组中的赋值需要强转一下,这里不像C++中会提供隐士转换,因为SDL的库大部分都是C语言编写的

    最后一个绘制函数中的参数介绍

    • 第一个参数就是你要绘制到哪个渲染器
    • 第二个参数就是数组
    • 第三个参数就是点的个数

    同样,如果你要绘制一个点,那么就用SDL_RenderDrawPoint,把s去掉即可,参数中第二个填点就好了

    2、线

    在这里插入图片描述

    //2、绘制线
    SDL_RenderDrawLine(render, 0, 480, 640, 0);
    SDL_Point ps[] = { {0,240},{320,0},{640,480} };
    SDL_RenderDrawLines(render, ps, 3);
    
    • 1
    • 2
    • 3
    • 4

    效果如上图,这里的API参数的意思也显而易见

    • 最后四个数字就是两个点的坐标
    • 然后加s的系列和点加s的系列差不多,第二个参数为数组名,第三个参数为点的个数,这个函数会把这几个连到一起,成为折线
    3、矩形

    在这里插入图片描述

    //3、矩形
    SDL_Rect rect = { 100,100,20,20 };
    SDL_RenderDrawRect(render, &rect);
    rect = (SDL_Rect){ 125,125,20,20 };
    SDL_RenderFillRect(render, &rect);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ✨画矩形就简单了,后面一个参数就是一个矩形类,这里介绍一下矩形类,例如SDL_Rect rect = { 100,100,20,20 };就是矩形左上角坐标为100,100,然后宽和高分别为20,20,带fill的是填充矩形,颜色要通过SDL_SetRenderDrawColor来设置

    4、圆和椭圆

    SDL库中并没有提供原生的画圆函数,这里我们自己写了,但是效率不高,大家看看就行,少用

    //圆
    void SDL_RenderDrawCircle(SDL_Renderer* pRender, float x, float y, float r)
    {
    	float angle = 0;
    	for (angle = 0; angle < 360; angle += 0.1)
    	{
    		SDL_RenderDrawPoint(pRender, x + r * SDL_cos(angle), y + r * SDL_sin(angle));
    	}
    }
    void SDL_RenderFillCircle(SDL_Renderer* pRender, float x, float y, float r)
    {
    	register float angle = 0;
    	while (r > 0)
    	{
    		for (angle = 0; angle < 360; angle += 0.1)
    		{
    			SDL_RenderDrawPoint(pRender, x + r * SDL_cos(angle), y + r * SDL_sin(angle));
    		}
    		r -= 1;
    	}
    }
    
    //椭圆
    void SDL_RenderDrawEllipse(SDL_Renderer* pRender, SDL_Rect* rect)
    {
    	//半轴长
    	int aHalf = rect->w / 2;
    	int bHalf = rect->h / 2;
    
    	int x, y;
    	//求出圆上每个坐标点
    	for (float angle = 0; angle < 360; angle += 0.1f)
    	{
    		x = (rect->x + aHalf) + aHalf * SDL_cos(angle);
    		y = (rect->y + bHalf) + bHalf * SDL_sin(angle);
    		SDL_RenderDrawPoint(pRender, x, y);
    	}
    }
    void SDL_RenderFillEllipse(SDL_Renderer* pRender, SDL_Rect* rect)
    {
    	SDL_Rect r = *rect;
    	//半轴长
    	float aHalf;
    	float bHalf;
    	//椭圆上每点坐标
    	float x, y;
    
    	while (r.w >= 0 && r.h >= 0)
    	{
    		aHalf = r.w / 2.f;
    		bHalf = r.h / 2.f;
    		//求出圆上每个坐标点
    		for (float angle = 0; angle < 360; angle += 0.1f)
    		{
    			x = (r.x + aHalf) + aHalf * SDL_cos(angle);
    			y = (r.y + bHalf) + bHalf * SDL_sin(angle);
    			SDL_RenderDrawPointF(pRender, x, y);
    		}
    		r.x++;
    		r.y++;
    		r.w -= 2;
    		r.h -= 2;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    在这里插入图片描述
    在这里插入图片描述

    	//绘制圆,自定义函数----->40ms,比较慢,不建议使用
    	SDL_RenderDrawCircle(render, 300, 300, 20);
    	SDL_RenderFillCircle(render, 330, 330, 20);
    
    	//绘制椭圆
    	SDL_Rect r = { 200,200,40,50 };
    	SDL_RenderDrawEllipse(render, &r);
    	r = (SDL_Rect){ 250,200,40,50 };
    	SDL_RenderFillEllipse(render, &r);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    圆的参数就不说,就是一个原心,一个半径;许多小伙半看到椭圆的参数就蒙圈了,怎么还放一个矩形类进去了,其实椭圆就是矩形大的内切,所以就不难解释了

    四、贴图

    在这里插入图片描述
    在这里插入图片描述

    //加载图片
    SDL_Surface* sfc = SDL_LoadBMP("./assets/images/1.bmp");
    if (!sfc)
    {
    	SDL_Log("surface load failed!%s\n", SDL_GetError());
    	goto done;
    }
    //将surface转换为texture------->效率更高,加载更快
    SDL_Texture* tex = SDL_CreateTextureFromSurface(render, sfc);
    if (!tex)
    {
    	SDL_Log("Texture load failed!%s\n", SDL_GetError());
    	//用完释放图片,后面都用Texture
    	SDL_FreeSurface(sfc);
    	goto done;
    }
    //用完释放图片,后面都用Texture
    SDL_FreeSurface(sfc);
    //输出图片
    SDL_Rect desRect = { 0,0 };
    SDL_Rect srcRect = { 145,246,38,73 };
    //查询Texture的宽度和高度
    SDL_QueryTexture(tex, NULL, NULL, &desRect.w, &desRect.h);
    SDL_RenderCopy(render, tex, NULL, &desRect);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    第一步,加载图片

    //加载图片
    SDL_Surface* sfc = SDL_LoadBMP("./assets/images/1.bmp");
    if (!sfc)
    {
    	SDL_Log("surface load failed!%s\n", SDL_GetError());
    	goto done;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    返回的是SDL_Surface*需要接收一下,看有没有加载成功,需要注意的是后面要填图片路径,但是提一下,填相对路径比较好,绝对路径是带盘符的,如果你发一个SDl的游戏给别人,填的相对路径就可以加载,发绝对路径,人家可能没下到和你同一个盘符,更不用说还有其他的文件前缀

    第二步,转换图片

    //将surface转换为texture------->效率更高,加载更快
    SDL_Texture* tex = SDL_CreateTextureFromSurface(render, sfc);
    if (!tex)
    {
    	SDL_Log("Texture load failed!%s\n", SDL_GetError());
    	//用完释放图片,后面都用Texture
    	SDL_FreeSurface(sfc);
    	goto done;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    为什么要转换,大家一看第一个APISDL_Surface就应该明白一点意思,表面,代表什么,加载深度不快,而SDL_Texture采用CPU加速,渲染,也就是常常说的硬件家速

    最后一步,输出图片

    //输出图片
    SDL_Rect desRect = { 0,0 };
    SDL_Rect srcRect = { 145,246,38,73 };
    //查询Texture的宽度和高度
    SDL_QueryTexture(tex, NULL, NULL, &desRect.w, &desRect.h);
    SDL_RenderCopy(render, tex, NULL, &desRect);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • SDL_QueryTexture是查询图片的宽度和高度,第二第三个参数不用管,填NULL就可以了,第三四个参数,把查询到的宽度和高度赋给矩形
    • SDL_RenderCopy让图片显示出来,第四个参数填的是图片的实际大小,那这张图片显示的时候就会在窗口全屏显示,进行了缩放
      • 第三个参数什么意思呐?就是图片的局部显示,放入的是一个矩形类,x、y、w、h,即要显示的区域的左上角坐标和区域的宽度、高度

    五、事件处理

    //事件处理,主循环
    	SDL_bool isDown = SDL_FALSE;
    	while (!isDown)
    	{
    		//处理事件
    		SDL_Event ev = { 0 };
    		//处理队列上的事件
    		if (SDL_PollEvent(&ev))
    		{
    			//事件等于退出事件,结束主循环,退出程序
    			if (ev.type == SDL_QUIT)   //枚举类型,注意不要写错了
    			{
    				isDown = SDL_TRUE;
    				//goto done;
    			}
    			//键盘按下事件
    			else if (ev.type == SDL_KEYDOWN)
    			{
    				//按下的什么键
    				switch (ev.key.keysym.sym)
    				{
    				case SDLK_UP:
    					SDL_Log("up\n");
    					break;
    				case SDLK_DOWN:
    					SDL_Log("down\n");
    					break;
    				case SDLK_LEFT:
    					SDL_Log("left\n");
    					break;
    				case SDLK_RIGHT:
    					SDL_Log("right\n");
    					break;
    				default:
    					SDL_Log("key:%d\n", ev.key.keysym.sym);
    					break;
    				}
    			}
    			//鼠标按下/弹起事件
    			else if (ev.type == SDL_MOUSEBUTTONDOWN)
    			{
    				switch (ev.button.button)
    				{
    				case SDL_BUTTON_LEFT:
    					SDL_Log("button left pos(%d,%d)\n", ev.button.x, ev.button.y);
    					break;
    				case SDL_BUTTON_MIDDLE:
    					SDL_Log("button middle\n");
    					break;
    				case SDL_BUTTON_RIGHT:
    					SDL_Log("button right pos(%d,%d)\n", ev.button.x, ev.button.y);
    					break;
    				case SDL_BUTTON_X1:
    					SDL_Log("button X1 pos(%d,%d)\n", ev.button.x, ev.button.y);
    					break;
    				case SDL_BUTTON_X2:
    					SDL_Log("button X2 pos(%d,%d)\n", ev.button.x, ev.button.y);
    					break;
    				}
    			}
    			else if (ev.type == SDL_MOUSEBUTTONUP)
    			{
    
    			}
    			//鼠标移动事件
    			else if (ev.type == SDL_MOUSEMOTION)
    			{
    				//SDL_Log("mouse motion pos(%d,%d)\n", ev.motion.x, ev.motion.y);
    			}
    			//鼠标滚轮
    			else if (ev.type == SDL_MOUSEWHEEL)
    			{
    				SDL_Log("mouseWheel dir(%d,%d) pos(%d,%d)\n",
    					ev.wheel.x, ev.wheel.y, ev.wheel.mouseX, ev.wheel.mouseY);
    			}
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    1、事件处理流程

    ✨除了将图像放在屏幕上之外,游戏还要求您处理来自用户的输入。您可以使用 SDL事件处理系统来做到这一点。(把一下代码放到main函数中)

    //主循环标志
    bool isDone = false;
    
    • 1
    • 2

    ✨在输出表面和更新窗口表面之后,我们声明了一个退出标志,用于跟踪用户是否已退出。由于此时我们刚刚启动了应用程序,显然它被初始化为false。

    ✨我们还声明了一个SDL_Event联合。SDL事件是有意义的,比如 按键鼠标移动操纵杆 按钮按下等。在这个应用程序中,我们将找到退出事件来结束应用程序。

    //应用程序主循环
     while( !isDone )
     {
    
    • 1
    • 2
    • 3

    ✨在之前的教程中,我们让程序在关闭前等待几秒钟。 在这个应用程序中,我们让应用程序等到用户退出后才关闭。

    ✨当用户没有退出时,我们会有应用程序循环。 这种在应用程序处于活动状态时持续运行的循环称为主循环,有时也称为游戏循环。 它是任何游戏应用程序的核心。

    //事件处理
    static SDL_Event ev ={0};
    //处理队列上的事件
    while( SDL_PollEvent( &ev ) != 0 )
    {
       //用户请求退出
        if( ev.type == SDL_QUIT )
      	{
             isDone = true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    ✨在主循环里面写了事件循环。 这样做的作用是继续处理事件队列,直到它为空。

    ✨当您按下一个键、移动鼠标或触摸触摸屏时,事件就会被放到事件队列中。
    在这里插入图片描述

    ✨然后事件队列将按照事件发生的顺序存储它们,等待您处理它们。 当您想知道发生了什么事件以便处理它们时,您可以轮询事件队列,通过调用SDL_PollEvent获取最近的事件。 SDL_PollEvent所做的就是从事件队列中取出最近的事件,并将该事件中的数据放入我们传递给函数的SDL_Event中。

    在这里插入图片描述

    ✨SDL_PollEvent 将不断从队列中取出事件,直到队列为空。当队列为空时,SDL_PollEvent 将返回 0。所以这段代码所做的就是不断轮询事件队列中的事件,直到它为空。如果来自事件队列的事件是 SDL_QUIT 事件(用户点击窗口右上角关闭按钮产生的事件),我们将退出标志设置为 true,以便我们可以退出应用程序。

    //输出图片
    SDL_BlitSurface( gXOut, NULL, gScreenSurface, NULL );
    //更新窗口表面
    SDL_UpdateWindowSurface( gWindow );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ✨在我们处理完一帧的事件后,我们绘制到屏幕并更新它(如上一教程中所述)。如果退出标志设置为真,应用程序将在循环结束时退出。如果它仍然是假的,它将一直持续到用户 X 掉窗口。

    2、效果

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    java调用海康威视SDK实现车牌识别
    jetsonnano部署openpose姿态检测 cuda10.2
    Mybatis-Plus前后端分离多表联查模糊查询分页
    MAVEN-SNAPSHOT和RELEASE
    Golang 程序漏洞检测利器 govulncheck(二):漏洞数据库详解
    RS485温湿度检测单元 modbus通讯 环境温湿度 支持1路开关量接入
    Qt-OpenCV学习笔记--最小包覆矩形--minAreaRect()
    Django REST framework中的序列化Serializers
    高效的工作学习方法
    Docker 介绍
  • 原文地址:https://blog.csdn.net/qq_72157449/article/details/130701154