C语言——贪吃蛇 链表 (想看二维数组的请去隔壁(~▽~”)o(*~▽~*)o~)

前言

第一次写博客的小白来报道了,希望各位大佬手下留情,有不对的地方请多多指教|ू・ω・` ) 这个C语言贪吃蛇的小程序是我的一门课的作业,花了一天的时间才把这个程序弄出来,第一次体会到写BUG的快感(ಥ_ಥ) ,最后想想第一次写了这么多代码,不如把我做的过程记录下来。

环境

VS2019(千万别用Clion!!!

思路概述

在这里插入图片描述

运行效果

在这里插入图片描述

代码框架

具体函数含义在代码中均有注释

具体代码(附超详细注释,我自己第一次写的所有思路都写了下来o(一︿一+)o)

建议看完代码先瞅瞅代码后面写的,都是血的教训啊ε(┬┬﹏┬┬)3

#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#include <time.h>
#define DOWN_WALL 20//纵向长度
#define RIGHT_WALL 60//横向长度

typedef struct s_snack//定义结构体蛇的每一节身子,创建链表
{
 int x;//x坐标
 int y;//y坐标
 struct s_snack* next;//蛇的下一节身子
}Snake;

void InitSnake();
void Welcome();
void SetPos(int x, int y);
void DrawBorder();
void Point();
int Move();
int HitWall();
int BiteMyself();
void Food();
int check();
void PlayGame();
void free_snack(Snake* head);
void HideCursor();

int main() {
 //system("chcp 65001 > nul");//Clion中解决utf-8编码的方法
 HideCursor();   //消除光标(照着抄就完事了,不加就是你的蛇有点丑而已)
 Welcome();    //欢迎界面
 DrawBorder();   //画方框
 InitSnake();   //初始化蛇
 Point();    //游戏提示
 //游戏界面准备完成
 Food();     //随机生成food
 PlayGame();    //游戏运行
 //free_snack(head);  //释放内存
 SetPos(5, 23);
 return 0;
}

void SetPos(int x, int y)//控制光标的位置
{
 /*  COORD是Windows API中定义的一种结构体
 * typedef struct _COORD
 * {
 *  SHORT X;
 *  SHORT Y;
 * } COORD;
 *
 */
 COORD pos = { x, y };
 HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);//返回标准输出设备的句柄;获得输出屏幕缓冲区的句柄
 SetConsoleCursorPosition(output, pos);//BOOL SetConsoleCursorPosition(HANDLE hOut,COORD pos);
}

void Welcome()//欢迎界面
{
 SetPos(25, 6);//定义的移动光标的函数
 printf("---------这是一条贪吃蛇---------");
 SetPos(25, 9);
 printf("         作者:枫林晚霞");
 SetPos(25, 11);
 printf("游戏规则:o(≧口≦)o");
 SetPos(25, 13);
 printf("1、不能撞墙、不能咬到自己");
 SetPos(25, 15);
 printf("2、本蛇暂时不想支持自动变速,按下W加速,S减速");
 SetPos(25, 17);
 printf("2、按空格暂停/继续游戏");
 SetPos(25, 19);
 printf("3、按下Esc退出游戏");
 SetPos(25, 21);
 printf("(友情提示:请先把输入法切成英文ヽ(ー_ー)ノ)");
 printf("\n");
 SetPos(25, 23);
 system("pause");//暂停
 system("cls");//清屏
}

void DrawBorder()//绘制横向DOWN_WALL长度  纵向RIGHT_WALL长度的边框
{
 int i;
 for (i = 0; i < RIGHT_WALL + 2; i += 2) {      //绘制上下边框,(注意: 一段蛇身占用 x 2个单位, y 1个单位 )
  SetPos(i, 0);
  printf("■");
  SetPos(i, DOWN_WALL);
  printf("■");
 }
 for (i = 1; i < DOWN_WALL; i++) {      //绘制左右边框
  SetPos(0, i);
  printf("■");
  SetPos(RIGHT_WALL , i);
  printf("■");
 }
 printf("\n\n");
}

void InitSnake()//初始化蛇身,位置(20,15) 注意:结束时head为蛇头,在HitWall函数中会使用
{
 int i = 0;
 end = (Snake*)malloc(sizeof(Snake)); //动态分配内存,用来存放蛇的身子(不加的话蛇不会变长)
 end->x = 10;
 end->y = 5;
 end->next = NULL;//定义尾部指针,next为空
 for (i = 0; i < 3; i++)//开始蛇为3节
 {
  head = (Snake*)malloc(sizeof(Snake));//定义蛇头
  head->x = 10 + 2 * i;
  head->y = 5;
  head->next = end;//定义蛇身,使next为上一节身体
  end = head;//将上一节身体赋值为本节,为下一节准备
 }
 //此时end为蛇头的head
 while (end->next != NULL)
 {
  SetPos(end->x, end->y);//将光标指向end结构体中的x和y
  printf("■");//输出一节蛇身体
  end = end->next;//将end的值从蛇头向下一节蛇身传递
 }
}

void Point()//游戏旁的提示
{
 SetPos(RIGHT_WALL+3, 3);
 printf(" 得分:%d   ", score);
 SetPos(RIGHT_WALL+3, 5);
 printf(" 速度:%d毫秒/每次)", speed);
 SetPos(RIGHT_WALL+3, 7);
 printf(" 最低速600毫秒 最高速10毫秒");
 SetPos(RIGHT_WALL + 3, 13);
 printf("不能撞墙/咬到自己");
 SetPos(RIGHT_WALL + 3, 15);
 printf("按空格暂停/继续");
}

void PlayGame()//判断函数,将键盘操作存入游戏进程
{
 int feedback = 0;//定义蛇每次移动后的状态反馈变量
 key = 'R';//蛇的初始方向为向右
 while (1)
 {
  if ((GetAsyncKeyState(VK_UP) && 0x8000) && key != 'D')//GetAsyncKeyState(键值 && 0x8000)是为了防止其他按键干扰
  {
   key = 'U';           //注意:是键入向右且原方向不为向左
  }
  else if ((GetAsyncKeyState(VK_DOWN) && 0x8000) && key != 'U')//https://blog.csdn.net/six_sex/article/details/8653509 (关于此函数的解释)
  {
   key = 'D';
  }
  else if ((GetAsyncKeyState(VK_RIGHT) && 0x8000) && key != 'L')
  {
   key = 'R';
  }
  else if ((GetAsyncKeyState(VK_LEFT) && 0x8000) && key != 'R')//以上四个判断为判断键入上下左右四个键
  {
   key = 'L';
  }
  else if (GetAsyncKeyState(VK_SPACE) && 0x8000)//当键入空格时的处理方法
  {
   while (1)
   {
    Sleep(100);//消抖
    if ((GetAsyncKeyState(VK_SPACE) && 0x8000))//当再次按下空格,跳出循环,游戏继续
     break;
   }
  }
  else if ((GetAsyncKeyState(VK_ESCAPE) && 0x8000))//当按下Esc,退出游戏
  {
   return;
  }
  else if ((GetAsyncKeyState('S') && 0x8000) && speed <= 600)//'W'和'S'分别为提升10毫秒速度和降低10毫秒速度
  {
   speed += 10;
   SetPos(RIGHT_WALL + 3, 5);
   printf(" 速度:%d毫秒/每次)", speed);
  }
  else if ((GetAsyncKeyState('W') && 0x8000) && speed > 10)//最大速度为10毫秒动一次,最小速度为600毫秒动一次
  {
   speed -= 10;
   SetPos(RIGHT_WALL + 3, 5);
   printf(" 速度:%d毫秒/每次)", speed);
  }
  feedback = Move();//使反馈系数等于移动函数的返回值,在这里蛇动了Move()函数。
  if (feedback == 1)//当移动函数返回1时,游戏结束
  {
   SetPos(15, DOWN_WALL + 2);
   printf("请按Esc退出\n");
   while (1) {
    if (GetAsyncKeyState(VK_ESCAPE) && 0x8000)
     break;
   }
  }
 }
}

int Move()//蛇的移动和边长函数
{
 if ((key != 'U') && (key != 'D') && (key != 'R') && (key != 'L'))//当key为上下左右以外的值时不做移动
 {
  return 0;
 }
 if (HitWall() || BiteMyself())//判断是否撞墙或者咬到自己,两个函数均为int型,返回有1则Move函数直接返回1
 {
  return 1;
 }
 p = (Snake*)malloc(sizeof(Snake));//定义辅助指针
 p->next = head;//指向蛇头
 switch (key)//对上下左右进行移动
 {
 case 'U':
  p->y = head->y - 1;
  p->x = head->x;
  break;
 case 'D':
  p->y = head->y + 1;
  p->x = head->x;
  break;
 case 'R':
  p->x = head->x + 2;//方块占两个格
  p->y = head->y;
  break;
 case 'L':
  p->x = head->x - 2;
  p->y = head->y;
  break;
 }

 head = p;//在贪吃蛇的头部添加一个称为新的头 ,相当于是贪吃蛇移动一格 
 //如果 移动的一格刚好是食物的位置,新增的称为蛇头,不用去掉蛇尾
 //如果 移动的一格不是食物的位置,新增的称为蛇头,去掉蛇尾,就是贪吃蛇移动一格的效果 
 /*printf("■");
 Sleep(speed);*/
 //如果直接将这两句放在外面,而不是在if里面,就会造成蛇在吃苹果时
 //苹果先消失蛇长不变,然后下一步再增长一节,这个是程序执行的顺序的问题
 //而且,如果写在外面当按暂停时会造成少一格的问题
 if (p->x == foodx && p->y == foody)//当p与食物重合时
 {
  Food();//重新生成食物
  score += 10;//加分
  SetPos(RIGHT_WALL + 3, 3);
  printf(" 得分:%d   ", score);
  SetPos(p->x, p->y);
  printf("■");//只增加蛇头,不减去蛇尾
  Sleep(speed);
 }
 else {
  q = p;
  while (p->next->next != NULL)
  {
   p = p->next;
  }
  SetPos(q->x, q->y);
  printf("■");
  SetPos(p->x, p->y);//否则增加蛇头,将蛇尾输出“ ”
  printf("  ");//两个空格

  free(p->next);
  p->next = NULL;
  p = head;//释放蛇尾的空间
  Sleep(speed);
 }
 return 0;
}

int HitWall()//如果蛇头的x,y坐标等于墙的坐标,则返回1,输出“游戏结束”
{
 if (head->x == 0 || head->y == 0 || head->x == RIGHT_WALL - 2 || head->y == DOWN_WALL)//或!
 {
  SetPos(15, DOWN_WALL+1);
  printf("游戏结束!你撞到墙了( _ _)ノ|\n");
  SetPos(15, DOWN_WALL+1);//令 ‘按任意键继续...’居中显示
  return 1;
 }
 return 0;
}

int BiteMyself()//比较head和每一个p的x,y,如果相同,返回1,游戏结束
{
 p = head;//令辅助指针p为等于head
 while (p->next != NULL)//从head开始遍历整个蛇身,直到p->为NULL,即为蛇尾
 {
  p = p->next;
  if (head->x == p->x && head->y == p->y)
  {
   SetPos(15, DOWN_WALL + 1);
   printf("游戏结束!你咬到你自己了\n");
   SetPos(15, DOWN_WALL + 1);//令 “请按任意键继续”居中显示
   return 1;
  }
 }
 return 0;
}

void Food()//在这里本来是用p指针的,但是当我加上ifood=1时,蛇吃食物的过程出错,应该是指针空间分配的问题,把指针换成f即可
{

 srand(time(NULL));//播下随机数种子
 while (1)
 {
  foody = rand() % (DOWN_WALL - 1) + 1;
  foodx = rand() % (RIGHT_WALL - 4) + 3;//将随机数赋给食物坐标,范围为(1,边框-1)
  if (foodx % 2 == 0)//食物横坐标必须为偶数,因为方框为两格
  {
   break;
  }
 }
 
 while (ifood != -1) //循环检查函数,当check()返回0时继续运行,直接返回check值不行
 {
  check();
 }
 
 SetPos(foodx, foody);
 printf("☆");//显示食物
 /*SetPos(RIGHT_WALL + 3, 16);
 printf("%d,%d", foodx, foody);*/
 ifood = 1;//重置判断条件,否则只能跳过食物第一次在蛇身上的情况
}

int check()//检查食物的生成是否在蛇身上(为了避免用goto函数)
{
 f = head;
 while (f->next != NULL)//从蛇头开始检测,直到蛇尾
 {
  if (foodx == f->x && foody == f->y)//当检测出食物在蛇的某一节上时,重新赋值,并且重新循环
  {
   srand(time(NULL));
   while (1)
   {
    foody = rand() % (DOWN_WALL - 1) + 1;
    foodx = rand() % (RIGHT_WALL - 4) + 2;//将随机数赋给食物坐标,范围为(1,边框-1)
    if (foodx % 2 == 0)//食物横坐标必须为偶数,因为方框为两格
    {
     break;
    }
   }
   ifood = 1;
   return 0;
  }
  f = f->next;
 }
 if (f->next == NULL)//如果循环到蛇尾依旧没有返回,则返回0,令ifood为-1,跳出循环
  ifood = -1;
 return 0;
}

void free_snack(Snake* head)//释放资源后老是报错,所以主函数我也就没加上去,就把代码放这了
{
 if (head == NULL || head->next == NULL)
 {
  return;
 }
 
 while ((p = head) != NULL)
 {
  free(p);
  head = head->next;
 }
 
}

void HideCursor()//隐藏光标
{

 CONSOLE_CURSOR_INFO cursor_info = { 1, 0 };
 
 SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
 
}

本代码参考:https://blog.csdn.net/nanfeibuyi/article/details/82724532

注意:

1.编译器一定要用对,如果用的是老版Devc++,它的编码格式不是UTF-8,如果用CLion,那么这个代码,我感觉可以不用写了,那个不是cmd的结果输出把整个框架全乱了!!!(▼ヘ▼#)如果有大佬知道怎么调成控制台输出可以在评论说一下,我用的VS,至于VC++没用过,受不了那个界面,太丑了o(´^`)o。
2.代码中的■为两个单位,凡是进行横向操作的必须是将其乘以2,比如:食物生成的x坐标必须是偶数,在设为填充空格时需要填充两个“ ”。
3.Void函数可以有return 直接写reutrn;(不知道是否标准,有争议)
4.GetAsyncKeyState函数为无缓冲输入,即输入范围为整个程序,无论在任何位置键入参数值,均触发函数。
5.要时刻清楚指针指向的空间。
6.调试模式真的好用ヽ( ̄▽ ̄)ノ

结语

有想要源代码小伙伴可以私信我,这个代码块好烦啊,中间好像不能有空格吧,粘的累死我了。因为第一次写博客,所以不太熟,以后会慢慢写一点。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值