目录
本文章将介绍如何使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。
1.游戏效果演示
基于控制台实现贪吃蛇小游戏
2.技术要求
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API等。
3.win32API
在写关于贪吃蛇的代码之前,我们要先掌握Win32API如何使用
3.1控制台程序
平常我们运行起来的黑框程序其实就是控制台程序
我们可以用一些cmd命令来设置控制台窗口的长宽,如mode
也可以改变窗口命名,如title
3.2 控制台屏幕上的坐标COORD
COORD
是Windows API中定义的一个结构体,用于表示控制台屏幕上的一个字符的坐标。它有两个成员:X
(表示列位置)和Y
(表示行位置)。
COORD类型的声明:
给坐标赋值:
3.3 GetStdHandle
GetStdHandle
是一个 Windows API 函数,用于从特定的标准设备(标准输入、标准输出或标准错误)中获取一个句柄(handle)。句柄相当于控制台的权限,有了它才能操控控制台。需包含头文件windows.h。
函数原型:
实例:
3.4 GetConsoleCursorInfo
GetConsoleCursorInfo
是 Windows API 中的一个函数,用于检索有关指定的控制台屏幕缓冲区的光标的可见性和大小信息。
函数原型:
实例:
3.4.1 CONSOLE_CURSOR_INFO
CONSOLE_CURSOR_INFO
是一个在 Windows 编程中使用的结构体,用于表示控制台光标的信息。该结构体主要在 GetConsoleCursorInfo
和 SetConsoleCursorInfo
函数中使用,以获取或设置控制台光标的大小和可见性。
结构体定义:
- dwSize
描述:这个成员变量表示光标填充的字符单元格的百分比。其值通常在 1 到 100 之间,表示光标从完全不可见(接近0)到完全填充(100)的大小。
- bVisible
描述:这个成员变量表示光标的可见性。如果 bVisible
为 TRUE
,则光标是可见的;如果为 FALSE
,则光标是不可见的。
3.5 SetConsoleCursorInfo
SetConsoleCursorInfo
是 Windows API 中的一个函数,用于设置控制台屏幕缓冲区的光标信息,包括光标的大小和可见性。
函数原型:
实例:
3.6 SetConsoleCursorPosition
SetConsoleCursorPosition
是Windows API中的一个函数,用于设置控制台窗口中光标的位置。
函数原型:
实例:
SetPos:封装⼀个设置光标位置的函数
3.7 GetAsyncKeyState
GetAsyncKeyState是一个Windows API函数,用于检测程序运行时某个按键的状态,包括是否按下或弹起。
函数原型:
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调用GetAsyncKeyState 函数后,如果 返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬 起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.
实例:
4.贪吃蛇游戏设计和分析
为实现贪吃蛇游戏的运行,我们将分成三个文件(一个头文件和源文件)来实现
snake.h:包含头文件
snake.c:实现游戏各功能代码的函数实现
tect.c:贪吃蛇游戏中的类型声明和函数的声明。
4.1初始化蛇的结构体
我们通过链表的方式来维护蛇的结构体
创建一个snake的结构体来维护贪吃蛇的节点,食物,分数,方向,蛇的状态,蛇的速度,蛇的当前分数和食物的分数。
游戏的设计流程
首先修改当前地区为本地模式,为后面支持中文宽字符的打印做准备,然后创建“蛇”,开始初始化游戏
接下来通过三个函数来完成初始化游戏和运行游戏,并结束游戏和释放内存的工作
4.2 初始化游戏GameStart
在 snake.c文件创建GameStart函数并开始以下步骤
4.2.1 首先要打印游戏界面和规则,再隐藏光标和获取句柄
4.2.2 游戏地图的打印
根据下图举例,打印地图的墙用宽字符打印,要用到wprintf函数,打印格式串前使用L;打印地图要算好坐标,宽字符占两个字节,每次打印x坐标必须是2的倍数,食物坐标必须在墙体内
为了方便墙体的打印,我们定义宏(□)并通过for循环打印墙体
4.2.3 打印蛇身
设置蛇身开始的长度为5节,每个节点对应链表的节点,节点随对应相同的坐标,创建好节点,同样为了方便定义一个宏(●)并打印在屏幕上,同时设置好状态
4.2.4 创建第一个食物
食物的创建必须在墙内进行,不能在墙外生成和跟墙重合,食物的横坐标(X坐标)必须是2的倍数
定义食物的宏‘★’用于食物宽字符的打印
4.3 游戏运行GameRun
在 snake.c文件创建GameRun函数并开始以下步骤
4.3.1 打印右侧的规则
4.3.2 获取按键信息
为了方便检测按键状态,我们封装了⼀个宏
通过win32API函数KEY_PRESS来检测是否按下了特定的键,并基于按键和当前蛇的移动方向来更新蛇的移动方向。Sleep
函数(Windows API函数)来暂停程序执行指定的毫秒数,其主要目的是控制移动速度。
按下空格时将暂停游戏的函数
4.3.3 蛇身移动
蛇身每次移动在X坐标向左和向右移动时占一个宽字符,也就是两个字节。每次移动通过pNextNode为蛇的下一个节点分配内存并通过switch循环进行判断;NextIsFood
函数是用来检查给定的 pSnakeNode
(即蛇的下一个节点)的坐标是否与食物(ps->_pfood
)的坐标相同并通过调用EatFood
函数和NoFood
函数进行判断;通过调用KillBySelf
函数和KillByWall
函数判断是否撞到自己或墙判断死亡
4.3.4 下一个坐标是食物
如果下一个坐标是食物通过头插法使蛇身长度增加并通过while循环来更新蛇身的显示,吃掉食物后通过CreadFood函数重新生成食物
4.3.5 下一个坐标不是食物
将下⼀个节点头插入蛇的身体,并将之前蛇身最后⼀个节点打印为空格,释放掉蛇身的最后⼀个节 点。
4.3.6 蛇头撞到墙游戏失败
通过if判断当蛇头的坐标和墙的坐标重合时将ps->_status
设置为kill_by_wall
用于表示游戏失败的原因是由于蛇撞到了墙。
4.3.7 蛇头撞到蛇身游戏失败
使用while
循环遍历蛇的每一个节点判断当蛇头和蛇身重合时则表示蛇撞到了自己的身体并将ps->_status
设置为kill_by_self,
表示游戏失败的原因是由于蛇撞到了自己的身体。
4.4 游戏结束GameEnd
GameEnd
函数是负责在游戏结束时进行一些收尾操作的并释放贪吃蛇链表所占用的内存。switch循环通过ps->_status判断游戏结束的原因后,通过while
循环来遍历链表,并逐个释放节点完成整个游戏的善后工作
4.5 再来一局
我们在主函数写入是否再来一局的代码,通过ch = getchar()通过玩家写入“Y”或“y”传入do-while
循环来判断玩家是否再来一局;while (getchar() != '\n'),
这个循环用于读取并丢弃直到遇到换行符('\n')为止的所有字符。这是为了防止输入缓冲区中残留的字符(如用户按下的回车键产生的换行符)影响下一次的getchar()
调用。
5.贪吃蛇代码展示
5.1 snake.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<stdbool.h>
#include<time.h>
#include<locale.h>
#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 DIRECTION
{
up,
down,
left,
right
};
//蛇的状态
enum GAME_STATUS
{
ok,//正常
kill_by_wall,//撞墙死亡
kill_by_self,//撞到自己死亡
exit_normal//正常退出
};
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 creatfood(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 KillBySelf(pSnake ps);
//撞到墙游戏失败
void KillByWall(pSnake ps);
//游戏运行的逻辑
void GameRun(pSnake ps);
//结束游戏-善后工作
void GameEnd(pSnake ps);
5.2 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(40, 14);
wprintf(L"欢迎来到贪吃蛇\n");
setpos(40, 20);
system("pause");
system("cls");
setpos(25, 14);
wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
setpos(25, 15);
wprintf(L"加速能获得更高的分数\n");
setpos(40, 20);
system("pause");
system("cls");
}
//游戏地图的打印
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 = 0; i <= 25; i++)
{
setpos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (i = 0; i <= 25; i++)
{
setpos(56, i);
wprintf(L"%lc", WALL);
}
}
//打印蛇身
void InitSnake(pSnake ps)
{
//i用于循环计数,而cur是一个指向SnakeNode(贪吃蛇的节点)的指针,初始化为NULL`。
int i = 0;
pSnakeNode cur = NULL;
for ( i = 0; i < 5; i++)
{
//使用`malloc`为每个节点分配内存。
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake()::malloc()");
return;
}
//设置节点的`x`和`y`坐标(从`POS_X`开始,每个节点在`x`轴上移动2个单位)。
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用于设置光标位置
setpos(cur->x,cur->y);
//wprintf(用于在宽字符环境中打印)来在屏幕上显示蛇的初始形状。
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_dir = right;//设置贪吃蛇的初始方向为向右
ps->_score = 0;//设置贪吃蛇的初始分数为0
ps->_food_weight = 10;//设置食物的初始分数为10
ps->_sleep_time = 200;//设置贪吃蛇的移动速度
ps->_status = ok;//设置贪吃蛇的状态为正常
}
//打印食物
void creatfood(pSnake ps)
{
int x, y;
again:
//产生x的坐标应该是2的倍数,这样才能和蛇头的坐标对齐
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
//通过遍历蛇的节点(使用'pSnakeNode cur = ps->_psnake; 开始)
//检查新生成的食物坐标是否与蛇身的任何部分重叠。
pSnakeNode cur = ps->_psnake;//创建蛇头的坐标
//食物不能和蛇身对齐
while (cur)
{
//如果重叠,使用goto again;跳转到again标签,重新生成坐标。
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
//使用malloc分配内存给一个新的 `SnakeNode` 类型的节点,用于表示食物。
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
//如果 `malloc` 返回 `NULL`(即内存分配失败),则输出错误并返回。
if (pFood == NULL)
{
perror("CreateFood::malloc()");
return;
}
//将新生成的坐标x和y赋给食物节点。
pFood->x = x;
pFood->y = y;
//将食物节点的next指针设置为NULL,因为它将是一个单独的节点。
pFood->next = NULL;
setpos(x, y);//定位食物的位置
wprintf(L"%c", FOOD);
//将食物节点 `pFood` 赋值给 `ps- > _pfood`
ps->_pfood = pFood;
}
//游戏的初始化
void GameStart(pSnake ps)
{
//1.设置窗口大小
system("mode con cols=100 lines=30");
//设置cmd窗⼝名称
system("title 贪吃蛇");
//2.隐藏光标
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorIofo;
GetConsoleCursorInfo(houtput, &CursorIofo);
CursorIofo.bVisible = false;
SetConsoleCursorInfo(houtput, &CursorIofo);
//3.打印环境界面和功能介绍
welcometogame();
//4. 绘制地图
CreateMap();
//5. 创建蛇
InitSnake(ps);
//6. 创建食物
creatfood(ps);
}
//打印提示信息
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退出游戏,按空格暂停游戏");
}
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return(ps->_pfood->x == pn ->x && ps->_pfood->y == pn->y);
}
//下一个坐标是食物
void EatFood(pSnakeNode pn, pSnake ps)
{
//通过头插法食物节点(ps->_pfood)插入到蛇的头部(ps->_psnake)
//使蛇身长度增加
ps->_pfood->next = ps->_psnake;
ps->_psnake = ps->_pfood;
//释放下一个位置的节点
free(pn);
pn = NULL;
//打印蛇
//使用setpos函数和wprintf函数来更新蛇的显示。
pSnakeNode cur = ps->_psnake;
while (cur)
{
setpos(cur->x, cur ->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//蛇吃掉食物后分数(ps->_score)会增加。
//食物分数的增加随着函数(ps->_food_weight)的
//变化而变化
ps->_score += ps->_food_weight;
//重新创建食物
creatfood(ps);
}
//下一个坐标不是食物
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插法
pn->next = ps->_psnake;
ps->_psnake = pn;
//打印蛇
pSnakeNode cur = ps->_psnake;
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->x == 0 || ps->_psnake->x == 56 ||
ps->_psnake->y == 0 || ps->_psnake->y == 26 )
{
ps->_status = kill_by_wall;
}
}
//撞到自己游戏失败
void KillBySelf(pSnake ps)
{
//定义一个pSnakeNode类型的指针cur,并将其初始化为贪吃蛇的第二个节点
//目的是为了避免蛇的头部与蛇的头部进行比较
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;
}
}
//蛇的移动
void SnakeMove(pSnake ps)
{
//为蛇的下一个节点分配内存
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
//内存分配失败则malloc返回NULL,则打印一个错误消息并返回。
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);
}
//检测蛇是否撞到自己
KillBySelf(ps);
//检测蛇是否撞墙
KillByWall(ps);
}
//游戏运行的逻辑
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();
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_status = exit_normal;
}
else if (KEY_PRESS(VK_F3))
{
//加速蛇的移动速度,并增加食物的分数。
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速蛇的移动速度,并减少食物的分数。
if (ps->_food_weight > 2)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
//调用SnakeMove函数来移动蛇
SnakeMove(ps);
//控制贪吃蛇的移动速度
Sleep(ps->_sleep_time);
} while (ps->_status==ok);
}
void GameEnd(pSnake ps)
{
setpos(24, 12);
switch(ps->_status)
{
case exit_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 循环遍历链表,并逐个释放节点。
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
5.3 tect.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"snake.h"
//此函数用来完成游戏测试逻辑
void test()
{
int ch = 0;
do
{
system("cls");
//创建游戏
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;
}