目录
一、游戏说明
- 贪吃蛇地图绘制
- 蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞自身死亡
- 计算得分
- 蛇身加速、减速
- 暂停游戏
二、地图坐标
我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,如下:
三、头文件
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>
#define Case break;case
#define WALL L'□'
#define BODY L'●'
#define FOOD L'☆'
//默认的起始坐标
#define POS_X 24
#define POS_Y 5
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
//贪吃蛇,蛇身节点的定义
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;
enum GAME_STATUS
//游戏状态
{
OK = 1,//正常运行
ESC,//按了ESC键退出,正常退出
KILL_BY_WALL,//撞墙
KILL_BY_SELF//撞到自身
};
//行走的方向
enum DIRECTION
//方向
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//贪吃蛇
typedef struct Snake
{
pSnakeNode pSnake;//维护整条蛇的指针,是指向蛇头
pSnakeNode pFood;//指向食物的指针
int Score;//当前累积的分数
int FoodWeight;//一个食物的分数,默认每个食物10分
int SleepTime;//每走一步休眠时间?
//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢
enum GAME_STATUS status;//游戏当前的状态
enum DIRECTION dir;//蛇当前走的方向,蛇头的方向默认是向右
//...
}Snake,* pSnake;
//typedef struct Snake* pSnake;
//定位控制台的光标位置
void SetPos(int x, int y);
//游戏开始的准备
void GameStart(pSnake ps);
//打印欢迎界面
void welcomeToGame();
//绘制地图
void CreateMap();
//初始化蛇
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//游戏运行的整个逻辑
void GameRun(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
//蛇移动的函数- 每次走一步
void SnakeMove(pSnake ps);
//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pNext);
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext);
//下一步要走的位置处不是食物,不吃食物
void NotEatFood(pSnake ps, pSnakeNode pNext);
//检测是否撞墙
void KillByWall(pSnake ps);
//检测是否撞自己
void KillBySelf(pSnake ps);
//游戏结束的资源释放
void GameEnd(pSnake ps);
四、蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。
注意 :蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。
五、数据结构设计
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信
息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行。
#define WALL L'□' 墙
#define BODY L'●' 蛇身
#define FOOD L'☆' 食物
蛇节点结构如下:
//贪吃蛇,蛇身节点的定义
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
//是一个指向下一个 SnakeNode 类型节点的指针,用于构建链表来表示蛇的身体。
}SnakeNode, * pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;
封装一个Snake的结构来维护整条贪吃蛇:
pSnakeNode pSnake:这是一个指向 SnakeNode 类型的指针,代表蛇的头部。通常,贪吃蛇的实现会用一个链表来表示蛇的身体,其中每个节点(SnakeNode)代表蛇身体的一部分,而 pSnake 指向这个链表的第一个节点,即蛇头。
pSnakeNode pFood:这是一个指向 SnakeNode 类型的指针,代表食物的位置。在贪吃蛇游戏中,食物会被随机放置在游戏区域内,当蛇吃到食物时,这个食物会被移除,并且蛇的身体会增长。
enum GAME_STATUS status;:这是一个枚举类型,表示游戏当前的状态。具体的枚举值没有在代码中给出,但可能包括"游戏中"、"游戏结束"等状态。
enum DIRECTION dir;:这是一个枚举类型,表示蛇当前移动的方向。具体的枚举值也没有在代码中给出,但通常包括"向上"、"向下"、"向左"、"向右"等方向。
typedef struct Snake
{
pSnakeNode pSnake;//维护整条蛇的指针,是指向蛇头
pSnakeNode pFood;//指向食物的指针
int Score;//当前累积的分数
int FoodWeight;//一个食物的分数,默认每个食物10分
int SleepTime;//每走一步休眠时间?
//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢
enum GAME_STATUS status;//游戏当前的状态
enum DIRECTION dir;//蛇当前走的方向,蛇头的方向默认是向右
//...
}Snake,* pSnake;
//typedef struct Snake* pSnake;
蛇的方向,可以一一列举,使用枚举:
//行走的方向
enum DIRECTION
//方向
{
UP = 1,
DOWN,
LEAF,
RIGHT
};
游戏状态,可以一一列举,使用枚举:
enum GAME_STATUS
//游戏状态
{
OK = 1,//正常运行
ESC,//按了ESC键退出,正常退出
KILL_BY_WALL,//撞墙
KILL_BY_SELF//撞到自身
};
六、Snake.c
5.1、游戏开始函数
void GameStart(pSnake ps)
{
//设置控制台的信息,窗口大小,窗口名
system("mode con cols=120 lines=40");
system("title 贪吃蛇");
//隐藏光标
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);//设置光标信息
//打印欢迎信息
welcomeToGame();
//绘制地图
CreateMap();
//初始化蛇
InitSnake(ps);
//创建食物
CreateFood(ps);
}
定位控制台的光标位置
//定位控制台的光标位置
void SetPos(int x, int y)
{
//获得设备句柄
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
//根据句柄设置光标的位置
COORD pos = { x,y };
SetConsoleCursorPosition(handle, pos);
}
欢迎来到贪吃蛇游戏
void welcomeToGame()
{
//欢迎信息
SetPos(35, 10);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(38, 20);
system("pause");
system("cls");
//功能介绍信息
SetPos(15, 10);
printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速\n");
SetPos(15, 11);
printf("加速能得到更高的分数");
SetPos(38, 20);
system("pause");
system("cls");
}
创建一个地图
创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L
打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
void CreateMap()
{
int i = 0;
//上
SetPos(0, 0);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 25);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
初始化创建蛇身的节点
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。
创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,蛇的状态,每个食物的分数。
结构体成员:记录它们的坐标:(x,y),和记录下一个位置的前驱结构体指针:next。
void InitSnake(pSnake ps)
{
//创建五个蛇身的节点
pSnakeNode cur = NULL;
int i = 0;
for (i = 0; i < 5; ++i)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InsitSnkae():malloc()");
return;
}
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
cur->next = NULL;
//头插法
if (ps->pSnake == NULL)
{
ps->pSnake = cur;
}
else {
cur->next = ps->pSnake;
ps->pSnake = cur;
}
}
//打印蛇身
cur = ps->pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//贪吃蛇的其他信息
ps->dir = RIGHT;
ps->FoodWeight = 10;
ps->pFood = NULL;
ps->Score = 0;
ps->SleepTime = 200;
ps->status = OK;
}
创建第一个食物
-
先随机生成食物的坐标
-
x坐标必须是2的倍数
-
食物的坐标不能和蛇身每个节点的坐标重复
-
创建食物节点,打印食物
void CreateFood(pSnake ps)
{
int x = 0;//x范围: 2~54 -> 0~52 + 2 -> rand()%53 + 2
int y = 0;//y范围: 1~25 -> 0~24 + 1 -> rand()%24 + 1again:
do {
x = rand() % 53 + 2;
y = rand() % 24 + 1;
} while (x % 2 != 0);//坐标和蛇的身体的每个几点的坐标比较 pSnakeNode cur = ps->pSnake; while (cur) { if (x == cur->x && y == cur->y) { goto again; } cur = cur->next; } //创建食物 pSnakeNode pFood = malloc(sizeof(SnakeNode)); if (pFood == NULL) { perror("CreateFood:malloc()"); return; } pFood->x = x; pFood->y = y; ps->pFood = pFood; SetPos(x, y); wprintf(L"%lc", FOOD);
}
5.2、游戏运行函数
游戏运行期间,右侧打印帮助信息,提示玩家
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
确定了蛇的方向和速度,蛇就可以移动了。
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
//检测按键
do
{
//当前的分数情况
SetPos(62, 10);
printf("总分:%5d\n", ps->Score);
SetPos(62, 11);
printf("食物的分支:%02d\n", ps->FoodWeight);
//检测按键
//上、下、左、右、ESC、空格、F3、F4
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_ESCAPE))
{
ps->status = ESC;
break;
}
else if (KEY_PRESS(VK_SPACE))
{
//游戏要暂停
pause();//暂停和开始
}
else if (KEY_PRESS(VK_F3))
{
if (ps->SleepTime >= 80)
{
ps->SleepTime -= 30;
ps->FoodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->FoodWeight > 2)
{
ps->SleepTime += 30;
ps->FoodWeight -= 2;
}
}
//走一步
SnakeMove(ps);
//睡眠一下
Sleep(ps->SleepTime);
}while(ps->status == OK);
}
检测按键状态,我们封装了一个宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
打印帮助信息
//打印帮助信息
void PrintHelpInfo()
{
SetPos(62, 15);
printf("1. 不能穿墙. 不能咬到自己");
SetPos(62, 16);
printf("2. 用 ↑ . ↓ . ← . → 来控制蛇的移动");
SetPos(62, 17);
printf("3. F3是加速,F4是减速");
SetPos(62, 19);
printf(" ");
}
暂停函数
void pause()
{
while (1)
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
下一个是否是食物
//pSnakeNode psn 是下一个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(pSnake ps, pSnakeNode pNext)
{
if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)
return 1;
else
return 0;
}
下一步要走的位置处就是食物,就吃掉食物
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext)
{
pNext->next = ps->pSnake;
ps->pSnake = pNext;
pSnakeNode cur = ps->pSnake;
//打印蛇身
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->Score += ps->FoodWeight;
//释放旧的食物
free(ps->pFood);
//新建食物
CreateFood(ps);
}
如果下一步不是食物
将下一个节点头插入蛇的身体,并将之前蛇身最后一个节点打印为空格,放弃掉蛇身的最后一个节点
//pSnakeNode psn 是下一个节点的地址
//pSnake ps 维护蛇的指针
void NotEatFood(pSnake ps, pSnakeNode pNext)
{
//头插法
pNext->next = ps->pSnake;
ps->pSnake = pNext;
//释放尾结点
pSnakeNode cur = ps->pSnake;
while (cur->next->next)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//将尾节点的位置打印成空白字符
SetPos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;//易错
}
检测是否撞墙
判断蛇头的坐标是否和墙的坐标冲突
//检测是否撞墙
void KillByWall(pSnake ps)
{
if (ps->pSnake->x == 0 ||
ps->pSnake->x == 56 ||
ps->pSnake->y == 0 ||
ps->pSnake->y == 25)
{
ps->status = KILL_BY_WALL;
}
}
检测是否撞自己
判断蛇头的坐标是否和蛇身体的坐标冲突
//检测是否撞自己
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;
return;
}
cur = cur->next;
}
}
蛇移动的函数
先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。
确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理
(EatFood),如果不是食物则做前进一步的处理(NoFood)。
蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。
void SnakeMove(pSnake ps)
{
pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNext == NULL)
{
perror("SnakeMove():malloc()");
return;
}
pNext->next = NULL;
switch (ps->dir)
{
case UP:
pNext->x = ps->pSnake->x;
pNext->y = ps->pSnake->y - 1;
break;
Case DOWN:
pNext->x = ps->pSnake->x;
pNext->y = ps->pSnake->y + 1;
Case LEFT:
pNext->x = ps->pSnake->x - 2;
pNext->y = ps->pSnake->y;
Case RIGHT:
pNext->x = ps->pSnake->x + 2;
pNext->y = ps->pSnake->y;
break;
}
//下一个坐标是否是食物
if (NextIsFood(ps, pNext))
{
//是食物就吃掉
EatFood(ps, pNext);
}
else {
//不是食物就正常一步
NotEatFood(ps, pNext);
}
//检测撞墙
KillByWall(ps);
//检测是否撞自己
KillBySelf(ps);
}
七、游戏结束的资源释放
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。
//游戏结束的资源释放
void GameEnd(pSnake ps)
{
SetPos(20, 15);
switch (ps->status)
{
case ESC:
printf("主动退出游戏,正常退出\n");
Case KILL_BY_WALL:
printf("很遗憾,撞墙了,游戏结束\n");
Case KILL_BY_SELF:
printf("很遗憾,撞到自己了,游戏结束\n");
break;
}
//释放贪吃蛇的链表资源
pSnakeNode cur = ps->pSnake;
pSnakeNode del = NULL;
while (cur)
{
del = cur;
cur = cur->next;
free(del);
}
free(ps->pFood);
ps = NULL;
}
八、Test.c
void test()
{
//创建贪食蛇
Snake snake = { 0 };
//GameStart(&snake);//游戏开始前的初始化
//GameRun();//玩游戏的过程
//GameEnd();//善后的工作
int ch = 0;
do
{
Snake snake = { 0 };
GameStart(&snake);//游戏开始前的初始化
GameRun(&snake);//玩游戏的过程
GameEnd(&snake);//善后的工作
SetPos(15, 20);
printf("再来一局吗?(Y/N):");
ch = getchar();
getchar();// 清理\n
} while (ch == 'Y' || ch == 'y');
}
int main()
{
//修改适配本地的环境
setlocale(LC_ALL, "");
test();//贪吃蛇游戏的测试
SetPos(0, 30);
return 0;
}
今天就先到这了!!!
看到这里了还不给博主扣个:
⛳️ 点赞☀️收藏 ⭐️ 关注!
你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。