C语言小游戏: 2048.c
概要:2048.c是一个C语言编写的2048游戏,本文将详细分析它的源码和实现。C语言是一种经典实用的编程语言,本身也不复杂,但是学会C语言和能够编写实用的程序还是有一道鸿沟的。本文试图通过一个例子展示如何用C语言实现一个简单但有用的程序。
一、程序简介
本文分析的代码是mevdschee在GitHub上的项目2048.c,游戏的规则和安装说明都可以到主页查看,本文不再赘述。顺便一提,这个程序虽然是纯C编写的,但是它适用于Linux终端,因此如果你想要看一下运行效果可能需要一个Linux.
2048.c源代码只有一个文件,也就是2048.c。它支持图形和色彩,右上角显示分数,下面是操作说明。界面整体看起来挺简洁美观,我们一会看一下它是怎么做到的。
二、代码结构
我们先看一下程序所包含的函数,大体了解它的结构和功能。
程序入口和测试:
- main (argc,argv[])
- test ()
绘制界面相关:
- getColor (value,color,length)
- drawBoard (board[][])
- setBufferedInput (enable):设置终端的行为
- signal_callback_handler (signum)
游戏逻辑:
- findTarget (array[],x,stop)
- slideArray (array[])
- rotateBoard (board[][])
- moveUp (board[][])
- moveLeft (board[][])
- moveDown (board[][])
- moveRight (board[][])
- findPairDown (board[][])
- countEmpty (board[][])
- gameEnded (board[][])
- addRandom (board[][])
- initBoard (board[][])
从函数的参数中可以看出,游戏使用的主要的数据结构是一个二维数组,在主函数中定义: uint8_t board[SIZE][SIZE] 。SIZE的值默认是4,这是2048游戏面板的一般大小,下文直接称为4。数组中的元素保存的是指数,例如如果显示的数是1024,那么存储的应该是10。在初始化过程中,该数组被填满0.
主函数中完成一些初始化和设置工作,然后进入主循环。在循环中接受用户的键盘输入,然后调用相应的函数。
三、图形绘制函数
1 void drawBoard(uint8_t board[SIZE][SIZE]) { 2 uint8_t x,y; 3 char color[40], reset[] = "\033[m"; 4 printf("\033[H"); 5 6 printf("2048.c %17d pts\n\n",score); 7 8 for (y=0;y<SIZE;y++) { 9 for (x=0;x<SIZE;x++) { 10 getColor(board[x][y],color,40); 11 printf("%s",color); 12 printf(" "); 13 printf("%s",reset); 14 } 15 printf("\n"); 16 for (x=0;x<SIZE;x++) { 17 getColor(board[x][y],color,40); 18 printf("%s",color); 19 if (board[x][y]!=0) { 20 char s[8]; 21 snprintf(s,8,"%u",(uint32_t)1<<board[x][y]); 22 uint8_t t = 7-strlen(s); 23 printf("%*s%s%*s",t-t/2,"",s,t/2,""); 24 } else { 25 printf(" · "); 26 } 27 printf("%s",reset); 28 } 29 printf("\n"); 30 for (x=0;x<SIZE;x++) { 31 getColor(board[x][y],color,40); 32 printf("%s",color); 33 printf(" "); 34 printf("%s",reset); 35 } 36 printf("\n"); 37 } 38 printf("\n"); 39 printf(" ←,↑,→,↓ or q \n"); 40 printf("\033[A"); // one line up 41 }
在drawBoard函数中我们看到绘制的实现过程。函数的主体是一个for循环,每循环一次画一行,这里指的是Board中的一行。循环体中有3个小for循环,每个循环画出终端中的一行,也就是说Board的一行是终端的3行。每个格子的尺寸是3行7列,最中间的位置是数字,如果没有数字则输出一个点。其他区域则用空格填充。
细心的朋友可能已经发现,外循环的变量是y,内循环的变量为x,这样一来board[0][0]到board[3][0]表示的是第1行,board[0][1]到board[3][1]表示第2行,这种对应关系需要特别注意。
"\033m"之类的符号用于控制终端的颜色和其他一些行为。下面给出本程序中出现的用法,更多控制序列的用法可以参考这个网页。通过输出带颜色的空格和字符,2048.c在终端中实现了类似图形界面的效果。
\33[0m 关闭所有属性
\33[30m -- \33[37m 设置前景色
\33[40m -- \33[47m 设置背景色
\33[nA 光标上移n行
\33[nB 光标下移n行
\33[nC 光标右移n行
\33[nD 光标左移n行
\33[y;xH设置光标位置
\33[2J 清屏
\33[?25l 隐藏光标
\33[?25h 显示光标
四、游戏逻辑
我们现在已经知道游戏的主要数据结构,以及如何将它显示在屏幕上,我们接下来要关注游戏罗杰是怎么实现的。2048游戏本身非常简单,其实我们只想关心划的那一下是怎么实现的。我们已经看到2048.c实现了moveUp、moveLeft、moveDown、moveRight四个函数,表示4个划的方向。
moveUp函数看起来也非常简单,它仅仅调用4次slideArray函数。还记得刚刚说过的二维数组和盘面的对应规则吗,矩阵的每一行代表的是盘面的一列,因此每次滑动一个一维数组,实际上滑动的是一列。slideArray函数负责将数组从高index到低index滑动,对应在屏幕上,也就是向上滑动了。
1 bool moveUp(uint8_t board[SIZE][SIZE]) { 2 bool success = false; 3 uint8_t x; 4 for (x=0;x<SIZE;x++) { 5 success |= slideArray(board[x]); 6 } 7 return success; 8 }
slideArray函数和它的辅助函数findTarget任务已经比较简单明了,就不需要详细说了。需要注意的就是在滑的时候合并的块不能第二次合并了,例如2 2 2 2一次合并的结果是4 4,而不会是8.
其他几个函数实现比较巧妙,作者先把盘面进行旋转,然后再调用这个moveUp函数实现。作者通过rotateBoard函数把这个4x4的矩阵旋转90度。数组的下标可以通过建立坐标系得到。
1 void rotateBoard(uint8_t board[SIZE][SIZE]) { 2 uint8_t i,j,n=SIZE; 3 uint8_t tmp; 4 for (i=0; i<n/2; i++) { 5 for (j=i; j<n-i-1; j++) { 6 tmp = board[i][j]; 7 board[i][j] = board[j][n-i-1]; 8 board[j][n-i-1] = board[n-i-1][n-j-1]; 9 board[n-i-1][n-j-1] = board[n-j-1][i]; 10 board[n-j-1][i] = tmp; 11 } 12 } 13 }
了解了这些信息,再看其他的函数比如countEmpty、addRandom等就非常简单了,大家直接去看代码就可以了。
五、总结
2048.c这个小游戏虽然只有400多行,但复现了2048游戏的精髓。而且程序以纯C语言实现,没有使用ncurses之类的第三方库,得到了很不错的效果。实现的过程也有一些精巧的地方,例如如何把问题化繁为简的,如何避免多次编写move函数。其实2048.c不仅可以拿来阅读,无聊的时候玩一局也是相当不错的。