【C语言】实践:贪吃蛇小游戏(附源码)(一)https://developer.aliyun.com/article/1621352
四、游戏逻辑实现
程序开始就设置程序本地化,然后就进入到游戏的主逻辑当中
根据游戏大概分析,游戏可以分为三个阶段
阶段一:游戏开始 --- 完成游戏的初始化
阶段二:游戏运行 --- 完成游戏运行逻辑的实现
阶段三:游戏结束 --- 完成游戏结束的说明,实现资源释放
当然,这里我们玩完一局游戏后,可以选择继续或者结束(这里就以输入Y/N来判断游戏是否继续运行)
这里我们在测试test.c文件开始就让程序本地化
void test() { Snake snake = { 0 }; int ch = 0; do { ch = 0; system("cls"); //游戏初始化 GameStart(&snake); //游戏运行 GameRun(&snake); //游戏结束 GameOver(&snake); KeyFun(); SetPos(30, 20); wprintf(L"再来一局吗? (Y/N)"); ch = getchar(); while (getchar() != '\n'); } while (ch == 'Y' || ch == 'y'); SetPos(0, 27); } int main() { //本地化 setlocale(LC_ALL, ""); srand((unsigned int)time(NULL)); test(); //KeyFun(); return 0; }
测试大概框架就是这样,接下来就分别来实现这些框架的内容
4.1 游戏开始(GameStart)
1. 设置控制台大小和名字
这里设置控制台大小,100列,33行;设置控制台名称为:贪吃蛇
//设置窗口名称大小 system("title 贪吃蛇"); system("mode con cols=100 lines=33");
2. 隐藏屏幕光标
隐藏屏幕光标,这里就用到了前面Win32 API的知识
//隐藏光标 HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(houtput, &CursorInfo); //获得有关指定控制台屏幕缓冲区的光标大小和可见的信息 CursorInfo.bVisible = false; //隐藏控制台光标 SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态
3. 打印欢迎界面
输出欢迎界面,这里分装成函数WelcomeToGame
我们观察欢迎界面,可以发现这里并不是在坐标为(0,0)处打印的,这里就要用到设置光标位置(这里单独写一个函数,设置光标位置)
设置光标位置
//设置光标位置 void SetPos(int x, int y) { HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos = { x,y }; SetConsoleCursorPosition(houtput, pos); }
接下来就是游戏欢迎界面的打印,这里中间会用到 pause 和 cls(清理屏幕) 指令
//欢迎界面打印 void WelcomeToGame() { //设置光标位置 SetPos(40, 15); printf("欢迎进入贪吃蛇小游戏\n"); SetPos(42, 20); system("pause"); system("cls");//清理屏幕 SetPos(20, 11); printf("请使用↑ 、 ↓ 、 ← 、 → 来控制贪吃蛇的移动,按F3加速、F4减速 "); SetPos(20, 13); printf("加速可以获得更多的分数"); SetPos(20, 15); system("pause"); system("cls"); //清理屏幕 }
这样就可以实现预期效果图那样了,接下来就是绘制我们贪吃蛇游戏的地图了。
4. 绘制地图
这里我们使用宽字符来打印地图,先来看一下预期效果
我们把地图分为上、下、左、右这四个部分,这样我们只需依次打印这些宽字符就可以了
//地图绘制 void CreatMap() { //上 int i = 0; for (i = 0; i < 29; i++) { wprintf(L"%lc", WALL); } //下 SetPos(0, 26); for(i = 0; i < 29; i++) { wprintf(L"%lc", WALL); } //左 for (i = 1; i < 26; i++) { SetPos(0, i); wprintf(L"%lc", WALL); } //右 for (i = 1; i < 26; i++) { SetPos(56, i); wprintf(L"%lc", WALL); } }
5. 初始化贪吃蛇
初始化贪吃蛇,也是创建贪吃蛇,贪吃蛇身体这里其实就是一个链表,里面存放着每个节点的坐标
初始化贪吃蛇也要给上一些初始数据
初始长度为 -- 5
初始方向 -- 向右(RIGHT)
初始状态 -- 正常(OK)
每个食物得分 -- 10
初始总分 -- 0
初始速度 -- 这里设定眠时间为200毫秒
初始蛇的位置 -- 这里就随机生成(也可以指定)
当然初始指向食物的指针置为NULL(因为这里还未创建食物)
//创建贪吃蛇 void InitSnake(pSnake ps) { //创建蛇的身体 pSnakenode pcur = NULL; int i = 0; int x, y;//蛇初始位置 do { x = rand() % 31 + 4; //x: 4 - 34 y = rand() % 20 + 2; //y: 1 - 25 } while (x % 2 != 0); for (i = 0; i < 5; i++) { pcur = (pSnakenode)malloc(sizeof(Snakenode)); if (pcur == NULL) { perror("InitSnake()::malloc()"); return; } pcur->next = NULL; //pcur->x = SNAKE_X + i * 2; //pcur->y = SNAKE_Y; pcur->x = x + i * 2; pcur->y = y; //头插到贪吃蛇链表中 if (ps->psnake == NULL) //链表为空 { ps->psnake = pcur; } else { pcur->next = ps->psnake; ps->psnake = pcur; } } //输出蛇的初始位置 pcur = ps->psnake; while (pcur) { SetPos(pcur->x, pcur->y); wprintf(L"%lc", SNAKENODE); pcur = pcur->next; } //初始化贪吃蛇的信息 ps->dir = RIGHT; //蛇的方向 ps->pfood = NULL; //指向食物 --NULL ps->state = OK; //状态 ps->food_scores = 10; //每个食物的得分 ps->all_scores = 0; //总分 ps->sleep_time = 200;//速度,即休息时间 单位是毫秒 //getchar(); }
6. 创建食物
创建完贪吃蛇,接下来就是创建食物了,其实食物和贪吃蛇身体节点一样,都存放着坐标;所以这里就创建一个结构体,再随机生成坐标
这里需要注意:
坐标x必须是偶数
坐标必须在地图内
生成食物的坐标不能与蛇的身体重复
//创建食物 void CreatFood(pSnake ps) { int x, y;//随机生成坐标 x , y //x 2-54 //y 1-25 again: do { x = rand() % 53 + 2; y = rand() % 25 + 1; } while (x % 2 != 0); //x y 不能与贪吃蛇身体重复 pSnakenode pcur = ps->psnake; while (pcur) { if (x == pcur->x && y == pcur->y) { goto again; } pcur = pcur->next; } pSnakenode food = (pSnakenode)malloc(sizeof(pSnakenode)); if (food == NULL) { perror("CreatFood()::malloc"); return; } food->x = x; food->y = y; food->next = NULL; ps->pfood = food; SetPos(x, y); wprintf(L"%lc", FOOD); //getchar(); }
这样我们的初始化就完成了
4.2 游戏运行(GameRun)
1.输出右侧提示信息和分数详情
看预期效果图,我们在地图的右侧输出一些提示信息,并且输出当前得分详情
void Printgame(pSnake ps) { SetPos(60, 15); printf("请使用↑ 、 ↓ 、 ← 、 → 来控制贪吃蛇"); SetPos(60, 16); printf("按F3加速、F4减速 "); SetPos(60, 18); printf("加速可以获得更高的分数 "); SetPos(60, 20); printf("ESC:退出游戏 space:暂停 "); SetPos(60, 10); printf("当前总得分:%d", ps->all_scores); SetPos(60, 12); printf("当前每个食物得分:%d", ps->food_scores); SetPos(60, 22); printf("努力学习的小廉"); }
2. 获取按键情况
现在就要获取我们键盘按键的信息了,这里写一个宏来判断按键是否被按过
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
这里把获取按键信息直接写到游戏运行这个函数内,顺便看一下游戏运行都需要实现哪些东西?
//游戏运行 void GameRun(pSnake ps) { do { Printgame(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_SPACE)) //空格 -- 暂停 { Pause(); } else if (KEY_PRESS(VK_ESCAPE)) //游戏正常退出 { ps->state = NORMAL_END; break; } else if (KEY_PRESS(VK_F3)) { if (ps->sleep_time >= 100) { ps->sleep_time -= 50; ps->food_scores += 5;//设定食物分数最高25 } } else if (KEY_PRESS(VK_F4)) { if (ps->sleep_time < 300) { ps->sleep_time += 100; ps->food_scores -= 5;//⼀个⻝物分数最低是5分 } } Sleep(ps->sleep_time); //贪吃蛇的移动 SnakeMove(ps); //判断贪吃蛇是否撞墙 KillByWall(ps); //判断贪吃蛇是否撞到自己 KillBySelf(ps); } while (ps->state == OK); }
这里当游戏状态不是正常运行时,就结束了循环(即游戏结束)
3. 贪吃蛇移动
看上述游戏运行代码,可以看到贪吃蛇的移动还有判断蛇是否撞到墙和自己,这些的实现在贪吃蛇移动当中。
1> 蛇身的移动
蛇身的移动,其实就是根据当前蛇的方向,找到下一个节点,再判断下一个节点是否是食物,和判断是否撞到墙和自己
2> 判断是否吃到食物
判断蛇的下一个节点是否是食物,就是判断下一个位置的坐标和实物的坐标是否重复
如果重复,就让蛇身变长一节,如果不是,就让蛇往前走
这里蛇移动还有一些知识,就是直接为蛇下一个位置创建一个新的节点
再判断下一个位置是否是食物,如果是就将节点头插到蛇身链表中,不删除尾节点;如果不是就直接将节点头插到蛇身链表中,删除尾节点(这里还需在蛇的尾部输出两个空格" ")
//下一个位置是食物 void IsFood(pSnakenode next, pSnake ps) { //把下一个位置的节点头插到贪吃蛇中 next->next = ps->psnake; ps->psnake = next; //打印贪吃蛇 pSnakenode cur = ps->psnake; while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", SNAKENODE); cur = cur->next; } ps->all_scores += ps->food_scores; CreatFood(ps); //SetPos(ps->pfood->x, ps->pfood->y); //wprintf(L"%lc", FOOD); } //下一个位置不是食物 void NoFood(pSnakenode next, pSnake ps) { //把下一个位置的节点头插到贪吃蛇中 next->next = ps->psnake; ps->psnake = next; pSnakenode cur = ps->psnake; while (cur->next->next != NULL) { SetPos(cur->x, cur->y); wprintf(L"%lc", SNAKENODE); cur = cur->next; } SetPos(cur->next->x, cur->next->y); wprintf(L"%ls", L" "); free(cur->next); cur->next = NULL; } //贪吃蛇的移动 void SnakeMove(pSnake ps) { pSnakenode next = (pSnakenode)malloc(sizeof(Snakenode)); if (next == NULL) { perror("SnakeMove():malloc()"); exit(1); } switch (ps->dir) { case UP: next->x = ps->psnake->x; next->y = ps->psnake->y - 1; break; case DOWN: next->x = ps->psnake->x; next->y = ps->psnake->y + 1; break; case LEFT: next->x = ps->psnake->x - 2; next->y = ps->psnake->y; break; case RIGHT: next->x = ps->psnake->x + 2; next->y = ps->psnake->y; break; } //判断下一个位置是不是食物 if (NextIsFood(next, ps)) { IsFood(next, ps); } else { NoFood(next, ps); } }
3> 判断是否撞到墙和自己
判断蛇是否撞墙,就是判断蛇身节点的坐标是否超出地图的范围
//判断贪吃蛇是否撞墙 void KillByWall(pSnake ps) { if (ps->psnake->x == 0 || ps->psnake->x == 56 || ps->psnake->y == 0 || ps->psnake->y == 26) ps->state = KILL_WALL; }
判断蛇是否撞到自己,就遍历链表,判断蛇身的头结点是否和身体其他节点重复
//判断贪吃蛇是否撞到自己 void KillBySelf(pSnake ps) { pSnakenode pcur = ps->psnake->next; while (pcur) { if (pcur->x == ps->psnake->x && pcur->y == ps->psnake->y) { ps->state = KILL_SELF; break; } pcur = pcur->next; } }
到这里,代码就已经实现的差不多了,接下来就是游戏结束后的一些善后工作
4.3 游戏结束(GameOver)
1. 打印游戏结束的原因
游戏结束,打印出来游戏结束的原因,是撞到墙了呢?还是撞到自己了呢?还是按Esc正常退出了呢?
2. 释放蛇身节点
因为我们蛇身的节点是动态申请的内存,我们需要手动释放掉内存(养成好习惯!)
//游戏结束 void GameOver(pSnake ps) { SetPos(8, 12); switch(ps->state) { case KILL_WALL: wprintf(L"Sorry,game over because you hit the wall !\n"); break; case KILL_SELF: wprintf(L"Sorry,game over because you hit youself !\n"); break; case NORMAL_END: wprintf(L"Game exits normally !"); break; } //释放贪吃蛇的节点内存 pSnakenode pcur = ps->psnake; while (pcur) { pSnakenode del = pcur; pcur = pcur->next; free(del); } ps->psnake = NULL; }
到这里我们游戏的代码就已经全部实现了。
【C语言】实践:贪吃蛇小游戏(附源码)(三)https://developer.aliyun.com/article/1621362