贪吃蛇小游戏简单制作-C语言

文章目录

      • 游戏背景介绍
      • 实现目标
      • 适合人群
      • 所需技术
      • 浅玩Window API
        • 什么是API
        • 控制台程序
          • 窗口大小,名称设置
        • Handle(句柄)
          • 获取句柄
        • 坐标结构体
          • 设置光标位置
        • 光标属性
          • 获取光标属性
          • 设置光标属性
        • 按键信息获取
      • 贪吃蛇游戏设计
        • 游戏前的初始化
          • 设置窗口的大小和名称
          • 本地化设置
        • 宽字符
          • Waht is 宽字符
          • 宽字符的打印
          • 光标的设置
          • 欢迎界面打印
          • 地图绘制
          • 帮助信息
          • 蛇身的管理
          • 贪吃蛇游戏的管理(插播,重要)
          • 蛇的初始化
          • 食物的管理
        • 蛇的移动
      • 游戏结束
      • 拓展建议
      • 寄语
      • 源码

游戏背景介绍

贪吃蛇是一个非常经典的小游戏,笔者曾今在古早的案件手机,mp4上面玩过这款游戏,今天就让我们使用C语言一起复刻这个简单的小游戏吧~,好玩简单-

实现目标

在这个游戏中,我们需要控制一条可以上下左右移动的小蛇,在指定的墙体内进行移动,吃到食物后,蛇的身体会变长,当蛇撞墙或者撞到自己的身体的时候,游戏结束,我们需要实现的功能有:

  • 蛇的移动
  • 食物的生成
  • 蛇的身体的增长
  • 游戏结束的判断
  • 游戏结束后的处理

适合人群

这是我学习完C语言语法和顺序表,链表之后的一个小项目,也同样适合像我一样刚学完C语言的同学,通过这个项目,巩固一下C语言的基础知识,也可以学习一下C语言的一些高级知识,比如Windows API的使用,宽字符的打印等等

所需技术

  1. C语言基础
  2. 链表
  3. 简单的Windows API
  4. 动态内存分配

浅玩Window API

什么是API

API全称Application Programming Interface,翻译过来就是应用程序接口,是一组预先定义的函数,类,协议的集合,这些函数,类,协议可以被其他程序调用,用来实现一些功能,比如Windows API就是用来实现Windows系统的一些功能的

上面说的有些复杂,我们可以把API想象成一个工厂,我们只需要知道向工厂输入什么,工厂会输出什么,而不需要知道工厂内部是怎么实现的

比如你向一个面包工厂输入了一些小麦,工厂会给你输出一些面包,而工厂内部可能是用了一些机器,工人等等来实现的,但是你不需要知道这些,你只需要知道你给他小麦,他给你面包就行了

小麦
面包工厂加工
面包
控制台程序

控制台程序其实就是我们平时编译完文件之后运行打开的那个黑框框,我们可以在这个黑框框里面输入一些命令,然后程序会给我们输出一些结果,这个黑框框就是一个控制台,我们可以通过控制台程序来和用户进行交互

窗口大小,名称设置

既然是贪吃蛇小游戏,那么我们需要一个固定的窗口和一个有趣的名字,我们可以怎么做呢?
可以使用cmd命令在控制台程序中设置窗口的大小,名称

mode con cols=100 lines=30
title 贪吃蛇
  • mode con cols=100 lines=30 设置窗口的大小为100列,30行
  • title 贪吃蛇 设置窗口的名称为贪吃蛇

但是,我们只希望贪吃蛇程序运行后自动设置窗口的大小和名称,而不是让用户手动输入这些命令,因此我们可以使用system函数来调用这些命令,让程序自动设置窗口的大小和名称

#include <stdlib.h>
int main()
{system("mode con cols=100 lines=30");system("title 贪吃蛇");return 0;
}
Handle(句柄)

如果我们想要对控制台的一些属性进行设置,比如设置光标的位置,设置控制台的颜色等等,我们就需要使用句柄来进行操作,我们可以把句柄想象成是控制台大哥的身份标识,只有知道大哥的身份标识才能找到大哥并更改他的一些属性

获取句柄

我们可以使用GetStdHandle函数来获取控制台的句柄,这个函数的原型是

HANDLE GetStdHandle(DWORD nStdHandle);

我们可以使用一个HANDLE类型变量来接受返回值

坐标结构体

控制台上面的每一个字符都有一个坐标,
坐标是从左上角开始计算的,左上角的坐标是(0,0),向右是x轴正方向,向下是y轴正方向
我们可以使用一个结构体来表示这个坐标
COORD是Windows API中的一个结构体,定义如下

typedef struct _COORD {SHORT X;SHORT Y;
} COORD, *PCOORD;
设置光标位置

我们平时在使用printf打印字符的时候,每打印一个字符,光标都会自动向后移动一个位置,我们可以使用SetConsoleCursorPosition函数来手动设置光标的位置

BOOL SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD  dwCursorPosition
);

eg:

#include <windows.h>int main()
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台句柄COORD pos = {10, 10};                          // 设置坐标SetConsoleCursorPosition(hOut, pos);           // 设置光标位置printf("hello world");return 0;
}
光标属性

我们平时运行控制台程序的时候,会发现光标是一个闪烁的小方块,那我们有没有什么办法可以让小方块变大变小或是直接隐藏呢?
答案肯定是有哒

我们先来介绍一下光标的两个属性

  • bVisible : 是否可见
  • dwSize : 光标的大小 当这个值是100的时候,光标是一个小方块█,这个值也就是显示这个方块的百分比,比如50就是显示一半的方块
获取光标属性

我们可以使用GetConsoleCursorInfo函数来获取光标的属性

BOOL GetConsoleCursorInfo(HANDLE               hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

eg:

#include <windows.h>int main()
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台句柄CONSOLE_CURSOR_INFO cursorInfo;GetConsoleCursorInfo(hOut, &cursorInfo;);printf("bVisible:%d, dwSize:%d\n", cursorInfo.bVisible, cursorInfo.dwSize);return 0;
}

大家可以自行运行试一试

设置光标属性

在贪吃蛇游戏中,我们肯定希望没有光标的出现,因此我们可以使用SetConsoleCursorInfo函数来设置光标的属性

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

eg:

#include <windows.h>int main()
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 获取控制台句柄CONSOLE_CURSOR_INFO cursorInfo;cursorInfo.bVisible = 0; // 设置光标不可见 也可以写flasecursorInfo.dwSize = 100;  // 设置光标大小SetConsoleCursorInfo(hOut, &cursorInfo);return 0;
}
按键信息获取

我们需要使用↑↓←→来控制蛇的移动,因此我们可以使用GetAsyncKeyState函数来获取按键信息

SHORT GetAsyncKeyState(int vKey
);

在返回的值中,如果最高位是1,表示这个键正在被按下,如果最低位是1,表示这个键被按过
我们可以使用一个预定义的宏来判断这个键是否被按下

#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)

这个大家肯定看的懂,如果不懂的话可以去学习一下预编译和位运算

贪吃蛇游戏设计

我们可以把贪吃蛇游戏分为三个部分:

  1. 游戏前的初始化,包括窗口的设置,光标的设置,欢迎界面打印,地图的初始化,蛇的初始化,食物的初始化等等
  2. 游戏中的循环,包括蛇的移动,食物的生成,蛇的身体的增长,游戏结束的判断等等
  3. 游戏结束后的处理,包括释放资源,打印游戏结束的信息等等

补充:当程序需要实现按任意键继续的时候,我们使用system(“pause”)函数即可

游戏前的初始化
设置窗口的大小和名称
void CmdInit(void)
{// 设置控制台窗口大小 100列 30行system("mode con cols=100 lines=30");// 设置控制台名称system("title snake");
}
本地化设置

正常在C语言中,我们使用的是ASCII字符集,他只使用了一个字节来表示一个字符,而在不同的国家和地区,字符集是不一样的,因此我们可以使用setlocale函数来设置字符集

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

在C标准库中,我们可以更改的地区设置有以下这些:

  • LC_ALL:所有的地区设置
  • LC_COLLATE:字符串比较
  • LC_CTYPE:字符分类和转换
  • LC_MONETARY:货币格式
  • LC_NUMERIC:非货币数字格式
  • LC_TIME:时间格式

我么可以使用setlocale函数来设置地区,比如把地区设置为当前地区

#include <locale.h>
int main()
{setlocale(LC_ALL, "");return 0;
}
宽字符
Waht is 宽字符

宽字符是指一个字符占用两个字节的字符,中文以及一些特殊字符都是宽字符,
而且宽字符在控制台中是占用两个x坐标

graph LR
A[宽字符] --> B[占用两个坐标位置] --> D[普通字符:█]
B --> E[宽字符:██]
A --> C[占用两个字节]
宽字符的打印

在C语言中,我们可以使用wprintf函数来打印宽字符,在打印宽字符之前,我们需要进行本地化设置

int wprintf(const wchar_t *format, ...);

eg:

#include <stdio.h>
int main()
{setlocale(LC_ALL, "");wchar_t str[] = L"你好,世界\n";wprintf(L"%s", str);return 0;
}
光标的设置
  1. 定义一个变量来接受控制台的句柄
  2. 定义一个变量来接受光标的属性
  3. 改变光标的属性
  4. 通过SetConsoleCursorInfo函数,输入句柄和光标属性,设置光标的属性
欢迎界面打印

这里我们需要设置光标位置并打印所需信息,因此我们可以封装一个函数来快捷地设置光标位置

void SetPos(int x, int y)
{HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = {x, y};SetConsoleCursorPosition(hOut, pos);
}

然后我们就可以打印欢迎信息和游戏规则了

// 打印欢迎信息
void WelcomeToGame(void)
{SetPos(45, 10);wprintf(L"欢迎来到贪吃蛇游戏");// 暂停SetPos(45, 20);system("pause");
}// 游戏介绍
void GameIntroduction(void)
{// 清屏system("cls");SetPos(45, 10);wprintf(L"游戏介绍:");SetPos(45, 12);wprintf(L"1. 使用↑,↓,←,→控制蛇的移动");SetPos(45, 14);wprintf(L"2. 吃到食物蛇的长度加1");SetPos(45, 16);wprintf(L"2. F3加速, F4减速");SetPos(45, 18);wprintf(L"3. 空格暂停");SetPos(45, 20);wprintf(L"4. Esc退出游戏");// 暂停SetPos(45, 22);system("pause");
}

在这里插入图片描述

在这里插入图片描述

地图绘制

我们这里使用□来表示墙体,然后运用宽字符的打印知识来绘制一个27 * 27的地图

// 地图绘制 
// 上(0,0) - (56,0)
// 下(0,26) - (56,26)
// 左(0,0) - (0,26)
// 右(56,0) - (56,26)
// 注意打印的是宽字符 占两个x坐标 因此左右打印的时候要每打印一个x坐标加2
void MapDraw(void)
{   // 清屏system("cls");// 上墙for (int i = 0; i < 57; i+=2){SetPos(i, 0);wprintf(L"%c", WALL);}// 下墙for (int i = 0; i < 57; i+=2){SetPos(i, 26);wprintf(L"%c", WALL);}// 最上面和最下面已经打印过了// 左墙for (int i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}// 右墙for (int i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%c", WALL);} 
}
帮助信息

我们在地图的右侧打印一些帮助信息,比如当前的分数,按键操作等等

// 打印静态帮助信息
// 单个食物分数,总分数的名称
// 操作说明
void PrintStaticHelp(void)
{SetPos(70, 5);wprintf(L"单个食物分数: 10");SetPos(70, 7);wprintf(L"总分数: 0");SetPos(70, 9);wprintf(L"操作说明:");SetPos(70, 11);wprintf(L"↑ : 上移");SetPos(70, 13);wprintf(L"↓ : 下移");SetPos(70, 15);wprintf(L"← : 左移");SetPos(70, 17);wprintf(L"→ : 右移");SetPos(70, 19);wprintf(L"F3: 加速");SetPos(70, 21);wprintf(L"F4: 减速");SetPos(70, 23);wprintf(L"空格: 暂停");SetPos(70, 25);wprintf(L"Esc: 退出");
}

在这里插入图片描述

蛇身的管理

我们可以将蛇看作是一个一个节点相互连接组成,因此我们可以使用链表来管理蛇的身体,我们先创建一个结构体来管理蛇身的节点

// 蛇节点
typedef struct SnakeNode
{int x;int y;struct SnakeNode *next;
} SnakeNode, * pSnakeNode;
贪吃蛇游戏的管理(插播,重要)

使用一个结构体来管理整个贪吃蛇游戏,包括:

  • 蛇身
  • 食物
  • 单个食物分数
  • 总分数
  • 睡眠时间
  • 方向
  • 游戏状态
// 方向
typedef enum Direction
{UP = 1,     // 上DOWN,       // 下LEFT,       // 左RIGHT       // 右} Direction;// 游戏状态
typedef enum GameStatus
{OK = 0,         //运行中KILL_BY_WALL,   //撞墙KILL_BY_SELF,   //撞自己PAUSE,          //暂停ESC             //退出
} GameStatus;// 贪吃蛇游戏
typedef struct Snake
{pSnakeNode _pSnake;     // 蛇头pSnakeNode _pFood;      // 食物 可以共用蛇节点比较方便,下面会提int _foodScore;         // 单个食物分数int _totalScore;        // 总分数int _sleepTime;         // 睡眠时间 -- 控制速度Direction _dir;         // 方向GameStatus _status;     // 游戏状态
} Snake, * pSnake;

在游戏开始前对这个结构体进行初始化

pSnake ps = (pSnake)malloc(sizeof(Snake));
蛇的初始化

我们可以使用一个链表来存储蛇的身体,这里我们创建一个有5个节点的蛇,并且初始化蛇的位置


#define BODY L'●'
#define POS_X 6
#define POS_Y 6// 蛇初始化
// 在地图的(6,6)位置初始化蛇
// 蛇的长度为5
// 使用头插法
void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;int i = 0;// 创建蛇身节点 并初始化坐标    for (i = 0; i < 5; i++){pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("pSnakeNode malloc failed");exit(1);}node->x = POS_X + i * 2;node->y = POS_Y;node->next = NULL;// 头插法if (ps->_pSnake == NULL){ps->_pSnake = node;}else{node->next = ps->_pSnake;ps->_pSnake = node;}}// 打印蛇cur = ps->_pSnake;while (cur != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}
}

在这里插入图片描述

食物的管理

我们可以把食物也看作是一个蛇节点,只不过还没有加入到蛇身体中,因此我们可以使用蛇身结构体来存储食物
我们在游戏开始和蛇吃到食物的时候,生成一个食物节点,并且打印食物到屏幕上

// 创建食物节点
void CreatFood(pSnake ps)
{// 申请节点空间pSnakeNode foodnode = (pSnakeNode)malloc(sizeof(SnakeNode));if (foodnode == NULL){perror("foodnode malloc failed");exit(1);}foodnode->next = NULL;// 随机生成食物坐标 在墙范围内但是不能在蛇身上while (1){int x = rand() % 54 + 2;int y = rand() % 24 + 2;pSnakeNode cur = ps->_pSnake;while (cur != NULL){if (cur->x == x && cur->y == y){break;}cur = cur->next;}if (cur == NULL){foodnode->x = x;foodnode->y = y;break;}}// 打印食物SetPos(foodnode->x, foodnode->y);wprintf(L"%c", FOOD);
}

这里的随机数并不是真正的随机数,而是伪随机数,因此我们可以在mian里面使用srand函数来设置随机数的种子

int main()
{srand(time(NULL));return 0;
}

在这里插入图片描述

蛇的移动
  1. 按键检测, 改变方向
  2. 判断是否吃到食物
    • 吃到食物,头插食物节点,创建新的食物节点
    • 没吃到食物,头插一个新的节点,新的节点是蛇头移动到的下一个坐标,尾删一个节点
  3. 蛇移动后进行打印,判断是否撞到自己或者墙壁,是则游戏结束

这里封装了一个宏来读取按键状态,如果最低位是1,则返回1,反之返回0
也就是一个按键按下过返回1,否则返回0

#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)
// 蛇的移动 -- 主游戏程序 在此循环
// 吃到食物 创建新的食物节点
// 没有吃到食物头插新节点删除尾节点并释放空间
// 减速睡眠时间-30 睡眠时间最少减4次 单个食物分数加2
// 加速睡眠时间+30 睡眠时间最多加4次 单个食物分数减2
void SnakeMove(pSnake ps)
{int x = 0;int y = 0;again:while (ps->_status == OK){// 按键检测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)){ps->_status = PAUSE;}else if (KEY_PRESS(VK_F3) && ps->_foodScore < 20){ps->_sleepTime -= 30;ps->_foodScore += 2;// 更改单个食物分数SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_F4) && ps->_foodScore > 2){ps->_sleepTime += 30;ps->_foodScore -= 2;// 更改单个食物分数SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_ESCAPE)){ps->_status = ESC;}// 设置下一个节点的x,y坐标switch (ps->_dir){case UP:x = ps->_pSnake->x;y = ps->_pSnake->y - 1;break;case DOWN:x = ps->_pSnake->x;y = ps->_pSnake->y + 1;break;case LEFT:x = ps->_pSnake->x - 2;y = ps->_pSnake->y;break;case RIGHT:x = ps->_pSnake->x + 2;y = ps->_pSnake->y;break;}// 判断是否吃到食物if (ps->_pFood->x == x && ps->_pFood->y == y){EatFood(ps);}else {NotEatFood(ps, x, y);}// 撞墙检测if (IsKillByWall(ps)){ps->_status = KILL_BY_WALL;}// 撞自己检测if (IsKillBySelf(ps)){ps->_status = KILL_BY_SELF;}Sleep(ps->_sleepTime);}while (ps->_status == PAUSE){if (KEY_PRESS(VK_SPACE)){ps->_status = OK;}goto again; //使用goto 返回到前面}// 撞墙打印信息if (IsKillByWall(ps)){SetPos(45, 10);wprintf(L"墙墙被你撞西了,110把你抓走了");}// 撞自己打印信息if (IsKillBySelf(ps)){SetPos(45, 10);wprintf(L"自己撞自己了,120把你带走了");}return;
}

EatFood函数

void EatFood(pSnake ps)
{// 加分ps->_totalScore += ps->_foodScore;// 打印总分数SetPos(82, 7);printf("%4d", ps->_totalScore);// 是 头插食物节点 创建新节点ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;// 打印新蛇头SetPos(ps->_pSnake->x, ps->_pSnake->y);wprintf(L"%c", BODY);// 创建新食物CreatFood(ps);
}

NotEatFood函数

// 没有迟到食物的处理
void NotEatFood(pSnake ps, int x, int y)
{// 创建新节点并头插pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("node malloc failed");exit(1);}node->x = x;node->y = y;node->next = ps->_pSnake;ps->_pSnake = node;// 打印新蛇 顺便删除尾节点 释放空间 打印空格pSnakeNode cur = ps->_pSnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}SetPos(cur->x, cur->y);wprintf(L"%c", BODY);SetPos(cur->next->x, cur->next->y);wprintf(L"  ");free(cur->next);cur->next = NULL;
}

IsKillByWall函数

// 撞墙检测
// 撞墙返回1 否则返回0
int IsKillByWall(pSnake ps)
{if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26){return 1;}return 0;
}

在这里插入图片描述

IsKillBySelf函数

// 撞自己检测
// 撞自己返回1 否则返回0
int IsKillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;// 从第二个节点开始遍历 并判断是否和蛇头坐标相同while (cur != NULL){if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){return 1;}cur = cur->next;}return 0;
}

在这里插入图片描述

除了提到的以外,我们还要对加速减速的按键进行处理,还有总分的统计,失败的打印信息等等,这里就由大家自由发挥啦,不行的话也可以看我的源码

游戏结束

恭喜你,你已经得到了一个属于你的小小贪吃蛇游戏了,童年的游戏已经被你复刻,但是你现在还不可以拍拍屁股走人哦,我们还需要做好善后工作啦

  1. 释放资源
// 善后 释放蛇节点 以及食物节点
void GameEnd(pSnake ps)
{// 释放蛇节点pSnakeNode cur = NULL;while (ps->_pSnake != NULL){cur = ps->_pSnake;ps->_pSnake = ps->_pSnake->next;free(cur);}// 释放食物节点free(ps->_pFood);
}

拓展建议

本文会有一些不足之处,但不影响整体的学习,如果大家有问题可以用自己的方法解决或者发在评论区
拓展建议:

  1. 可以优化画面,多加一些符号,可以fancy一点,比如这样
// 使用--,|,/ 绘制英文SNAKE
/* _____                  _                 _____|    \   |\_    |     / \      |   _/   ||_____   |  \   |    /   \     |__/     |_____|   |   \_ |   /_____\    |  \_    |\____|   |     \|  /       \   |    \   |_____*/
void PrintSnake(void)
{SetPos(32, 5);printf(" _____                  _                 _____");SetPos(32, 6);printf(" |    \\   |\\_    |     / \\      |   _/   |");SetPos(32, 7);printf(" |_____   |  \\   |    /   \\     |__/     |_____");SetPos(32, 8);printf("      |   |   \\_ |   /_____\\    |  \\_    |");SetPos(32, 9);printf(" \\____|   |     \\|  /       \\   |    \\   |_____");
}

在这里插入图片描述

  1. 可以配合EasyX图形库,做一个图形化的贪吃蛇游戏
  2. 可以加入再来一局的功能
  3. 可以加入排行榜,计分等,配合写入读取文件
  4. 可以加入音效,配合Windows API的Beep函数,不过会有阻塞
  5. 加入双人模式,可以使用WSAD控制第二条蛇
  6. 等等等等还有好多啦,大家可以自行探索

寄语

感谢每一位看到这里的自己🌹🌹🌹
分享一句话:
我将玫瑰藏于身后,风起花落,从此鲜花赠自己,纵马踏花向自由

源码

test.c

#include "snake.h"int main()
{// 使用当前的时间作为种子值srand(time(NULL));pSnake ps = (pSnake)malloc(sizeof(Snake));SnakeInit(ps);GameStart(ps); // 游戏初始化GameRun(ps); // 游戏运行GameEnd(ps); // 游戏结束return 0;
}

snake.h

#pragma once#include <Windows.h>
#include <locale.h>
#include <wchar.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>// 方向
typedef enum Direction
{UP = 1,     // 上DOWN,       // 下LEFT,       // 左RIGHT       // 右} Direction;// 游戏状态
typedef enum GameStatus
{OK = 0,         //运行中KILL_BY_WALL,   //撞墙KILL_BY_SELF,   //撞自己PAUSE,          //暂停ESC             //退出
} GameStatus;// 蛇节点
// struct SnakeNode 取别名 SnakeNode
// struct SnakeNode * 取别名 pSnakeNode
typedef struct SnakeNode
{int x;int y;struct SnakeNode *next;
} SnakeNode, * pSnakeNode;// 贪吃蛇游戏
typedef struct Snake
{pSnakeNode _pSnake;     // 蛇头pSnakeNode _pFood;      // 食物int _foodScore;         // 单个食物分数int _totalScore;        // 总分数int _sleepTime;         // 睡眠时间 -- 控制速度Direction _dir;         // 方向GameStatus _status;     // 游戏状态
} Snake, * pSnake;void GameStart(pSnake ps);
void SnakeInit(pSnake ps);
void GameRun(pSnake ps);
void GameEnd(pSnake ps);

snake.c

#include "snake.h"
#include <stdio.h>
#include <stdlib.h>#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x01 ? 1 : 0)
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 6
#define POS_Y 6// ↑↓←→●□★ // 贪吃蛇游戏结构体初始化
void SnakeInit(pSnake ps)
{ps->_pSnake = NULL;ps->_pFood = NULL;ps->_foodScore = 10;ps->_totalScore = 0;ps->_status = OK;ps->_dir = RIGHT;ps->_sleepTime = 200;
}// 设置控制台窗口大小和名称
void CmdInit(void)
{// 设置控制台窗口大小 100列 30行system("mode con cols=100 lines=30");// 设置控制台名称system("title snake");
}// 光标隐藏
void CursorHide(void)
{// 获取句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);// 光标信息CONSOLE_CURSOR_INFO cursor_info = {0};// 获取光标信息GetConsoleCursorInfo(handle, &cursor_info);// 设置光标属性cursor_info.dwSize = 10;cursor_info.bVisible = 0;SetConsoleCursorInfo(handle, &cursor_info);
}// 设置光标位置
void SetPos(int x, int y)
{// 获取句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);// 设置光标位置COORD pos = {x, y};SetConsoleCursorPosition(handle, pos);
}void PrintSnake(void)
{SetPos(45, 10);wprintf(L"欢迎来到贪吃蛇游戏");
}// 打印欢迎信息
void WelcomeToGame(void)
{PrintSnake();// 暂停SetPos(45, 20);system("pause");
}// 游戏介绍
void GameIntroduction(void)
{// 清屏system("cls");SetPos(45, 10);wprintf(L"游戏介绍:");SetPos(45, 12);wprintf(L"1. 使用↑,↓,←,→控制蛇的移动");SetPos(45, 14);wprintf(L"2. 吃到食物蛇的长度加1");SetPos(45, 16);wprintf(L"2. F3加速, F4减速");SetPos(45, 18);wprintf(L"3. 空格暂停");SetPos(45, 20);wprintf(L"4. Esc退出游戏");// 暂停SetPos(45, 22);system("pause");
}// 地图绘制 
// 上(0,0) - (56,0)
// 下(0,26) - (56,26)
// 左(0,0) - (0,26)
// 右(56,0) - (56,26)
// 注意打印的是宽字符 占两个x坐标 因此左右打印的时候要每打印一个x坐标加2
void MapDraw(void)
{   // 清屏system("cls");// 上墙for (int i = 0; i < 57; i+=2){SetPos(i, 0);wprintf(L"%c", WALL);}// 下墙for (int i = 0; i < 57; i+=2){SetPos(i, 26);wprintf(L"%c", WALL);}// 最上面和最下面已经打印过了// 左墙for (int i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}// 右墙for (int i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%c", WALL);} 
}// 打印静态帮助信息
// 单个食物分数,总分数的名称
// 操作说明
void PrintStaticHelp(void)
{SetPos(70, 5);wprintf(L"单个食物分数: 10");SetPos(70, 7);wprintf(L"总分数:        0");SetPos(70, 9);wprintf(L"操作说明:");SetPos(70, 11);wprintf(L"↑ : 上移");SetPos(70, 13);wprintf(L"↓ : 下移");SetPos(70, 15);wprintf(L"← : 左移");SetPos(70, 17);wprintf(L"→ : 右移");SetPos(70, 19);wprintf(L"F3: 加速");SetPos(70, 21);wprintf(L"F4: 减速");SetPos(70, 23);wprintf(L"空格: 暂停");SetPos(70, 25);wprintf(L"Esc: 退出");
}// 蛇初始化
// 在地图的(6,6)位置初始化蛇
// 蛇的长度为5
// 使用头插法
void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;int i = 0;// 创建蛇身节点 并初始化坐标    for (i = 0; i < 5; i++){pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("pSnakeNode malloc failed");exit(1);}node->x = POS_X + i * 2;node->y = POS_Y;node->next = NULL;// 头插法if (ps->_pSnake == NULL){ps->_pSnake = node;}else{node->next = ps->_pSnake;ps->_pSnake = node;}}// 打印蛇cur = ps->_pSnake;while (cur != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}
}// 创建食物节点
void CreatFood(pSnake ps)
{// 申请节点空间pSnakeNode foodnode = (pSnakeNode)malloc(sizeof(SnakeNode));if (foodnode == NULL){perror("foodnode malloc failed");exit(1);}foodnode->next = NULL;// 随机生成食物坐标 在墙范围内但是不能在蛇身上 而且x坐标是偶数while (1){int x = rand() % 54 + 2;int y = rand() % 24 + 2;pSnakeNode cur = ps->_pSnake;while (cur != NULL){if (cur->x == x && cur->y == y || x % 2 != 0){break;}cur = cur->next;}if (cur == NULL){foodnode->x = x;foodnode->y = y;break;}}// 打印食物SetPos(foodnode->x, foodnode->y);wprintf(L"%c", FOOD);ps->_pFood = foodnode;
}// 撞墙检测
// 撞墙返回1 否则返回0
int IsKillByWall(pSnake ps)
{if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26){return 1;}return 0;
}// 撞自己检测
// 撞自己返回1 否则返回0
int IsKillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;// 从第二个节点开始遍历 并判断是否和蛇头坐标相同while (cur != NULL){if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){return 1;}cur = cur->next;}return 0;
}// 吃掉食物后的处理
void EatFood(pSnake ps)
{// 加分ps->_totalScore += ps->_foodScore;// 打印总分数SetPos(82, 7);printf("%4d", ps->_totalScore);// 是 头插食物节点 创建新节点ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;// 打印新蛇头SetPos(ps->_pSnake->x, ps->_pSnake->y);wprintf(L"%c", BODY);// 创建新食物CreatFood(ps);
}// 没有迟到食物的处理
void NotEatFood(pSnake ps, int x, int y)
{// 创建新节点并头插pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));if (node == NULL){perror("node malloc failed");exit(1);}node->x = x;node->y = y;node->next = ps->_pSnake;ps->_pSnake = node;// 打印新蛇 顺便删除尾节点 释放空间 打印空格pSnakeNode cur = ps->_pSnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%c", BODY);cur = cur->next;}SetPos(cur->x, cur->y);wprintf(L"%c", BODY);SetPos(cur->next->x, cur->next->y);wprintf(L"  ");free(cur->next);cur->next = NULL;
}// 蛇的移动 -- 主游戏程序 在此循环
// 吃到食物 创建新的食物节点
// 没有吃到食物头插新节点删除尾节点并释放空间
// 减速睡眠时间-30 睡眠时间最少减4次 单个食物分数加2
// 加速睡眠时间+30 睡眠时间最多加4次 单个食物分数减2
void SnakeMove(pSnake ps)
{int x = 0;int y = 0;again:while (ps->_status == OK){// 按键检测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)){ps->_status = PAUSE;}else if (KEY_PRESS(VK_F3) && ps->_foodScore < 20){ps->_sleepTime -= 30;ps->_foodScore += 2;// 更改单个食物分数SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_F4) && ps->_foodScore > 2){ps->_sleepTime += 30;ps->_foodScore -= 2;// 更改单个食物分数SetPos(84, 5);printf("%2d", ps->_foodScore);}else if (KEY_PRESS(VK_ESCAPE)){ps->_status = ESC;}// 设置下一个节点的x,y坐标switch (ps->_dir){case UP:x = ps->_pSnake->x;y = ps->_pSnake->y - 1;break;case DOWN:x = ps->_pSnake->x;y = ps->_pSnake->y + 1;break;case LEFT:x = ps->_pSnake->x - 2;y = ps->_pSnake->y;break;case RIGHT:x = ps->_pSnake->x + 2;y = ps->_pSnake->y;break;}// 判断是否吃到食物if (ps->_pFood->x == x && ps->_pFood->y == y){EatFood(ps);}else {NotEatFood(ps, x, y);}// 撞墙检测if (IsKillByWall(ps)){ps->_status = KILL_BY_WALL;}// 撞自己检测if (IsKillBySelf(ps)){ps->_status = KILL_BY_SELF;}Sleep(ps->_sleepTime);}while (ps->_status == PAUSE){if (KEY_PRESS(VK_SPACE)){ps->_status = OK;}goto again;}// 撞墙打印信息if (IsKillByWall(ps)){SetPos(45, 10);wprintf(L"墙墙被你撞西了,110把你抓走了");}// 撞自己打印信息if (IsKillBySelf(ps)){SetPos(45, 10);wprintf(L"自己撞自己了,120把你带走了");}return;
}// 游戏前的初始化
void GameStart(pSnake ps)
{// 初始化控制台CmdInit();CursorHide();// 本地化配置setlocale(LC_ALL, "");WelcomeToGame();GameIntroduction();MapDraw();InitSnake(ps);CreatFood(ps);
}// 游戏运行
void GameRun(pSnake ps)
{PrintStaticHelp();SnakeMove(ps);SetPos(45, 28);system("pause");
}// 善后 释放蛇节点 以及食物节点
void GameEnd(pSnake ps)
{// 释放蛇节点pSnakeNode cur = NULL;while (ps->_pSnake != NULL){cur = ps->_pSnake;ps->_pSnake = ps->_pSnake->next;free(cur);}// 释放食物节点free(ps->_pFood);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/347548.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

金士顿U盘被写保护的解决方法

1.适用的U盘芯片信息 USB设备ID: VID 0951 PID 1666 设备供应商: Kingston 设备名称: DataTraveler 3.0 设备修订版: 0110 产品制造商: Kingston 产品型号: DataTraveler 3.0 产品修订版: PMAP 主控厂商: Phison(群联) 主控型号: PS2251-07(PS2307) - F/W 08.03.50 [2018-…

ViewModel原理分析

认识 ViewModel ViewModel 是一种用来存储和管理UI相关数据的类。 ViewModel 的作用可以从两个方面去理解&#xff1a; UI界面控制器&#xff1a;在最初的MVC模式中&#xff0c;由于 Activity / Fragment 承担的职责过重&#xff0c;因此在后续的 MVP、MVVM 模式中&#xff…

【C++进阶】模板与仿函数:C++编程中的泛型与函数式编程思想

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;C “ 登神长阶 ” &#x1f921;往期回顾&#x1f921;&#xff1a;栈和队列相关知识 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀模板进阶 &#x1f9e9;<&…

OpenGauss数据库-8.权限管理

第2关&#xff1a;权限设置 gsql -d postgres -U gaussdb -W passwd123123 CREATE ROLE lily WITH CREATEDB PASSWORD passwd123123; GRANT lily TO gaussdb; 第3关&#xff1a;管理员 gsql -d postgres -U gaussdb -W passwd123123 CREATE USER peter WITH SYSADMIN PASSWOR…

uniapp地图选择位置

直接上代码 通过一个点击事件调用官方api即可调用 点击调用成功后显示如下 然后选择自己所需要的位置即可

解读光纤模块的参数有哪些

光模块的具体参数有传输速率、传输距离、中心波长、光纤类型、光口类型、工作温度范围、最大功耗等。下面给大家详解一下各个参数的作用 因为光纤本身对光信号有色散、损耗等副作用。因此不同类型的光源发出的光所能传输的距离不一样。对接光接口时&#xff0c;应根据最远的信号…

AutoKG:为语言模型打造高效自动化知识图谱

在人工智能领域&#xff0c;大型语言模型&#xff08;LLMs&#xff09;如BERT、RoBERTa、T5和PaLM等&#xff0c;以其在自然语言处理&#xff08;NLP&#xff09;任务中的卓越性能而著称。然而&#xff0c;这些模型在提供信息时可能会产生“幻觉”&#xff0c;即提供看似合理但…

Vue 路由传递参数 query、params

1、to的对象写法,绑定参数 <template> 2 <ul> 3 <li v-for"m in messlist" :key"m.id"> 4 <router-link :to"{ //使用params时&#xff0c;这个路径必须用name及别名......name: xiangqing, path: /bbb/message/deta…

Python酷库之旅-比翼双飞情侣库(01)

目录 一、xlrd库的由来 二、xlrd库优缺点 1、优点 1-1、支持多种Excel文件格式 1-2、高效性 1-3、开源性 1-4、简单易用 1-5、良好的兼容性 2、缺点 2-1、对.xlsx格式支持有限 2-2、功能相对单一 2-3、更新和维护频率低 2-4、依赖外部资源 三、xlrd库的版本说明 …

如何格式化SQL语句(以MySQL和SQLynx为例)

目录 1 SQLynx 格式化MySQL的SQL 语句功能介绍 1.1 主要特点 1.2 使用步骤 1.3 操作示例 2 结论 SQLynx 是一款功能强大的 SQL 集成开发环境&#xff08;IDE&#xff09;&#xff0c;它提供了多种工具和功能来提高用户的生产力和代码质量。其中&#xff0c;SQL 语句的格式…

Mybatis Log Free

安装后重启 在 application.yml 配置 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 选择效果

Java---BigInteger和BigDecimal和枚举

1.简介 1.BigInteger可以支持任意长度的整数 2.BigDecimal可以支持任意精度的浮点数 3.用来做精确计算 2.创建方式 new BigInteger(); new BigInteger(参数1,进制)&#xff1a;可以将不同进制转成10进制显示 new BigDecimal(); BigInteger.valueOf(); BigDecimal.valueOf();…

vue30:props详解

1&#xff1a;props类型校验&#xff1a; 2&#xff1a;props类型自定义校验&#xff1a; 3:子组件不能通过prop直接修改父组件的数据

阿里云物联网平台案例教程

1、定义&#xff1a; ​ 物联网&#xff08;简称IOT&#xff09;把任何物体与物联网相连接&#xff0c;进行消息的交换和通信&#xff0c;实现对物品的智能化识别。简单说是&#xff1a;物联网就是把所有的物体连接起来相互作用&#xff0c;形成一个互联互通的网络&#xff0c…

[发布]嵌入式系统远程测控软件-基于Qt

目录 一. 引言二. 软件功能2.1 原理2.2 软件功能2.3 运行环境 三. 软件操作使用3.1 软件界面3.2 软件功能使用详解3.2.1 连接3.2.2 数据监测&#xff08;串口示波器&#xff09;3.2.3 数据修改3.2.4 数据保存 3.3 软件的硬件连接 四. 通信协议——STM32移植篇4.1 通信协议4.2 S…

提升易用性,OceanBase生态管控产品的“从小到大”

2022年&#xff0c;OceanBase发布4.0版本“小鱼”&#xff0c;并首次公开提出了单机分布式一体化这一理念&#xff0c;旨在适应大小不同规模的工作负载&#xff0c;全面满足用户数据库“从小到大”全生命周期的需求。当时&#xff0c;我们所说的“从小到大”主要聚焦于数据库的…

JVM产生FullGC的原因有哪些?

JVM产生FullGC的原因有哪些&#xff1f; 在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;垃圾回收&#xff08;Garbage Collection&#xff0c;简称GC&#xff09;是一个非常重要的机制。GC的目的是自动管理内存&#xff0c;回收不再使用的对象&#xff0c;防止内存泄…

数据合规怎么做?哪些机构可以做数据合规

企业将数据资源入表的工作是一项复杂而全面的任务 财务部门负责统计数据资源的成本、销售数据等信息,并确保数据资源的会计处理符合会计要求&#xff1b; 数据部门则负责统计数据成本来源、价值实现路径等信息&#xff1b; 法务部门需要确认数据的收集和使用遵循相关的合规要求…

UE5 Sequencer 使用指导 - 学习笔记

https://www.bilibili.com/video/BV1jG411L7r7/?spm_id_from333.337.search-card.all.click&vd_source707ec8983cc32e6e065d5496a7f79ee6 Sequencer 01 1.1 调整视口 调整窗口数量 调整视口类型为Cinematic视口 视口显示网格&#xff0c;或者条件参考线 1.2 关卡动画与…

Redis链表

Redis链表 C语言没有内置链表&#xff0c;Redis自己构建的链表 链表在redis中的实现 typedef struct list {//表头节点listNode *head;//表尾节点listNode *tail;//节点数量unsigned long len;//节点值复制函数void *(*dup) (void *ptr);//节点值释放函数void (*free) (void …