【C语言】实践:贪吃蛇小游戏(附源码)(二)

简介: 【C语言】实践:贪吃蛇小游戏(附源码)

【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);
}

接下来就是游戏欢迎界面的打印,这里中间会用到 pausecls(清理屏幕) 指令

//欢迎界面打印
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

相关文章
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
137 9
|
2月前
|
存储 算法 C语言
通义灵码在考研C语言和数据结构中的应用实践 1-5
通义灵码在考研C语言和数据结构中的应用实践,体验通义灵码的强大思路。《趣学C语言和数据结构100例》精选了五个经典问题及其解决方案,包括求最大公约数和最小公倍数、统计字符类型、求特殊数列和、计算阶乘和双阶乘、以及求斐波那契数列的前20项和。通过这些实例,帮助读者掌握C语言的基本语法和常用算法,提升编程能力。
73 4
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
67 16
|
28天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
78 8
|
28天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
74 7
|
27天前
|
C语言 Windows
C语言课设项目之2048游戏源码
C语言课设项目之2048游戏源码,可作为课程设计项目参考,代码有详细的注释,另外编译可运行文件也已经打包,windows电脑双击即可运行效果
32 1
|
1月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
107 8
|
1月前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
62 4
|
1月前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
65 3
|
2月前
|
C语言 C++
【C语言】编写“猜数字“小游戏
【C语言】编写“猜数字“小游戏