C语言项目实战:贪吃蛇

简介: C语言项目实战:贪吃蛇

一、游戏背景

大家都玩过贪吃蛇吧,和扫雷,俄罗斯方块一样,作为世界上最负盛名且历史最为悠久的游戏之一,可以说,它几乎成为了人类游戏史上经典之作之一。程序是游戏的载体,那当我们已经掌握了C语言的很多知识之后,我们能否实现一个贪吃蛇游戏呢?下面让我们从无到有,来完整的书写一个C语言为基础的贪吃蛇游戏。


二、游戏效果展示


三、游戏基本功能


  • 贪吃蛇地图绘制
  • 蛇吃食物的功能
  • 键盘上、下、左、右控制蛇的移动
  • 蛇撞墙、撞自身死亡
  • 计算分数
  • 蛇行动加速和减速
  • 暂停游戏


如果想要扩展其他功能,如双人、多人、联网、不同地图等等趣味性玩法,可自行在此基础上实现,在此不做过多展示。

四、控制台设置

如果win11的控制台窗口是这样显示的,需要进行一定的调整,不然很多命令无法在终端上显示


操作如下所示:

点击窗口旁边向下的箭头,点击设置,切换为widows控制主机,点击保存,就可以关闭设置界面了。


设置之后的效果如下:


五、Win32 API技术要点

我们需要操控控制台面板,需要用到一些Win32 API的知识。


5.1 Win32 API

Windows 这个多作业系统是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启 视窗、描绘图形、使⽤周边设备等⽬的。这些函数也被简称为API函数


5.2 控制台程序

平常我们运⾏起来的⿊框程序其实就是控制台程序,能在控制台窗⼝执⾏的命令,也可以调⽤C语⾔函数system来执⾏。


5.2.1 mode命令

我们可以用mode命令来设置控制台窗口的长和宽

1. //设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
2. system("mode con cols=100 lines=30");


5.2.2 title命令

用来设置控制台窗口的名字

1. 设置cmd窗⼝名称
2. system("title 贪吃蛇");


5.3.3 pause命令

在 Windows 系统下经常使用的一个技巧,主要用于在控制台应用程序中暂停程序的执行,以便用户可以看到程序的输出,并有机会查看结果。

1. //暂停程序的运行
2. system("pause");


代码示例:

#include <stdio.h>
int main()
{
    //设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
    system("mode con cols=100 lines=30");
    //设置cmd窗⼝名称
    system("title 贪吃蛇"); 
    //暂停程序的运行
    system("pause");
    return 0;
}


运行结果如下:

5.3 控制台屏幕上的坐标COORD

COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系 (0,0) 的原点位于缓冲区的顶部左侧单元格。


COORD类型的声明:

typedef struct _COORD {
 SHORT X;
 SHORT Y;
} COORD, *PCOORD


给坐标赋值

1. //pos设置为屏幕上的一个点,其中X=10,Y=15
2. COORD pos = { 10, 15 };


5.4 GetStdHandle函数

⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标 准错误)中获取⼀个句柄,来操作控制台窗口

HANDLE是一个类型名,是一个void*类型的指针

1. HANDLE houtput = NULL;
2. //获取标准输出的句柄(⽤来标识不同设备的数值)
3. houtput = GetStdHandle(STD_OUTPUT_HANDLE);


5.5 GetConsoleCursorInfo函数

GetConsoleCursorInfo函数用来获得有关指定控制台屏幕缓冲区的光标大小和可见性的信息句柄

在我们运行程序的时候,控制台窗口会出现光标在闪烁:


如果运行贪吃蛇游戏一直有一个光标在闪烁就很不美观,GetConsoleCursorInfo就是用来设置光标的大小和可见性的

BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针


代码示例如下:

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

5.5.1 CONSOLE_CURSOR_INFO结构体

这个结构体,包含有关控制台光标的信息

typedef struct _CONSOLE_CURSOR_INFO {
 DWORD dwSize;
 BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

dwSize,由光标填充的字符单元格的百分⽐。 此值介于1到100之间。 光标外观会变化,范围从完

全填充单元格到单元底部的⽔平线条。

bVisible,游标的可⻅性。 如果光标可⻅,则此成员为 TRUE。


注:想要将bVisible修改为false,需要一个头文件stdbool.h,在cpp中是自带的不需要这个头文件,但在.c文件中需要该头文件来使用bool类型的值(false,true)


如下图所示:

光标的高度为正常一个字符高度的25%

我们获得了光标的句柄信息,我们想要修改该怎么办呢?


我们发现光标高度并没有改变也没有隐藏,那该怎么修改呢,我们需要用到SetConsoleCursorInfo函数了。


5.6 SetConsoleCursorInfo函数

该函数用来设置指定控制台屏幕缓冲区的光标的大小和可见性

BOOL WINAPI SetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);


代码示例如下:

HANDLE houtput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
 
//定义一个光标信息
CONSOLE_CURSOR_INFO CursorInfo = { 0 };
GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
 
CursorInfo.dwSize = 50;
//CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(houtput, &CursorInfo);


效果如下所示:

5.7 SetConsoleCursorPosition函数

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调 ⽤SetConsoleCursorPosition函数将光标位置设置到任意的位置。

BOOL WINAPI SetConsoleCursorPosition(
 HANDLE hConsoleOutput,
 COORD pos
);


代码示例如下:

int main()
{
  HANDLE houtput = NULL;
  //获取标准输出的句柄(⽤来标识不同设备的数值)
  houtput = GetStdHandle(STD_OUTPUT_HANDLE);
 
  //定位光标位置
  COORD pos = { 10, 5 };
  SetConsoleCursorPosition(houtput, pos);
 
    //getchar();
  system("pause");
  return 0;
}


但是如果我们需要多次调用光标出现在不同位置会很麻烦,因此我们可以封装一个函数来实现定位光标的操作。

5.7.1 set_pos:封装一个设置光标位置得函数

void set_pos(short x, short y)
{
 COORD pos = { x, y };
 HANDLE hOutput = NULL;
 //获取标准输出的句柄(⽤来标识不同设备的数值)
 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
 //设置标准输出上光标的位置为pos
 SetConsoleCursorPosition(hOutput, pos);
}


5.8 GetAsyncKeyState函数

我们用通过键盘上的按键来操控贪吃蛇,但我们需要获取我们按了什么键。

我们可以通过GetAsyncKeyState函数,将键盘上每个键的虚拟键值(虚拟键码-Win32传递给函数,函数通过返回值来分辨按键的状态。


函数返回一个short类型的值,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。 如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1


那我们如何判断最低为是否为1呢?

可以通过与0x01按位与(&)来进行判断

代码示例如下:

int main()
{
  short ret = GetAsyncKeyState(0x50);
 
  if ((ret & 1) == 1)
    printf("5被按过\n");
  else
    printf("没有被按过\n");
 
  return 0;
}


在多次调用的时候为了避免重复,选择用宏定义进行封装

1. //结果是1表示按过,结果是0表示没按过
2. #define KEY_PRESS(vk) (GetAsyncKeyState(vk)&1)?1:0)


检测数字键:

int main()
{ 
 while (1)
 {
 if (KEY_PRESS(0x30))
 {
     printf("0\n");
 }
 else if (KEY_PRESS(0x31))
 {
     printf("1\n");
 }
 else if (KEY_PRESS(0x32))
 {
     printf("2\n");
 }
 else if (KEY_PRESS(0x33))
 {
     printf("3\n");
 }
 else if (KEY_PRESS(0x34))
 {
     printf("4\n");
 }
 else if (KEY_PRESS(0x35))
 {
     printf("5\n");
 }
 else if (KEY_PRESS(0x36))
    {
     printf("6\n");
 }
 else if (KEY_PRESS(0x37))
 {
     printf("7\n");
 }
 else if (KEY_PRESS(0x38))
 {
     printf("8\n");
 }
 else if (KEY_PRESS(0x39))
 {
     printf("9\n");
 }
 }
 return 0;
}


六、贪吃蛇游戏设计

6.1 地图

在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。


因为各国语言符号等等存在很多区别,存在大量不同字符,原本的一个字节的char显然是不够用的,为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊了宽字符的类型 wchar_t 和宽字符的输⼊和输出函数,加⼊了头⽂件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。


6.1.1 <locale.h>本地化

提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。



通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部 分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏, 指定⼀个类项:


  • LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。
  • LC_CTYPE:影响字符处理函数的⾏为。
  • LC_MONETARY:影响货币格式。
  • LC_NUMERIC:影响 printf() 的数字格式。
  • LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
  • LC_ALL :针对所有类项修改,将以上所有类别设置为给定的语⾔环境。


6.1.2 setlocale函数

char* setlocale (int category, const char* locale);

setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。


setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参 数是LC_ALL,就会影响所有的类项。 C标准给第⼆个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)

int main()
{
  char* ret = setlocale(LC_ALL, NULL);
  printf("%s\n", ret);
 
  ret = setlocale(LC_ALL, "");
  printf("%s\n", ret);
  ret = setlocale(LC_ALL, "C");
  printf("%s\n", ret);
 
  return 0;
}

6.1.3 宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?


宽字符的字⾯量必须加上前缀“L”,否则 C 语⾔会把字⾯量当作窄字符类型处理。前缀“L”在单引 号前⾯,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表⽰宽字符串,对应 wprintf() 的占位符为 %ls 。

代码示例如下:

int main()
{
  setlocale(LC_ALL, "");
 
  char a = 'a';
  char b = 'b';
  printf("%c%c\n", a, b);
 
  wchar_t ch1 = L'比';
  wchar_t ch2 = L'特';
 
  wprintf(L"%lc\n", ch1);
  wprintf(L"%lc\n", ch2);
 
  return 0;
}


从输出的结果来看,我们发现⼀个普通字符占⼀个字符的位置 但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果 要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标的计算。



6.1.4 地图坐标

我们假设实现⼀个棋盘27⾏,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙, 如下:


6.2 蛇身与食物

初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24, 5)处开始出现蛇,连续5个节点。


注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中, 另外⼀般在墙外的现象,坐标不好对⻬。 关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然 后打印★。


6.3 数据结构设计

在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信 息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏, 所以蛇节点结构如下:

1. typedef struct SnakeNode
2. {
3. int x;
4. int y;
5. struct SnakeNode* next;
6. }SnakeNode, * pSnakeNode;


要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:

typedef struct Snake
{
 pSnakeNode _pSnake;//维护整条蛇的指针
 pSnakeNode _pFood;//维护⻝物的指针
 enum DIRECTION _Dir;//蛇头的⽅向,默认是向右
 enum GAME_STATUS _Status;//游戏状态
 int _Socre;//游戏当前获得分数
 int _foodWeight;//默认每个⻝物10分
 int _SleepTime;//每⾛⼀步休眠时间
}Snake, * pSnake;

蛇的⽅向,可以⼀⼀列举,使⽤枚举

enum DIRECTION
{
 UP = 1,
 DOWN,
 LEFT,
 RIGHT
};


游戏状态,可以⼀⼀列举,使⽤枚举

游戏状态,可以⼀⼀列举,使⽤枚举


6.4 游戏流程设计

当然功能远不止如此,你可以添加其他功能,例如穿墙,多个食物,蛇的位置随机出现,记录最高成绩,用文件管理记录历史最高成绩等等,你可以自行实现。


七、参考代码

7.1 snake.h

#pragma once
#include<iostream>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<stdio.h>
#include<locale.h>
#include<time.h>
using namespace std;
 
// 地图大小 
#define MAP_X 29
#define MAP_Y 27
//创建地图和、蛇、食物所用的宽字符
#define WALL L'□'
#define SNAKE_HEAD L'⊙'
#define SNAKE_BODY L'●'
#define FOOD L'★'
//判断按键是否按过
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
 
 
//类型的声明
 
//蛇的方向
enum DIRECTION
{
  UP = 1,
  DOWN,
  LEFT,
  RIGHT
};
 
//蛇的状态
//正常、撞到自己、正常退出
enum GAME_STATUS
{
  OK,
  KILL_BY_SELF,
  END_NORMAL
};
 
//贪吃蛇的结点类型
typedef struct SnakeNode
{
  int x;
  int y;
  struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
//tepedef struct SnakeNode* pSnakeNode;
 
//贪吃蛇本身的信息
typedef struct Sanke
{
  pSnakeNode _pSnake;//指向蛇头的指针
  pSnakeNode _pFood;//指向食物节点的指针
  enum DIRECTION _dir;//蛇的方向
  enum GAME_STATUS _status;//蛇的状态
  int _food_weight;//食物分数
  int _score;//总成绩
  int _sleep_time;//休息时间,时间越短,速度越快
}Snake, * pSnake;
 
//函数的声明
 
void SetPos(short x, short y);//定位光标位置
void PrintSnake(pSnake ps);//打印蛇
 
//游戏初始化
void GameStart(pSnake ss);
void SetWindows();//设置窗口大小名字,再隐藏光标
void WelcomeToGame();//打印欢迎界面
void CreatMap();//创建地图
void InitSnake(pSnake ps);//初始化蛇身
void CreatFood(pSnake ps);//创建食物
 
//游戏运行
void GameRun(pSnake ps);
void PrintHelpInfo();//打印右下角的帮助信息
void PrintScore(pSnake ps);//打印分数和总分数
//检测按键状态
void CheckKey(pSnake ps);
void Pause();//暂停
//蛇的移动
void SnakeMove(pSnake ps);
int NextIsFood(pSnakeNode pn, pSnake ps);//判断下一个坐标是否是食物
//下一个是食物就吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);
void NoFood(pSnakeNode pn, pSnake ps);
void ThroughWall(pSnake ps);//蛇穿墙
void KillBySelf(pSnake ps);//检测蛇是否撞到自己
 
//游戏结束
void GameEnd(pSnake ps);


7.2 snake.c

#include"snake.h"
 
void SetPos(short x, short y)
{
  COORD pos = { x, y };
  HANDLE hOutput = NULL;
  //获取标准输出的句柄(⽤来标识不同设备的数值)
  hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
  //设置标准输出上光标的位置为pos
  SetConsoleCursorPosition(hOutput, pos);
}
 
void PrintSnake(pSnake ps)
{
  pSnakeNode cur = ps->_pSnake;
  //打印蛇头和蛇身
  while (cur)
  {
    SetPos(cur->x, cur->y);
    if (cur == ps->_pSnake)
    {
      wprintf(L"%lc", SNAKE_HEAD);// 蛇头  
    }
    else
    {
      wprintf(L"%lc", SNAKE_BODY);// 蛇身  
    }
    cur = cur->next;
  }
}
 
 
void SetWindows()
{
  //设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
  system("mode con cols=100 lines=32");
  system("title 贪吃蛇");//设置cmd窗⼝名称
 
  HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得控制台窗口句柄
  CONSOLE_CURSOR_INFO CursorInfo;
  GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
  CursorInfo.bVisible = false; //隐藏控制台光标
  SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态
}
 
void WelcomeToGame()
{
  SetPos(40, 14);
  wprintf(L"欢迎来到贪吃蛇小游戏");
  SetPos(40, 20);
  system("pause");
  system("cls");
  SetPos(25, 14);
  wprintf(L"用↑、↓、←、→来控制蛇的移动,按F3加速,按F4减速");
  SetPos(25, 15);
  wprintf(L"加速能得到更多分数");
  SetPos(40, 20);
  system("pause");
  system("cls");
}
 
void CreatMap()
{
  //上
  for (int i = 0; i < MAP_X; i++)
  {
    wprintf(L"%lc", WALL);
  }
  //下
  SetPos(0, MAP_Y - 1);
  for (int i = 0; i < MAP_X; i++)
  {
    wprintf(L"%lc", WALL);
  }
  //左
  for (int i = 1; i < (MAP_Y - 1); i++)
  {
    SetPos(0, i);
    wprintf(L"%lc", WALL);
  }
  //右
  for (int i = 1; i < (MAP_Y - 1); i++)
  {
    SetPos((MAP_X - 1) * 2, i);
    wprintf(L"%lc", WALL);
  }
 
  SetPos(0, MAP_Y + 1);
}
 
void InitSnake(pSnake ps)
 
{
  pSnakeNode cur = NULL;
  int pos_x, pos_y;
  int n;
  pos_x = 2 * (rand() % (MAP_X - 2) + 1);
  pos_y = rand() % (MAP_Y - 2) + 1;
  do
  {
 
    n = rand() % 4 + 1;
  } while ((pos_x >= (MAP_X - 5) * 2 && n == 1) || (pos_x <= 10 && n == 2) || (pos_x >= (MAP_Y - 5) && n == 3) || (pos_y <= 5 && n == 4));
 
  for (int i = 0; i < 5; i++)
  {
    //链表实现
    cur = (pSnakeNode)malloc(sizeof(SnakeNode));
    if (cur == NULL)
    {
      perror("InitSnake()::malloc");
      return;
    }
    cur->next = NULL;
 
    //根据随机值选择蛇的方向
    switch (n)
    {
    case 1:cur->x = pos_x + 2 * i;
      cur->y = pos_y;//横向,从尾向右创建贪吃蛇
      ps->_dir = RIGHT;
      break;
    case 2:cur->x = pos_x - 2 * i;
      cur->y = pos_y;//横向,从尾向左创建贪吃蛇
      ps->_dir = LEFT;
      break;
    case 3:cur->x = pos_x;
      cur->y = pos_y + i;//纵向,从尾向下创建贪吃蛇
      ps->_dir = DOWN;
      break;
    case 4:cur->x = pos_x;
      cur->y = pos_y - i;//纵向,从尾向上创建贪吃蛇
      ps->_dir = UP;
      break;
    default:
      break;
    }
 
    //头插链表
    if (ps->_pSnake == NULL)//空链表 
    {
      ps->_pSnake = cur;
    }
    else
    {
      cur->next = ps->_pSnake;
      ps->_pSnake = cur;
    }
  }
  //打印蛇
  PrintSnake(ps);
 
  //设置贪吃蛇的属性
  ps->_score = 0;
  ps->_food_weight = 5;
  ps->_sleep_time = 250;
  ps->_status = OK;
}
 
void CreatFood(pSnake ps)
{
  int x = 0;
  int y = 0;
again:
  x = 2 * (rand() % 27 + 1);
  y = rand() % 25 + 1;
 
  //x,y和蛇的坐标不能冲突
  pSnakeNode cur = ps->_pSnake;
  while (cur)
  {
    if (x == cur->x && y == cur->y)
    {
      goto again;
    }
    cur = cur->next;
  }
  //创建食物节点
  pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
  if (pFood == NULL)
  {
    perror("CreatFood()::malloc");
  }
  pFood->x = x;
  pFood->y = y;
  pFood->next = NULL;
  ps->_pFood = pFood;
  SetPos(x, y);
  wprintf(L"%lc", FOOD);
}
 
void GameStart(pSnake ps)
{
  //1. 先设置窗口大小名字,再隐藏光标
  SetWindows();
  //2. 打印欢迎界面,功能介绍
  WelcomeToGame();
  //3. 绘制地图
  CreatMap();
  //4. 创建蛇
  InitSnake(ps);
  //5. 创建食物
  CreatFood(ps);
}
 
 
void PrintHelpInfo()
{
  SetPos(64, 17);
  wprintf(L"可以穿墙,但不能咬到自己");
  SetPos(64, 18);
  wprintf(L"用↑、↓、←、→来控制蛇的移动");
  SetPos(64, 19);
  wprintf(L"F3加速,F4减速");
  SetPos(64, 20);
  wprintf(L"加速能得到更多分数");
  SetPos(64, 21);
  wprintf(L"按esc退出游戏,按空格暂停游戏");
 
  SetPos(0, MAP_Y + 2);
  system("pause");
}
 
void PrintScore(pSnake ps)
{
  SetPos(64, 7);
  printf("总分数:%d", ps->_score);
  SetPos(64, 8);
  printf("当前食物的分数是:%2d", ps->_food_weight);
  SetPos(64, 9);
  printf("当前难度为:%d 级", (ps->_food_weight) / 5);
}
 
void Pause()
{
  while (1)
  {
    Sleep(200);
    if (KEY_PRESS(VK_SPACE))
    {
      break;
    }
  }
}
 
void CheckKey(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_SPACE))
  {
    //暂停
    Pause();
  }
  else if (KEY_PRESS(VK_ESCAPE))
  {
    //正常退出
    ps->_status = END_NORMAL;
  }
  else if (KEY_PRESS(VK_F3))
  {
    //加速
    if (ps->_sleep_time > 80)
    {
      ps->_sleep_time -= 25;
      ps->_food_weight += 5;
    }
  }
  else if (KEY_PRESS(VK_F4))
  {
    //减速
    if (ps->_food_weight > 5)
    {
      ps->_sleep_time += 25;
      ps->_food_weight -= 5;
    }
  }
}
 
int NextIsFood(pSnakeNode pn, pSnake ps)
{
  if (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y)
    return 1;
  else
    return 0;
}
 
void EatFood(pSnakeNode pn, pSnake ps)
{
  //头插
  ps->_pFood->next = ps->_pSnake;
  ps->_pSnake = ps->_pFood;
 
  //释放下一个位置的结点
  free(pn);
  pn = NULL;
  //打印蛇身
  PrintSnake(ps);
  ps->_score += ps->_food_weight;
 
  //重新创建一个食物
  CreatFood(ps);
}
 
void NoFood(pSnakeNode pn, pSnake ps)
{
  pn->next = ps->_pSnake;
  ps->_pSnake = pn;
 
  //将最后一个结点的位置打印成空白字符,并释放掉
  pSnakeNode cur = ps->_pSnake;
  while (cur->next->next != NULL)
  {
    cur = cur->next;
  }
  PrintSnake(ps);
  SetPos(cur->next->x, cur->next->y);
  printf("  ");
  free(cur->next);
  cur->next = NULL;
}
 
void ThroughWall(pSnake ps)
{
  if (ps->_pSnake->x == 0)
  {
    ps->_pSnake->x = (MAP_X - 2) * 2;
    SetPos(0, ps->_pSnake->y);
    wprintf(L"%lc", WALL);
    SetPos(ps->_pSnake->x, ps->_pSnake->y);
    wprintf(L"%lc", SNAKE_HEAD);
  }
  else if (ps->_pSnake->x == (MAP_X - 1) * 2)
  {
    ps->_pSnake->x = 2;
    SetPos((MAP_X - 1) * 2, ps->_pSnake->y);
    wprintf(L"%lc", WALL);
    SetPos(ps->_pSnake->x, ps->_pSnake->y);
    wprintf(L"%lc", SNAKE_HEAD);
  }
  else if (ps->_pSnake->y == 0)
  {
    ps->_pSnake->y = MAP_Y - 2;
    SetPos(ps->_pSnake->x, 0);
    wprintf(L"%lc", WALL);
    SetPos(ps->_pSnake->x, ps->_pSnake->y);
    wprintf(L"%lc", SNAKE_HEAD);
  }
  else if (ps->_pSnake->y == (MAP_Y - 1))
  {
    ps->_pSnake->y = 1;
    SetPos(ps->_pSnake->x, MAP_Y - 1);
    wprintf(L"%lc", WALL);
    SetPos(ps->_pSnake->x, ps->_pSnake->y);
    wprintf(L"%lc", SNAKE_HEAD);
  }
}
 
void KillBySelf(pSnake ps)
{
  pSnakeNode cur = ps->_pSnake->next;
  while (cur)
  {
    if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
    {
      ps->_status = KILL_BY_SELF;
      break;
    }
    cur = cur->next;
  }
}
 
void SnakeMove(pSnake ps)
{
  //创建下一个位置的结点
  pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
  if (pNextNode == NULL)
  {
    perror("SnakeMove()::malloc()");
    return;
  }
 
  switch (ps->_dir)
  {
  case UP:
    pNextNode->x = ps->_pSnake->x;
    pNextNode->y = ps->_pSnake->y - 1;
    break;
  case DOWN:
    pNextNode->x = ps->_pSnake->x;
    pNextNode->y = ps->_pSnake->y + 1;
    break;
  case LEFT:
    pNextNode->x = ps->_pSnake->x - 2;
    pNextNode->y = ps->_pSnake->y;
    break;
  case RIGHT:
    pNextNode->x = ps->_pSnake->x + 2;
    pNextNode->y = ps->_pSnake->y;
    break;
  }
 
  //检测下一个坐标处是否是食物
  if (NextIsFood(pNextNode, ps))
  {
    EatFood(pNextNode, ps);
  }
  else
  {
    NoFood(pNextNode, ps);
  }
  pNextNode = NULL;
 
  //穿墙
  ThroughWall(ps);
  //检测蛇是否撞到自己
  KillBySelf(ps);
}
 
void GameRun(pSnake ps)
 
{
  //打印帮助信息
  PrintHelpInfo();
  do
  {
    //打印分数和总分数
    PrintScore(ps);
    //检查按键状态
    CheckKey(ps);
    //贪吃蛇走一步
    SnakeMove(ps);
    Sleep(ps->_sleep_time);
 
  } while (ps->_status == OK);
}
 
void GameEnd(pSnake ps)
{
  SetPos(20, 12);
  switch (ps->_status)
  {
  case END_NORMAL:
    printf("您主动结束游戏");
    break;
  case KILL_BY_SELF:
    printf("您撞到自己,游戏结束");
    break;
  }
 
  //释放蛇身的链表
  pSnakeNode cur = ps->_pSnake;
  while (cur)
  {
    pSnakeNode tail = cur;
    cur = cur->next;
    free(tail);
  }
}


7.3 test.c

#include"snake.h"
 
void test()
{
  char ch;
  do
  {
    system("cls");
    //创建贪吃蛇
    Snake snake = { 0 };
    //初始化实现
    GameStart(&snake);
    //运行游戏
    GameRun(&snake);
    //结束游戏-善后工作
    GameEnd(&snake);
    SetPos(20, 14);
    printf("是否再来一局?(Y/N):");
    cin >> ch;
  } while (ch == 'y' || ch == 'Y');
 
  SetPos(0, MAP_Y + 1);
}
 
int main()
{
  //设置配置本地环境
  setlocale(LC_ALL, "");
  srand((unsigned int)time(NULL));
  test();
 
  return 0;
}


结语:

C语言的内容就到此结束了,后续将从数据结构的顺序表开始

愿与你一同在学习的道路上走得更远,与诸君共勉!

相关文章
|
2天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1519 4
|
29天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
5天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
503 19
|
2天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
179 1
|
8天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
457 5
|
7天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
314 2
|
23天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
25天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2608 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析