一、游戏前准备
我们首先需要创建三个文件:
Snake.h
:结构体、枚举的定义,方法的声明。
Snake.c
:方法的实现。
test.c
:方法的测试。
首先先在Snake.h
文件中定义贪吃蛇的结构体、枚举、以及包含的头文件和各种方法的声明:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<stdbool.h>
#include<time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
//蛇的方向
enum DIRECTION
{
UP = 1,//1
DOWN,//2
LEFT,//3
RIGHT//4
};
//蛇的状态
enum GAME_STATUS
{
OK,//正常运行 0
KILL_BY_WALL,//撞墙 1
KILL_BY_SELF,//咬到自己 2
END_NORMAL//正常结束 3
};
//蛇身的节点
typedef struct SnakeNode
{
//坐标
int x;
int y;
//指向下一个节点的指针
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
//贪吃蛇
typedef struct Snake
{
pSnakeNode _pSnake;//指向蛇头的指针
pSnakeNode _pFood;//指向食物节点的指针
enum DIRECTION _dir;//蛇的方向
enum GAME_STATUS _status;//游戏的状态
int _food_weight;//一个食物的分数
int _score;//总成绩
int _sleep_time;//休息时间,时间越短,速度越快
}Snake, * pSnake;
//定位光标的位置
void SetPos(short x, short y);
//游戏的初始化
void GameStart(pSnake ps);
//欢迎界面
void WelcomeToGame();
//创建地图
void CreateMap();
//初始化蛇身
void InitSnake(pSnake ps);
//游戏运行的逻辑
void GameRun(pSnake ps);
//蛇的移动(走一步)
void SnakeMove(pSnake ps);
//判断下一个坐标是否是食物
int 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 GameEnd(pSnake ps);
接着我们再在Snake.c
文件中编写定位光标位置的方法:
void SetPos(short x, short y)
{
//取得坐标
COORD pos = { x,y };
//获得输出句柄
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置光标位置
SetConsoleCursorPosition(hOutput, pos);
}
然后再进入test.c
文件设置程序⽀持本地模式,并进⼊游戏的主逻辑。
主逻辑分为3个过程:
- 游戏开始(GameStart):完成游戏的初始化。
- 游戏运⾏(GameRun):完成游戏运⾏逻辑的实现。
- 游戏结束(GameEnd):完成游戏结束的说明,实现资源释放。
#include<locale.h>
#include"Snake.h"
void test()
{
int ch = 0;
do
{
system("cls");
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//1.打印环境界面和功能介绍
//2.绘制地图
//3.创建蛇
//4.创建食物
//5.设置游戏的相关信息
GameStart(&snake);
//运行游戏
GameRun(&snake);
//结束游戏
GameEnd(&snake);
SetPos(20, 15);
printf("再来一局吗!(Y/N):");
ch = getchar();
while (getchar() != '\n');//清理\n
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
}
int main()
{
//设置适配本地环境
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
test();
return 0;
}
二、游戏开始
1、游戏开始函数(GameStart)
这个模块完成游戏的初始化任务:
- 控制台窗⼝⼤⼩的设置。
- 控制台窗⼝名字的设置。
- ⿏标光标的隐藏。
- 打印欢迎界⾯。
- 创建地图。
- 初始化蛇。
- 创建第⼀个⻝物。
我们先在Snake.c
文件中创建游戏开始的函数GameStart
:
void GameStart(pSnake ps)
{
//设置窗口大小和名字
system("mode con cols=100 lines=30");
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);
}
然后再在test.c中进行测试:
void test()
{
Snake snake = { 0 };
GameStart(&snake);
getchar();//暂停程序执行,等待用户输入
}
int main()
{
test();
return 0;
}
运行结果:
可以看到此时窗口的大小和名字已经被成功设置,并且光标也被隐藏了。
接着就可以在GameStart函数中调用其他函数的方法。
1)打印欢迎界⾯(WelcomeToGame)
在游戏正式开始之前,做⼀些功能提醒。
欢迎界面函数:
void WelcomeToGame()
{
SetPos(40, 14);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(42, 20);
system("pause");//等待用户按任意键
system("cls"); //清屏
SetPos(25, 14);
wprintf(L"用↑.↓.←.→来控制蛇的移动,按F3加速,F4减速\n");
SetPos(25, 15);
wprintf(L"加速能够得到更高的分数\n");
SetPos(42, 20);
system("pause");
system("cls");
}
再进行测试:
void test()
{
Snake snake = { 0 };
GameStart(&snake);
getchar();
}
int main()
{
//设置为本地环境
setlocale(LC_ALL,"");
test();
return 0;
}
运行结果:
2)创建地图(CreateMap)
创建地图就是将墙打印出来,因为是宽字符打印,所以使⽤wprintf
函数,打印格式串前使⽤L。打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
墙体打印的宽字符先在Snake.h
文件中进行宏定义。
#define WALL L'□'
易错点: 就是坐标的计算
上:(0,0)到(56,0)
下:(0,26)到(56,26)
左:(0,1)到(0,25)
右:(56,1)到(56,25)
创建地图函数:
void CreateMap()
{
//上
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);//一个宽字符占2格横坐标
}
//下
SetPos(0, 26);
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//左
for (int i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (int i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
再进行测试:
void test()
{
Snake snake = { 0 };
GameStart(&snake);
getchar();
}
int main()
{
//设置为本地环境
setlocale(LC_ALL,"");
test();
return 0;
}
运行结果:
3)初始化蛇⾝(InitSnake)
蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。
- 创建5个节点,然后将每个节点存放在链表中进⾏管理。
- 创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。
- 蛇的初始位置从(24,5)开始
需要将初始位置在Snake.h
文件中进行宏定义:
#define POS_X 24
#define POS_Y 5
设置蛇的属性:
- 游戏状态是:OK
- 蛇的移动速度:200毫秒
- 蛇的默认⽅向:RIGHT
- 初始成绩:0
- 每个⻝物的分数:10
蛇⾝打印的宽字符也在Snake.h
文件中进行宏定义:
#define BODY L'●'
初始化蛇⾝函数:
void InitSnake(pSnake ps)
{
//当前开辟的蛇节点的指针
pSnakeNode cur = NULL;
//开辟5个节点
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)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//设置蛇的属性
ps->_status = OK;
ps->_sleep_time = 200;//毫秒
ps->_dir = RIGHT;
ps->_score = 0;
ps->_food_weight = 10;
}
再进行测试:
void test()
{
Snake snake = { 0 };
GameStart(&snake);
getchar();
}
int main()
{
//设置为本地环境
setlocale(LC_ALL,"");
test();
return 0;
}
运行结果:
4)创建⻝物(CreateFood)
- 先随机⽣成⻝物的坐标
- x坐标必须是2的倍数
- ⻝物的坐标不能和蛇⾝任意节点的坐标重复
- 创建⻝物节点,打印⻝物
⻝物打印的宽字符在Snake.h
文件中进行宏定义:
#define FOOD L'★'
创建⻝物的函数:
void CreateFood(pSnake ps)
{
//先随机生成食物的坐标
int x = 0;
int y = 0;
again:
do
{
x = rand() % 53 + 2; //x:2~54
y = rand() % 25 + 1; //y:1~25
} while (x % 2 != 0);
//食物坐标不能和蛇身体坐标冲突
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;
//打印食物节点
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;//食物节点地址赋值给_pFood
}
再进行测试:
void test()
{
Snake snake = { 0 };
GameStart(&snake);
getchar();
}
int main()
{
//设置为本地环境
setlocale(LC_ALL,"");
test();
return 0;
}
运行结果:
三、游戏运⾏
在完成了游戏开始部分后,现在我们开始进入游戏运行的部分。
1、准备工作
1)帮助信息(PrintHelpInfo)
游戏运⾏期间,右侧需要打印帮助信息,提⽰玩家。
帮助信息函数:
void PrintHelpInfo()
{
SetPos(64, 14);
wprintf(L"%ls", L"不能穿墙,不能咬到自己");
SetPos(64, 15);
wprintf(L"%ls", L"用↑.↓.←.→来控制蛇的移动");
SetPos(64, 16);
wprintf(L"%ls", L"按F3加速,F4减速");
SetPos(64, 17);
wprintf(L"ls", L"按Esc退出游戏,按空格暂停游戏");
SetPos(64, 19);
wprintf(L"ls", L"《怀旧》制作");
}
运行结果:
2)判断按键状态(宏KEY_PRESS)
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。
因此我们还需要判断按键使用情况,通过宏定义来实现:
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
需要的虚拟按键的罗列:
- 上:VK_UP
- 下:VK_DOWN
- 左:VK_LEFT
- 右:VK_RIGHT
- 空格:VK_SPACE
- ESC:VK_ESCAPE
- F3:VK_F3
- F4:VK_F4
3)暂停(Pause)
当我们按空格键之后,游戏要能达到暂停的效果。
暂停函数:
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
2、游戏运行函数(GameRun)
接着再在Snake.c
文件中创建游戏运行的函数GameRun
:
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
//游戏主体
do
{
//打印总分数和食物分
SetPos(64, 10);
printf("总分数:%d\n", ps->_score);
SetPos(64, 11);
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();
}
//Esc(正常退出游戏)
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_status = END_NORMAL;
}
//F3(加速)
else if (KEY_PRESS(VK_F3))
{
//休息时间如果大于80 休息时间减少30 一个食物的分数加2
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
//F4(减速)
else if (KEY_PRESS(VK_F4))
{
//一个食物的分数如果大于2 休息时间增加30 一个食物的分数-2
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
//蛇的移动
SnakeMove(ps);
//休眠时间
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
}
再进行测试:
void test()
{
Snake snake = { 0 };
GameStart(&snake);
GameRun(&snake);
getchar();
}
int main()
{
//设置为本地环境
setlocale(LC_ALL,"");
srand((unsigned int)time(NULL));//确保食物产生的坐标是真正随机的
test();
return 0;
}
运行结果:
1)蛇移动(SnakeMove)
- 先创建下⼀个节点,根据移动⽅向和蛇头的坐标,得到蛇移动到下⼀个位置的坐标。
- 看下⼀个位置是否是⻝物,是⻝物就做吃⻝物处理。
- 如果不是⻝物则做前进⼀步的处理。
- 蛇⾝移动后,判断是否会撞墙或者撞上⾃⼰蛇⾝,从⽽影响游戏的状态。
蛇移动函数:
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);
}
1.a)下一个节点是否是食物(NextIsFood)
判断传入的节点 pn的坐标是否与食物坐标相同。
下一个节点是否是食物函数:
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
1.b)是食物(EatFood)
是食物函数:
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插法
//食物节点成为新的蛇头
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
//释放pn节点(下一个位置)
free(pn);
pn = NULL;
//打印蛇身
pSnakeNode cur = ps->_pSnake;
while (cur);
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//吃掉单个食物的分数加到总分
ps->_score += ps->_food_weight;
//重新创建食物
CreateFood(ps);
}
1.c)不是食物(NoFood)
下⼀个节点成为新的蛇头,并将之前蛇⾝最后⼀个节点打印为空格,并释放掉。
注意: 释放最后⼀个结点后,还得将结点的指针改为NULL,保证蛇尾打印可以正常结束,不会越界访问。
不是食物函数:
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插法
//下一个节点成为新的蛇头
pn->next = ps->_pSnake;
ps->_pSnake = pn;
//打印蛇身
pSnakeNode cur = ps->_pSnake;
while (cur->next->next)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//蛇身最后一个节点打印为空白(2个空格)
SetPos(cur->next->x, cur->next->y);
printf(" ");
//再释放掉并置为NULL
free(cur->next);
cur->next = NULL;
}
1.d)撞墙(KillByWall)
判断蛇头的坐标是否和墙的坐标冲突。
撞墙函数:
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;
}
}
1.e)撞到自身(KillBySelf)
判断蛇头的坐标是否和蛇⾝体的坐标冲突。
撞到自身函数:
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;
}
}
四、游戏结束
1、游戏结束函数(GameEnd)
当游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇⾝节点。
在Snake.c
文件中创建游戏运行的函数GameRun
:
void GameEnd(pSnake ps)
{
//判断游戏状态
SetPos(24, 12);
switch (ps->_status)
{
//正常结束
case END_NORMAL:
wprintf(L"您主动结束游戏\n");
break;
//撞墙
case KILL_BY_WALL:
wprintf(L"您撞到墙上,游戏结束\n");
break;
//咬到自身
case KILL_BY_SELF:
wprintf(L"您咬到了自己,游戏结束\n");
break;
}
//释放蛇身链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
当完成了上面的所有方法后,我们就可以在test.c
文件中来实现整个游戏了。
#include<locale.h>
#include"Snake.h"
void test()
{
int ch = 0;
do
{
Snake snake = { 0 };
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 15);
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;
}
运行结果:
点击查看:贪吃蛇源代码