使用C语言在VS环境下基本实现贪吃蛇游戏
- 一丶 实现前的准备工作
- 二丶 实现后的游戏效果
- 三丶 游戏的项目结构和逻辑大纲
- 四丶 项目所需要实现的具体功能
- 五丶 源代码
一丶 实现前的准备工作
1. 设置vs运行环境为window控制台而非window终端
本项目实现环境是在window控制台下,因此需要多vs的运行环境进行设置
1. 正确的运行环境页面
window控制台页面在鼠标右键右击上边栏是这样的:
如果你的运行页面是这样的:
那么你就需要进行设置了。
2. 设置正确的运行环境
1.首先在该页面下鼠标右键右击上边栏,然后点击设置
2. 在启动的默认终端应用程序中切换成Window控制台主机并保存更改
3.正确的运行环境窗口
2. 了解句柄(下面代码能看明白会照葫芦画瓢用就行)
GetStdHandle
GetStdHandle是⼀个WindowsAPI函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
HANDLE GetStdHandle(DWORD nStdHandle);
实例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
//STD_OUTPUT_HANDLE --标准设备
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
3. 利用system函数丶cmd命令设置window控制台窗口的尺寸
控制台程序:平常我们运⾏起来的⿊框程序其实就是控制台程序
我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩和窗口的标题
mode con cols=100 lines=30
title 贪吃蛇
这些能在控制台窗⼝执⾏的命令,也可以调⽤C语⾔函数system来执⾏。
#include <stdio.h>
#include <stdlib.h> //system函数在stdlib.h头文件中
int main()
{
//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
system("mode con cols=100 lines=30");
//设置cmd窗⼝名称
system("title 贪吃蛇");
//设置system("pause") 让程序在结束前停下来
//这样就能看到我们设置的窗口信息了
//设置暂停
system("pause");
return 0;
}
运行后如下图:
4. 了解控制台屏幕上的坐标COORD和设置光标属性
1. COORD
COORD是WindowsAPI中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕上的坐标。
定义:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
给坐标赋值:
COORD pos = { 10, 15 };
2. CONSOLE_CURSOR_INFO
CONSOLE_CURSOR_INFO这个结构体,包含有关控制台光标的信息。
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- dwSize,由光标填充的字符单元格的百分⽐。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的⽔平线条
- bVisible,游标的可⻅性。如果光标可⻅,则此成员为TRUE
简单地讲,dwSize控制的是光标闪烁时的占比,基于一个字符的半分比1~100之间;bVisible用于控制光标是否可见(闪烁)。
CursorInfo.bVisible = false; //隐藏控制台光标
3. GetConsoleCursorInfo
GetConsoleCursorInfo函数用于检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息。
BOOL WINAPI GetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
功能:
获取光标相关信息
参数:
hConsoleOutput 控制台屏幕缓冲区的句柄。句柄必须具有GENERIC_READ访问权限。
lpConsoleCursorInfo 指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关控制台游标的信息。
返回值:
如果函数成功,则返回值为非零值。
如果函数失败,则返回值为零。要获取扩展错误信息,请调用GetLastError。
与句柄配合的实例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
4. SetConsoleCursorInfo
SetConsoleCursorInfo用于设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。
BOOL WINAPI SetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
功能:
设置光标的属性
参数:
hConsoleOutput 控制台屏幕缓冲区的句柄。句柄必须具有GENERIC_READ访问权限。
lpConsoleCursorInfo 指向CONSOLE_CURSOR_INFO结构的指针,该结构为控制台屏幕缓冲区的游标提供新规范。
返回值:
如果函数成功,则返回值为非零值。
如果函数失败,则返回值为零。要获取扩展错误信息,请调用GetLastError。
代码实例:
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
5. SetConsoleCursorPosition
SetConsoleCursorPosition用于设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
功能:
设置光标的位置
参数:
hConsoleOutput 控制台屏幕缓冲区的句柄。句柄必须具有GENERIC_READ访问权限。
dwCursorPosition 用于指定新的光标位置(以字符为单位)。坐标是屏幕缓冲区字符单元格的列和行。坐标必须位于控制台屏幕缓冲区的边界内。
返回值:
如果函数成功,则返回值为非零值。
如果函数失败,则返回值为零。要获取扩展错误信息,请调用GetLastError。
代码实例:
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
由于后面需要多次设置光标位置属性,于是我们将它封装成一个函数以便复用。
SetPos:封装⼀个设置光标位置的函数
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
5.了解键盘响应函数GetAsyncKeyState
获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState(
int vKey
);
- 将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
- GetAsyncKeyState 的返回值是short类型,在上⼀次调用GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
利用宏将该函数进行简化,以便后续使用:
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
6. 宽字符的输出
这是我们游戏运行时的页面:
在游戏地图上,我们打印墙体使⽤宽字符:口,打印蛇头使用宽字符♛,打印蛇体使⽤宽字符●,打印⻝物使⽤宽字符★。普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
如何使用C语言在VS环境下打印宽字符?
#include <stdio.h>
#include <locale.h> //setlocale的头文件 用于本地化设置
int main()
{
//想正确使用wprintf 需要使用setlocale设置本地化
//setlocale(LC_ALL, "")表示采用当前地区的语言风格
setlocale(LC_ALL, "");
//在设置初值前加L 表示该字符为宽字节数据
wchar_t ch1 = L'♛';
wchar_t ch2 = L'●';
wchar_t ch3 = L'口';
wchar_t ch4 = L'★';
wchar_t ch5 = L'天';
wchar_t ch6 = L'下';
wchar_t ch7 = L'无';
wchar_t ch8 = L'双';
//宽字体占两个字节 这里不纠结汉字的字节大小
//因为汉字具体的字节大小跟编码有关
//在打印前也需要加L 表示该字符为宽字节数据
wprintf(L"%c\n", ch1);
wprintf(L"%c\n", ch2);
wprintf(L"%c\n", ch3);
wprintf(L"%c\n", ch4);
wprintf(L"%c\n", ch5);
wprintf(L"%c\n", ch6);
wprintf(L"%c\n", ch7);
wprintf(L"%c\n", ch8);
return 0;
}
运行效果:
setlocale还可以使用C语言模式,这里不做过多描述,有需要可自行查阅。
二丶 实现后的游戏效果
1.游戏的欢迎页面
2.开始游戏的页面
3.游戏结束的两种情况:撞墙和撞到自身
4.游戏中如果未撞到墙或自身,那么游戏会一直进行下去,我这里是这样设计的。
三丶 游戏的项目结构和逻辑大纲
1. 项目结构
1. 头文件snake.h用于存放蛇体结构体丶函数的声明和枚举
2. 源文件snake.c用于存放函数的具体实现
3. 源文件main.c用于存放菜单框架和测试程序
2. 游戏逻辑
- 游戏欢迎页面,点击后进行开始游戏页面
- 操作空格键开始和暂停游戏
- ↑ .↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速
- F3加速后单个食物分数会变高 F4减速后单个食物分数会降低
- 撞到墙或撞到自身时游戏结束,否则将一直进行下去
- 游戏结束页面可以输入来选择是否再来一局
3. 项目逻辑大纲
四丶 项目所需要实现的具体功能
先将蛇体结构体丶蛇体节点丶枚举和宏放到这里,方便下面的代码查阅
//宏定义 利用三目来接收敲击的情况 按了为1 否则为0
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
//方向
//蛇的移动方向有四种 上下左右
enum DIRECION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//游戏状态
enum GAME_STATUS
{
OK, //游戏可以继续
KILL_BY_WALL, //撞到墙
KILL_BY_SELF, //撞到自身
END_NOMAL //正常结束
};
//墙 蛇体结点和食物的符号
#define WALL L'口' //墙体
#define HEAD L'♛' //蛇头
#define BODY L'⬤' //蛇体
#define FOOD L'★' //食物
//蛇出现的初始位置
//同时也是蛇头的起始位置
#define POS_X 24
#define POS_Y 5
//蛇身结点-存放单个蛇体节点的坐标和下一个身体节点的指针
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 _Score; //用户当前得分
int _foodWeight; //食物此时的权重 默认是10分
int _SleepTime; //每走一步休眠的时间
}Snake, *pSnake;
void GameStart(pSnake ps)
void GameStart(pSnake ps)函数用于初始化游戏,它整合完成了很多功能:
- 设置游戏窗口尺寸和标题
- 设置欢迎页面-WelcomeToGame()
- 设置游戏地图-CreatMap()
- 设置初始化蛇的部分数据-InitSnake(ps)
- 设置第一个食物-CreateFood(ps)
void GameStart(pSnake ps);
//游戏开始前的初始化
void GameStart(pSnake ps)
{
//用system系统函数设置控制台尺寸和窗口名称
//mode 为DOS命令
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();
//打印地图
CreatMap();
//初始蛇的相关数据
InitSnake(ps);
//创造第一个食物
CreateFood(ps);
}
void WelcomeToGame()
void WelcomeToGame()用于设置游戏开始前的欢迎页面,它配合SetPos(short, short)使用,实现了在屏幕上的定位数据输出。
//欢迎界面
void WelcomeToGame()
{
SetPos(40, 15);
printf("欢迎来到贪吃蛇小游戏");
SetPos(40, 25);
system("pause");
system("cls");
SetPos(25, 12);
printf("⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速");
SetPos(25, 13);
printf("加速将能得到更高的分数。\n");
SetPos(40, 25);
//暂停
system("pause");
//清屏
system("cls");
}
void CreateMap()
void CreateMap()用于地图的打印,它配合SetPos(short, short),在屏幕的上下左右四个方向打印墙体。
- 上面的墙体:上( 0, 0)-( 56, 0)
- 下面的墙体:下( 0, 26)-( 56, 26)
- 左面的墙体:左( 0, 1)-( 0, 25)
- 右面的墙体:右(56, 1)-( 56, 25)
这里有一个格外需要注意的两个点,一个是我们是用宽字符墙体进行打印地图,它单个字体占字节数为2,在winodw控制台下占两个单位坐标,这将会影响蛇的移动轨迹,这个在蛇的移动那块我们再谈;另一个是window控制台下的坐标系,基于window控制台窗口的坐标是从窗口的左上角为原点,横向为X轴,纵向为Y轴,而且单位距离下Y轴的实际长度要比X轴长,于是我们设计横纵向长度比2:1的比例(设置横向的28个口,占X轴单位长度56;纵向的28,占Y轴单位长度28),让游戏地图的布局等于正方形。
window控制台下的坐标系
不要被上面地图墙体2:1的设置误导,误以为控制台X,Y轴单位长度是1:2,注意看下图X丶Y轴上的各个口之间是有间隙的,在X轴上间隙的长度和在Y轴上间隙的长度并不相同。
void CreateMap();
//创建地图
void CreatMap()
{
int i = 0;
//上( 0, 0)-(56, 0)
SetPos(0, 0);
for (i = 0; i < 58; i += 2)
{
wprintf(L"%c", WALL);
}
//下( 0, 26)-(56, 26)
SetPos(0, 26);
for (i = 0; i < 58; i += 2)
{
wprintf(L"%c", WALL);
}
//左 需用SetPos定位行来设置墙体 由( 0, 1)-( 0, 25)
// x从0开始设置过墙体了 所以y从1开始设置 设置到25处墙体正好
for (i = 1; i < 26; ++i)
{
SetPos(0, i);
wprintf(L"%c", WALL);
}
//右 用SetPos定位设置墙体 由(56, 1)-( 56, 25)
// x是56,y同样从1开始 到25
for (i = 1; i < 26; ++i)
{
SetPos(56, i);
wprintf(L"%c", WALL);
}
}
void InitSnake(pSnake ps)
void InitSnake(pSnake ps) 用于创建整条蛇,打印蛇体在地图内,并且初始化游戏的各项数据。
代码逻辑:
- 初始创建5个蛇身节点,采用头插法。
- 打印蛇体在地图中。
- 默认蛇初始的移动方向向右,正常速度移动。
- 初始化一部分游戏信息数据
//初始化蛇
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;
int count = 1;
while (cur)
{
if (count == 1)
{
count++;
SetPos(cur->x, cur->y);
wprintf(L"%c", HEAD);
cur = cur->next;
}
else
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
}
//初始化贪吃蛇的数据
ps->_SleepTime = 200;
ps->_Score = 0;
ps->_Status = OK;
ps->_Dir = RIGHT;
ps->_foodWeight = 10;
}
void CreateFood(pSnake ps)
void InitSnake(pSnake ps)用于创建食物
要求:
- 食物的横坐标需是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;
}
}
void GameRun(pSnake ps)
void GameRun(pSnake ps)实现了控制台和键盘响应的交互,通过键盘的输入来控制游戏的运行以及蛇的移动,内层循环包裹实现了重复交互。
运行逻辑:
- 打印游戏信息同时完成输入的接收
- 实现蛇的移动-SnakeMove(ps)
这里在输入的时候需要注意,蛇不能直接反向移动,比如蛇正在向左移动,键入右键是不管用的,因为KEY_PRESS(VK)会等于0,这属于游戏的默认规则。
//游戏运行过程
void GameRun(pSnake ps)
{
PrintHelpInfo();
do
{
SetPos(64, 10);
printf("您当前的游戏得分等分:%d分", ps->_Score);
SetPos(64, 11);
printf("每个食物可获得的分数:%d分", ps->_foodWeight);
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_NOMAL;
break;
}
//F3和F4控制加速和减速,但都有一定限度
else if (KEY_PRESS(VK_F3))
{
if (ps->_SleepTime >= 50)
{
ps->_SleepTime -= 30;
ps->_foodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->_SleepTime < 350)
{
ps->_SleepTime += 30;
ps->_foodWeight -= 2;
if (ps->_SleepTime == 350)
{
ps->_foodWeight = 1;
}
}
}
//移动的间隙时间构成蛇的移动 时间越短 蛇的移动速度就越快
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_Status == OK);
}
void SnakeMove(pSnake ps)
void SnakeMove(pSnake ps) 用于实现蛇体的移动。
它具体的编写逻辑是这样的:
- 首先开辟一个临时内存作为新节点,通过移动方向的规则来给该节点赋值坐标
- 其次它判定该节点对应的坐标是否也正是食物的坐标,根据不同的情况进行不同的操作
- 最后判定蛇在连接这个新节点(蛇头位置更新)后是否合法,比如是否撞到了墙或自身
在这里需要说明一下蛇移动轨迹的限制了。
我们在上面是以(24, 5)为蛇头初始结点开始移动,我们使用的蛇头也好,蛇体,食物也好,都是使用的宽字符打印在地图上;墙体设置时横坐标时从0开始,每2个单位长度(x轴上)为一个墙体;那么蛇在水平方向上单次移动的单位长度应该是多少呢?
- 这个问题答案归结于蛇体和墙体丶食物在碰到时会发生的效果。我们想要的是蛇能够在碰到食物时将整个食物吞入腹中,而不是吃掉半个;我们要求当蛇碰到墙体时游戏结束,而不是蛇头卡在一半的墙体时还可以继续游戏。
- 这时蛇的移动轨迹限制应显而易见了,蛇头在水平方向上单次移动的距离应为2个在X轴上的单位长度,并且初始蛇头的X坐标也为偶数,食物每次出现时X坐标也为偶数,这样确保了当下一步无论是墙丶食物还是正常坐标,都不会出现卡位或游戏逻辑错误的情况,这都归功于我们规定了蛇的移动原则。
//蛇的移动
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;
}
}
//上面的switch确定了下一步蛇头的坐标
//此时需要确定下一次蛇头要达到的位置是不是食物
if (NextIsFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
//同时还需要确定下一步的位置是否合法
//是否撞到了墙 是否撞到了自己
KillByWall(ps);
KillBySelf(ps);
}
int NextIsFood(pSnakeNode psn, pSnake ps)
int NextIsFood(pSnakeNode psn, pSnake ps)用于判定新节点是否为食物节点,若为食物则返回1,否则返回0;
- 形参psn接收的实参是在SnakeMove中新开辟的节点
//在蛇移动的过程中 判断下一个结点是否为食物
int NextIsFood(pSnakeNode psn, pSnake ps)
{
return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
void EatFood(pSnakeNode psn, pSnake ps)
void EatFood(pSnakeNode psn, pSnake ps)用于执行蛇吃到食物的逻辑操作
它的具体逻辑是这样的:
- 将食物作为新的头结点插入(头插法)
- 在链入新节点后更新地图上的蛇体打印,并且更新游戏得分情况
- 在打印完更新的蛇体后要消除原食物在地图上的打印,用两个空格在原食物的坐标下替代,然后要释放原食物节点
- 最后创建出新食物,使得游戏继续
这里一定要注意:无论蛇的下一步移动是否是食物,下一步开辟的位置节点和食物节点都是独立的,在完成相应的操作后要注意是否需要释放内存,防止内存泄露
void EatFood(pSnakeNode psn, pSnake ps)
{
//头插法 将食物作为头结点
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
//打印出更新后的蛇体
int count = 1;
while (cur)
{
if (count == 1)
{
count++;
SetPos(cur->x, cur->y);
wprintf(L"%c", HEAD);
cur = cur->next;
}
else
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
}
//更新当前得分
ps->_Score += ps->_foodWeight;
//注意:我们在SnakeMove中创建的下一步蛇移动的位置坐标
// 并且判定 如果与食物的坐标相同就将创建的新坐标
// 链接到蛇身上 此时原先的食物节点开辟的内存已经
// 没有意义了 需要释放
//还需要将原先的尾节点打印为两个空格 否则吃了食物但还是会显示在上面
SetPos(ps->_pFood->x, ps->_pFood->y);
printf(" ");
free(ps->_pFood);
//每一次吃完食物后 都要重新创建出新的食物 让游戏可以继续
CreateFood(ps);
}
void NoFood(pSnakeNode psn, pSnake ps)
void NoFood(pSnakeNode psn, pSnake ps)用于执行蛇的下一次移动到的坐标不是食物的操作
它的操作逻辑是这样的:
- 将下一步坐标对应的内存节点链接到蛇头(头插法)
- 将新的蛇体的尾节点对应在地图上的打印消除,同时释放尾节点
- 在地图上打印出更新后的蛇体
我这里是先打印后释放的,只要注意释放前消除相应的地图信息即可。
//蛇不吃食物
void NoFood(pSnakeNode psn, pSnake ps)
{
//同样的逻辑 进行头插法 对于不是食物的节点
//要进行蛇头移动 同时释放尾节点
psn->next = ps->_pSnake;
ps->_pSnake = psn;
//打印蛇体
pSnakeNode cur = ps->_pSnake;
int count = 1;
while (cur->next->next)
{
if (count == 1)
{
count++;
SetPos(cur->x, cur->y);
wprintf(L"%c", HEAD);
cur = cur->next;
}
else
{
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;
}
int KillByWall(pSnake ps)
int KillByWall(pSnake ps)用于蛇的撞墙检测
执行逻辑:
- 当蛇头x或y坐标任一个与墙体重合时,代表蛇头移动到了墙体内
- 修改当前的游戏状态,退出子程序KillByWall
//撞墙检测
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;
Sleep(1000);
return 1;
}
return 0;
}
int KillBySelf(pSnake ps)
int KillBySelf(pSnake ps)用于蛇的撞墙检测
执行逻辑:
- 当蛇头坐标与任一个蛇体节点坐标重合时,代表蛇撞到了自身
- 修改当前的游戏状态,退出子程序KillBySelf
结合实际情况容易知道,蛇头只可能从第四个蛇体节点及往后节点重合(从蛇头开始数,蛇头本身也算蛇体节点)
这里为了简便,直接从第二个节点开始检测了
int KillBySelf(pSnake ps)
{
//cur是遍历整条蛇的临时节点
pSnakeNode cur = ps->_pSnake->next;
while (cur)
{
//ps->_pSnake是蛇头 从第二个蛇体节点开始检测是否重合
if ((ps->_pSnake->x == cur->x)
&& (ps->_pSnake->y == cur->y))
{
ps->_Status = KILL_BY_SELF;
Sleep(1000);
return 1;
}
cur = cur->next;
}
return 0;
}
void GameEnd(pSnake ps)
void GameEnd(pSnake ps)用于判定游戏结束属于何种情况
- 实际只有两个结束的情况:撞到墙体和撞到自身,主动退出游戏的相应函数没有实现,相应的case语句可以忽略
- 释放蛇的所有结点,释放当前的食物结点,此时游戏结束,将进入用户输入选择
//游戏结束
void GameEnd(pSnake ps)
{
pSnakeNode cur = ps->_pSnake;
SetPos(24, 12);
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)
{
pSnake del = cur;
cur = cur->next;
free(del);
}
free(ps->_pFood);
ps->_pFood = NULL;
}
void SetPos(short x, short y)
void SetPos(short x, short y)用于定位控制台的光标坐标
- 封装句柄和光标设置函数
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来识别不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
void pause()
void pause()实现空格暂停响应操作
//暂停响应
void pause()
{
while (1)
{
//相当于每0.3秒检测一次空格键的响应
Sleep(300);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
五丶 源代码
1.snake.h
snake.h
#pragma once
#include <stdio.h>
#include <time.h>
#include <windows.h>
#include <stdbool.h>
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
//方向
enum DIRECION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//游戏状态
enum GAME_STATUS
{
OK,
KILL_BY_WALL,
KILL_BY_SELF,
END_NOMAL
};
//墙 蛇体结点和食物的符号
#define WALL L'口'
#define HEAD L'♛'
#define BODY L'⬤'
#define FOOD L'★'
//蛇出现的初始位置
#define POS_X 24
#define POS_Y 5
//蛇身结点
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 _Score; //用户当前得分
int _foodWeight; //食物此时的权重 默认是10分
int _SleepTime; //每走一步休眠的时间
}Snake, *pSnake;
//游戏开始前的初始化
void GameStart(pSnake ps);
//游戏运行过程
void GameRun(pSnake ps);
//游戏结束
void GameEnd(pSnake ps);
//设置光标的坐标
void SetPos(short x, short y);
//欢迎界面
void WelcomeToGame();
//打印帮助信息
void PrintHelpInfo();
//创建地图
void CreatMap();
//初始化蛇
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);
2.snake.c
snake.c
#include "snake.h"
//游戏开始前的初始化
void GameStart(pSnake ps)
{
//用system系统函数设置控制台尺寸和窗口名称
//mode 为DOS命令
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();
//打印地图
CreatMap();
//初始蛇的相关数据
InitSnake(ps);
//创造第一个食物
CreateFood(ps);
}
//游戏运行过程
void GameRun(pSnake ps)
{
PrintHelpInfo();
do
{
SetPos(64, 10);
printf("您当前的游戏得分等分:%d分", ps->_Score);
SetPos(64, 11);
printf("每个食物可获得的分数:%d分", ps->_foodWeight);
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_NOMAL;
break;
}
else if (KEY_PRESS(VK_F3))
{
if (ps->_SleepTime >= 50)
{
ps->_SleepTime -= 30;
ps->_foodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->_SleepTime < 350)
{
ps->_SleepTime += 30;
ps->_foodWeight -= 2;
if (ps->_SleepTime == 350)
{
ps->_foodWeight = 1;
}
}
}
//移动的间隙时间构成蛇的移动 时间越短 蛇的移动速度就越快
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_Status == OK);
}
//游戏结束
void GameEnd(pSnake ps)
{
pSnakeNode cur = ps->_pSnake;
SetPos(24, 12);
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)
{
pSnake del = cur;
cur = cur->next;
free(del);
}
free(ps->_pFood);
ps->_pFood = NULL;
}
//设置光标的坐标
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为减速");
SetPos(25, 13);
printf("加速将能得到更高的分数。\n");
SetPos(40, 25);
system("pause");
system("cls");
}
//打印帮助信息
void PrintHelpInfo()
{
SetPos(64, 15);
printf("不能穿墙,不能咬到自己");
SetPos(64, 16);
printf("⽤↑.↓.←.→分别控制蛇的移动.");
SetPos(64, 17);
printf("F3 为加速, F4 为减速");
SetPos(64, 18);
printf("ESC :退出游戏.space:暂停游戏.");
SetPos(64, 20);
printf("加油 干巴嘚!");
}
//创建地图
void CreatMap()
{
int i = 0;
//上( 0, 0)-(56, 0)
SetPos(0, 0);
for (i = 0; i < 58; i += 2)
{
wprintf(L"%c", WALL);
}
//下( 0, 26)-(56, 26)
SetPos(0, 26);
for (i = 0; i < 58; i += 2)
{
wprintf(L"%c", WALL);
}
//左 需用SetPos定位行来设置墙体
// x从0开始设置过墙体了 所以y从1开始设置 设置到25处墙体正好
for (i = 1; i < 26; ++i)
{
SetPos(0, i);
wprintf(L"%c", WALL);
}
//右 用SetPos定位设置墙体
// x是56,y同样从1开始 到25
for (i = 1; i < 26; ++i)
{
SetPos(56, i);
wprintf(L"%c", WALL);
}
}
//初始化蛇
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;
int count = 1;
while (cur)
{
if (count == 1)
{
count++;
SetPos(cur->x, cur->y);
wprintf(L"%c", HEAD);
cur = cur->next;
}
else
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
}
//初始化贪吃蛇的数据
ps->_SleepTime = 200;
ps->_Score = 0;
ps->_Status = OK;
ps->_Dir = RIGHT;
ps->_foodWeight = 10;
}
//创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
//产生的左边必须是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;
}
}
//暂停响应
void pause()
{
while (1)
{
Sleep(300);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
//在蛇移动的过程中 判断下一个结点是否为食物
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;
//打印出更新后的蛇体
int count = 1;
while (cur)
{
if (count == 1)
{
count++;
SetPos(cur->x, cur->y);
wprintf(L"%c", HEAD);
cur = cur->next;
}
else
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
}
//更新当前得分
ps->_Score += ps->_foodWeight;
//注意:我们在SnakeMove中创建的下一步蛇移动的位置坐标
// 并且判定 如果与食物的坐标相同就将创建的新坐标
// 链接到蛇身上 此时原先的食物节点开辟的内存已经
// 没有意义了 需要释放
//还需要将原先的尾节点打印为两个空格 否则吃了食物但还是会显示在上面
SetPos(ps->_pFood->x, ps->_pFood->y);
printf(" ");
free(ps->_pFood);
//每一次吃完食物后 都要重新创建出新的食物 让游戏可以继续
CreateFood(ps);
}
//蛇不吃食物
void NoFood(pSnakeNode psn, pSnake ps)
{
//同样的逻辑 进行头插法 对于不是食物的节点
//要进行蛇头移动 同时释放尾节点
psn->next = ps->_pSnake;
ps->_pSnake = psn;
//打印蛇体
pSnakeNode cur = ps->_pSnake;
int count = 1;
while (cur->next->next)
{
if (count == 1)
{
count++;
SetPos(cur->x, cur->y);
wprintf(L"%c", HEAD);
cur = cur->next;
}
else
{
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;
}
//撞墙检测
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;
Sleep(1000);
return 1;
}
return 0;
}
//撞到自身检测
int KillBySelf(pSnake ps)
{
//cur是遍历整条蛇的临时节点
pSnakeNode cur = ps->_pSnake->next;
while (cur)
{
//ps->_pSnake是蛇头 从第二个蛇体节点开始检测是否重合
if ((ps->_pSnake->x == cur->x)
&& (ps->_pSnake->y == cur->y))
{
ps->_Status = KILL_BY_SELF;
Sleep(1000);
return 1;
}
cur = cur->next;
}
return 0;
}
//蛇的移动
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;
}
}
//上面的switch确定了下一步蛇头的坐标
//此时需要确定下一次蛇头要达到的位置是不是食物
if (NextIsFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
//同时还需要确定下一步的位置是否合法
//是否撞到了墙 是否撞到了自己
KillByWall(ps);
KillBySelf(ps);
}
3.test.c
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
#include <locale.h>
void test()
{
int ch = 0;
srand((unsigned int)time(NULL));
do
{
Snake snake = { 0 };
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 15);
printf("再来一局?(Y/N):");
ch = getchar();
getchar();
} while (ch == 'Y'||ch == 'y');
//此光标设置是控制好控制台打印程序结束信息的位置
SetPos(0, 27);
}
int main()
{
setlocale(LC_ALL, "");
test();
return 0;
}
本博客仅供个人参考,如有错误请多多包含。
Aruinsches-项目日志-在VS环境下利用C语言基本实现贪吃蛇-3/30/2024
标签:ps,cur,pSnake,SetPos,void,VS,C语言,int,贪吃蛇 From: https://blog.csdn.net/m0_73968399/article/details/137159232