游戏说明
本代码实现了Windows下(使用了Win API)基于c语言的控制台运行的贪吃蛇游戏.
代码开源至:https://github.com/gngtwhh/snake
代码行数: 537行
文件包括:
game.c 游戏运行逻辑支持
main.c 主函数控制
menu.c 菜单选择控制
snake.h 声明头文件
system.c 系统支持相关
细节描述:
实现菜单选择,游戏基本元素,自定义游戏设置,完善用户交互,处理用户错误输入
实现效果
主界面
自定义
游戏开始
结算界面
代码详解
注:代码中一些函数进行了简化和适当的省略,主要为了体现代码的逻辑
游戏数据结构—game.c
使用一个双向链表来存储蛇,使用一个包含一个坐标对的结构体来存储苹果的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 typedef struct snake { int x, y; struct snake *prior , *next ; } snake; snake *head, *tail; struct APPLE { int x, y; } apple; int score, pre_x, pre_y, wait = 500 ;int HEIGHT = 30 ;int WIDTH = 30 ;int curSnakeLen = 0 ;int maxSnakeLen = 0 ;
游戏控制—main.c
头文件包含
1 2 3 4 #include <stdio.h> #include <conio.h> #include <stdlib.h> #include "snake.h"
代码逻辑
使用一个while(1)循环来重复开始游戏,从而实现一局游戏结束后可以回到主菜单准备下一次游戏
在循环中使用一个char c;配合_getch()来进行菜单选择,同时对于错误输出专门使用一个函数来处理
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int main () { printMenu(); char c; while (1 ) { gotoxy(37 , 17 ); c = _getch(); if (c == '1' ) { initGame(); start(); destoryGameData(); system("cls" ); printMenu(); } else if (c == '2' ) { color(7 ); gotoxy(0 , 18 ); printf ("游戏结束!\n" ); break ; } else wrongInput(); } return 0 ; }
包含了菜单界面的图形打印,用户错误输入处理,以及游戏初始化的调用接口
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void printMenu () ; void initGame () { system("cls" ); setDifficulty(); printBox(); initSnakeAndApple(); } void wrongInput () { gotoxy(43 , 17 ); printf ("输入错误!" ); Sleep(1000 ); gotoxy(43 , 17 ); printf (" " ); gotoxy(39 , 17 ); }
系统相关支持—system.c
因为是一定程度上基于Windows的程序(主要是一些优化和完善,还有关键的光标跳转),需要使用一些win API函数
使用_kbhit()来实现检测按键
头文件包含:
1 2 3 4 5 #include <stdio.h> #include <stdlib.h> #include <windows.h> #include <conio.h> #include "snake.h"
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void color (int i) { SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), i); } void gotoxy (int x, int y) { COORD c; static HANDLE h; h = GetStdHandle(STD_OUTPUT_HANDLE); c.X = x; c.Y = y; SetConsoleCursorPosition(h, c); } int keyboard (int pre) { char c; int n = pre; if (_kbhit()) { c = _getch(); if (c == 'w' || c == 'W' ) n = 1 ; else if (c == 'a' || c == 'A' ) n = 2 ; else if (c == 's' || c == 'S' ) n = 3 ; else if (c == 'd' || c == 'D' ) n = 4 ; else if (c == ' ' ) n = 5 ; } rewind(stdin ); if ((pre == 1 && n == 3 ) || (pre == 2 && n == 4 ) || (pre == 3 && n == 1 ) || (pre == 4 && n == 2 )) return pre; return n; }
游戏逻辑—game.c
包含了所有的初始化操作,游戏运行逻辑,结算处理
头文件包含:
1 2 3 4 5 6 #include <stdio.h> #include <stdlib.h> #include <windows.h> #include <conio.h> #include <time.h> #include "snake.h"
关键代码:
游戏主循环—start()函数
逻辑伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void start () { print_tips(); while (1 ){ if (againstTheWall() || againstSelf()){ gameover(); return ; } print_score(); snake_move(); if (eat_apple()){ snake_growth(); if (SnakeLen == maxSnakeLen) { gamewin(); return ; } generate_new_apples(); score++; } if (pause_game){ pause_the_game(); } Sleep(wait_time); } }
游戏初始化
设置游戏难度—setDifficulty()函数
设置了5个难度,分别对应start()中不同的Sleep时间,以此来影响蛇移动的速度
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void setDifficulty () { int n, difficulties[6 ] = {0 , 1000 , 800 , 600 , 400 , 200 }; bool flag = 1 ; while (flag){ if (scanf ("%d" , &n) && n > 0 && n < 6 ) flag = 0 ; else { system("cls" ); printf ("请输入难度[1~5](按回车键确认):" ); printf ("输入错误!" ); rewind(stdin ); } } wait = difficulties[n]; score = 0 ; rewind(stdin ); system("cls" ); }
打印界面—printBox()函数
关键是坐标的计算,要在正确的位置进行打印
使用"□"字符串进行地图的打印,因为该字符占用2字节,所以x坐标每次要+=2而不是+=1
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void printBox () { set_map_size(); system("cls" ); for (int i = 0 ; i < WIDTH * 2 ; i += 2 ) { gotoxy(i, 0 ); printf ("□" ); gotoxy(i, HEIGHT - 1 ); printf ("□" ); } for (int i = 1 ; i <= HEIGHT - 1 ; i++) { gotoxy(0 , i); printf ("□" ); gotoxy(WIDTH * 2 - 2 , i); printf ("□" ); } color(7 ); for (int j = 1 ; j <= HEIGHT - 2 ; j++) { for (int i = 2 ; i < WIDTH * 2 - 2 ; i += 2 ) { gotoxy(i, j); printf ("□" ); } putchar ('\n' ); } }
初始化蛇和苹果数据
此处涉及到链表操作!
1.首先容易想到苹果只需要记录其x,y坐标即可
2.然后对于蛇,其行为有2种—向前移动一格而不增长,前进并增长一格
3.而且蛇的每一个结点的运动方向都不一定相同—因为蛇会拐弯
如果使用单链表,就需要对每一个结点保存其下次前进的方向,也就是要知道其前一个结点的坐标是在当前结点的哪个方向(上/下/左/右)并进行存储,不仅浪费空间,而且频繁修改会导致效率低下
所以,这里使用双链表来实现,很容易找到前一个结点的坐标来确定移动方向
实际上这里的双链表实际只影响了当前的蛇尾,因为蛇的每一个节点都是相同的,所以前进仅需在蛇头前进方向新增一个蛇头结点作为新蛇头(代码实现实际上是在蛇头后添加一个蛇身结点),然后删除当前的最后一个结点,即蛇尾
由此为了效率还需使用一个尾指针tail来指向蛇尾(链表尾)
综上所述,我们要存储(初始化)的数据即为:
1.苹果的坐标结构体
2.一个带有头结点(直接作为蛇头)的双向链表
3.一个指向当前链表尾结点(蛇尾)的指针tail
同时该部分还要进行游戏刚开始的苹果(默认在地图左上角),蛇身(默认在苹果右边,初始长度为4)的打印
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void initSnakeAndApple () { make_a_snake(4 ); curSnakeLen = 4 ; maxSnakeLen = (WIDTH - 2 ) * (HEIGHT - 2 ); init_apple(8 ,4 ); pre_x = tail->x; pre_y = tail->y; }
蛇移动—moveSnake()函数
因为蛇移动并没有长度变化,即蛇头增长,蛇尾缩短,所以直接把蛇尾结点移动到蛇头即可
对应到链表操作即为修改指针指向,将头结点之后的第一个蛇结点修改为蛇尾,蛇尾结点指向第二个蛇结点,然后倒数第二个结点后驱指向NULL
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void moveSnake (int flag) { int move[4 ][2 ] = { {0 , -1 }, {-2 , 0 }, {0 , 1 }, {2 , 0 } }; flag--; pre_x = tail->x; pre_y = tail->y; gotoxy(tail->x, tail->y); color(7 ); printf ("□" ); gotoxy(head->x, head->y); color(6 ); printf ("■" ); snake *temp = tail; tail = tail->prior; tail->next = NULL ; temp->next = head; temp->prior = NULL ; head->prior = temp; head = temp; head->x = head->next->x + move[flag][0 ]; head->y = head->next->y + move[flag][1 ]; gotoxy(head->x, head->y); color(2 ); printf ("■" ); }
蛇长长—snakeGrowth()函数
实际上是双向链表的尾插结点操作
关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void snakeGrowth () { tail->next = (snake *) malloc (sizeof (snake)); tail->next->prior = tail; tail = tail->next; tail->next = NULL ; tail->x = pre_x; tail->y = pre_y; ++curSnakeLen; gotoxy(pre_x, pre_y); color(6 ); printf ("■" ); return ; }
吃到苹果的处理
要进行胜负判断苹果的重新生成和打印
关键代码:
1 2 3 4 5 6 7 8 9 10 srand((unsigned int ) time(NULL )); do { apple.x = ((rand() % (WIDTH - 2 )) + 1 ) * 2 ; apple.y = (rand() % (HEIGHT - 2 )) + 1 ; } while (isOverlap()); gotoxy(apple.x, apple.y); color(4 ); printf ("■" );score++;
判断坐标是否正确—isOverlap()函数
1 2 3 4 5 6 7 8 9 bool isOverlap () { snake *temp = head; while (temp != NULL ) { if (apple.x == temp->x && apple.y == temp->y) return true ; temp = temp->next; } return false ; }
胜负判断—是否撞墙或是否撞到自己
是否撞墙—againstTheWall()函数
只需要判断蛇头的坐标是否和边界重合
关键代码:
1 2 3 4 5 6 bool againstTheWall () { if (head->x == 0 || head->x == WIDTH * 2 - 2 || head->y == 0 || head->y == HEIGHT - 1 ) return true ; return false ; }
是否撞到自己—againstTheWall()函数
此时需要遍历整个链表(除了蛇头)来和蛇头的坐标进行比较
关键代码:
1 2 3 4 5 6 7 8 9 bool againstSelf () { snake *temp = head->next; while (temp != NULL ) { if (head->x == temp->x && head->y == temp->y) return true ; temp = temp->next; } return false ; }
销毁数据—destoryGameData()函数
每局游戏需要进行数据的销毁(链表)
1 2 3 4 5 6 7 8 9 void destoryGameData () { snake *temp = head; snake *next = NULL ; while (temp != NULL ) { next = temp->next; free (temp); temp = next; } }