贪吃蛇是一个兴久不衰的经典小游戏,相信绝大多数的人都多多少少接触过这一款游戏,今天我将用C语言来实现一个贪吃蛇的小游戏。
一、效果展示
游戏运行效果如图所示,接下来我会一一详解该项目中应有的功能。
二、整体代码展示
为了代码整体的规范管理,这里用了三个程序分别管理,放在了文章的头部,如果有需要的可以复制粘贴到自己的IDE里面。
这里是第一个代码,snake.c文件,负责贪吃蛇关键框架代码的搭建。
#define _CRT_SECURE_NO_WARNINGS 1
#include"snake.h"
//移动光标位置
void SetPos(short x, short y)
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x , y };
SetConsoleCursorPosition(houtput, pos);
}
//打印欢迎界面
void WelcomeToGame()
{
SetPos(35, 14);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(37, 20);
system("pause");
system("cls"); //清理屏幕
SetPos(20, 14);
wprintf(L"用“w”“a”“s”“d”分别操控蛇的移动,“j”为加速,“k”为减速\n");
SetPos(37, 15);
wprintf(L"加速将能得到更高的分数\n");
SetPos(37, 20);
system("pause");
system("cls");
}
//打印地图
void CreateMap()
{
//上
int i = 0;
for (i = 0; i <= 28; i++)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (i = 0; i <= 28; 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 CreateFood(pSnake ps)
{
int x = 0; //x取值范围 2 - 54,而且最好是2的倍数
int y = 0; //y取值范围 1 - 25
again:
do
{
x = rand() % 53 + 2; // %53 得到0 - 52的数
y = rand() % 25 + 1; // %25得到0 - 24的数
} while (x % 2 != 0); //如果x是奇数,重新取随机值
//x 和 y不能和蛇的身体冲突
pSnakeNode cur = ps->pSnake_head;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto again; //如果蛇身坐标与食物坐标重合,回到again处重新开始
}
cur = cur->next; //如果cur不为空,遍历下一个指向的位置
}
//创建食物节点
pSnakeNode pCrateFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pCrateFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
pCrateFood->x = x;
pCrateFood->y = y;
pCrateFood->next = NULL;
//打印食物
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->pfood = pCrateFood;
}
void CreateSnake(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode)); //为贪吃蛇链表开辟内存空间,大小为SnakeNode结构体
if (cur == NULL)
{
perror("CreateSnake()::malloc()");
return;
}
cur->next = NULL;
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
//头插法插入链表
if (ps->pSnake_head == NULL) //如果蛇头节点为空指针
{
ps->pSnake_head = cur; //把新创建的节点赋给头节点
}
else //如果蛇头节点非空
{
cur->next = ps->pSnake_head; //把新创建节点指向蛇头节点
ps->pSnake_head = cur; // 把新节点的值赋给蛇头节点
}
}
cur = ps->pSnake_head;
while (cur)
{
SetPos(cur->x,cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//设置贪吃蛇的属性
ps->dir = RIGHT; //默认向右
ps->food_weight = 10; //默认食物为10分
ps->score = 0; //默认总分0分
ps->sleep_time = 200; //默认休眠200毫秒
ps->status = NORMAL;
}
void GameInit(pSnake ps)
{
//0.光标隐藏
//设置控制台相关属性
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_info上
GetConsoleCursorInfo(houtput, &cursor_info);
//改变光标的属性
//cursor_info.dwSize = 50; 改变光标大小
cursor_info.bVisible = false; //改变光标可见度,使其隐藏光标
//设置和houtput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
//1.打印欢迎界面、
//2.功能介绍
WelcomeToGame();
//3.绘制地图
CreateMap();
//4.创建蛇
CreateSnake(ps);
//5.创建食物
CreateFood(ps);
}
void PrintHelpInfo()
{
SetPos(60, 17);
wprintf(L"%ls",L"不能撞墙,不能咬到自己\n");
SetPos(60, 18);
wprintf(L"%ls", L"用“w”“a”“s”“d”分别操控蛇的移动\n");
SetPos(60, 19);
wprintf(L"%ls",L"“j”为加速,“k”为减速\n");
SetPos(60, 20);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏\n");
}
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
int NextIsFood(pSnakeNode pn, pSnake ps)
{
if (ps->pfood->x == pn->x && ps->pfood->y == pn->y)
return 1;
else
return 0;
}
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插法把节点挂上去
ps->pfood->next = ps->pSnake_head;
ps->pSnake_head = ps->pfood;
//释放下一个位置的节点
free(pn);
pn = NULL;
pSnakeNode cur = ps->pSnake_head;
//打印
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->score += ps->food_weight;
//重新创建食物
CreateFood(ps);
}
//下一个节点不是食物
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插法
pn->next = ps->pSnake_head;
ps->pSnake_head = pn;
pSnakeNode cur = ps->pSnake_head;
while (cur->next->next != NULL)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc",BODY);
cur = cur->next; //把前面的节点遍历
}
//把最后一个节点打印成空白
SetPos(cur->next->x, cur->next->y);
printf(" ");
//释放最后一个节点
free(cur->next);
//把倒数第二个节点的地址置为NULL
cur->next = NULL;
}
void KillByWall(pSnake ps)
{
if (ps->pSnake_head->x == 0 || ps->pSnake_head->x == 56 ||
ps->pSnake_head->y == 0 || ps->pSnake_head->y == 26)
{
ps->status = KILL_BY_WALL;
}
}
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->pSnake_head->next;
while (cur)
{
if (cur->x == ps->pSnake_head->x && cur->y == ps->pSnake_head->y)
{
ps->status = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
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_head->x;
pNextNode->y = ps->pSnake_head->y - 1;
break;
case DOWN:
pNextNode->x = ps->pSnake_head->x;
pNextNode->y = ps->pSnake_head->y + 1;
break;
case LEFT:
pNextNode->x = ps->pSnake_head->x - 2;
pNextNode->y = ps->pSnake_head->y;
break;
case RIGHT:
pNextNode->x = ps->pSnake_head->x + 2;
pNextNode->y = ps->pSnake_head->y;
break;
}
//检测下一个节点是否是食物
if (NextIsFood(pNextNode,ps))
{
EatFood(pNextNode, ps);
pNextNode = NULL; //避免pn变成野指针
}
else
{
NoFood(pNextNode, ps);
}
//检测蛇是否撞墙
KillByWall(ps);
//检测蛇是否撞自己
KillBySelf(ps);
}
void GameStart(pSnake ps)
{
//打印右下角帮助信息
PrintHelpInfo();
do
{
//打印总分数和食物的分数
SetPos(60, 10);
printf("总分数:%d\n", ps->score);
SetPos(60, 11);
printf("当前每个食物的分值为:%2d\n", ps->food_weight);
//按键检测
if (KEY_PRESS(0x57) && ps->dir != DOWN) //检测W键,并且蛇的行动方向不能相悖
{
ps->dir = UP;
}
else if (KEY_PRESS(0x53) && ps->dir != UP) //检测s键
{
ps->dir = DOWN;
}
else if (KEY_PRESS(0x41) && ps->dir != RIGHT) //检测a键
{
ps->dir = LEFT;
}
else if (KEY_PRESS(0x44) && ps->dir != LEFT) //检测d键
{
ps->dir = RIGHT;
}
else if (KEY_PRESS(0x4A)) //检测j键
{
//加速
if (ps->sleep_time > 80) //加速的阈值
{
ps->sleep_time -= 30;
ps->food_weight += 2;
}
}
else if (KEY_PRESS(0x4B)) //检测k键
{
//减速
if (ps->food_weight > 2) //加速的阈值
{
ps->sleep_time += 30;
ps->food_weight -= 2;
}
}
else if (KEY_PRESS(VK_SPACE)) //检测空格
{
//暂停游戏
Pause();
}
else if (KEY_PRESS(VK_ESCAPE) && ps->dir != LEFT) //检测ESC键
{
//退出游戏
ps->status = END_NORMAL;
}
//贪吃蛇走一步
SnakeMove(ps);
Sleep(ps->sleep_time);
} while (ps->status == NORMAL);
}
void GameEnd(pSnake ps)
{
SetPos(19, 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_head;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
这里是第二个代码,snake.h文件,头文件,负责声明项目内用到的函数以及类型的声明。
#pragma once
#include<stdio.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>
#include<time.h>
//类型的声明
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0) //判断某个键是否被按下,vk为虚拟键值
//检测某一个键的虚拟键值最后一位是否为1,如果是1证明按键被按下,如果为0表示按键没有被按过
//蛇身的坐标
#define POS_X 24
#define POS_Y 5
//宽字符的宏定义
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
//蛇的方向
enum DIRECTION {
UP = 1,//上
DOWN, //下
LEFT, //左
RIGHT //右
};
//蛇的状态
enum GAME_STATUS {
NORMAL, //正常
KILL_BY_WALL, //撞墙
KILL_BY_SELF,//自杀
END_NORMAL //esc结束
};
//蛇身的节点类型
typedef struct SnakeNode {
//坐标
int x;
int y;
//指向下一个节点的指针
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
//typedef struct SnakeNode *pSnakeNode;
//蛇的参数管理
typedef struct Snake {
pSnakeNode pSnake_head; //指向蛇头的指针
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 GameInit(pSnake ps);
//欢迎界面的打印
void WelcomeToGame();
//创建地图
void CreateMap();
//创建蛇
void CreateSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//开始游戏
void GameStart(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
//蛇的移动
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);
这里是第三个代码,test.c文件,负责测试以及运行游戏
#define _CRT_SECURE_NO_WARNINGS 1
#include"snake.h"
void test()
{
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
GameInit(&snake);
//运行游戏
GameStart(&snake);
//结束游戏 - 善后工作
GameEnd(&snake);
SetPos(0, 27);
}
int main()
{
//设置本地环境
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
test();
return 0;
}
三、基本功能概述
- 在运行程序时,打印欢迎界面以及游戏教程
- 绘制地图
- 创建贪吃蛇和食物
- 实现按键的功能(“w”、“a”、“s”、“d”操控上下左右,“j”、“k”分别实现蛇的加速和减速,ESC退出游戏界面,空格暂停游戏)
- 蛇头撞到墙壁后结束游戏
- 蛇头撞到自身节点后结束游戏
- 计算游戏得分
四、技术要点
枚举、宏定义、结构体、函数、指针、动态内存管理、Win32 API
五、程序实现详解
(一)初始化游戏
在游戏初始化阶段,建立一个GameInit()函数,在函数体内我们要解决以下几件事:
- 控制台的相关设置
- 打印欢迎界面和功能介绍界面
- 绘制游戏地图
- 创建贪吃蛇
- 创建食物
1、控制台的相关设置
在程序的最初,我们要决定贪吃蛇这一个程序我们大概要用到多大的空间。
这里使用30行,100列的空间大小,再把标题设置为“贪吃蛇”。
//设置控制台相关属性
system("mode con cols=100 lines=30"); //使用控制台30行,100列的大小
system("title 贪吃蛇"); //将标题设置为“贪吃蛇”
通过Win32 API的内置函数,获取标准输出设备的句柄,并隐藏控制台上的光标。
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_info上
GetConsoleCursorInfo(houtput, &cursor_info);
//改变光标的属性
//cursor_info.dwSize = 50; 改变光标大小
cursor_info.bVisible = false; //改变光标可见度,使其隐藏光标
//设置和houtput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
定义一个函数SetPos(),实现光标的移动。
//移动光标位置
void SetPos(short x, short y)
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x , y };
SetConsoleCursorPosition(houtput, pos);
}
2、打印欢迎和功能介绍界面
这里利用一个WelcomeToGame()函数,实现在屏幕中间打印欢迎和功能介绍的效果。
void WelcomeToGame()
{
SetPos(35, 14);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(37, 20);
system("pause");
system("cls"); //清理屏幕
SetPos(20, 14);
wprintf(L"用“w”“a”“s”“d”分别操控蛇的移动,“j”为加速,“k”为减速\n");
SetPos(37, 15);
wprintf(L"加速将能得到更高的分数\n");
SetPos(37, 20);
system("pause");
system("cls");
}
3、绘制游戏地图
创建一个CreateMap()函数,上下左右打印出27行58列的‘□’宽字符,使其实现打印墙壁的效果。
void CreateMap()
{
//上
int i = 0;
for (i = 0; i <= 28; i++)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);
for (i = 0; i <= 28; 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);
}
}
3.1 <locate.h>本地化使用宽字符
在这里介绍一下宽字符的用法,一般计算机常用的字符是用ASCII字符组成的单字符,但是ASCII只有128个字符,如果要使用特殊字符的话,需要用到两个字节来组成一个字符,这就是宽字符的概念,如果在我们的程序里需要用到宽字符,那么此时需要将编程环境本地化,这样再使用宽字符就不会报错,我们在test.c开头里声明一下locate
//设置本地环境
setlocale(LC_ALL, ""); //LC_ALL代表所有类项都本地化
//""会根据本地语言而改为不同的地区
注:程序在一开始就从snake.h文件中对需要用到的宽字符(蛇身节点、地图、食物)都进行了宏定义
//宽字符的宏定义
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
4、创建贪吃蛇
在snake.h文件下定义一下贪吃蛇的蛇身节点和蛇的参数管理结构体变量
//蛇身的节点类型
typedef struct SnakeNode {
//坐标
int x;
int y;
//指向下一个节点的指针
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
//typedef struct SnakeNode *pSnakeNode;
//蛇的参数管理
typedef struct Snake {
pSnakeNode pSnake_head; //指向蛇头的指针
pSnakeNode pfood; //指向食物的指针
enum DIRECTION dir; //蛇的方向
enum GAME_STATUS status; //蛇的状态
int food_weight; //单个食物的分数
int score; //总分数
int sleep_time; //休息的时间,时间越大蛇的速度越慢,时间越小蛇的速度越快
}Snake,*pSnake;
建立完了结构体变量后 ,在snake.c文件里创建CreateSnake()函数,利用头插法创建贪吃蛇链表并实行管理,贪吃蛇的身体用宽字符‘●’表示。
void CreateSnake(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode)); //为贪吃蛇链表开辟内存空间,大小为SnakeNode结构体
if (cur == NULL)
{
perror("CreateSnake()::malloc()");
return;
}
cur->next = NULL;
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
//头插法插入链表
if (ps->pSnake_head == NULL) //如果蛇头节点为空指针
{
ps->pSnake_head = cur; //把新创建的节点赋给头节点
}
else //如果蛇头节点非空
{
cur->next = ps->pSnake_head; //把新创建节点指向蛇头节点
ps->pSnake_head = cur; // 把新节点的值赋给蛇头节点
}
}
cur = ps->pSnake_head;
while (cur)
{
SetPos(cur->x,cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//设置贪吃蛇的属性
ps->dir = RIGHT; //默认向右
ps->food_weight = 10; //默认食物为10分
ps->score = 0; //默认总分0分
ps->sleep_time = 200; //默认休眠200毫秒
ps->status = NORMAL;
}
5、创建食物
在snake.c文件下创建函数CreateFood(),随机在地图上生成食物节点,使用宽字符‘★’表示
void CreateFood(pSnake ps)
{
int x = 0; //x取值范围 2 - 54,而且最好是2的倍数
int y = 0; //y取值范围 1 - 25
again:
do
{
x = rand() % 53 + 2; // %53 得到0 - 52的数
y = rand() % 25 + 1; // %25得到0 - 24的数
} while (x % 2 != 0); //如果x是奇数,重新取随机值
//x 和 y不能和蛇的身体冲突
pSnakeNode cur = ps->pSnake_head;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto again; //如果蛇身坐标与食物坐标重合,回到again处重新开始
}
cur = cur->next; //如果cur不为空,遍历下一个指向的位置
}
//创建食物节点
pSnakeNode pCrateFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pCrateFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
pCrateFood->x = x;
pCrateFood->y = y;
pCrateFood->next = NULL;
//打印食物
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->pfood = pCrateFood;
}
6、阶段性总结
把以上功能全部整合在GameInit()函数上,便完成了贪吃蛇程序的初始化部分。
(二)运行游戏
在运行游戏这个阶段,创建一个GaneStart()函数,在函数体内我们要实现一下功能:
- 打印游戏运行界面右下角帮助信息
- 打印总得分数和当前食物的分值
- 对按键的功能检测实现
- 实现贪吃蛇走起来的效果
1、打印游戏运行界面右下角帮助信息
在snake.c文件中创建PrintHelpInfo()函数,在控制台右下角打印出帮助信息
void PrintHelpInfo()
{
SetPos(60, 17);
wprintf(L"%ls",L"不能撞墙,不能咬到自己\n");
SetPos(60, 18);
wprintf(L"%ls", L"用“w”“a”“s”“d”分别操控蛇的移动\n");
SetPos(60, 19);
wprintf(L"%ls",L"“j”为加速,“k”为减速\n");
SetPos(60, 20);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏\n");
}
2、打印总得分数和当前食物的分值
与打印帮助信息差不多,都是利用SetPos()函数先移动好光标再进行打印。
//打印总分数和食物的分数
SetPos(60, 10);
printf("总分数:%d\n", ps->score);
SetPos(60, 11);
printf("当前每个食物的分值为:%2d\n", ps->food_weight);
3、对按键的功能检测实现
在snake.h文件下宏定义检测按键按下的类型。
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0) //判断某个键是否被按下,vk为虚拟键值
//检测某一个键的虚拟键值最后一位是否为1,如果是1证明按键被按下,如果为0表示按键没有被按过
vk代表按键的虚拟键值,需要用的时候可以上网查询,当最后一位置0表示没有被按下,最后一位置1表示被按下。
然后在GameStart()函数体下直接编写按键检测相关代码
//按键检测
if (KEY_PRESS(0x57) && ps->dir != DOWN) //检测W键,并且蛇的行动方向不能相悖
{
ps->dir = UP;
}
else if (KEY_PRESS(0x53) && ps->dir != UP) //检测s键
{
ps->dir = DOWN;
}
else if (KEY_PRESS(0x41) && ps->dir != RIGHT) //检测a键
{
ps->dir = LEFT;
}
else if (KEY_PRESS(0x44) && ps->dir != LEFT) //检测d键
{
ps->dir = RIGHT;
}
else if (KEY_PRESS(0x4A)) //检测j键
{
//加速
if (ps->sleep_time > 80) //加速的阈值
{
ps->sleep_time -= 30;
ps->food_weight += 2;
}
}
else if (KEY_PRESS(0x4B)) //检测k键
{
//减速
if (ps->food_weight > 2) //加速的阈值
{
ps->sleep_time += 30;
ps->food_weight -= 2;
}
}
else if (KEY_PRESS(VK_SPACE)) //检测空格
{
//暂停游戏
Pause();
}
else if (KEY_PRESS(VK_ESCAPE) && ps->dir != LEFT) //检测ESC键
{
//退出游戏
ps->status = END_NORMAL;
}
4、实现贪吃蛇走起来的效果
在实现了按键检测的功能后,要实现的是贪吃蛇正常走起来的效果,创建一个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_head->x;
pNextNode->y = ps->pSnake_head->y - 1;
break;
case DOWN:
pNextNode->x = ps->pSnake_head->x;
pNextNode->y = ps->pSnake_head->y + 1;
break;
case LEFT:
pNextNode->x = ps->pSnake_head->x - 2;
pNextNode->y = ps->pSnake_head->y;
break;
case RIGHT:
pNextNode->x = ps->pSnake_head->x + 2;
pNextNode->y = ps->pSnake_head->y;
break;
}
//检测下一个节点是否是食物
if (NextIsFood(pNextNode,ps))
{
EatFood(pNextNode, ps);
pNextNode = NULL; //避免pn变成野指针
}
else
{
NoFood(pNextNode, ps);
}
//检测蛇是否撞墙
KillByWall(ps);
//检测蛇是否撞自己
KillBySelf(ps);
}
4.1 贪吃蛇经过的下一个节点是食物
建立函数NextIsFood(),模拟下一个是食物的情况,如果食物节点的坐标与蛇头节点重合,执行EatFood()函数,否则执行NoFood()函数;
int NextIsFood(pSnakeNode pn, pSnake ps)
{
if (ps->pfood->x == pn->x && ps->pfood->y == pn->y)
return 1;
else
return 0;
}
EatFood()函数具体如下:
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插法把节点挂上去
ps->pfood->next = ps->pSnake_head;
ps->pSnake_head = ps->pfood;
//释放下一个位置的节点
free(pn);
pn = NULL;
pSnakeNode cur = ps->pSnake_head;
//打印
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->score += ps->food_weight;
//重新创建食物
CreateFood(ps);
}
4.2 贪吃蛇经过的下一个节点没有食物
如果下一个节点不是食物,那么贪吃蛇的长度就不会增长,在贪吃蛇走了一个单位的长度时,会把下一个节点的值赋给蛇头节点,然后遍历找到最末尾的节点,打印空格,然后释放内存并置为空指针,最后再把倒数第二个节点指向的下一个节点next置为NULL。
//下一个节点不是食物
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插法
pn->next = ps->pSnake_head;
ps->pSnake_head = pn;
pSnakeNode cur = ps->pSnake_head;
while (cur->next->next != NULL)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc",BODY);
cur = cur->next; //把前面的节点遍历
}
//把最后一个节点打印成空白
SetPos(cur->next->x, cur->next->y);
printf(" ");
//释放最后一个节点
free(cur->next);
//把倒数第二个节点的地址置为NULL
cur->next = NULL;
}
4.3 贪吃蛇走的时候撞到墙壁
创建函数KillByWall(),判断贪吃蛇的蛇头节点有没有和四周的墙壁重合,如果有重合,便将贪吃蛇的状态值改成KILL_BY_WALL,结束游戏。
void KillByWall(pSnake ps)
{
if (ps->pSnake_head->x == 0 || ps->pSnake_head->x == 56 ||
ps->pSnake_head->y == 0 || ps->pSnake_head->y == 26)
{
ps->status = KILL_BY_WALL;
}
}
4.4 贪吃蛇走的时候撞到自身
创建函数KillBySelf(),判断贪吃蛇的蛇头节点有没有和自身的任一节点有重合,如果有重合,便将贪吃蛇的状态值改为KILL_BY_SELF,游戏结束。
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->pSnake_head->next;
while (cur)
{
if (cur->x == ps->pSnake_head->x && cur->y == ps->pSnake_head->y)
{
ps->status = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
5、阶段性总结
将以上代码整合到GameStart()函数体里,此时程序基本成型,能够正常游玩贪吃蛇。
(三)结束游戏
最后一个阶段是该程序的最后一部分,创建GameEnd()函数,这一函数主要用于游戏结束界面的打印以及将之前所申请的动态内存空间释放掉的善后工作
void GameEnd(pSnake ps)
{
SetPos(19, 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_head;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}