温馨提示:这篇文章已超过433天没有更新,请注意相关的内容是否还可用!
摘要:本文介绍了贪吃蛇游戏的简单实现,采用C语言进行编程。该游戏通过控制蛇的移动,使其不断进食成长,同时避免触碰到游戏边界或自己的身体。文章详细阐述了游戏的基本逻辑和代码实现,包括蛇的移动、食物的生成以及碰撞检测等关键部分。该游戏具有简单易懂、易于上手的特点,适合初学者了解游戏开发的基本流程。
前言:学完了C语言的基础语法,和一点数据结构的知识,拿贪吃蛇来练练手,并熟悉以前的知识。写完之后,有一种成就感,为以后的学习饱满激情。
注意这里的讲解是由部分到整体的思路。
目录
控制台不能是终端:
mode和title命令
GetStdHandle函数
隐藏光标函数(HideCurso)的实现:
GetConsoleCursorInfo函数
SetConsoleCursorInfo函数
综上,可以利用以上函数分装成一个函数隐藏光标HideCurso:
在窗口内任意位置打印(SetPos)的函数的实习:
SetCursorPostion函数:
SetPos函数的实现:
实现KEY_PRESS来检测按键被按的情况
按键检测函数GetAsynckeyState
KEY_PRESS的实现:
setlocale函数
游戏实现的大体框架:
游戏开始GameStart实现
1.欢迎界面WelcomeGame的实现
2.创建地图CreateMap的实现
3.CreateSnake创建蛇
3.1蛇身体节点的定义
3.2 蛇的定义
3.3创建蛇CreateSnake的实现
3.4创建食物并打印,CreateFood函数的实现
3.4将上面的函数分装进函数IniteSnake
4.打印蛇,PrintSnake函数的实现
5.最后一步将上述的函数分装进GameStart函数
游戏运行GameRun函数的实现
GameRun 中需要实现的逻辑
PrintHelpInfo打印帮助信息函数的实现:
PrintScore的实现
SnakeMove的实现
Pause暂停函数的实现:
SnakeNext蛇走一步函数的实现:
接下来说一下NextIsFood函数的实现:
IsKill函数的实现:
游戏结束GameEnd函数的实现:
整个游戏逻辑的运行逻辑的实现:
整个游戏的源码(有感兴趣的可以自取):
这里先放一张最后的成果图和一段视频来展示效果
贪吃蛇游戏
这里先讲一些可能需要用到的windows的控制台函数和一些系统操作。
控制台不能是终端:
终端的控制台:
修改过程:
mode和title命令
system("title 贪吃蛇"); system("mode con cols=100 lines=30");
mode改变控制台窗口的大小,cols表示行,lines表示列。
title就是改变控制台的名称。
效果展示:
GetStdHandle函数
其返回值类型是HANDLE(是一个指针),获得一个句柄。
//获得一个句柄 HANDLE hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;//CONSOLE_CURSOR_INFO是控制台光标标的结构体类型 //CursorInfo是我们创建的变量
CONSOLE_CURSOR_INFO是一个结构体其中有两个成员,dwSize 和 dVisible ,dwSize 表示光标占一个单位光标高度的百分比,比如下图。
dVisiable 表示光标是否可见,将光标的信息为不可见:
CursorInfo.bVisible = false;
隐藏光标函数(HideCurso)的实现:
GetConsoleCursorInfo函数
获取控制台的光标信息:
CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutPut, &CursorInfo);//将光标信息放入CursorInfo这个变量中
SetConsoleCursorInfo函数
设置控制台光标的信息:
将光标的信息设置为不可见。
CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutPut, &CursorInfo); CursorInfo.bVisible = false; SetConsoleCursorInfo(hOutPut, &CursorInfo);//将CursorInfo中的数据设置为控制台的光标的信息。
综上,可以利用以上函数分装成一个函数隐藏光标HideCurso:
void HideCursor() { HANDLE hOutPut = NULL; hOutPut = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutPut, &CursorInfo); CursorInfo.bVisible = false; SetConsoleCursorInfo(hOutPut, &CursorInfo); }
效果:
上图看不见光标。
在窗口内任意位置打印(SetPos)的函数的实习:
SetCursorPostion函数:
他有两个参数一个是句柄Ll类型为HANDLE,另一个类型为COORD这是一个结构体类型。
typedef sruct COORD { short x; short y; }COORD;
定位光标的位置,说到位置这里就不得不聊聊控制台的坐标系的定义
这里注意:一个单位的x不等于一个的单位的y,两个单位的x才等于一个单位的y。
SetPos函数的实现:
void SetPos(short x, short y) { HANDLE hOutPut = NULL; hOutPut = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos = { x,y }; SetConsoleCursorPosition(hOutPut, pos); }
调用函数SetPos可以直接将光标定位到你给的坐标处。
实现KEY_PRESS来检测按键被按的情况
按键检测函数GetAsynckeyState
short GetAsyncKeyState(int vKey);
他的返回值是short,如果按下了一个间他会返回一个二进制形势下最低位为1的数,否则为0。
虚拟键码:这里会用到的比如,
上:VK_UP
下:VK_DOWN
左:VK_LEFT
右:VK_RIGHT
f3(加速):VK_F3
f4(减速):VK_F4
空格暂停:SPACE
esc退出:ESCAE
KEY_PRESS的实现:
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
如果vk这个虚拟键位代表的按键被按过,则返回1,否则返回0。
setlocale函数
setlocale(LC_ALL, "");//可以将模式改为当前所在地区的模式,可以打印一些特殊的字符
int main() { char* ret = setlocale(LC_ALL, ""); printf("%s\n", ret); return 0; }
宽字符:
一个宽字符是两个字符的大小。
setlocale(LC_ALL, ""); printf("ab\n"); wprintf(L"%lc", L'我');
将C改为当前地区的模式时,可以打印宽字符。
宽字符打印与普通字符打印的区别
宽字符 | 普通字符 | |
使用函数 | wprintf | printf |
换位符 | %lc %ls | %c %s |
使用 | wprintf(L"%ls",L"helloworld"); wprintf(L"%lc",L'a'); | printf("%s","helloworld"); printf("%c",'a'); |
游戏实现的大体框架:
游戏大揽
游戏开始GameStart实现
1.欢迎界面WelcomeGame的实现
void WelcomeGame() { system("mode con cols=100 lines=30"); system("title 贪吃蛇"); HideCursor(); SetPos(35, 15); wprintf(L"%ls", L"欢迎来到贪吃蛇小游戏"); SetPos(36, 22); system("pause"); system("cls"); SetPos(30, 12); wprintf(L"%ls", L"你可以用↑.↓.←.→来控制蛇的移动"); SetPos(30, 13); wprintf(L"%ls", L"F3加速,F4减速"); SetPos(36, 22); system("pause"); system("cls"); }
这里实现的结果就是大揽里的前两周照片,SetPos,HideCurso两个函数再前面已经实现完了这里就不讲了。
2.创建地图CreateMap的实现
#define WALL L'□' void CreateMap() { for (int i = 1; i x = 10; tmp->y = 4; ps->_psnake = tmp; } else { tmp->x = 10 + 2 * i; tmp->y = 4; tmp->next = ps->_psnake; ps->_psnake = tmp; } } }
运用for循环创建5个节点作为蛇的身体,注意这五个节点每两个节点的坐标应该相邻,确保蛇的身体是连续的。还有蛇的节点坐标别等于墙的坐标。
注意:这里生成的蛇的节点的坐标都是偶数,那么食物的x坐标必须为偶数,否则蛇的头一半吃到食物另一半吃不到食物,就等于永远吃不到食物,就是一个bug了。
3.4创建食物并打印,CreateFood函数的实现
//注意rand的使用需要有srand((unsigned)time(NULL))这一句,这里没有是因为在源码的main函数中 void RandPos(pSnakeNode* tmp) { int x1 = 0; int y1 = 0; do { x1 = rand() % 53 + 2; y1 = rand() % 25 + 1; } while (x1%2);//保证食物的x坐标为偶数 tmp->x = x1; tmp->y = y1; } #define FOOD L'★' void CreateFood(pSnake ps) { pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode)); if (tmp == NULL) { perror("CreateFood():malloc:"); return; } again: RandPos(tmp); pSnakeNode cur = ps->_psnake; //生成的食物的坐标不能与蛇的节点的坐标一样 while (cur) { if (tmp->x == cur->x && tmp->y == cur->y) goto again; cur = cur->next; } ps->_pfood = tmp; SetPos(tmp->x, tmp->y); //打印食物 wprintf(L"%lc", FOOD); return; }
RandPos函数是随机生成一个坐标,这里需要注意的是生成的坐标的范围,因为我想要的地图的大小为58(x)*27(y)去除墙的所以x的范围为[2,54],y的范围为[1,25],注意这里生成的食物x坐标的奇偶性要与蛇的节点的x保持一致。
注意:生成的食物的坐标不能与蛇的节点的坐标一样。
3.4将上面的函数分装进函数IniteSnake
void IniteSnake(pSnake ps) { CreateSnake(ps); CreateFood(ps); //以下的为其他初始化蛇的信息 ps->dir = RIGHT; ps->status = OK; ps->sleep_time = 200; ps->food_score = 10; ps->score = 0; }
4.打印蛇,PrintSnake函数的实现
上面已经初始化完了蛇的节点,那么就可以着手打印了。
void PrintSnake(pSnake ps) { pSnakeNode cur = ps->_psnake; while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } }
初始化完了节点,打印就很简单了,定位光标位置直接按链表的顺序直接打印就完了。
5.最后一步将上述的函数分装进GameStart函数
void GameStart(pSnake ps) { WelcomeGame(); CreateMap(); IniteSnake(ps); PrintSnake(ps); //getchar();可以用getchar函数来观察打印的效果 }
辛苦了这么久看一下打印效果:
游戏运行GameRun函数的实现
这里采用总分的方式来挨个实现:
GameRun 中需要实现的逻辑
void GameRun(pSnake ps) { PrintHelpInfo();//打印游戏提示信息 do { PrintScore(ps);//打印游戏分数 SnakeMove(ps);//蛇的移动 IsKill(ps);//判断蛇的状态并修改蛇的状态 Sleep(ps->sleep_time); } while (ps->status==OK);//蛇为其他状态时,跳出循环 return; }
简单的一句话总结就是,蛇每走一步判断一下状态并更新一下分数。
PrintHelpInfo打印帮助信息函数的实现:
void PrintHelpInfo() { SetPos(60, 20); wprintf(L"%ls", L"你可以用↑.↓.←.→来控制蛇的移动"); SetPos(60, 21); wprintf(L"%ls", L"F3加速,得分增加;F4减速,得分减少"); SetPos(60, 22); wprintf(L"%ls", L"空格是暂停"); SetPos(60, 23); wprintf(L"%ls", L"ESC是退出游戏"); }
这里比较简单,需要注意的就是找个合适的位置打印。
PrintScore的实现
void PrintScore(pSnake ps) { SetPos(60, 5); printf("食物分数:%2d 总分数:%d", ps->food_score, ps->score); }
每次打印时都会覆盖上次打印的数据。
SnakeMove的实现
这里比较重要,也比较难一些。
void SnakeMove(pSnake ps) { //以下的判断按键按的情况,并做出相应的反应 //蛇现在的运动方向为下,那么我们知道,按上时不改变蛇的方向 if (KEY_PRESS(VK_UP) && ps->dir != DOWN) { //如果是方向键则改变蛇的方向 ps->dir = UP; } else if (KEY_PRESS(VK_DOWN) && ps->dir != UP) { ps->dir = DOWN; } else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) { ps->dir = LEFT; } else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT) { ps->dir = RIGHT; } else if (KEY_PRESS(VK_F3)) { //f3是加速,将系统休眠时间改小就可以了,别忘了每个食物的加分,并限制一下,不能一直加速 if (ps->sleep_time > 100) { ps->sleep_time -= 20; ps->food_score += 2; } } else if (KEY_PRESS(VK_F4)) { //f4跟f3一样 if (ps->sleep_time food_score>10) { ps->sleep_time += 20; ps->food_score -= 2; } } else if (KEY_PRESS(VK_SPACE)) { Pause();//这个函数实现暂停 } else if (KEY_PRESS(VK_ESCAPE)) { //只需要改变蛇状态就可以了 ps->status = END_NORMAL; } SnakeNext(ps);//蛇走下一步 }
注意: 蛇现在的运动方向为下,那么我们根据以前玩贪吃蛇的知识知道,按上时不改变蛇的方向
Pause暂停函数的实现:
void Pause() { while (1) { if (KEY_PRESS(VK_SPACE)) break; Sleep(200); } }
实现思路:让系统一致Sleep就可以了,当再次检测到你按下空格时,则跳出循环。
SnakeNext蛇走一步函数的实现:
void SnakeNext(pSnake ps) { //创建一个新节点,这个新节点可以理解为新的蛇头 pSnakeNode movenext = (pSnakeNode)malloc(sizeof(SnakeNode)); if (movenext == NULL) { perror("SnakeNext():malloc:"); return; } //基于蛇头的坐标和蛇移动的方向,确定新节点的坐标 if (ps->dir == UP) { movenext->x = ps->_psnake->x; movenext->y = ps->_psnake->y-1; } else if (ps->dir == DOWN) { movenext->x = ps->_psnake->x; movenext->y = ps->_psnake->y + 1; } else if (ps->dir == LEFT) { movenext->x = ps->_psnake->x-2; movenext->y = ps->_psnake->y; } else if (ps->dir == RIGHT) { movenext->x = ps->_psnake->x+2; movenext->y = ps->_psnake->y; } //判断蛇的下一步是否是食物,如果是食物那么,就将食物的这个节点改为新的蛇头,并释放掉movenext if (NextIsFood(ps, movenext)) { SetPos(ps->_pfood->x, ps->_pfood->y); //将食物的位置覆盖式打印为蛇的身体,表示被吃掉 wprintf(L"%lc", BODY); ps->_pfood->next = ps->_psnake; ps->_psnake = ps->_pfood; //总分增加 ps->score += ps->food_score; free(movenext); //食物被吃掉,这在重新创建一个新的食物。 CreateFood(ps); } else { //下一步没有吃到食物,则将movenext节点头插到蛇的链表中 movenext->next = ps->_psnake; ps->_psnake = movenext; pSnakeNode cur = ps->_psnake; //循环打印蛇身,注意跳出循环的条件,当cur指向倒数第二个节点时跳出 while (cur->next->next != NULL) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } //打印倒数第二个节点 SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); //将原来最后一节身体被覆盖打印成空格,因为这里是没有吃到食物身体没有变长 SetPos(cur->next->x, cur->next->y); wprintf(L" "); //一定要释放最后一个节点,因为加上了一个节点movenext free(cur->next); //现在cur指向尾结点,将尾结点的next置为NULL cur->next = NULL; } }
思路:根据创建一个新的节点,这个节点是由蛇的方向和头结点的坐标确定的;然后,如果movenext的坐标与食物一样,表示吃到食物,直接将食物的节点头插进蛇的链表;如果movenext与食物的坐标没重合,那么就将movenext头插进链表,并将原链表的尾结点释放。
接下来说一下NextIsFood函数的实现:
int NextIsFood(pSnake ps, pSnakeNode movenext) { if (ps->_pfood->x == movenext->x && ps->_pfood->y == movenext->y) return 1; else return 0; }
如果与食物坐标重合就返回1,否则返回0.不用怕,这个函数就是很简单。
IsKill函数的实现:
void IsKill(pSnake ps) { //判断蛇头的坐标是否与坐标重合,如果重合那么就改变蛇的状态 if (ps->_psnake->x == 0 || ps->_psnake->x == 56 || ps->_psnake->y == 0 || ps->_psnake->y == 26) { ps->status = KILL_BY_WALL; return; } //判断是否要到自己 else { //要从蛇头的下一个节点开始,遍历链表 pSnakeNode cur = ps->_psnake->next; while (cur) { if (ps->_psnake->x == cur->x && ps->_psnake->y == cur->y) { ps->status = KILL_BY_SELF; return; } cur = cur->next; } } return; }
思路:判断是否撞墙,是否要咬自己,如果为真则改变蛇的相应状态,并跳出函数。
这里说一些为什么在判断是否咬到自己这种情况时,不能从蛇头开始遍历,如果是这样,则cur和ps->_psnake都指向蛇头,坐标重合,状态被修改,结果就是一进游戏你就咬到自己,所从蛇头的下一个节点开始遍历。
游戏结束GameEnd函数的实现:
这里实现逻辑比较简单,直接上代码:
void GameEnd(pSnake ps) { SetPos(30, 14); //根据蛇的状态打印信息 if (ps->status== END_NORMAL) { printf("正常退出\n"); } else if (ps->status == KILL_BY_SELF) { printf("咬到自己,死亡\n"); } else if (ps->status == KILL_BY_WALL) { printf("撞到了墙,死亡\n"); } //将开辟的空间释放 pSnakeNode cur = ps->_psnake; pSnakeNode prev = cur; while (cur) { prev = cur; cur = cur->next; free(prev); } free(ps->_pfood); }
注意:释放链表的方式,使用的是前后指针的方法,如果不太懂可以看我双链表的博客。
整个游戏逻辑的运行逻辑的实现:
void test() { char op; do { Snake s; GameStart(&s); GameRun(&s); GameEnd(&s); SetPos(30, 15); wprintf(L"%ls",L"是否再来一局?(Y/N):"); scanf(" %c", &op); } while (op == 'y' || op == 'Y'); } int main() { setlocale(LC_ALL, ""); test(); return 0; }
这里就不多说了哈,主要一点就是加入了,游戏结束你是否还要再来一局。
整个游戏的源码(有感兴趣的可以自取):
里面也有单向链表,和双向链表的源码。
这里是我的gitee仓库的链接,项目的名称为Snake追风逐梦又一天/newer_C - 码云 - 开源中国 (gitee.com)https://gitee.com/small-bit-big-dream/newer_-c
到这里就结束了,拜拜!
还没有评论,来说两句吧...