汉诺塔问题是利用递归实现的经典程序问题。递归即函数自身调用自身的方法。虽然递归只能算是一种程序的实现方法而不是一种算法,但有许多算法都是可以用递归实现的,例如递推、递归搜索和分治等。
汉诺塔可以分为计数类问题(求搬动盘子次数)和构造问题(求搬动盘子过程)本课不讨论汉诺塔的程序实现,而是说明如何利用SDL2实现一个简单的三层汉诺塔移动过程的动画,如图所示。
本程序显示三层汉诺塔移动过程三次。核心问题是精灵的概念和坐标系的使用。
一、SDL中的相关概念
1.精灵spirit的概念
在动画制作中,精灵泛指动画中一切可以活动的部分。无论是主角、敌人还是背景中的一草一木,只要可以活动就可以认为是精灵。通常一个动画会有一个背景(background)图片,其余的部分都可以定义为精灵。例如,在汉诺塔动画中,有四幅图片,如下所示:
其中,bg,jpg表示背景,其余b1、b2和b3都可以认为是精灵。
2.动画的概念
前面说过,在动画中每秒显示12帧以上连续的图像就是动画,但这样计算量和存储量都很巨大,是不得已而为之。在SDL程序设计中,动画的产生主要是精灵的改变。精灵的改变又分两种,一种是精灵图像的改变,一种是精灵位置的改变。
精灵图像的改变,可以借助一个精灵列表实现。所谓精灵列表就是在一幅精灵图像上有很多个精灵形象,每次选取图像中的不同形象以实现精灵图像的改变。如下例,就定义了一个人物的上下左右行走的过程。(说明:这个精灵列表来自于网络)
图4- 3一个精灵列表
精灵位置的改变,就是通过修改精灵的坐标,以实现在不同位置的显示。如汉诺塔动画就是通过每次改变精灵的位置来实现的。下面我们来具体说明。
3.程序中的坐标系
在数学中,我们定义平面直角坐标系来描述平面上每个点的位置。其中横轴为x轴且向右为正向;竖轴叫做y轴,向上为正向,这样我们就可以用(x,y)这样一对数据来描述平面上每个点的位置,如下图(a)所示。在计算机中的坐标系略有不同,就是y轴向下为正向,且x轴和y轴没有负数坐标,屏幕左上角对应(0,0)点,如下图(b)所示。
图4- 4数学和计算机中的坐标系统
4. SDL中图像的描述
在SDL中如何描述一幅图像呢?通常采用四个参数,即:图像左上角的x轴坐标、图像左上角的y轴坐标,图像的宽和图像的高。如下图所示,其中红色图像可以表示为:
img1(x1,y1,w,h)
图4- 5 图像的描述
另外,在SDL中,坐标系是相对坐标系,所有精灵的坐标都是相对于窗口的而不是相对于屏幕的。即当新建一个窗口时,窗口的左上角坐标为(0,0)。
二、 程序基本原理
本程序基本原理是通过不断修改3个精灵的位置,以达到动画效果。因此有必要了解背景图片上的坐标。
1.背景图片坐标说明
图4- 背景图片坐标说明
背景图片坐标说明如图所示。由图可知如下信息:
背景图片大小是宽640像素,高480像素;
A柱上的盘子1左上角坐标为(75,190),宽78像素,高40像素;
A柱上的盘子2左上角坐标为(55,230),宽118像素,高40像素;
A柱上的盘子3左上角坐标为(35,270),宽158像素,高40像素;
从A柱子平移到B柱子,需要x轴相应增加190;
从B柱子平移到C柱子,需要x轴相应增加190;
从A柱子平移到C柱子,需要x轴相应增加380;
由上述说明,我们可以把各个盘子在不同柱子上的位置,抽象成一个3列3行的表格,如下图所示,其中行高=40,列宽=190,注意x轴表示列,y轴表示行,为了与坐标顺序对应所以在后面描述位置时列在前,行在后。这样描述盘子移动过程更加直观。例如:初始时盘子1坐标为(x1,y1),位置在第1列第1行,当把盘子移动到第2列第3行时,即在原有基础上增加了1列,2行,所以盘子1的坐标变为(x1+1190,y1+240)。
2.三层汉诺塔移动过程说明
三层汉诺塔移动7步,包含初始化共显示8幅图像,每次图像显示都要修改三个盘子的位置,同时每个盘子的位置都是相对初始化时位置的修改。
可以使用打表方法记录每次盘子移动的位置。
定义数组InitPos记录三个盘子的初始坐标,定义如下:
int InitPos[6]={75,190,55,230,35,270};
其中:
(75,190)是盘1的初始x和y坐标;
(55,230)是盘2的初始x和y坐标;
(35,270)是盘3的初始x和y坐标。
定义数组pos记录8幅图像中,每次3个盘子的位置改变值,定义如下:
int pos[8][6]={
{0,0,0,0,0,0},//初始化
{2,2,0,0,0,0},//第1步
{2,2,1,1,0,0},//第2步
{1,1,1,1,0,0},//第3步
{1,1,1,1,2,0},//第4步
{0,2,1,1,2,0},//第5步
{0,2,2,0,2,0},//第6步
{2,0,2,0,2,0}};//第7步
注意:两个数组一个记录坐标,一个记录位置。
三、 编写程序
1.Update()函数
函数Update()循环8次,每次修改一幅图像中3个盘子的坐标并调用OnDraw()函数绘图。
1. void Game::Update(){
2. for(int i=0;i<=7;i++){
3. int p[6]={};//定义三个盘子的坐标,必须放在循环中进行初始化
4. for(int j=0;j<6;j+=2){//计算三个盘子的坐标
5. p[j]=InitPos[j]+pos[i][j]*190;
6. p[j+1]=InitPos[j+1]+pos[i][j+1]*40;
7. }
8. OnDraw(p); //显示图像
9. }
10. }
需要说明的是第47行的循环,由于数组InitPos都是用下标05来描述3个盘子的x和y坐标,而每次需要同时处理一个盘子的x和y,所以循环3次。且每增加1列即增加190个像素,每增加1行即增加40个像素。
P[j]为盘子i的x坐标=起始坐标+列位置改变量190;
P[j+1]是盘子i的y坐标=起始坐标+行位置改变量40;
2.OnDraw()函数
函数OnDraw()用于显示一副图像,包括一个背景和3个精灵(盘子),代码如下:
1. void Game::OnDraw(int p[]){
2. const char*s[4]={"bg.jpg","b1.bmp","b2.bmp","b3.bmp"};
3. int W[]={0,78,118,158};
4. SDL_Surface *b[4];
5. SDL_Texture *bt[4];
6. for(int i=0;i<4;i++){//载入图像创建纹理
7. b[i]=IMG_Load(s[i]);
8. bt[i]=SDL_CreateTextureFromSurface(r,b[i]);
9. }
10. SDL_RenderClear(r);
11. SDL_RenderCopy(r,bt[0], NULL, NULL);//载入背景
12. for(int i=1;i<=3;i++){//载入三个精灵(盘子)
13. SDL_Rect rect1;
14. rect1.x=p[(i-1)*2+0];
15. rect1.y=p[(i-1)*2+1];
16. rect1.w=W[i];
17. rect1.h=40;
18. SDL_RenderCopy(r,bt[i], NULL, &rect1);
19. }
20. SDL_RenderPresent(r);
21. for(int i=0;i<4;i++){
22. SDL_FreeSurface(b[i]);
23. SDL_DestroyTexture(bt[i]);
24. }
25. SDL_Delay(1000);
26. }
该函数基本过程见第2讲的显示一幅图像部分。
其中第13~17行,每次定义了一个矩形区域rect1,用于填充目标渲染器。由于位置坐标p从0开始,而精灵从下标1开始且包含x和y两个坐标(下标0表示背景图)。所以取值如下:
rect1.x=p[(i-1)2+0];
rect1.y=p[(i-1)2+1];
第28行图像载入渲染器函数说明如下:
int SDL_RenderCopy(SDL_Renderer renderer, SDL_Texture texture,
const SDL_Rect* srcrect, const SDL_Rect* dstrect)
参数说明:
Renderer表示渲染器指针
Texture表示纹理指针
Srcrect 表示获取源纹理的一个矩形区域填充渲染器,NULL表示整个纹理
Dstrect表示用纹理填充目标渲染器的一个矩形区域,NULL表示这个渲染器。
返回值:返回值为0表示复制成功,为一个负数表示操作失败。
3.完整代码
代码清单1 三层汉诺塔简单动画实现
1. #include "SDL.h"
2. #include "SDL_image.h"
3. int InitPos[6]={75,190,55,230,35,270};
4. int pos[8][6]={{0},{2,2,0,0,0,0},{2,2,1,1,0,0},{1,1,1,1,0,0},
5. {1,1,1,1,2,0},{0,2,1,1,2,0},{0,2,2,0,2,0},{2,0,2,0,2,0}};
6. class Game{
7. private:
8. SDL_Window *w;
9. SDL_Renderer* r;
10. public:
11. Game();
12. ~Game();
13. void Init();//初始化
14. void EveProc(int&);//预留接口
15. void Update();//后台操作
16. void OnDraw(int p[]);//前台显示
17. };
18. Game::Game(){
19. SDL_Init(SDL_INIT_EVERYTHING);
20. w= SDL_CreateWindow("hanoi", SDL_WINDOWPOS_CENTERED,
21. SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_SHOWN);
22. r= SDL_CreateRenderer(w, -1, SDL_RENDERER_ACCELERATED
23. | SDL_RENDERER_PRESENTVSYNC);
24. SDL_SetRenderDrawColor(r,255,255,255,255) ;
25. }
26. Game::~Game(){
27. SDL_DestroyRenderer(r);
28. SDL_DestroyWindow(w);
29. SDL_Quit();
30. }
31. void Game::Init(){
32. SDL_Surface *bg =IMG_Load("bg.jpg");//文本表面
33. SDL_Texture *bgt = SDL_CreateTextureFromSurface(r, bg);//文本纹理
34. SDL_RenderClear(r);
35. SDL_RenderCopy(r,bgt, 0, 0);
36. SDL_RenderPresent(r);
37. SDL_Delay(1000);
38. SDL_FreeSurface(bg);
39. SDL_DestroyTexture(bgt);
40. }
41. void Game::Update(){
42. for(int i=0;i<=7;i++){
43. int p[6]={};//定义三个盘子的坐标,必须放在循环中进行初始化
44. for(int j=0;j<6;j+=2){//计算三个盘子的坐标
45. p[j]=InitPos[j]+pos[i][j]*190;
46. p[j+1]=InitPos[j+1]+pos[i][j+1]*40;
47. }
48. OnDraw(p); //显示图像
49. }
50. }
51. void Game::OnDraw(int p[]){
52. const char*s[4]={"bg.jpg","b1.bmp","b2.bmp","b3.bmp"};
53. int W[]={0,78,118,158};
54. SDL_Surface *b[4];
55. SDL_Texture *bt[4];
56. for(int i=0;i<4;i++){//载入图像创建纹理
57. b[i]=IMG_Load(s[i]);
58. bt[i]=SDL_CreateTextureFromSurface(r,b[i]);
59. }
60. SDL_RenderClear(r);
61. SDL_RenderCopy(r,bt[0], NULL, NULL);//载入背景
62. for(int i=1;i<=3;i++){//载入三个盘子
63. SDL_Rect rect1;
64. rect1.x=p[(i-1)*2+0];
65. rect1.y=p[(i-1)*2+1];
66. rect1.w=W[i];
67. rect1.h=40;
68. SDL_RenderCopy(r,bt[i], NULL, &rect1);
69. }
70. SDL_RenderPresent(r);
71. for(int i=0;i<4;i++){
72. SDL_FreeSurface(b[i]);
73. SDL_DestroyTexture(bt[i]);
74. }
75. SDL_Delay(1000);
76. }
77. int main(int argc, char* argv[]){
78. Game game;
79. for(int i=0;i<3;i++){
80. game.Init();
81. game.Update();
82. }
83. return 0;
84. }