总结出以下几点:

1.需要多次被包含的头文件里不能定义全局变量,否则会报错“重定义”

2.char *strncpy(char *dest, const char *src, int n),

把src所指向的字符串中以src地址开始的前n个字节复制到dest所指的数组中,并返回被复制后的dest。

3.蛇变长现象:虽然我们会将蛇的每个结点的位置向前移动,但是原先蛇的尾节点的背景位置的方块却没有被清除,所以看起来蛇会变长,

  我们只是把新的位置涂黑,而没有把原来被涂黑的位置字符置为空格。

snake.h

 1 #ifndef SNAKE_H_INCLUDE
 2 #define SNAKE_H_INCLUDE
 3 
 4 #include <stdio.h>
 5 #include <stdlib.h>
 6 #include <Windows.h>
 7 #include <time.h>
 8 #include <conio.h>
 9 #include <string.h>
10 #include "resource.h"
11 
12 #pragma comment(lib,"winmm.lib")    //链接这个库
13 
14 #define SNAKELENGTH 25
15 #define false 0
16 #define true 1
17 
18 
19 enum {                //snake[i][2]的数据,存放了蛇的结点移动方向,
20                         //他们的数值代表方向增量,这样结点向某个方向移动时只需将x或y加上这个增量即可
21     to_east = 2,        
22     to_west = -2,
23     to_north = -1,
24     to_south = 1
25 };
26 
27 // 设置窗口标题
28 void windowTitle();
29 
30 //设置窗口大小和颜色
31 void windowSizeColor();
32 
33 //封面
34 void FirstStage();
35 
36 //播放音乐
37 void MyPlaySound(unsigned int id);
38 
39 //检测空格键
40 void TestSpace();
41 
42 //停止播放音乐
43 void StopMusic();
44 
45 //显示背景
46 void showBackGround();
47 
48 //为蛇随机生成一个随机位置
49 void SetSnakeRandPos();
50 
51 //将蛇的结点画到背景上
52 void DrawSnake();
53 
54 //蛇移动
55 void snakeMove();
56 
57 //清除蛇尾部的残余阴影
58 void DestructionOfResidual();
59 
60 //控制蛇的转向和移动
61 void snakeWheel();
62 
63 //判断蛇是否死亡
64 boolean IsSnakeDie();
65 
66 boolean IsSnakeDie2();
67 
68 //随机产生一个食物
69 void ProduceFood();
70 
71 //蛇吃食物和变长
72 void SnakeGrowUp();
73 
74 //过程分数
75 void ProcessScore();
76 
77 //最终成绩
78 void FinalScore();
79 
80 //清除+5
81 void clear();
82 
83 #endif

snake.c

  1 #define _CRT_SECURE_NO_WARNINGS    //若无次宏定义,则strncpy()会报错
  2 #include "snake.h"
  3 
  4 int snakeDir = to_west;
  5 clock_t start, stop;
  6 double duration;
  7 boolean ReproduceFood = true;
  8 
  9 int g_snakeLength = 3;        //蛇的长度
 10 
 11 int g_grade = 0;        //分数
 12 
 13 int g_nline;
 14 int g_ncol;
 15 
 16 int g_gradeFlag = 1;        //过程分数显示标志
 17 
 18 char BackGround[20][48] = {
 19     "███████████████████████\n",
 20     "█                                          █\n",
 21     "█                                          █\n",
 22     "█                                          █\n",
 23     "█                                          █\n",
 24     "█                                          █\n",
 25     "█                                          █\n",
 26     "█                                          █\n",
 27     "█                                          █\n",
 28     "█                                          █\n",
 29     "█                                          █\n",
 30     "█                                          █\n",
 31     "█                                          █\n",
 32     "█                                          █\n",
 33     "█                                          █\n",
 34     "█                                          █\n",
 35     "█                                          █\n",
 36     "█                                          █\n",
 37     "█                                          █\n",
 38     "███████████████████████\n"
 39 };
 40 
 41 
 42 int snake[SNAKELENGTH][3] = { 0 };        //表示蛇的结点,每个结点有三个数据,行号,列号,方向
 43 
 44 // 设置窗口标题
 45 void windowTitle()
 46 {
 47     system("title 贪吃蛇");
 48 }
 49 
 50 //设置窗口大小和颜色
 51 void windowSizeColor()
 52 {
 53     system("mode con cols=46 lines=33");    //窗口的宽高
 54 
 55     //0 - 黑色 1 - 蓝色 2 - 绿色 3 - 浅绿色 4 - 红色 5 - 紫色 6 - 黄色 7 - 白色 8 - 灰色
 56     //        9-浅蓝色 10(A)-淡绿色 11(B)-淡浅绿色 12(C)-淡红色 13(D)-淡紫色 14(E)-淡红色 15(F)-亮白色
 57     system("color 6A");    //窗口的颜色, 前面那位6是是背景颜色, 后面的A是前景色
 58 
 59 }
 60 
 61 //封面
 62 void FirstStage()
 63 {
 64     printf("\n\n\n\n");
 65     printf("\t\t《欢迎进入贪吃蛇》\n\n");
 66     printf("\t\t《按空格开始游戏》\n\n");
 67     printf("\t\t《W A S D 控制移动》\n\n\n\n\n");
 68 }
 69 
 70 //播放音乐
 71 void MyPlaySound(unsigned int id)
 72 {
 73     //常用
 74     //返回值是bool类型,参数一位 播放文件的路径
 75     //第二个参数是NULL, 第三个参数
 76     //PlaySound(path, NULL, SND_FILENAME | SND_LOOP | SND_ASYNC);     //    用异步方式播放声音,PlaySound函数在开始播放后立即返回。
 77     //1.wav是相对路径
 78     //E:\\KuGou\\1.wav是绝对路径
 79     //这种方式生成的.exe可执行文件很小,且能方便改动音乐等资源,更灵活
 80 
 81     //如果第一个参数是资源文件的ID,那么参数三是SND_RESOURCE
 82     //MAKEINTSOURCE这个宏是把一个数字类型转换成指针类型的宏,用这个宏的主要原因是有的资源是用序号定义的,而不是字符串.
 83     //所以要把数字转换成字符串指针,然后再传递给LoadResource之类的函数,这样才加载了资源. 
 84     PlaySound(MAKEINTRESOURCE(id), NULL, SND_RESOURCE | SND_ASYNC);    //生成的.exe可执行文件很大,因为他将音乐也整合进去了,且资源文件不能改动,不灵活
 85 
 86 }
 87 
 88 void TestSpace()
 89 {
 90     //检测空格键
 91     char chInput;
 92 
 93     while (1)
 94     {
 95         //chInput = getchar();    //getchar()读取一个字符且将它输出在显示屏上,并且这个函数在结束输入时必须按下enter键,输入流才会开始读取
 96         //_getch()读取一个字符但是不会将它显示在显示屏上,这个函数读取字符不必等到用户按下enter键,用户输入字符后立即读取
 97         chInput = _getch();        //其实如果是中文输入法的话会暂时将读取的字母输出在屏幕上,但是按下enter键后会马上消失
 98         //同步检测
 99         if (' ' == chInput)        //将常量放在等号左侧,因为这样如果少些一个等号会报错,可以马上定位的报错的位置
100             break;
101     }
102 
103 }
104 
105 void StopMusic()
106 {
107     //停止播放
108     PlaySound(NULL, 0, 0);
109 }
110 
111 //void show1()
112 //{
113 //    int i, j;
114 //    while (1)
115 //    {
116 //        start = clock();            //用clock()函数检测打印一次所需时间,测试后结果为24ms~53ms,超过20ms,人眼所能识别最小的时间间隔是20ms,所以会造成闪烁现象
117 //        system("cls");
118 //    printf("游戏界面\n");
119 //        for (i = 0; i < 20; i++)
120 //        {
121 //            for (j = 0; j < 23; j++)
122 //            if (1 == BlackBlock[i][j])
123 //                printf("█");
124 //            else
125 //                printf("  ");
126 //            printf("\n");
127 //        }
128 //        stop = clock();
129 //        duration = ((double)(stop - start)) / CLK_TCK;
130 //        printf("duration = %lf\n", duration);
131 //        Sleep(1000);    //时间间隔是1000ms,即1s
132 //    }
133 //}
134 
135 void showBackGround()
136 {
137     int i;
138     ////用clock()函数检测打印一次所需时间,测试后结果为0ms~10ms,
139     //且大多数情况下在0ms ~ 4ms,远小于20ms,所以几乎看不到闪烁现象
140     start = clock();
141     for (i = 0; i < 20; i++)
142         printf(BackGround[i]);
143     stop = clock();
144     duration = ((double)(stop - start)) / CLK_TCK;
145     printf("duration = %lf\n", duration);
146     /*Sleep(1000);*/
147 
148 
149 }
150 
151 //void show3()
152 //{
153 //    while (1)
154 //    {
155 //        system("cls");
156 //        printf(backGround1);
157 //        Sleep(1000);
158 //    }
159 //}
160 
161 //为蛇产生一个随机位置
162 void SetSnakeRandPos()
163 {
164     int nx = -1;
165     int ny = -1;
166 
167     //产生随机数
168     //srand()函数包含在time.h头文件中,参数类型时unsigned int
169     //time(NULL);的返回类型是time_t 也就是int64,所以需要一个强制转换
170     srand((unsigned int)time(NULL));        //种随机种子
171 
172     //因为蛇默认起始状态为3节,所以最右边要预留3个位置
173     nx = rand() % 19 + 1;        //nx的大小范围为1 ~ 19, rand() % 19范围就是0 ~ 18, +1之后就是1 ~ 19
174     ny = rand() % 18 + 1;        //ny的大小范围为1 ~ 18, rand() % 18范围就是0 ~ 17, +1之后就是 1 ~ 18
175 
176     //初始化蛇的起始三个结点
177     snake[0][0] = ny;            //表示蛇的结点的行号
178     snake[0][1] = nx * 2;        //表示结点的列号
179     snake[0][2] = to_west;        //表示这个结点的运动方向
180 
181     snake[1][0] = ny;
182     snake[1][1] = nx * 2 + 2;
183     snake[1][2] = to_west;
184 
185     snake[2][0] = ny;
186     snake[2][1] = nx * 2 + 4;
187     snake[2][2] = to_west;
188 
189     //将这三个结点画到背景上
190     DrawSnake();
191 }
192 
193 void DrawSnake()
194 {
195     int i;
196     for (i = 0; snake[i][0] != 0; i++)        //因为蛇的每个结点的行号都是初始化为0的,所以如果行号为0,代表这个结点还没有被使用
197     {
198         strncpy(&BackGround[snake[i][0]][snake[i][1]], "", 2);        //这个不能用带_s 会出问题 将蛇的结点对应在背景图上的2个位置字符赋值为█
199     }
200 }
201 
202 
203 //蛇的移动并判断是否死亡
204 void snakeMove()
205 {
206     //将snake[i]的前一个结点snake[i - 1]的三个数据赋值给它,
207     //这样第i个结点在背景中的位置就会变成它前一个结点i - 1的位置,看起来好像就是结点移动了一样
208     int i = SNAKELENGTH - 1;
209     int flag = 0;
210 
211     //先把蛇从背景上清除掉
212     DestructionOfResidual();
213 
214     for (i; i >= 1; i--)
215     {
216         //后面还未生成的结点无需赋值,跳过即可
217         if (0 == snake[i][0])
218         {
219             continue;
220         }
221         //虽然我们会将蛇的每个结点的位置向前移动,但是原先蛇的尾节点的背景位置的方块却没有被清除,所以看起来蛇会变长
222         //我们只是把新的位置涂黑,而没有把原来被涂黑的位置字符置为空格
223 
224 
225         //这时候处理蛇结点尾部残余阴影的一个方法,即将尾结点在背景的位置置为空格
226         //if (0 == flag)
227         //{
228         //    //从尾部删除一个结点,即将尾部结点在背景上的位置字符置位空格
229         //    strncpy(&BackGround[snake[i][0]][snake[i][1]], "  ", 2);
230         //    flag = 1;
231         //}
232 
233 
234         snake[i][0] = snake[i - 1][0];
235         snake[i][1] = snake[i - 1][1];
236         snake[i][2] = snake[i - 1][2];
237     }
238 
239     snake[0][2] = snakeDir;
240 
241     //处理蛇头
242     if (to_east == snake[0][2] || to_west == snake[0][2])
243     {
244         
245         snake[0][1] += snake[0][2];        //如果是东西方向,即是左右方向,那么snake结点的x分量加上方向增量
246     }
247 
248     if (to_north == snake[0][2] || to_south == snake[0][2])
249     {
250         snake[0][0] += snake[0][2];            //如果是南北方向,即上下方向移动,那么snake结点的y分量加上方向增量
251     }
252 
253     //判断蛇头与食物是否重合
254     /*if (0 == strncmp(&BackGround[snake[0][0]][snake[0][1]], "★", 2))
255         ReproduceFood = true;*/
256     //将蛇画在背景上
257     DrawSnake();
258 
259 }
260 
261 //清除蛇尾部的残余阴影
262 void DestructionOfResidual()
263 {        //将背景恢复原样,我们只是改变了背景,并没与改变蛇的结点中的数据,所以不影响蛇的下次绘制
264     int i = 0;
265     for (i; snake[i][0] != 0; i++)
266         strncpy(&BackGround[snake[i][0]][snake[i][1]], "  ", 2);
267 }
268 
269 void snakeWheel()
270 {
271     /*char dir;
272     dir = _getch();
273     if ('a' == dir)
274     snake[0][2] = to_west;
275     else if ('d' == dir)
276     snake[0][2] = to_east;
277     else if ('w' == dir)
278     snake[0][2] = to_north;
279     else if ('d' == dir)
280     snake[0][2] = to_south;*/
281 
282     //异步检测,高字节非0,低字节为1
283     if (GetAsyncKeyState('W'))
284     {
285         if (snake[0][2] != to_south)        //防止蛇反向掉头
286             snakeDir = to_north;
287     }
288     else if (GetAsyncKeyState('S'))
289     {
290         if (snake[0][2] != to_north)
291             snakeDir = to_south;
292     }
293     else if (GetAsyncKeyState('A'))
294     {
295         if (snake[0][2] != to_east)
296             snakeDir = to_west;
297     }
298     else if (GetAsyncKeyState('D'))
299     {
300         if (snake[0][2] != to_west)
301             snakeDir = to_east;
302     }
303 
304 }
305 
306 //判断蛇是否死亡, 死亡返回真,否则返回假
307 boolean IsSnakeDie()
308 {
309     //如果蛇头在它的方向上在走一格就是"█"的话,那么蛇死亡,碰到了边界或咬到了自己
310     if (to_west == snake[0][2] || to_east == snake[0][2])
311     {
312         if (strncmp(&BackGround[snake[0][0]][snake[0][1] + snake[0][2]], "", 2) == 0)
313             return true;
314         else
315             return false;
316     }
317     else
318     {
319         if (0 == strncmp(&BackGround[snake[0][0] + snake[0][2]][snake[0][1]], "", 2))
320             return true;
321         else
322             return false;
323     }
324 
325 }
326 
327 
328 //下面这种判断蛇头为方块则蛇死亡的方式是错误的,因为蛇头一定是方块
329 //boolean IsSnakeDie2()
330 //{
331 //    if (0 == strncmp(&BackGround[snake[0][0]][snake[0][1]], "█", 2))
332 //        return true;
333 //    else 
334 //        return false;
335 //}
336 
337 //随机产生一个食物
338 void ProduceFood()
339 {
340     //int g_nline, g_ncol;        //食物的行、列坐标
341     srand((unsigned int)time(NULL));    //产生一个种子
342     int i;
343 
344     //判断是都需要产生食物
345     if (!ReproduceFood)        //如果ReproduceFood为假,则不需产生食物,直接返回
346     {
347         return;
348     }
349 
350 
351     //防止蛇结点的位置和食物的位置产生冲突
352     while (1)
353     {
354         g_nline = rand() % 16 + 2;    //食物的行号范围为2 ~ 17,rand()% 16 为 0 ~ 15, + 2就成了 2 ~ 17
355         g_ncol = rand() % 19 + 2;        //食物的列号范围为2 ~ 20, rand() % 20 为 0 ~ 18, +2 就成了2 ~ 20
356         boolean bFlag = false;
357         for (i = 0; snake[i][0] != 0; i++)
358         if (snake[i][0] == g_nline && snake[i][1] == g_ncol)
359             bFlag = true;
360         if (!bFlag)
361             break;
362     }
363 
364     //将食物绘制在背景上
365     strncpy(&BackGround[g_nline][g_ncol * 2], "", 2);
366     ReproduceFood = false;
367 }
368 
369 //蛇吃食物和变长
370 void SnakeGrowUp()
371 {
372     int i;
373     if (g_nline == snake[0][0] && g_ncol * 2 == snake[0][1])
374     {
375         printf("good!\n");
376         ReproduceFood = true;
377 
378         //for (i = 0; snake[i][0] != 0; i++);        //统计蛇的长度,此处用一个全局变量来统计
379         if (to_west == snake[g_snakeLength - 1][2] && to_east == snake[g_snakeLength - 1][2])
380         {
381             snake[g_snakeLength][0] = snake[g_snakeLength - 1][0];
382             snake[g_snakeLength][1] = snake[g_snakeLength - 1][1] - snake[g_snakeLength - 1][2];
383         }
384         else
385         {
386             snake[g_snakeLength][0] = snake[g_snakeLength - 1][0] - snake[g_snakeLength - 1][2];
387             snake[g_snakeLength][1] = snake[g_snakeLength - 1][1];
388         }
389         snake[g_snakeLength][2] = snake[g_snakeLength - 1][2];
390 
391         g_snakeLength++;        //蛇的长度加1
392 
393         if (g_gradeFlag)
394         {
395             g_gradeFlag = 0;
396         }
397         else
398             ProcessScore();        //显示成绩+5
399     }
400 }
401 
402 void ProcessScore()
403 {
404     strncpy(&BackGround[10][23], "+5", 2);
405     g_grade += 5;
406 }
407 
408 //最终成绩
409 void FinalScore()
410 {
411     COORD rd;
412     rd.X = 20;
413     rd.Y = 10;
414     SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), rd);    //这个函数用来设置光标位置
415     printf("game over!\n");
416     printf("\t\tYour Final grade is: %d\n\n\n\n\n", g_grade);
417 }
418 
419 //清除+5
420 void clear()
421 {
422     strncpy(&BackGround[10][23], "  ", 2);
423 }

 

背景的三种实现方式:https://blog.51cto.com/u_14201949/2832033

main.c

 1 #include "snake.h"
 2 
 3 
 4 int main()
 5 {
 6     //设置窗口标题
 7     windowTitle();
 8 
 9     //设置窗口大小和颜色
10     windowSizeColor();
11 
12     //播放音乐
13     MyPlaySound(IDR_WAVE1);
14 
15     //封面
16     FirstStage();
17 
18     //检测空格键
19     TestSpace();
20 
21     //停止播放
22     StopMusic();
23     
24     //播放游戏内部音乐
25     MyPlaySound(IDR_WAVE2);
26 
27     //清屏
28     system("cls");
29 
30     //随机生成蛇的位置
31     SetSnakeRandPos();
32     //showBackGround();
33     
34     while (1)
35     {
36         system("cls");
37         //游戏界面
38         printf("游戏界面\n");        //界面闪烁的原因是“如果打印左上角第一个字符到打印右下角最后一个字符时间间隔超过20ms,就会闪烁”
39         
40         //判断蛇是否死亡
41         if (IsSnakeDie())
42         {
43             FinalScore();
44             break;
45         }
46 
47         snakeWheel();
48 
49         //蛇移动并判断蛇是否死亡        //其实是每次循环刷新蛇的位置,造成蛇在动感觉
50         snakeMove();
51 
52         ProduceFood();
53 
54         SnakeGrowUp();
55 
56         //将印有蛇图案的背景显示出来
57         showBackGround();
58 
59         clear();
60 
61         Sleep(500);
62     }
63 
64 
65 
66     system("pause");  //防止程序退出,若无此语句,则音乐无法播放
67     return 0;
68 }