✨✨所属专栏:C语言✨✨
✨✨作者主页:嶔某✨✨
游戏源代码链接:function/贪吃蛇 · 钦某/c-language-learning - 码云 - 开源中国 (gitee.com)
最终实现效果:
实现基本的功能:
cpp
void set_pos(short x, short y);//定位光标位置
void Game_Start(pSnake ps);//初始化
void WelcomeToGame(void);//打印欢迎界面
void CreateMap(void);//创建地图
void InitSnake(pSnake ps);//初始化蛇身
void CreateFood(pSnake ps);//创建食物
void Game_Run(pSnake ps);//游戏运行逻辑
void SnakeMove(pSnake ps);//蛇的移动
bool NextIsFood(pSnakeNode pn,pSnake ps);//判断下一位置是否为食物
void EatFood(pSnakeNode pn, pSnake ps);//吃掉食物
void NoFood(pSnakeNode pn, pSnake ps);//下一个位置不是食物
void KillByWall(pSnake ps);//检测撞墙
void KillBySelf(pSnake ps);//检测撞自己
void Game_End(pSnake ps);//游戏善后
• 贪吃蛇地图绘制
• 蛇吃⻝物的功能(上、下、左、右⽅向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞⾃⾝死亡
• 计算得分
• 蛇⾝加速、减速
• 暂停游戏
运用到的知识:C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API等
根据游戏进程解释代码:
这里分为下面几个函数对游戏进行实现:
cpp
system("cls");
//创建贪吃蛇
pSnakeNode pSnake = NULL;
Snake snake = { 0 };
//初始化游戏
Game_Start(&snake);
//运行游戏
Game_Run(&snake);
结束游戏
Game_End(&snake);
游戏初始化:
cpp
void Game_Start(pSnake ps)//初始化
{
//0.设置窗口大小/名字
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//1.隐藏光标
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput,&CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(houtput,&CursorInfo);
//2.打印欢迎界面,介绍功能
WelcomeToGame();
//3.绘制地图
CreateMap();
//4.创建蛇
InitSnake(ps);
//5.创建食物
CreateFood(ps);
}
首先进入游戏,我们应该将窗口名称改为 "贪吃蛇" 并将光标隐藏掉。再在中间打印游戏信息。
这里用到的函数有:
(1)system("mode con cols=100 lines=30");
将窗口设置为100列,30行
(2)system("title 贪吃蛇");
将title设置为贪吃蛇
(3)system("pause");
暂停程序,按下任意键继续
(4)system("cls");
清理屏幕
cpp
/*******************************************/
//0.设置窗口大小/名字
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
/*******************************************/
void set_pos(short x, short y)
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x,y };
SetConsoleCursorPosition(houtput,pos);
}
/*******************************************/
void WelcomeToGame()
{
set_pos(40, 12);
wprintf(L"欢迎来到贪吃蛇小游戏");
set_pos(42,18);
system("pause");
system("cls");
set_pos(30, 12);
wprintf(L"用上下左右控制蛇的移动,按 {[ 加速, ]} 减速\n");
set_pos(38, 13);
wprintf(L"加速可以得到更高的分数\n");
set_pos(42, 18);
system("pause");
system("cls");
}
/********************************************/
之后我们要把地图打印出来:
分别打印上下左右的墙,将墙宏定义为WALL,字符为'□'
cpp
void CreateMap()
{
//上
int i = 0;
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//下
set_pos(0,25);
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//左
for (i = 1; i < 25; i++)
{
set_pos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i < 25; i++)
{
set_pos(56, i);
wprintf(L"%lc", WALL);
}
}
然后我们将贪吃蛇创建出来,将蛇有关的信息用结构体和枚举类型封装起来,将蛇身用链表维护。
蛇头指针:指向链表头节点的指针,方便对蛇身进行维护
食物指针:从开发角度来说,其实食物也是蛇的一个节点,当蛇头的下一个位置为食物时,将食物的节点头插到蛇身上面。
方向:对蛇的方向进行枚举
游戏状态:方便判断蛇的状态:(1)正常(2)撞墙(3)撞到自己(4)正常退出每一次while循环后判断游戏状态
食物权重:每次加速食物权重+2,减速-2。
总成绩:每吃掉一个食物,蛇身长度+1,分数+=食物权重。
每走一步的缓冲时间:缓冲时间越短,蛇走得越快;反之越慢。
cpp
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
typedef struct SnakeNode
{
//坐标
int x;
int y;
//下一个节点
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
enum DRECCTION//方向
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//蛇的状态
enum GAME_STATUS
{
OK,//正常
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//撞自己
END_NORMAL//正常退出
};
typedef struct Snake
{
pSnakeNode _pSnake;//指向蛇头的指针
pSnakeNode _pFood;//指向食物节点的指针
enum DRECCTION _dir;//蛇的方向
enum GAME_STAYUS _status;//游戏状态
int _food_weight;//一个食物都分数
int _score;//总成绩
int _sleep_time;//休息时间
}Snake,*pSnake;
/************************************/
void InitSnake(pSnake ps)//初始化蛇
{
int i = 0;
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake()::malloc()");
return;
}
cur->next = NULL;
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
//头插插入
if (ps->_pSnake == NULL)
ps->_pSnake = cur;
else
{
cur->next = ps->_pSnake;
ps->_pSnake = cur;
}
}
cur = ps->_pSnake;
while (cur)
{
set_pos(cur->x,cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//设置蛇的属性
ps->_dir = RIGHT;//默认
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//单位为ms
ps->_status = OK;
}
创建食物
生成随机数,赋给x,y。
这里x和y都有范围,不能超出地图边界,并且不能与蛇身重合。
cpp
void CreateFood(pSnake ps)//创建食物
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % 52 + 2;//2~54
y = rand() % 24 + 1;//1~25
} while (x % 2 != 0);//x为2的倍数
//不能和蛇身的坐标相同
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("CreateFood()::malloc");
return;
}
pFood->x = x;
pFood->y = y;
pFood->next = NULL;
set_pos(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;
}
游戏开始:
cpp
void Game_Run(pSnake ps)//游戏运行逻辑
{
//打印帮助信息
PrintHelpInfo();
do
{
//打印分数,食物权重
set_pos(64, 8);
printf("总分数:%d\n",ps->_score);
set_pos(64, 9);
printf("当前食物权重:%2d\n", ps->_food_weight);
//按键检测
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_OEM_6))
{
//加速
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_OEM_4))
{
//减速
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
SnakeMove(ps);//蛇走一步的过程
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
}
用dowhile循环对主体进行不断刷新
每次循环后让系统暂停一段时间(初始为200ms)
打印相关信息
蛇每走一步,分数都有可能变化,每次循环都打印一次。
按键检测
cpp
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
(1)用虚拟键值检测是否按下上下左右键,按下相应键并且蛇的当前方向不能与之相反。
(2)检测是否按下空格,按下就进函数:
cpp
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
break;
}
}
再次按下空格退出函数。
(3)检测加速减速,按下加速键就将缓冲时间变短,食物权重增加;反之变长,食物权重减少。(这里也是有范围的,食物权重不能为负数,也不能过大)
蛇的移动
进入函数,创建蛇头的下一个位置所在的节点,并根据方向算出所在位置。
判断下一位置是否为食物:
cpp
bool NextIsFood(pSnakeNode pn, pSnake ps)//判断下一位置是否为食物
{
return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
下一位置是食物:
先将新节点头插进蛇身,打印蛇身在屏幕上,总分数加上食物权重。
再次创建食物。
cpp
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
//释放下一个位置的节点
free(pn);
pn = NULL;
//打印
pSnakeNode cur = ps->_pSnake;
while (cur)
{
set_pos(cur->x, cur->y);
wprintf(L"%lc",BODY);
cur = cur->next;
}
ps->_score += ps->_food_weight;
//重新创建食物
CreateFood(ps);
}
下一位置不是食物:
创建下一位置的节点,也是头插,但是在打印蛇身之后,将蛇尾位置打印两个空格(不打印空格蛇身就不会清除一直留在屏幕上:拖尾),将蛇尾的节点释放掉。(cur->next一定要置空,不能让它为野指针)
cpp
void NoFood(pSnakeNode pn, pSnake ps)//下一个位置不是食物
{
//头插
pn->next = ps->_pSnake;
ps->_pSnake = pn;
pSnakeNode cur = ps->_pSnake;
while (cur->next->next)
{
set_pos(cur->x,cur->y);
wprintf(L"%lc",BODY);
cur = cur->next;
}
//把最后一个节点打印空格
set_pos(cur->next->x,cur->next->y);
printf(" ");
//将最后一个节点释放
free(cur->next);
//将倒数第二个节点置为空
cur->next = NULL;
}
检测是否撞墙:
判断蛇头坐标位置是否超出范围,若超出范围,将蛇的状态改为KILL_BY_WALL
cpp
void 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;
}
}
检测是否撞到自己:
遍历蛇身链表,若坐标重合,将蛇的状态改为KILL_BY_SELF,并且跳出循环。
cpp
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;
}
}
蛇移动一步的总函数:
cpp
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);
}
游戏结束:
判断游戏结束的原因,并打印。
释放蛇身链表
cpp
void Game_End(pSnake ps)//游戏善后
{
set_pos(24,12);
switch (ps->_status)
{
case END_NORMAL:
printf("您主动结束游戏\n");
break;
case KILL_BY_WALL:
printf("您被墙单杀了\n");
break;
case KILL_BY_SELF:
printf("您被自己单杀了\n");
break;
}
//释放蛇身链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
后续改进:
1.穿墙
2.食物分类
3.多个食物
4.双人游戏
......
本期博客到这里就结束了,如果有什么错误,欢迎指出,如果对你有帮助,请点个赞,谢谢!