项目-贪吃蛇-代码逻辑梳理和实现(手搓无ai)
·
目录
做有意义的事,过意义的人生!欢迎大家一起讨论!创作不易,小博主求求赞啦!
一、游戏效果预览
实现的功能
- 方向控制( 上 / 下 / 左 / 右)
- 随机生成食物
- 蛇吃到食物变长、得分增加
- 撞墙 / 撞自身游戏结束
- 实时显示分数
- 游戏速度随分数提升变快
二、核心思路拆解
- 蛇的表示:用结构体存储坐标,用数组存储蛇的每一节身体
- 地图:固定大小的矩形区域,边界为围墙
- 食物:随机生成坐标,不能出现在蛇身上
- 移动逻辑:蛇头向指定方向移动,身体跟随前一节位置
- 碰撞检测:判断蛇头是否撞墙 / 撞自己
- 键盘控制:监听按键,实时改变蛇的移动方向
三,Win32API补充说明
1. COORD
控制台屏幕上的坐标COORD
COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕上的坐标
typedef struct _COORD { SHORT X; SHORT Y; } COORD, *PCOORD;
2. GetStdHandle
HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
3. GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息。
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
4. CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息。
CursorInfo.bVisible = false; //隐藏控制台光标
5. SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
6. SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调 ⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
7.GetAsyncKeyState
获取按键情况 将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果
返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态 是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
四、贪吃蛇游戏思路整理
1.地图
1.游戏菜单
在贪吃蛇游戏地图中,我们要创建两大部分 游戏菜单和游戏界面 ,我们先从游戏菜单说起。在这里我们用到SetPos函数,它的功能是用来定义光标位置,这样我们就能在控制台的不同位置打印我们需要的文字。
void SetPos(short x, short y) { COORD pos = { x,y }; HANDLE hOutput = NULL; //获取标准输出的句柄(用来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos SetConsoleCursorPosition(hOutput, pos); }void WelcomeToGame() { SetPos(40, 15); printf("欢迎来到贪吃蛇小游戏"); SetPos(40, 25);//让按任意键继续的出现的位置好看点 system("pause"); system("cls"); SetPos(25, 12); printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n"); SetPos(25, 13); printf("加速将能得到更高的分数。\n"); SetPos(40, 25);//让按任意键继续的出现的位置好看点 system("pause"); system("cls"); }
2.游戏界面
这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道 该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。
控制台窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。 在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符 ●,打印⻝物使⽤宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识,过去C语⾔并不适合⾮英语国家(地区)使⽤。 C语⾔最初假定字符都是但⾃⼰的。但是这些假定并不是在世界的任何地⽅都适⽤。
后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊和宽字符的类型 wchar_t 和宽字符的输⼊和输出函数,加⼊和<locale.h>头⽂件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。
setlocale函数
char* setlocale (int category, const char* locale);
1 setlocale(LC_ALL, "C");//切换到正常环境
2 setlocale(LC_ALL, " ");//切换到本地环境
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤" "作为第2个参数,调⽤setlocale
函数就可以切换到本地模式,这种模式下程序会适应本地环境。⽐如:切换到我们的本地模式后就⽀
持宽字符(汉字)的输出等。
了解完宽字符的打印后,看到这个页面时,我们会思考如何打印游戏的边框,可是宽字符和普通字符的区别在哪呢?这里可以通过图解对比。
我们假设实现⼀个棋盘27⾏,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙,即可完成基本的游戏界面。
void CreateMap()
{
int i = 0;
//上(0,0)-(56, 0)
SetPos(0, 0);
for (i = 0; i < 58; i += 2)
{
SetPos(i,0);
wprintf(L"%c", WALL);
}
//下(0,26)-(56, 26)
SetPos(0, 26);
for (i = 0; i < 58; i += 2)
{
SetPos(i, 26);
wprintf(L"%c", WALL);
}
//左
//x是0,y从1开始增长
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%c", WALL);
}
//x是56,y从1开始增长
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%c", WALL);
}
}
2.游戏角色
蛇和⻝物
初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24, 5)处开始出现 蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。
关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然后打印★。
1.蛇的设计
在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信
息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏, 所以蛇节点结构如下:
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇要管理整条贪吃蛇,我们再封装⼀个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
};
蛇的游戏状态,可以一一 列举,使⽤枚举
enum GAME_STATUS
{
OK,//正常运⾏
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//咬到⾃⼰
END_NOMAL//正常结束
};
2.食物的设计
对于食物的设计,我们要思考它在游戏中扮演的角色作用,蛇通过吃它,增加身体长度,对于由链表组成的蛇体,食物必然也是单个节点,然后遇到蛇头后,被连接到链表中。对此,我们首先要创建单节点充当食物的角色,然后它会出现在哪呢?是完全随机的吗?我们思考下蛇头出现的位置,对于宽字符,x的坐标必然是偶数,那么食物的x坐标也需要出现在偶数位,才能与蛇头对齐,其次它的位置也不能是位于墙体上,我们要对x和y进行限制。
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐。
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针
//食物不能和蛇身冲突
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建食物
if (pFood == NULL)
{
perror("CreateFood::malloc()");
return;
}
else
{
pFood->x = x;
pFood->y = y;
SetPos(pFood->x, pFood->y);
wprintf(L"%c", FOOD);
ps->_pFood = pFood;
}
}
3.游戏运行核心逻辑
我们先看下头文件中的函数代码定义进行思路的梳理。
//游戏开始前的初始化 void GameStart(pSnake ps);
//游戏运行过程 void GameRun(pSnake ps);
//游戏结束 void GameEnd(pSnake ps);
//设置光标的坐标 void SetPos(short x, short y);
//欢迎界面 void WelcomeToGame();
//打印帮助信息 void PrintHelpInfo();
//创建地图 void CreateMap();
//初始化蛇 void InitSnake(pSnake ps);
//创建食物 void CreateFood(pSnake ps);
//暂停响应 void pause();
//下一个节点是食物 int NextIsFood(pSnakeNode psn, pSnake ps);
//吃食物 void EatFood(pSnakeNode psn, pSnake ps);
//不吃食物 void NoFood(pSnakeNode psn, pSnake ps);
//撞墙检测 int KillByWall(pSnake ps);
//撞自身检测 int KillBySelf(pSnake ps);
//蛇的移动 void SnakeMove(pSnake ps);
//游戏初始化 void GameStart(pSnake ps);
//游戏运行 void GameRun(pSnake ps);
//游戏结束 void GameEnd(pSnake ps);
1. 蛇的本质 蛇是一串连续的点。
只有蛇头会主动动,身体都是跟着前面一节走。
2. 移动规则(最核心)
从尾巴开始,每一节身体,都变成前一节的位置。
最后只需要移动蛇头,整个蛇就动起来了。
一句话:身体跟着走,蛇头往前冲。
3. 方向控制 蛇只能上下左右四个方向移动。
不能直接掉头(比如正在往右走,不能立刻往左),否则会瞬间撞死自己。
4. 食物机制 地图上随机出现一个食物。当蛇头碰到食物:蛇长度加 1+ 分数增加+游戏速度变快+重新生成一个
5. 死亡判定
只有两种情况会死:撞墙:蛇头走出地图边界 撞自己:蛇头碰到自己身体任意一节
6. 游戏主循环
整个游戏就是不断重复:看有没有按键,改方向。
蛇移动一步判断是否吃到食物
判断是否死亡
刷新面稍微停顿一下控制速度
回到第一步继续循环
我们一个一个来梳理,具体的代码实现,我们先不管。
1.游戏开始
创建地图,蛇,食物。
void GameStart(pSnake ps)
{
system("mode con lines=30 cols=100");
system("title 贪吃蛇");
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄(用来标识不同设备的数值
CONSOLE_CURSOR_INFO CursorInfo; //影藏光标操作
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
//打印欢迎界面
WelcomeToGame();
//打印地图
CreateMap();
//初始化蛇
InitSnake(ps);
//创造第一个食物
CreateFood(ps);
}
2.游戏运行
规则提示,计算分数,蛇的移动,游戏的结束与暂停。
void GameRun(pSnake ps)
{
PrintHelpInfo();
do
{
SetPos(64, 10);
printf("得分:%d分", ps->_Socre);
printf(" 每个食物得分:%d分", ps->_Add);
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))
{
Sleep(3000);
while (1)
{
Sleep(10);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_Status = END_NOMAL;
break;
}
else if (KEY_PRESS(VK_F3))
{
if (ps->_SleepTime >= 50)
{
ps->_SleepTime -= 30;
ps->_Add += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->_SleepTime <350)
{
ps->_SleepTime += 30;
ps->_Add -= 2;
if (ps->_SleepTime == 350)
{
ps->_Add = 1;
}
}
}
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_Status == OK);
}
3.游戏结束
归纳失败情况,结束程序。
void GameEnd(pSnake ps)
{
pSnakeNode cur = ps->_pSnake;
SetPos(64, 5);
switch (ps->_Status)
{
case END_NOMAL:
printf("您主动退出游戏\n");
break;
case KILL_BY_SELF:
printf("您撞上自己了 ,游戏结束!\n");
break;
case KILL_BY_WALL:
printf("您撞墙了,游戏结束!\n");
break;
}
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
4.详细代码实现
1.蛇的初始化
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("InitSnake()::malloc()");
return;
}
//设置坐标
cur->next = NULL;
cur->x = POS_X + i * 2;
cur->y = POS_Y;
//头插法
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"%c", BODY);
cur = cur->next;
}
//初始化贪吃蛇数据
ps->_SleepTime = 200;
ps->_Socre = 0;
ps->_Status = OK;
ps->_Dir = RIGHT;
ps->_Add = 10;
}
2.食物的初始化
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐。
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针
//食物不能和蛇身冲突
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建食物
if (pFood == NULL)
{
perror("CreateFood::malloc()");
return;
}
else
{
pFood->x = x;
pFood->y = y;
SetPos(pFood->x, pFood->y);
wprintf(L"%c", FOOD);
ps->_pFood = pFood;
}
}
3.蛇与食物位置关系(遇到与没遇到)
int NextIsFood(pSnakeNode psn, pSnake ps)
{
return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
void EatFood(pSnakeNode psn, pSnake ps)
{
//头插法
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
//打印蛇
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
ps->_Socre += ps->_Add;
free(ps->_pFood);
CreateFood(ps);
}
void NoFood(pSnakeNode psn, pSnake ps)
{
//头插法
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
//打印蛇
while (cur->next->next)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
//最后一个位置打印空格,然后释放节点
SetPos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
4.死亡原因(撞墙和撞到自己)
int KillByWall(pSnake ps)
{
if ((ps->_pSnake->x == 0)
|| (ps->_pSnake->x == 56)
|| (ps->_pSnake->y == 0)
|| (ps->_pSnake->y == 26))
{
ps->_Status = KILL_BY_WALL;
return 1;
}
return 0;
}
int KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->_pSnake->next;
while (cur)
{
if ((ps->_pSnake->x == cur->x )&& (ps->_pSnake->y == cur->y))
{
ps->_Status = KILL_BY_SELF;
return 1;
}
cur = cur->next;
}
return 0;
}
5.蛇的移动(核心)
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);
}
KillByWall(ps);
KillBySelf(ps);
自取哦!
加油少年 (wxx547803_0) - Gitee.com
做有意义的事,过意义的人生!欢迎大家一起讨论!创作不易,小博主求求赞啦!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)