本程序通过敲击键盘上的“向下箭头”键或者在窗口中的鼠标左键单击来控制汉诺塔移动的移动。实际上如果有键盘或鼠标控制的动画就相当于是游戏了,可以认为游戏就是带有交互的动画。当然,这只是一种简单理解,大部分游戏是要有博弈在其中的,毕竟没有胜败的游戏是不好玩的。
一、 SDL中事件的概念
大部分操作系统如Windows、Linux和MacOS等,如果开发窗口程序,都主要是基于消息(Message)和事件(Event)的。简单地说,消息是用户或系统的输入部分,在窗口程序开发中,涉及消息循环的处理函数,例如鼠标单击或键盘某个键按下等。而事件是基于消息的,是消息到达后触发的操作。可以简单理解事件是一个函数,是操作系统收到消息后所做的反应。例如当您用鼠标左键单击窗口右上角关闭按钮是,产生了鼠标左键单击的消息,这时窗口关闭这就是一个事件,即执行了关闭操作。
在SDL中好像不提消息,所有的操作都是事件鼠标操作就是鼠标事件、键盘操作就是键盘事件等。在前面介绍的动画制作中利用循环显示了3次三层汉诺塔的动画过程,这个过程中每幅图形通过延时函数显示了1秒。如果不利用循环而是使用键盘或鼠标去控制每幅图片的显示,该如何做呢?这个时候就需要事件了。所谓事件相当于是程序的输入,键盘事件是敲击键盘,鼠标事件就是单击或双击鼠标键,窗口事件就是窗口的改变,如最大化,还原等。在SDL中,将所有的事件都存放在一个事件队列中,然后通过一个循环从队列中取出事件,进行处理,所有对事件的操作,就是对队列的操作。
二、事件处理的实现
EveProc()函数是在Game类中预留的事件处理函数。消息事件的处理都是在这个函数中进行的。本程序中定义了三个事件处理功能,即关闭窗口事件、窗口鼠标左键单击事件和键盘的向下箭头按键事件。
1. 关于窗口关闭功能的实现
功能描述:利用鼠标左键单击窗口右上角关闭(x)按钮,实现窗口关闭。
实现方法:主要通过在Game类中定义一个窗口start公有数据成员来标记窗口的状态,当start=1即表示窗口运行,=0表示窗口关闭,程序首先将start设为1,在事件处理中遇到关闭窗口消息就将其设置为0;然后在主函数中根据start的状态决定是否结束程序。在程序中的修改主要包括四个部分:
(1)类中添加公有数据成员start,并修改EveProc函数参数,如图所示:
说明:数据成员公有好像不太符合类设计的思想,但确实可以这样用的。
(2)在构造函数中设置start=1,如图所示:
(3)添加EveProc()函数定义,在其中加入消息处理,如图所示:
其中:
首先,定义了一个SDL接收事件变量e;
然后,while循环调用SDL_PollEvent(&e)事件轮询函数,反复接收消息;
最后,Switch…case结构用于判断接收到的消息类型是否是SDL_QUIT(表示关闭窗口的消息),如果是则修改Start。
这个while+switch…case结构可以作为接受消息检测的模板,后面直接使用即可。
说明:在SDL中可以处理的事件包括Window窗口相关的事件、键盘相关的事件、鼠标移动相关的事件、退出事件和用户自定义事件。
(4)修改主函数,如图所示:
此时,根据game.Start的状态决定程序是否继续运行。
运行该程序后,汉诺塔移动的动画消失,但却多了一个关闭窗口功能。
2. 键盘事件的处理
功能描述:利用键盘上的“向下箭头“键控制动画一幅一幅显示。
实现方法:通过在EveProc()中添加键盘处理事件,循环监听是否有“向下箭头“键按下,如果有则调用Update()修改盘子参数再显示。
这个过程中要注意两点:
(1)由于原来Update()是利用i循环显示8幅图片的,现在变为一次显示1幅图片,因此需要去掉i循环;同时在类中定义私有的数据成员i,用于传递显示的是第几幅图片。
(2)由于有了事件监听处理,所以原理图片显示过程中的延时不再需要了,可以去掉;
具体操作分为6步,介绍如下:
(1)修改Game类添加数据成员i用于控制显示的图片数,同时修改函数参数,如图所示:
说明:i是私有成员只能在类的成员函数中使用,Start是公有成员可以在类外使用。
(2)构造函数中初始化私有成员,如图所示:
(3)去掉Init()函数中的显示延时操作,如图所示:
(4)修改EveProc()函数添加键盘事件处理,如图所示:
其中,首先判断是否是SDL_KEYDOWN键盘按下事件,如果是则通过e.key.keysym.sym来获得按键类型,如果是SDLK_DOWN“向下箭头”按键,则调用Update()函数,其参数i表示第i幅图,在修改坐标并显示图片后,将i增加1个,因为共有8幅图(0~7),所以需要当i大于7时将i置为0,重新开始显示。
说明:
1)键盘事件主要分两种:SDL_KEYDOWN、SDL_KEYUP(键盘按下和抬起)。
2)按键类型很多包括组合按键和单独按键,这里介绍常用的单独按键包括:
注意:很奇怪的是,有些按键需要按下大小写锁定键才有效。
(5)修改Update函数变成一次修改显示一幅图像,如图所示:
(6)最后去掉OnDraw()函数中的延时操作,即可。
3.鼠标事件的处理
有了前面的铺垫,实现鼠标事件很简单了,只需在EveProc()函数中添加鼠标事件处理即可。如图所示:
鼠标事件可以是SDL_MOUSEMOTION(鼠标移动),SDL_MOUSEBUTTONDOWN(鼠标按下),SDL_MOUSEBUTTONUP(鼠标抬起)和SDL_MOUSEWHEEL(鼠标移动)类型。具体各种类型如何使用,可以从网上自行搜索。这里介绍一个常用的操作:
SDL_GetMouseState(&x,&y);
用于获取鼠标左键点击处的x坐标和y坐标。
利用这个函数加上输出操作,可以在程序开始时帮助你很好的了解背景图各部分的坐标。
三、 完整代码
1. //添加事件处理功能
2. #include "SDL.h"
3. #include "SDL_image.h"
4. int InitPos[6]={75,190,55,230,35,270};
5. int pos[8][6]={{0},{2,2,0,0,0,0},{2,2,1,1,0,0},{1,1,1,1,0,0},
6. {1,1,1,1,2,0},{0,2,1,1,2,0},{0,2,2,0,2,0},{2,0,2,0,2,0}};
7. class Game{
8. private:
9. SDL_Window *w;
10. SDL_Renderer* r;
11. int i;
12. public:
13. Game();
14. ~Game();
15. void Init();//初始化
16. void EveProc();//预留接口
17. void Update(int &);//后台操作
18. void OnDraw(int p[]);//前台显示
19. bool Start;
20. };
21. Game::Game(){
22. SDL_Init(SDL_INIT_EVERYTHING);
23. w= SDL_CreateWindow("hanoi", SDL_WINDOWPOS_CENTERED,
24. SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_SHOWN);
25. r= SDL_CreateRenderer(w, -1, SDL_RENDERER_ACCELERATED
26. | SDL_RENDERER_PRESENTVSYNC);
27. SDL_SetRenderDrawColor(r,255,255,255,255) ;
28. Start=1;
29. i=0;
30. }
31. Game::~Game(){
32. SDL_DestroyRenderer(r);
33. SDL_DestroyWindow(w);
34. SDL_Quit();
35. }
36. void Game::Init(){
37. SDL_Surface *bg =IMG_Load("bg.jpg");//文本表面
38. SDL_Texture *bgt = SDL_CreateTextureFromSurface(r, bg);//文本纹理
39. SDL_RenderClear(r);
40. SDL_RenderCopy(r,bgt, 0, 0);
41. SDL_RenderPresent(r);
42. //SDL_Delay(1000);
43. SDL_FreeSurface(bg);
44. SDL_DestroyTexture(bgt);
45. }
46. void Game::EveProc(){
47. SDL_Event e;
48. while (SDL_PollEvent(&e)){
49. switch(e.type){
50. case SDL_QUIT: Start = 0;break;
51. case SDL_KEYDOWN:
52. switch(e.key.keysym.sym){
53. case SDLK_DOWN:
54. Update(i);
55. i=(++i>7)?0:i;
56. break;
57. }
58. break;
59. case SDL_MOUSEBUTTONDOWN:
60. Update(i);
61. i=(++i>7)?0:i;
62. break;
63. }
64. }
65. }
66. void Game::Update(int &i){
67. //for(int i=0;i<=7;i++){
68. int p[6]={};//定义三个盘子的坐标,必须放在循环中进行初始化
69. for(int j=0;j<6;j+=2){//计算三个盘子的坐标
70. p[j]=InitPos[j]+pos[i][j]*190;
71. p[j+1]=InitPos[j+1]+pos[i][j+1]*40;
72. }
73. OnDraw(p); //显示图像
74. //}
75. }
76. void Game::OnDraw(int p[]){
77. const char*s[4]={"bg.jpg","b1.bmp","b2.bmp","b3.bmp"};
78. int W[]={0,78,118,158};
79. SDL_Surface *b[4];
80. SDL_Texture *bt[4];
81. for(int i=0;i<4;i++){//载入图像创建纹理
82. b[i]=IMG_Load(s[i]);
83. bt[i]=SDL_CreateTextureFromSurface(r,b[i]);
84. }
85. SDL_RenderClear(r);
86. SDL_RenderCopy(r,bt[0], NULL, NULL);//载入背景
87. for(int i=1;i<=3;i++){//载入三个盘子
88. SDL_Rect rect1;
89. rect1.x=p[(i-1)*2+0];
90. rect1.y=p[(i-1)*2+1];
91. rect1.w=W[i];
92. rect1.h=40;
93. SDL_RenderCopy(r,bt[i], NULL, &rect1);
94. }
95. SDL_RenderPresent(r);
96. for(int i=0;i<4;i++){
97. SDL_FreeSurface(b[i]);
98. SDL_DestroyTexture(bt[i]);
99. }
100. //SDL_Delay(1000);
101. }
102. int main(int argc, char* argv[]){
103. Game game;
104. game.Init();
105. while(game.Start){
106. game.EveProc();
107. }
108. return 0;
109. }