文章目录
- 演示效果
- 实现的基本功能
- 技术要点
- 源代码
- 实现功能
- GameStart
- 打印欢迎界面和功能介绍
- 绘制地图
- 创建蛇
- 创建食物
- GameRun
- 打印提示信息
- 蛇每走一步
- GameEnd
- 蛇死亡后继续游戏
演示效果
贪吃蛇1.0演示视频
将终端应用程序改为控制台主机
实现的基本功能
- 贪吃蛇地图绘制
- 蛇吃食物的功能(上、下、左、右⽅向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞⾃⾝死亡
- 计算得分
- 蛇⾝加速、减速
- 暂停游戏
技术要点
c语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等等……
源代码
snake.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <locale.h>
#include <stdbool.h>
#include <time.h>
#include <stdbool.h>
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 24
#define POS_Y 5
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
enum CONDITION
{
OK,
KILL_BY_WALL,
KILL_BY_SELF,
END_NORMAL
};
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode,* pSnakeNode;
typedef struct Snake
{
pSnakeNode pSnakebody;//指向蛇头的节点
pSnakeNode pFood;//指向食物的节点
enum DIRECTION Dir;//方向
enum CONDITION Cdt;//状态
int Food_Weight;//食物的权重
int Socre;//总分数
int Sleep_Time;//睡眠时间
}Snake,* pSnake;
//初始化
void GameStart(pSnake ps);
//定位
void SetPos(short x, short y);
//打印欢迎界面和功能介绍
void Welcome_To_Game();
//绘制地图
void CreateMap();
//创建蛇
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//运行游戏
void GameRun(pSnake ps);
//打印提示信息
void PrintHelpInfo();
//蛇走的过程
void SnakeMove(pSnake ps);
//判断下一个节点是不是食物
bool NextIsFood(pSnakeNode pn,pSnake ps);
//如果是食物,就吃掉,然后创建新的食物
void EatFood(pSnakeNode pn,pSnake ps);
//如果不是食物,就把最后一个节点释放
void NotFood(pSnakeNode pn, pSnake ps);
//判断下一步是否撞墙
void KillByWall(pSnake ps);
//判断下一步是否撞到自己
void KillBySelf(pSnake ps);
//善后工作
void GameEnd(pSnake ps);
snake.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Snake.h"
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//食物的坐标在棋盘内
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
//食物不能和蛇身冲突
pSnakeNode cur = ps->pSnakebody;
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
//创建食物
pSnakeNode Food = (pSnakeNode)malloc(sizeof(SnakeNode));
if (Food == NULL)
{
perror("malloc fail");
return;
}
Food->next = NULL;
Food->x = x;
Food->y = y;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->pFood = Food;
}
void SetPos(short x, short y)
{
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(houtput, pos);
}
void InitSnake(pSnake ps)
{
ps->pSnakebody = NULL;
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)
{
//创建蛇身节点
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("malloc fail");
return;
}
//设置坐标
cur->next = NULL;
cur->x = POS_X + i * 2;
cur->y = POS_Y;
//头插
if (ps->pSnakebody == NULL)
{
ps->pSnakebody = cur;
}
else
{
cur->next = ps->pSnakebody;
ps->pSnakebody = cur;
}
}
//打印身体
cur = ps->pSnakebody;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//初始化贪吃蛇的数据
ps->Cdt = OK;
ps->Dir = RIGHT;
ps->Food_Weight = 10;
ps->Socre = 0;
ps->Sleep_Time = 200;//单位毫秒
}
void CreateMap()
{
int i = 0;
//上
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (i = 0; i < 29; i++)
{
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);
}
}
void Welcome_To_Game()
{
SetPos(40, 14);
printf("欢迎来到贪食蛇小游戏");
SetPos(40, 25);
system("pause");
system("cls");
SetPos(25, 14);
printf("用↑ . ↓ . ← . → 分别控制蛇的移动,F1为加速,");
SetPos(25, 15);
printf("F2为减速,加速将得到更高的分数");
SetPos(40, 24);
system("pause");
system("cls");
}
void GameStart(pSnake ps)
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//获取标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义光标信息结构体
CONSOLE_CURSOR_INFO CursorInfo;
//获取光标信息
GetConsoleCursorInfo(houtput, &CursorInfo);
//修改
CursorInfo.bVisible = false;
//设置
SetConsoleCursorInfo(houtput, &CursorInfo);
//打印欢迎界面和功能介绍
Welcome_To_Game();
//绘制地图
CreateMap();
//创建蛇
InitSnake(ps);
//创建食物
CreateFood(ps);
}
//打印提示信息
void PrintHelpInfo()
{
SetPos(60, 15);
printf("不能穿墙,不能咬到自己");
SetPos(60, 16);
printf("用 ↑.↓.←.→ 分别控制蛇的移动,");
SetPos(60, 17);
printf("F1为加速,F2为减速,加速将得到更高的分数");
SetPos(60, 18);
printf("ESC:退出游戏,SPACE:暂停游戏");
SetPos(60, 20);
printf("我爱吃福鼎肉片@版权");
}
//暂停
void pause()
{
while (1)
{
Sleep(300);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
//判断下一个节点是不是食物
bool NextIsFood(pSnakeNode pn, pSnake ps)
{
return (pn->x == ps->pFood->x && pn->y == ps->pFood->y);
}
//如果是食物,就吃掉,然后创建新的食物
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插
ps->pFood->next = ps->pSnakebody;
ps->pSnakebody = ps->pFood;
//释放下一个位置的节点
free(pn);
pn = NULL;
//打印
pSnakeNode cur = ps->pSnakebody;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->Socre += ps->Food_Weight;
CreateFood(ps);
}
//如果不是食物,就把最后一个节点释放
void NotFood(pSnakeNode pn, pSnake ps)
{
//头插
pn->next = ps->pSnakebody;
ps->pSnakebody = pn;
//最后一个节点,要释放
pSnakeNode cur = ps->pSnakebody;
while (cur->next->next)//打印前n个
{
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->pSnakebody->x == 56
|| ps->pSnakebody->x == 0
|| ps->pSnakebody->y == 0
|| ps->pSnakebody->y == 26)
{
ps->Cdt = KILL_BY_WALL;
}
}
//判断下一步是否撞到自己
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->pSnakebody->next;
while (cur)
{
if (cur->x == ps->pSnakebody->x && cur->y == ps->pSnakebody->y)
{
ps->Cdt = KILL_BY_SELF;
}
cur = cur->next;
}
}
//蛇走的过程
void SnakeMove(pSnake ps)
{
//创建下一个节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("pNextNode malloc fail");
return;
}
switch (ps->Dir)
{
case UP://向上
pNextNode->x = ps->pSnakebody->x;
pNextNode->y = ps->pSnakebody->y - 1;
break;
case DOWN://向下
pNextNode->x = ps->pSnakebody->x;
pNextNode->y = ps->pSnakebody->y + 1;
break;
case LEFT://向左
pNextNode->x = ps->pSnakebody->x - 2;
pNextNode->y = ps->pSnakebody->y;
break;
case RIGHT://向右
pNextNode->x = ps->pSnakebody->x + 2;
pNextNode->y = ps->pSnakebody->y;
break;
}
//判断下一个节点是不是食物
if (NextIsFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NotFood(pNextNode, ps);
}
KillByWall(ps);//下一步是墙
KillBySelf(ps);//下一步是自己
}
//运行游戏
void GameRun(pSnake ps)
{
PrintHelpInfo();
do
{
SetPos(60, 10);
printf("食物分数:%2d", ps->Food_Weight);
SetPos(60, 11);
printf("总分数:%2d", ps->Socre);
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->Cdt = END_NORMAL;
break;
}
else if (KEY_PRESS(VK_SPACE))
{
pause();
}
else if (KEY_PRESS(VK_F1))
{
if (ps->Sleep_Time > 80)
{
ps->Sleep_Time -= 30;
ps->Food_Weight += 2;
}
}
else if (KEY_PRESS(VK_F2))
{
if (ps->Sleep_Time < 320)
{
ps->Sleep_Time += 30;
ps->Food_Weight -= 2;
}
}
//让蛇走起来
SnakeMove(ps);
Sleep(ps->Sleep_Time);
} while (ps->Cdt == OK);
}
//善后工作
void GameEnd(pSnake ps)
{
SetPos(20, 12);
switch (ps->Cdt)
{
case END_NORMAL:
printf("您主动结束了游戏");
break;
case KILL_BY_WALL:
printf("您撞到墙死了");
break;
case KILL_BY_SELF:
printf("您咬到了自己");
}
//释放蛇身节点
pSnakeNode cur = ps->pSnakebody;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
snaketest.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Snake.h"
void test()
{
int ch = 0;
do
{
system("cls");
Snake snake;
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 14);
printf("再来一局吗?(Y/N):");
ch = getchar();
while (getchar() != '\n');
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
}
int main()
{
//创建贪吃蛇
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
test();
return 0;
//初始化游戏
//运行游戏
//结束游戏
}
实现功能
在地图上我们打印墙体、蛇、食物都是使用宽字符,普通字符只占字节,宽字符占两个
中文也是两个宽字符,所以我们本地化所有类项即可
setlocale(LC_ALL, "");
再实现功能以前,我们可以将要实现的功能分成三大类:
GameStart(初始化游戏)
GameRun(运行游戏)
GameEnd(游戏善后工作)
GameStart
再绘制地图以前,我们需要设置控制台窗口的长宽:100列,30行
顺便设置窗口的名字为贪吃蛇
void GameStart(pSnake ps)
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
}
隐藏光标并且获得句柄
void GameStart(pSnake ps)
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//获取标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义光标信息结构体
CONSOLE_CURSOR_INFO CursorInfo;
//获取光标信息
GetConsoleCursorInfo(houtput, &CursorInfo);
//修改
CursorInfo.bVisible = false;
//设置
SetConsoleCursorInfo(houtput, &CursorInfo);
}
修改光标位置
void SetPos(short x, short y)
{
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(houtput, pos);
}
打印欢迎界面和功能介绍
前面提到如何定位光标位置,在每一页打印完以后暂停,并且清理该页,才会接着打印下一页
void Welcome_To_Game()
{
SetPos(40, 14);
printf("欢迎来到贪食蛇小游戏");
SetPos(40, 25);
system("pause");
system("cls");
SetPos(25, 14);
printf("用↑ . ↓ . ← . → 分别控制蛇的移动,F1为加速,");
SetPos(25, 15);
printf("F2为减速,加速将得到更高的分数");
SetPos(40, 24);
system("pause");
system("cls");
}
绘制地图
因为在后续功能中经常要用到各种图案,我们就统一在头文件里定义好
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
void CreateMap()
{
int i = 0;
//上
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (i = 0; i < 29; i++)
{
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);
}
}
创建蛇
#define POS_X 24
#define POS_Y 5
void InitSnake(pSnake ps)
{
ps->pSnakebody = NULL;
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++)
{
//创建蛇身节点
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("malloc fail");
return;
}
//设置坐标
cur->next = NULL;
cur->x = POS_X + i * 2;
cur->y = POS_Y;
//头插
if (ps->pSnakebody == NULL)
{
ps->pSnakebody = cur;
}
else
{
cur->next = ps->pSnakebody;
ps->pSnakebody = cur;
}
}
//打印身体
cur = ps->pSnakebody;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//初始化贪吃蛇的数据
ps->Cdt = OK;
ps->Dir = RIGHT;
ps->Food_Weight = 10;
ps->Socre = 0;
ps->Sleep_Time = 200;//单位毫秒
}
创建食物
棋盘大小:
要注意食物的范围
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//食物的坐标在棋盘内随机生成
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
//食物不能和蛇身冲突
pSnakeNode cur = ps->pSnakebody;
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
//创建食物
pSnakeNode Food = (pSnakeNode)malloc(sizeof(SnakeNode));
if (Food == NULL)
{
perror("malloc fail");
return;
}
Food->next = NULL;
Food->x = x;
Food->y = y;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->pFood = Food;
}
GameRun
打印提示信息
//打印提示信息
void PrintHelpInfo()
{
SetPos(60, 15);
printf("不能穿墙,不能咬到自己");
SetPos(60, 16);
printf("用 ↑.↓.←.→ 分别控制蛇的移动,");
SetPos(60, 17);
printf("F1为加速,F2为减速,加速将得到更高的分数");
SetPos(60, 18);
printf("ESC:退出游戏,SPACE:暂停游戏");
SetPos(60, 20);
printf("我爱吃福鼎肉片@版权");
}
蛇每走一步
当蛇正在向某个方向走时,不能操控蛇走相反方向!
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
//运行游戏
void GameRun(pSnake ps)
{
PrintHelpInfo();
do
{
SetPos(60, 10);
printf("食物分数:%2d", ps->Food_Weight);
SetPos(60, 11);
printf("总分数:%2d", ps->Socre);
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->Cdt = END_NORMAL;
break;
}
else if (KEY_PRESS(VK_SPACE))
{
pause();
}
else if (KEY_PRESS(VK_F1))
{
if (ps->Sleep_Time > 80)
{
ps->Sleep_Time -= 30;
ps->Food_Weight += 2;
}
}
else if (KEY_PRESS(VK_F2))
{
if (ps->Sleep_Time < 320)
{
ps->Sleep_Time += 30;
ps->Food_Weight -= 2;
}
}
//让蛇走起来
SnakeMove(ps);
Sleep(ps->Sleep_Time);
} while (ps->Cdt == OK);//状态为OK才能走
}
当蛇走向下一个节点时,要分为两种情况
- 下个节点是食物
- 下个节点不是食物
//蛇走的过程
void SnakeMove(pSnake ps)
{
//创建下一个节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("pNextNode malloc fail");
return;
}
switch (ps->Dir)
{
case UP://向上
pNextNode->x = ps->pSnakebody->x;
pNextNode->y = ps->pSnakebody->y - 1;
break;
case DOWN://向下
pNextNode->x = ps->pSnakebody->x;
pNextNode->y = ps->pSnakebody->y + 1;
break;
case LEFT://向左
pNextNode->x = ps->pSnakebody->x - 2;
pNextNode->y = ps->pSnakebody->y;
break;
case RIGHT://向右
pNextNode->x = ps->pSnakebody->x + 2;
pNextNode->y = ps->pSnakebody->y;
break;
}
//判断下一个节点是不是食物
if (NextIsFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NotFood(pNextNode, ps);
}
KillByWall(ps);//下一步是墙
KillBySelf(ps);//下一步是自己
}
- 下个节点是食物
先判断一下
//判断下一个节点是不是食物
bool NextIsFood(pSnakeNode pn, pSnake ps)
{
return (pn->x == ps->pFood->x && pn->y == ps->pFood->y);
}
//如果是食物,就吃掉,然后创建新的食物
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插
ps->pFood->next = ps->pSnakebody;
ps->pSnakebody = ps->pFood;
//释放下一个位置的节点
free(pn);
pn = NULL;
//打印
pSnakeNode cur = ps->pSnakebody;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->Socre += ps->Food_Weight;
CreateFood(ps);
}
为什么释放下一个位置的节点,有些同学有疑问
因为在CreateFood函数中,已经将ps->Food指向了食物节点,所以下一个位置的节点用不上的,当然要释放掉,因此,使用下一个位置的节点,释放食物节点也是可以的。
2. 下一个节点不是食物
//如果不是食物,就把最后一个节点释放
void NotFood(pSnakeNode pn, pSnake ps)
{
//头插
pn->next = ps->pSnakebody;
ps->pSnakebody = pn;
//最后一个节点,要释放
pSnakeNode cur = ps->pSnakebody;
while (cur->next->next)//打印前n个
{
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->pSnakebody->x == 56
|| ps->pSnakebody->x == 0
|| ps->pSnakebody->y == 0
|| ps->pSnakebody->y == 26)
{
ps->Cdt = KILL_BY_WALL;
}
}
//判断下一步是否撞到自己
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->pSnakebody->next;
while (cur)
{
if (cur->x == ps->pSnakebody->x && cur->y == ps->pSnakebody->y)
{
ps->Cdt = KILL_BY_SELF;
}
cur = cur->next;
}
}
GameEnd
//善后工作
void GameEnd(pSnake ps)
{
SetPos(20, 12);
switch (ps->Cdt)
{
case END_NORMAL:
printf("您主动结束了游戏");
break;
case KILL_BY_WALL:
printf("您撞到墙死了");
break;
case KILL_BY_SELF:
printf("您咬到了自己");
}
//释放蛇身节点
pSnakeNode cur = ps->pSnakebody;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
蛇死亡后继续游戏
到此还没有结束,如果玩家还想在玩一把呢,总不能关闭应用再重新点开吧
void test()
{
int ch = 0;
do
{
system("cls");
Snake snake;
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 14);
printf("再来一局吗?(Y/N):");
ch = getchar();
while (getchar() != '\n');//如果输入yyyy一样可以读取
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
}
希望这篇博客对你有所帮助!!!如果有不懂或者有错的地方欢迎私信探讨!