总结出以下几点:
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 "███████████████████████ ", 20 "█ █ ", 21 "█ █ ", 22 "█ █ ", 23 "█ █ ", 24 "█ █ ", 25 "█ █ ", 26 "█ █ ", 27 "█ █ ", 28 "█ █ ", 29 "█ █ ", 30 "█ █ ", 31 "█ █ ", 32 "█ █ ", 33 "█ █ ", 34 "█ █ ", 35 "█ █ ", 36 "█ █ ", 37 "█ █ ", 38 "███████████████████████ " 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(" "); 65 printf(" 《欢迎进入贪吃蛇》 "); 66 printf(" 《按空格开始游戏》 "); 67 printf(" 《W A S D 控制移动》 "); 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("游戏界面 "); 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(" "); 127 // } 128 // stop = clock(); 129 // duration = ((double)(stop - start)) / CLK_TCK; 130 // printf("duration = %lf ", 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 ", 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! "); 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! "); 416 printf(" Your Final grade is: %d ", g_grade); 417 } 418 419 //清除+5 420 void clear() 421 { 422 strncpy(&BackGround[10][23], " ", 2); 423 }
背景的三种实现方式:https://www.cnblogs.com/hi3254014978/p/9782497.html
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("游戏界面 "); //界面闪烁的原因是“如果打印左上角第一个字符到打印右下角最后一个字符时间间隔超过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 }