4.创建地图:
我们要打印的地图是由这样的正方形边框组成。
由于之前我们都学过打印空心正方体的方式,在这里,我们采取那样的方式就可以实现,但显然,在创建地图的时候我们有更重要的知识,我们应该如何理解我们的屏幕?
前面我们已经说过,我们的屏幕更像是一张二维坐标图,显然这没什么好说的,但本次我们的图形不再是基本的符号和数字了,你会从屏幕上看到,这些使用的符号都不是计算机默认的符号,我们将这样的符号统一称之为宽字符。在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节
C语言起初是不支持本地化符号的,后来为了C语言的全球推广化,针对不同的国家和地区,C语言单独封装了一个头文件库用来适配操作者所处的地区的带有本地色彩的符号,例如我们的汉字就是特殊符号。同样,在这里的三个特殊图形也是特殊符号,都以宽字节识别。我们在这里可以用setlocale函数来调整C语言本地化。
A.<locale.h>本地化
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。 在标准可以中,依赖地区的部分有以下⼏项: 数字量的格式 ,货币量的格式 , 字符集 , ⽇期和时间的表⽰形式等。
B.类项
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部 分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏, 指定⼀个类项: • LC_COLLATE
• LC_CTYPE
• LC_MONETARY
• LC_NUMERIC
• LC_TIME
• LC_ALL - 针对所有类项修改
C.setlocale函数
其函数的基本格式如下:
char setlocale (int category, const char locale);**
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参 数是LC_ALL,就会影响所有的类项。 C标准给第⼆个参数仅定义了2种可能取值:“C"和” “。其中的C代表标准模式,而“ ”则代表的本地化模式,而C语言程序默认都是以C开始,只有后续人为修改成’” “才可调整为本地化模式,在这之后的所有C语言程序皆为本地化编译方式。
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤” "作为第2个参数,调⽤setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。⽐如:切换到我们的本地模式后就⽀ 持宽字符(汉字)的输出等。这样,我们就可以宽字符的输出了。
D.宽字符的打印
宽字符类型为wchar_t,想要打印宽字符,必须加上前缀L,否则C语言会把字面量当成窄字符类型处理,前缀要加在单引号或双引号前面,无论是打印还是定义变量都要这么加前缀L,表示宽字符,其对应的打印函数也变成了wprintf,占位符不变,依旧是%c即可。若要打印字符串,则改成%s即可,其他的部分依旧遵守上面的规则。
例如:
int main() { wchar_t a=L'a'; wprintf(L"%c",a); return 0; }
有了上面知识的铺垫。让我们回到创建地图上来:
首先,我们把小方块作为墙体,但每次打小方块太麻烦了,故我对其进行预处理,让其变为WALL,同时我们决定构建一个X=58 Y=27的整个地图空间,故我们同时预处理让其分别为COLS和LINES,如下:
#define WALL L'□'//打印宽字符,定义一个墙体,这样方便后序去写 #define COLS 58 #define LINES 27
由于从{0 0}开始,所以本质上屏幕的坐标和我们学过的二维数组很像,而前面我们得知,宽字符是占有X方向每次两格位置的,这意味着我们的边界墙体要从57坐标的前一格56开始打印,否则会出现打印一半的情况,而Y方向坐标则正常,同时也提醒我们的一点是,由于宽字符的特殊性,我们每次的横向坐标移动必须是偶数坐标移动,否则没法跟我们的墙体对齐,会出现BUG。
故地图的打印程序如下:
void CreateMap()//创建地图 { Setpos(0, 0); int i = 0; for (i = 0; i < COLS; i += 2) { int j = 0; for (j = 0; j < LINES; j++) { if (i==0||j==0||i==56||j==26) { Setpos(i, j); wprintf(L"%c", WALL); } } } }
现在地图完成了,我们就可以开始打印我们的贪吃蛇了。
5.初始化贪吃蛇:
我们首先为我们的贪吃蛇设置一个出生点坐标:如下:
#define POS_X 22 #define POS_Y 6
然后,我们让蛇的初始长度为5,且蛇的每一个节点用实心圆点表示,并且吃食物会让其长度不断增加:
#define BODY L'●'//身体符号,BODY #define SIZE 5
如同蛇的每一个节点都要紧挨着并且如同有一根线一样将其串联起来,我们看到的蛇或许是一个图形,但实际在管理的时候,它其实是数据,对于线性的数据,我们这里使用单链表来最合适不过了。想一想,如何能流畅的利用坐标打印出来图形呢?即利用单链表遍历然后每次打印图形,故我们首先需要创建一个单链表,并且为其配置上我们每一个节点的坐标,具体的实现如下:
void InitSnake(pSnake snake)//初始化贪吃蛇并在出生点打印贪吃蛇 { //初始化贪吃蛇的节点有5个,故我们要创建5个蛇的身体节点,并且将蛇打印出来,同时注意,我们要对蛇结构体的成员进行初始化 pSnakeNode cur = NULL;//我们在创建和访问链表的时候一般都在前面设置一个空的指针,方便我们后序的使用 int i = 0; for (i = 0; i < SIZE; i++) { cur = (pSnakeNode)malloc(sizeof(SnakeNode)); if (cur == NULL) { perror("malloc failed"); exit(-1); } cur->next = NULL; cur->x = POS_X + i * 2; cur->y = POS_Y;//处理节点的坐标 //头插(实际上头插尾插都可以,主要是找一边为蛇头即可) if (snake->_pSnake == NULL) { snake->_pSnake = cur; } else { cur->next = snake->_pSnake; snake->_pSnake = cur; } } cur = snake->_pSnake; while (cur) { Setpos(cur->x, cur->y); wprintf(L"%c", BODY); cur = cur->next; } Setpos(50,27);//别忘了设置一下文字的位置,要不然蛇的身体会串行 //对蛇结构体进行初始化 snake->_Score = 0;//初始得分为0 snake->_FoodWeight = 10;//每吃一个星星得10分 snake->_Dir = RIGHT;//初始向右 snake->_Status = OK;//状态设置为正常运行 snake->_SleepTime = 200;//速度设置为0.2秒延时初速 }
注意这里有一个技巧,对于链表的创建和遍历而言,我们可以创建一个公共的指针cur,它不仅仅可以用来创建链表,还能用来遍历链表,故以后我们在处理这类问题的时候都可以先创建这么一个多面手方便使用。同时注意细节,我们的横坐标每次是POS_X+i*2,而不是仅仅+i,宽字符一次占两格这个问题一定要注意,很容易弄错。
构建完蛇并且打印完蛇之后,我们就要对蛇里面的数据进行一系列初始化:包括初始的分,每一个食物的初始分数,初始的方向,初始的游戏状态调整为OK,初始的速度,这样,我们的蛇就配置好了,如下:
6.创建食物:
在创建食物之前,我们首先要清楚的知道我们的食物生成点是不能跟蛇的任意位置的坐标重合的,故我们的食物要在蛇停顿的那一刻在蛇身体不在的地方生成,代码如下:
void CreateFood(pSnake snake)//设置第一个食物 { snake->_pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); if (snake->_pFood == NULL) { perror("malloc failed"); exit(-1); } int x = 25; int y = 13; again: while(x%2!=0) { x = rand() % 53 + 2;//这样控制随机数使其到不了两边边界 y = rand() % 25 + 1; } pSnakeNode cur = snake->_pSnake; while (cur) { if (cur->x == x && cur->y == y) { goto again; } cur = cur->next; } snake->_pFood->x= x; snake->_pFood->y = y; Setpos(x, y); wprintf(L"%c", STAR); Setpos(50, 27); }
首先我们要创建一个蛇节点作为来存储食物的坐标,然后利用我们熟悉的随机数创建的特点,将相应的坐标创建出来,然后遍历链表看是否有重合的点,倘若有,使用goto语句跳回上面的过程再生成随机数再向下判断,倘若没有就将i去赋给食物节点的x y成员,并且在对应的位置打印出来食物,这里最需要注意的点是:我们随机数的生成要恰好在墙体里面,也就是说,我们的食物要在X方向2到54之间 Y方向1到25之间,而且由于食物本身也是占两个,也要为其再预留出两格。
我们在这里使用星星符号来代表食物:
#define STAR L'★'//食物符号,STAR
由此,我们的初始化阶段便全部配置好了:
void GameStart(pSnake ps)//游戏第一阶段:游戏初始化阶段函数 { //控制台窗口的设置 system("mode con cols=150 lines=40");//别忘了给一些文字留空间 system("title 贪吃蛇"); //隐藏光标 HideCursor(); //打印欢迎界面 WelcomeToGame(); //创建地图 CreateMap(); //初始化贪吃蛇并在出生点打印贪吃蛇 InitSnake(ps); //设置第一个食物 CreateFood(ps); }
4.第二阶段:游戏运行阶段,进行游戏的正常运行
在本章节的第二阶段,我们将对游戏运行进行书写,包括贪吃蛇是如何移动的,我们如何将键盘和贪吃蛇的交互链接起来,使我们能够真正使用键盘控制人物移动,同时解决蛇吃食物和正常移动的判定问题,以及如何处理碰墙和吃到自己的游戏结束方式判断。
1.游戏说明打印:
和上面我们的欢迎界面一样,在这里我不多赘述,直接上代码了:
void PrintHelpInfp()//话语提示:帮助玩家如何控制方向移动,如何加速减速 { Setpos(64, 15); printf("不能穿墙,不能咬到自己\n"); Setpos(64, 16); printf("↑.↓.←.→分别控制蛇的移动.\n"); Setpos(64, 17); printf("F3 为加速,F4 为减速\n"); Setpos(64, 17); printf("ESC:退出游戏 . space:暂停游戏\n"); Setpos(64, 20); printf("xxxxxxxxxxxx制作\n"); Setpos(64, 21); printf("详细制作过程及其制作原理,源代码皆由 XXXXXX 所有 XXXXXXXXX\n"); }
2.游戏按键交互:
这应当是每一位程序学习者最为激动人心的时刻,因为终于,你通过自己的双手在自己和电脑之间构建起了一个关系,像那些大型游戏一样,至少在这里,键盘操控着蛇的移动,就好比你操控着褪色者移动,操控CT T探员移动,操控英雄移动…是的,我们在这里向着游戏世界迈出了我们交互的第一步。
在windows中为我们提供了一个用来实现键盘和缓存交互方式–-虚拟键值,即它将键盘上的每一个按键都设置为一个对应的数字,然后让计算机去识别这些数字从而确定对应的是哪个按键,从而实现对应的功能,这就是我们按键操作的基础。然后,由于按键有按下和松开两种状态,计算机需要去识别状态,故我们接下来要介绍一个函数—GetAsyncKeyState
GetAsyncKeyState函数:
函数基本格式如下:
SHORT GetAsyncKeyState( int vKey);
此函数的最大用处就是将键盘上的对应的虚拟键值作为参数传给函数,然后函数通过short形式返回来返回按键的状态,其具体的返回方式如下:
由此,让我们想想我们想怎样和键盘结合,我们按一次向上键位,倘若不改变,蛇就会一直向上走,只有当我们再按一次的时候蛇才会按照我们的意思改变方向或者我们仍然按上键位方向不变,故在这里,short的最低位就是我们最关注的,我们要判断short的最低位是否为1,从而判断蛇的方向或者暂停游戏,或者加速和减速,由此:我们可以利用按位与&1来获取这个最低位,而且,这种单一的判断方式,我们使用宏要比函数好很多,故我们这样写:
#define KEY_PRESS(VK)((GetAsyncKeyState(VK)&1)?1:0)//按键交互宏 • 1
这样,我们就得到了一个我们不按就保持原状,我们按键就会改变的键盘交互方式,故我们的第二阶段的主程序如下:
void GameRun(pSnake ps)//游戏第二阶段:游戏运行阶段函数 { //话语提示:帮助玩家如何控制方向移动,如何加速减速 PrintHelpInfp(); //统计分数,以及蛇身体的移动问题(对蛇的状态每一次按键都要进行实时统计) do { Setpos(64, 10); printf("得分:%d ", ps->_Score); printf("每个食物得分:%d", ps->_FoodWeight); 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_RIGHT) && ps->_Dir != LEFT)//输入右键 { ps->_Dir = RIGHT; } else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)//输入左键 { ps->_Dir = LEFT; } else if (KEY_PRESS(VK_SPACE))//暂停 { system("pause"); } else if (KEY_PRESS(VK_ESCAPE))//退出 { ps->_Status = END_NORMAL; } else if (KEY_PRESS(VK_F3))//F3加速 { if (ps->_SleepTime >= 50) { ps->_SleepTime -= 30; ps->_FoodWeight += 2; } } else if (KEY_PRESS(VK_F4))//F4减速 { if (ps->_SleepTime < 350) { ps->_SleepTime += 30; ps->_FoodWeight -= 2; if (ps->_SleepTime == 350) { ps->_FoodWeight = 1;//即吃每一个节点最低的得分为1分 } } } Sleep(ps->_SleepTime);//延时函数,将当前的程序停止多少秒重新进行 SnackMove(ps);//蛇移动函数,蛇吃到食物自身长度的增加,蛇碰墙,蛇碰到自己尾部,或者自身退出游戏结束运行阶段 }while (ps->_Status == OK);//根据游戏运行状态是否为OK正常状态来判断程序是否结束 }
其实,本质上,游戏画面根本不是静止的,而是不断的画面刷新使其给人感觉是静止,还有就是有些打印的位置数据存在,重复打印就会感觉画面不动,但我们的蛇由于一直在打印和尾部的打空格,故给人感觉就是蛇在移动,你可以从这组主程序中看到,我们每一次都输入一个键值,然后不断循环判断的是游戏状态,这个模板要记下来,游戏状态是一个很重要的东西,它是游戏进程的掌控者。延时函数的运用使得我们的看到的蛇的刷新率不断发生变化,从而影响了蛇的速度,这是一个很巧妙的方式。
在这里要强调一个事情,我们的蛇每次是不能移动和当前方向相反的方向的,比如向上的时候,你只能向左 上 右三个方向,而不能向后,所以我们的方向改变要加上这个一条判断!!!
3.蛇移动判断:
蛇的移动,本质上就是把下一个坐标先跟链表串联起来,然后打印出来,同时删除尾部节点,倘若吃到食物,就直接串联打印即可,这就是大致的思路,那我们怎样确定坐标呢?
在我们的主程序中,我们每次的按键都相应的改变了蛇结构体的移动方向的枚举体,这样,而我们规定蛇每次移动一步,故我们只要对相应方向位置+2格或者+1格即可,同时,创建一个下一个坐标的结构体节点,将坐标传给这个节点,然后再去判断这个节点是不是食物,倘若是就执行是的函数,反之执行正常走的指令,在进行这一步之后再去判断蛇是否撞墙或者是否吃到自己。
程序如下:
void SnackMove(pSnake snack)//蛇身移动函数 { //首先创建一个节点来存储下一步的坐标,为下面判断是否是食物,倘若是就吃食物,反之就移动不吃做铺垫 pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pNextNode == NULL) { perror("malloc failed"); exit(-1); } switch(snack->_Dir)//我们向哪里走是已经按过键盘了,在我们的运行函数里面已经写明了我们会根据按键改变方向,故我们只需要根据方向就知道应该向哪里走 { case UP: { pNextNode->x = snack->_pSnake->x; pNextNode->y = snack->_pSnake->y-1; } break; case DOWN: { pNextNode->x = snack->_pSnake->x; pNextNode->y = snack->_pSnake->y + 1; } break; case RIGHT: { pNextNode->x = snack->_pSnake->x+2;//注意,别忘了,对x操作是加2,而不是加1,因为宽字符一次占横坐标的两位 pNextNode->y = snack->_pSnake->y; } break; case LEFT: { pNextNode->x = snack->_pSnake->x-2;//注意,别忘了,对x操作是加2,而不是加1,因为宽字符一次占横坐标的两位 pNextNode->y = snack->_pSnake->y; } break; } //对下一个要走的节点是否为食物节点进行判断 if(NextIsFood(pNextNode,snack))//是食物 { EatFood(pNextNode, snack); } else//不是食物 { NoFood(pNextNode, snack); } //食物吃完后,最后再进行墙体触碰或者蛇本题是否触碰本题的函数判断: KillByWall(snack);//是否碰到墙体判断 KillBySelf(snack);//是否碰到自身判断 }
!!!我前面说过,坐标是最为关键的一点在贪吃蛇中,因为碰墙,吃自己,吃食物,这些都与坐标有关。!!!!
然后判断我们开创的下一个节点是否为食物节点:
bool NextIsFood(pSnakeNode pnext,pSnake ps)//判断下一个节点是否为食物节点 { return (pnext->x == ps->_pFood->x) && (pnext->y == ps->_pFood->y); }
倘若是,就进入吃食物函数:
void EatFood(pSnakeNode pnext, pSnake ps)//吃掉食物的函数 { //有食物就吃掉,利用头插的方法: //头插: pnext->next = ps->_pSnake; ps->_pSnake = pnext; //然后打印出新的贪吃蛇 pSnakeNode cur = ps->_pSnake; while (cur) { Setpos(cur->x, cur->y); wprintf(L"%c", BODY); cur = cur->next; } //加上得分: ps->_Score += ps->_FoodWeight; //释放原食物节点,创造新的食物节点 free(ps->_pFood); CreateFood(ps); }
注意,别忘了释放之前存在的食物节点,然后使用前面的函数再创建一个食物节点
倘若不是:就进入不吃食物节点:
void NoFood(pSnakeNode pnext, pSnake ps)//不吃食物的函数 { //先头插 pnext->next = ps->_pSnake; ps->_pSnake = pnext; //然后打印出新的贪吃蛇 //直接放弃掉最后一个节点,反正我们的操作都是针对头插,对尾部无要求 pSnakeNode cur = ps->_pSnake; while (cur->next->next) { Setpos(cur->x, cur->y); wprintf(L"%c", BODY); cur = cur->next; } //对尾部节点直接打印空格并且释放对应的堆区空间(注意看我们的堆区空间都是要及时释放的,这里就是一个很好的例子,及时的释放了堆区的内存) Setpos(cur->next->x, cur->next->y); printf(" ");//注意这里要打印两格子的空行而不是一格子,否则会显示一半,这样整体的判断就会出问题 free(cur->next); cur->next = NULL; }
我们的操作就是加一个节点给蛇,同时将最后一个节点的位置打印空格并且要释放掉,这里要强调很关键的一件事,我们的空格必须是空两格的,因为横向移动的时候一次走两个而不是一格,一旦这里处理错误就会出现蛇半个点移动的bug,这里是一定要注意的!!!
然后对蛇是否碰到墙体判断:
void KillByWall(pSnake ps)//是否碰到墙体判断 { if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26) { ps->_Status = KILL_BY_WALL; } }
对蛇是否吃到自身判断:
void KillBySelf(pSnake ps)//是否碰到自身判断 { pSnakeNode cur = ps->_pSnake->next;//注意,这里要从头节点的第二位开始,否则头节点和头节点必定坐标相同,这样一开始就判定蛇吃自己了,就出现bug了,必须是蛇吃到了除去它头节点之外的其他节点判定为蛇吃了自身 while (cur->next) { if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y) { ps->_Status = KILL_BY_SELF; } cur = cur->next; } }
这里最要强调的一点:要从头节点的第二位开始,否则头节点和头节点必定坐标相同,这样一开始就判定蛇吃自己了,就出现bug了,必须是蛇吃到了除去它头节点之外的其他节点判定为蛇吃了自身,这个很关键,我在这里思考了很长时间,由于我们操控的是头节点,故我们不可能碰到自己,故我们要从头节点的下一个节点开始!!!
5.第三阶段:游戏结束判断——内存资源清理
这里就很简单了,承接着上一步第二阶段主函数结束的原因,我们分别针对其游戏结束状态返回对应的游戏结束结果即可,由于我们动态开辟了蛇,故我们不要忘了在最后要将堆区的内存资源清理释放掉,养成好习惯,也是为了放置内存泄漏的出现!!!
代码如下:
void GameEnd(pSnake ps)//游戏第三阶段:游戏结束方式的判定以及游戏结束的内存释放和资源清理 { //对结束条件进行判定 switch(ps->_Status) { case END_NORMAL: { Setpos(20, 27); printf("您正常退出游戏,期待您的下一次游戏!\n"); break; } case KILL_BY_WALL: { Setpos(20, 27); printf("您撞墙了,多加练习!\n"); break; } case KILL_BY_SELF: { Setpos(20, 27); printf("您自己吃掉了您自己,下次可别犯这样的错误了!\n"); break; } } //内存清理释放 pSnakeNode cur = ps->_pSnake; while (cur) { pSnakeNode prev = cur; cur = cur->next; free(prev); } cur = NULL; }
总结:
以上就是贪吃蛇的全部实现过程了,按理来说,它应该是我学习以来的第一款实现了交互的游戏,但我收获很多,如何和按键交互,如何控制移动,游戏状态的重要性,API的意义,我想,不管是多么复杂的大型游戏,都应当是按照这种大致的思路进行的,后面有时间的话,我会尝试制作坦克大战,俄罗斯方块,以及如何实现双人游戏(非网络版本,单纯键盘两人操控)。
我曾经操控着无数个他人创造的人物,但现在,我知道,终有一天,我会操控我自己的人物走在我自己的地图上,书写的我自己的故事!!!!!
源代码在下面,想要的可以自取:
sanke.h文件:
#pragma once #include<locale.h> #include<stdio.h> #include<stdlib.h> #include<stdbool.h> #include<assert.h> #include<windows.h> #include<time.h> #define WALL L'□'//打印宽字符,定义一个墙体,这样方便后序去写 #define BODY L'●'//身体符号,BODY #define STAR L'★'//食物符号,STAR #define COLS 58 #define LINES 27 #define SIZE 5 #define POS_X 22 #define POS_Y 6 #define KEY_PRESS(VK)((GetAsyncKeyState(VK)&1)?1:0)//按键交互宏 enum DIRECTION//蛇移动方向枚举体 { UP=1, DOWN, LEFT, RIGHT }; enum GAME_STATUS//游戏状态枚举体 { OK,//游戏正常运行 END_NORMAL,//正常退出 KILL_BY_WALL,//撞墙 KILL_BY_SELF//自己吃自己了 }; //贪吃蛇单个节点的结构体 typedef struct SnakeNode { //描述蛇身节点的坐标 int x; int y; struct SnakeNode* next; }SnakeNode,*pSnakeNode;//*pSnakeNode为这个结构体的指针重命名 //贪吃蛇个体的结构体 //我们整个游戏要控制的是蛇 typedef struct Snake { pSnakeNode _pSnake;//指向贪吃蛇头节点的指针,我们在游戏过程中是操控蛇头进行游戏,利用蛇头来判定游戏状态,故控制蛇头很关键 pSnakeNode _pFood;//指向食物的节点,本质上食物被蛇吃了后也算蛇的节点的一部分,故我们也用一个指针来管理 int _Score;//累计的得分 int _FoodWeight;//吃一个食物的分数 int _SleepTime;//蛇的速度,本质上为一个延时函数,休眠的时间越长速度越慢,反之速度越快 enum DIRECTION _Dir;//描述蛇的方向,由于蛇的方向固定,且每次只能按一个按键,故我们利用枚举体来处理,未来的Dir取值只可能是4个方向中的一种 enum GAME_STATUS _Status;//游戏状态,判断蛇是撞墙还是自己吃到了自己还是按ESC键退出了 }Snake,*pSnake; //--------------------------------------------------------------- void GameStart(pSnake ps);//游戏第一阶段:游戏初始化阶段函数 void HideCursor();//隐藏光标 void Setpos(int x, int y);//光标位置调整 void WelcomeToGme();//打印欢迎界面 void CreateMap();//创建地图 void InitSnake(pSnake snake);//初始化蛇并在出生点打印出蛇 void CreateFood(pSnake snake);//设置第一个食物 //--------------------------------------------------------------- void GameRun(pSnake ps);//游戏第二阶段:游戏运行阶段函数 void PrintHelpInfp();//话语提示:帮助玩家如何控制方向移动,如何加速减速 void SnackMove(pSnake snack);//蛇身移动函数 bool NextIsFood(pSnakeNode pnext, pSnake ps);//判断下一个节点是否为食物节点 void EatFood(pSnakeNode pnext, pSnake ps);//吃掉食物的函数 void NoFood(pSnakeNode pnext, pSnake ps);//不吃食物的函数 void KillByWall(pSnake ps);//是否碰到墙体判断 void KillBySelf(pSnake ps);//是否碰到自身判断 //--------------------------------------------------------------- void GameEnd(pSnake ps);//游戏第三阶段:游戏结束的内存释放和资源清理以及游戏结束方式的判定
snack.c文件
#define _CRT_SECURE_NO_WARNINGS 1 #include"snack.h" //阶段一:游戏初始化阶段 //------------------------------------------------------------------------------- void HideCursor()//隐藏光标 { HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);// 获取输出设备句柄 CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(houtput, &CursorInfo);//获取光标属性信息 CursorInfo.bVisible = false;//调整属性隐藏光标 SetConsoleCursorInfo(houtput, &CursorInfo);//重置光标属性 } void Setpos(int x, int y)//光标位置调整 { HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos = { x,y }; SetConsoleCursorPosition(houtput, pos); } void WelcomeToGame()//打印欢迎界面 { Setpos(40, 15); printf("欢迎来到贪吃蛇小游戏!"); Setpos(40, 25);//让任意继续出现在别的位置,注意pause本质上也是要打印在屏幕上的,故也要调整位置 system("pause");//按任意键继续 system("cls");//刷新界面 Setpos(25, 12); printf(" ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n"); Setpos(25, 13); printf("加速将能得到更多的分数。\n"); Setpos(40, 25); system("pause"); system("cls"); } void CreateMap()//创建地图 { Setpos(0, 0); int i = 0; for (i = 0; i < COLS; i += 2) { int j = 0; for (j = 0; j < LINES; j++) { if (i==0||j==0||i==56||j==26) { Setpos(i, j); wprintf(L"%c", WALL); } } } } void InitSnake(pSnake snake)//初始化贪吃蛇并在出生点打印贪吃蛇 { //初始化贪吃蛇的节点有5个,故我们要创建5个蛇的身体节点,并且将蛇打印出来,同时注意,我们要对蛇结构体的成员进行初始化 pSnakeNode cur = NULL;//我们在创建和访问链表的时候一般都在前面设置一个空的指针,方便我们后序的使用 int i = 0; for (i = 0; i < SIZE; i++) { cur = (pSnakeNode)malloc(sizeof(SnakeNode)); if (cur == NULL) { perror("malloc failed"); exit(-1); } cur->next = NULL; cur->x = POS_X + i * 2; cur->y = POS_Y;//处理节点的坐标 //头插(实际上头插尾插都可以,主要是找一边为蛇头即可) if (snake->_pSnake == NULL) { snake->_pSnake = cur; } else { cur->next = snake->_pSnake; snake->_pSnake = cur; } } cur = snake->_pSnake; while (cur) { Setpos(cur->x, cur->y); wprintf(L"%c", BODY); cur = cur->next; } Setpos(50,27);//别忘了设置一下文字的位置,要不然蛇的身体会串行 //对蛇结构体进行初始化 snake->_Score = 0;//初始得分为0 snake->_FoodWeight = 10;//每吃一个星星得10分 snake->_Dir = RIGHT;//初始向右 snake->_Status = OK;//状态设置为正常运行 snake->_SleepTime = 200;//速度设置为0.2秒延时初速 } void CreateFood(pSnake snake)//设置第一个食物 { snake->_pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); if (snake->_pFood == NULL) { perror("malloc failed"); exit(-1); } int x = 25; int y = 13; again: while(x%2!=0) { x = rand() % 53 + 2;//这样控制随机数使其到不了两边边界 y = rand() % 25 + 1; } pSnakeNode cur = snake->_pSnake; while (cur) { if (cur->x == x && cur->y == y) { goto again; } cur = cur->next; } snake->_pFood->x= x; snake->_pFood->y = y; Setpos(x, y); wprintf(L"%c", STAR); Setpos(50, 27); } void GameStart(pSnake ps)//游戏第一阶段:游戏初始化阶段函数 { //控制台窗口的设置 system("mode con cols=150 lines=40");//别忘了给一些文字留空间 system("title 贪吃蛇"); //隐藏光标 HideCursor(); //打印欢迎界面 WelcomeToGame(); //创建地图 CreateMap(); //初始化贪吃蛇并在出生点打印贪吃蛇 InitSnake(ps); //设置第一个食物 CreateFood(ps); } //--------------------------------------------------------------------------------------- //阶段二:游戏运行阶段 void PrintHelpInfp()//话语提示:帮助玩家如何控制方向移动,如何加速减速 { Setpos(64, 15); printf("不能穿墙,不能咬到自己\n"); Setpos(64, 16); printf("↑.↓.←.→分别控制蛇的移动.\n"); Setpos(64, 17); printf("F3 为加速,F4 为减速\n"); Setpos(64, 17); printf("ESC:退出游戏 . space:暂停游戏\n"); Setpos(64, 20); printf("XXXXXXXXXXX制作\n"); Setpos(64, 21); printf("详细制作过程及其制作原理,源代码皆由 XXXXXXX 所有 XXXXXXXXX\n"); } bool NextIsFood(pSnakeNode pnext,pSnake ps)//判断下一个节点是否为食物节点 { return (pnext->x == ps->_pFood->x) && (pnext->y == ps->_pFood->y); } void EatFood(pSnakeNode pnext, pSnake ps)//吃掉食物的函数 { //有食物就吃掉,利用头插的方法: //头插: pnext->next = ps->_pSnake; ps->_pSnake = pnext; //然后打印出新的贪吃蛇 pSnakeNode cur = ps->_pSnake; while (cur) { Setpos(cur->x, cur->y); wprintf(L"%c", BODY); cur = cur->next; } //加上得分: ps->_Score += ps->_FoodWeight; //释放原食物节点,创造新的食物节点 free(ps->_pFood); CreateFood(ps); } void NoFood(pSnakeNode pnext, pSnake ps)//不吃食物的函数 { //先头插 pnext->next = ps->_pSnake; ps->_pSnake = pnext; //然后打印出新的贪吃蛇 //直接放弃掉最后一个节点,反正我们的操作都是针对头插,对尾部无要求 pSnakeNode cur = ps->_pSnake; while (cur->next->next) { Setpos(cur->x, cur->y); wprintf(L"%c", BODY); cur = cur->next; } //对尾部节点直接打印空格并且释放对应的堆区空间(注意看我们的堆区空间都是要及时释放的,这里就是一个很好的例子,及时的释放了堆区的内存) Setpos(cur->next->x, cur->next->y); printf(" ");//注意这里要打印两格子的空行而不是一格子,否则会显示一半,这样整体的判断就会出问题 free(cur->next); cur->next = NULL; } void KillByWall(pSnake ps)//是否碰到墙体判断 { if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26) { ps->_Status = KILL_BY_WALL; } } void KillBySelf(pSnake ps)//是否碰到自身判断 { pSnakeNode cur = ps->_pSnake->next;//注意,这里要从头节点的第二位开始,否则头节点和头节点必定坐标相同,这样一开始就判定蛇吃自己了,就出现bug了,必须是蛇吃到了除去它头节点之外的其他节点判定为蛇吃了自身 while (cur->next) { if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y) { ps->_Status = KILL_BY_SELF; } cur = cur->next; } } void SnackMove(pSnake snack)//蛇身移动函数 { //首先创建一个节点来存储下一步的坐标,为下面判断是否是食物,倘若是就吃食物,反之就移动不吃做铺垫 pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pNextNode == NULL) { perror("malloc failed"); exit(-1); } switch(snack->_Dir)//我们向哪里走是已经按过键盘了,在我们的运行函数里面已经写明了我们会根据按键改变方向,故我们只需要根据方向就知道应该向哪里走 { case UP: { pNextNode->x = snack->_pSnake->x; pNextNode->y = snack->_pSnake->y-1; } break; case DOWN: { pNextNode->x = snack->_pSnake->x; pNextNode->y = snack->_pSnake->y + 1; } break; case RIGHT: { pNextNode->x = snack->_pSnake->x+2;//注意,别忘了,对x操作是加2,而不是加1,因为宽字符一次占横坐标的两位 pNextNode->y = snack->_pSnake->y; } break; case LEFT: { pNextNode->x = snack->_pSnake->x-2;//注意,别忘了,对x操作是加2,而不是加1,因为宽字符一次占横坐标的两位 pNextNode->y = snack->_pSnake->y; } break; } //对下一个要走的节点是否为食物节点进行判断 if(NextIsFood(pNextNode,snack))//是食物 { EatFood(pNextNode, snack); } else//不是食物 { NoFood(pNextNode, snack); } //食物吃完后,最后再进行墙体触碰或者蛇本题是否触碰本题的函数判断: KillByWall(snack);//是否碰到墙体判断 KillBySelf(snack);//是否碰到自身判断 } void GameRun(pSnake ps)//游戏第二阶段:游戏运行阶段函数 { //话语提示:帮助玩家如何控制方向移动,如何加速减速 PrintHelpInfp(); //统计分数,以及蛇身体的移动问题(对蛇的状态每一次按键都要进行实时统计) do { Setpos(64, 10); printf("得分:%d ", ps->_Score); printf("每个食物得分:%d", ps->_FoodWeight); 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_RIGHT) && ps->_Dir != LEFT)//输入右键 { ps->_Dir = RIGHT; } else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)//输入左键 { ps->_Dir = LEFT; } else if (KEY_PRESS(VK_SPACE))//暂停 { system("pause"); } else if (KEY_PRESS(VK_ESCAPE))//退出 { ps->_Status = END_NORMAL; } else if (KEY_PRESS(VK_F3))//F3加速 { if (ps->_SleepTime >= 50) { ps->_SleepTime -= 30; ps->_FoodWeight += 2; } } else if (KEY_PRESS(VK_F4))//F4减速 { if (ps->_SleepTime < 350) { ps->_SleepTime += 30; ps->_FoodWeight -= 2; if (ps->_SleepTime == 350) { ps->_FoodWeight = 1;//即吃每一个节点最低的得分为1分 } } } Sleep(ps->_SleepTime);//延时函数,将当前的程序停止多少秒重新进行 SnackMove(ps);//蛇移动函数,蛇吃到食物自身长度的增加,蛇碰墙,蛇碰到自己尾部,或者自身退出游戏结束运行阶段 }while (ps->_Status == OK);//根据游戏运行状态是否为OK正常状态来判断程序是否结束 } //--------------------------------------------------------------------------------------- void GameEnd(pSnake ps)//游戏第三阶段:游戏结束方式的判定以及游戏结束的内存释放和资源清理 { //对结束条件进行判定 switch(ps->_Status) { case END_NORMAL: { Setpos(20, 27); printf("您正常退出游戏,期待您的下一次游戏!\n"); break; } case KILL_BY_WALL: { Setpos(20, 27); printf("您撞墙了,多加练习!\n"); break; } case KILL_BY_SELF: { Setpos(20, 27); printf("您自己吃掉了您自己,下次可别犯这样的错误了!\n"); break; } } //内存清理释放 pSnakeNode cur = ps->_pSnake; while (cur) { pSnakeNode prev = cur; cur = cur->next; free(prev); } cur = NULL; }
test.c文件:
#define _CRT_SECURE_NO_WARNINGS 1 #include"snack.h" void game() { Snake snake = { 0 };//首先创建一条蛇 //贪吃蛇游戏大概分为三个部分进行,故我们分为三个大函数来进行 //1.游戏开始--初始化游戏的过程,将蛇自身结构体里面的数据进行初始化,地图的构造,蛇的出生点设置以及蛇的打印,游戏界面的生成等 GameStart(&snake);//传地址直接改变实参 //2.游戏运行--游戏的正常运行过程 GameRun(&snake); //3.游戏结束--对游戏中的一些占用内存的资源进行释放回收和清理 GameEnd(&snake); } int main() { int a = 0; do { srand(time(NULL)); setlocale(LC_ALL, "");//设置为本地环境,注意配置本地化环境第二个参数中间不空格 game(); printf("您是否继续游戏?,倘若继续输入1,不继续就输入0:>"); scanf("%d", &a); } while (a); return 0; }