目录
[2.4GetStdHandle 函数](#2.4GetStdHandle 函数)
[2.5GetConsoleCursorInfo 函数](#2.5GetConsoleCursorInfo 函数)
[3.1.1 本地化](#3.1.1 本地化)
[3.1.2 类项](#3.1.2 类项)
[4.1GameStart - 初始化游戏](#4.1GameStart - 初始化游戏)
1.前言
1.1目标
使用C语言在Windows环境的控制台中模拟实现贪吃蛇游戏,并增加多种食物和个性化穿墙模式。
实现功能:
贪吃蛇游戏的地图,可通过方向键控制贪吃蛇的方向,蛇撞墙游戏结束,蛇撞自身游戏结束,实时计算得分,控制蛇加速或减速,暂停游戏,随机刷新不同食物以及穿墙果实提供穿墙功能。
1.2所需技术
C语言函数,枚举,结构体,动态内存管理,预处理指令,链表,Win32API等。
2.Win32API介绍
2.1Win32API
Win32 API 是 Windows 应用程序编程接口 的核心集合,是微软 Windows 操作系统(从Windows 95、NT 到现在的 Windows 10/11)为应用程序提供的底层服务接口 。你可以将它理为Windows 操作系统本身提供给开发者的"工具库"或"系统调用"。
-
Win32: 最初代表面向 32 位 Windows 的 API(区别于 16 位的 Win16)。现在它已成为一个品牌名称,涵盖了 32 位和 64 位 Windows 平台。
-
API: 应用程序编程接口,是一组预定义的函数、数据结构、常量和消息,用于构建软件应用程序。
简单比喻: 如果把 Windows 操作系统看作一个功能强大的"机器人",那么 Win32 API 就是操作这个机器人的遥控器面板。开发者通过按下遥控器上的不同按钮(调用不同的 API 函数),来指挥操作系统完成各种任务,例如创建窗口、绘制图形、读写文件、管理内存、处理用户输入等。
2.2控制台程序
平时我们运行起来的黑框程序就是控制台程序:


在Windows系统中,可以使用cmd命令来设置控制台窗口的长宽,如:
mode con cols=100 lines=30
也可以设置控制台窗口的名字:
title 贪吃蛇

通过C语言调用函数system来完成:
#include <stdio.h>
#include <stdlib.h>
// system函数可以用来执行系统命令
int main()
{
// 控制台相关属性
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
return 0;
}
2.3控制台屏幕上的坐标COORD
COORD 是Windows API中定义的一个简单结构体,专门用于表示控制台屏幕缓冲区中的字符位置坐标。
类型声明
typedef struct _COORD {
SHORT X; // 水平坐标(列)
SHORT Y; // 垂直坐标(行)
} COORD;
坐标系统:
(0,0) → (1,0) → (2,0) → ... → (n,0)
↓ ↓ ↓ ↓
(0,1) (1,1) (2,1) ... (n,1)
↓ ↓ ↓ ↓
... ... ... ... ...
↓ ↓ ↓ ↓
(0,m) (1,m) (2,m) ... (n,m)
赋值
#include <windows.h>
int main()
{
COORD pos1 = { 6,6 };
COORD pos2 = { 6,10 };
return 0;
}
2.4GetStdHandle 函数
官方文档:https://learn.microsoft.com/zh-cn/windows/console/getstdhandle
GetStdHandle是⼀个WindowsAPI函数。它会于从⼀个特定的标准设备(标准输入、标准输出或标 准错误)中取得⼀个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
// 获得标准输出设备的句柄
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE); // HANDLE是指针
2.5GetConsoleCursorInfo 函数
官方文档:https://learn.microsoft.com/zh-cn/windows/console/getconsolecursorinfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
语法:
BOOL WINAPI GetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
pConsoleCursorInfo是指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关主机光标的信息。
举例:
// 获得标准输出设备的句柄
HANDLE houtput = NULL; // HANDLE是指针
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(houtput, &cursor_info);
2.5.1CONSOLE_CURSOR_INFO
此结构体包含有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间,光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
bVisible,游标的可见性。如果光标可见,则此成员为TRUE。
2.6SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
举例:
// 获得标准输出设备的句柄
HANDLE houtput = NULL; // HANDLE是指针
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
// 定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
// 获取和houtput句柄有关的控制台上的光标信息,存放在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);
// 修改光标的占比
cursor_info.dwSize = 100;
// 设置和houtput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
system("pause");
2.7SetConsoleCursorPosition
官方文档:https://learn.microsoft.com/zh-cn/windows/console/setconsolecursorposition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
语法:
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
举例:
// 获得标准输出设备的句柄
HANDLE houtput = NULL; // HANDLE是指针
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
// 定位光标的位置
COORD pos = { 6,10 };
SetConsoleCursorPosition(houtput, pos);
system("pause");

SetPos:封装一个设置光标位置的函数
void set_pos(short x, short y)
{
// 获得标准输出设备的句柄
HANDLE houtput = NULL; // HANDLE是指针
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
// 定位光标的位置
COORD pos = { 6,10 };
SetConsoleCursorPosition(houtput, pos);
}
int main()
{
set_pos(5, 3);
system("pause");
return 0;
}
2.8GetAsyncKeyState
获取按键情况,语法:
SHORT GetAsyncKeyState(
[in] int vKey
);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
虚拟键位官方文档:https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
逻辑:
int main()
{
short ret = GetAsyncKeyState(0x35);
if ((ret & 1) == 1)
printf("5被按过\n");
else
printf("没有被按过\n");
return 0;
}
3.游戏设计与分析
3.1地图

在游戏地图中,打印墙体使用宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。
扩展C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是自己的,但这些假定并不是在世界的任何地方都适用。
C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的e的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel(α),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这一段。至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个学节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256×256=65536个符号。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型wchar_t 和宽字符的输入和输出函数,加入和<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
3.1.1<locale.h>本地化
<locale.h>提供的函数用于控制c标准库中对于不同的地区会产生不一样行为的部分。
在标准可以中,依赖地区的部分有以下几项:
数字量的格式,货币量的格式,字符集,日期和时间的表示形式
3.1.2 类项
官方文档:https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/setlocale-wsetlocale?view=msvc-170
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:
LC_COLLATE:影响字符串比较函数strcoll()和strxfrm()。
LC_CTYPE:影响字符处理函数的行为。
LC_MONETARY:影响货币格式。
LC_NUMERIC:影响printf()的数字格式。
LC_TIME:影响时间格式strftime()和wcsftime()。
LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语言环境。
3.1.3setlocale函数
文档:https://legacy.cplusplus.com/reference/clocale/setlocale/?kw=setlocale
char* setlocale (int category, const char* locale);
setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale的第一个参数可以是前面说明的类项中任意一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有类项。
C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。
在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常方式执行。
当程序运行起来后想改变地区,就只能显式调用setlocale函数。用""作为第二个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序就会适应本地环境。
比如:切换到本地模式就会支持宽字符的输出。
setlocale(LC_ALL, " ");//切换到本地环境
setlocale的返回值是一个字符串指针,表示已经设置好的格式。如果调用失败,则返回空指针NULL。setlocale()可以用来查询当前地区,这是第二个参数就可以设置为NULL了。
举例:
#include <locale.h>
int main()
{
char* ret = setlocale(LC_ALL, NULL);
printf("%s\n", ret);
ret = setlocale(LC_ALL, "");
printf("%s\n", ret);
return 0;
}

3.1.4宽字符的打印
宽字符的字面量必须加上前缀L,否则C语言会把字面量当作窄字符类项处理。前缀L在单引号前面,表示宽字符,宽字符的打印使用wprintf,对应wprintf()的占位符为%lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls。
#include <stdio.h>
#include<locale.h>
int main() {
setlocale(LC_ALL, ""); // 适配本地化
wchar_t ch1 = L'●';
wchar_t ch2 = L'比';
wchar_t ch3 = L'特';
wchar_t ch4 = L'★';
printf("%c%c\n", 'a', 'b');
wprintf(L"%c\n", ch1);
wprintf(L"%c\n", ch2);
wprintf(L"%c\n", ch3);
wprintf(L"%c\n", ch4);
return 0;
}
普通字符和宽字符打印出宽度的展示如下:

3.1.5地图坐标

3.2蛇身和食物
初始化状态,假设蛇的长度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,比如(24,5)处开始出现蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。 关于食物,就是在墙体内随机生成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然后打印★。

3.3数据结构设计
使用链表存储蛇的信息,蛇的每一节就是链表的每个节点,每个节点记录蛇身节点在地图上的坐标。蛇身节点结构:
// 蛇身的节点类型
typedef struct SnakeNode
{
// 坐标
int _x;
int _y;
// 指向下一个节点的指针
struct SnakeNode* next;
}SnakeNode, * pSnakeNode; // 对结构体指针重命名为pSnakeNode
通过封装一个Snake结构来管理整条贪吃蛇
// 贪吃蛇
typedef struct Snake
{
pSnakeNode _pSnake; // 指向蛇头的指针
pSnakeNode _pFood; // 指向食物节点的指针
enum DIRECTION _dir; // 蛇的方向
enum GAME_STATUS _status; // 游戏的状态
enum FOOD_TYPE _food_type; // 当前食物的种类
int _food_weight; // 食物的分数
int _score; // 总分
int _sleep_time; // 休息时间
int _wall_pass_steps; // 剩余穿墙步数
}Snake, * pSnake;
枚举蛇的方向
// 蛇的方向
enum DIRECTION
{
up = 1,
DOWN,
LEFT,
RIGHT
};
枚举游戏状态
// 蛇的状态
// 正常,撞墙,撞到自己,正常退出
enum GAME_STATUS
{
OK,
KILL_BY_WALL, // 撞墙
KILL_BY_SELF, // 撞到自己
END_NOMAL // 正常结束
};
4.核心逻辑实现
4.1GameStart - 初始化游戏
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); // 设置控制台光标状态
// 1.打印开始界面
// 2.功能介绍
WelcomeToGame();
// 3.绘制地图
CreateMap();
// 4.创建蛇
InitSnake(ps);
// 5.创建食物
CreateFood(ps);
}
4.1.1打印开始界面以及功能介绍
void WelcomeToGame()
{
SetPos(40, 14);
wprintf(L"欢迎来到贪吃蛇小游戏");
SetPos(42, 20);
system("pause");
system("cls"); // 清理屏幕
SetPos(25, 14);
wprintf(L"用↑ . ↓ . ← . →控制蛇的移动,按F3加速,F4减速\n");
SetPos(25, 15);
wprintf(L"加速可以得到更高的食物分\n");
SetPos(25, 16);
wprintf(L"特色玩法:\n");
SetPos(25, 17);
wprintf(L"★-普通 ▲-加速(双倍分) ▼-减速(分减半) ◆-大奖\n");
SetPos(25, 18);
wprintf(L"◎ - 穿墙果实 (获得30步穿墙能力)\n");
SetPos(42, 20);
system("pause");
system("cls"); // 清理屏幕
}
4.1.2绘制地图
假设棋盘27行,58列。

在VS2022 环境中,实心方块 L'█' 被当作了"半角字符"显示(宽度为1),而不是预期的"全角字符"(宽度为2)。因此对代码做出修改:
void CreatMap()
{
// 上
int i = 0;
for (i = 0; i < 29; i++)
{
wprintf(L"██");
}
// 下
SetPos(0, 26);
for (i = 0; i < 29; i++)
{
wprintf(L"██");
}
// 左
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"██");
}
// 右
for (i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"██");
}
}
4.1.3绘制游戏侧方帮助信息
void PrintHelpInfo()
{
SetPos(64, 15);
printf("不能穿墙,不能咬到自己\n");
SetPos(64, 16);
printf("用↑.↓.←.→分别控制蛇的移动.");
SetPos(64, 17);
printf("F3加速,F4为减速\n");
SetPos(64, 18);
printf("ESC :退出游戏.space:暂停游戏.");
SetPos(64, 20);
printf("--- 食物图鉴 ---\n");
SetPos(64, 21);
wprintf(L"%lc : 普通 (+10分)\n", FOOD_NORMAL);
SetPos(64, 22);
wprintf(L"%lc : 加速 (+20分, 速度UP)\n", FOOD_SPEED);
SetPos(64, 23);
wprintf(L"%lc : 减速 (+5分, 速度DOWN)\n", FOOD_SLOW);
SetPos(64, 24);
wprintf(L"%lc : 大奖 (+50分)\n", FOOD_BONUS);
SetPos(64, 25);
wprintf(L"%lc : 穿墙道具!!\n", FOOD_WALLPASS);
}

4.1.4初始化蛇身
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。创建5个节点,然后将每个节点存放在链表中进行管理,创建完蛇身后,将蛇的每一节打印在屏幕上。
蛇的初始位置为(24,5),再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。
游戏状态是:OK,蛇身移动速度:200ms,蛇的默认方向:RIGHT,初始成绩:0,每个食物的分数:10
void InitSnake(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL;
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 + 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->_dir = RIGHT;
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 150;
ps->_wall_pass_steps = 0; // 初始化穿墙步数为0
ps->_status = OK;
}
4.1.5创建食物
先随机生成食物的坐标:
x坐标是2的倍数,食物的坐标要在墙体内部,食物的坐标不能和蛇身每个节点的坐标重复。
创建食物节点,打印食物。
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
// x: 2 ~ 54
// y: 1~25
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
// x和y不能和蛇身冲突
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;
}
else
{
pFood->_x = x;
pFood->_y = y;
pFood->next = NULL;
ps->_pFood = pFood;
// 特色功能,随机生成不同类型的食物
// 0-4: 普通, 5-6: 加速, 7: 减速, 8: 奖励, 9: 穿墙
int chance = rand() % 10;
SetPos(x, y);
if (chance < 5)
{
ps->_food_type = TYPE_NORMAL;
ps->_food_weight = 10;
wprintf(L"%lc", FOOD_NORMAL);
}
else if (chance < 7)
{
ps->_food_type = TYPE_SPEED; // ▲
ps->_food_weight = 20;
wprintf(L"%lc", FOOD_SPEED);
}
else if (chance < 8)
{
ps->_food_type = TYPE_SLOW; // ▼
ps->_food_weight = 5;
wprintf(L"%lc", FOOD_SLOW);
}
else if (chance < 9)
{
ps->_food_type = TYPE_BONUS; // ◆
ps->_food_weight = 50;
wprintf(L"%lc", FOOD_BONUS);
}
else
{
ps->_food_type = TYPE_WALLPASS; // ◎
ps->_food_weight = 15;
wprintf(L"%lc", FOOD_WALLPASS);
}
}
}
4.2游戏运行
游戏运行期间,右侧打印帮助信息,提示玩家
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
确定了蛇的方向和速度,蛇就可以移动了。
void PrintHelpInfo()
{
SetPos(64, 15);
printf("不能穿墙,不能咬到自己\n");
SetPos(64, 16);
printf("用↑.↓.←.→分别控制蛇的移动.");
SetPos(64, 17);
printf("F3加速,F4为减速\n");
SetPos(64, 18);
printf("ESC :退出游戏.space:暂停游戏.");
SetPos(64, 20);
printf("--- 食物图鉴 ---\n");
SetPos(64, 21);
wprintf(L"%lc : 普通 (+10分)\n", FOOD_NORMAL);
SetPos(64, 22);
wprintf(L"%lc : 加速 (+20分, 速度UP)\n", FOOD_SPEED);
SetPos(64, 23);
wprintf(L"%lc : 减速 (+5分, 速度DOWN)\n", FOOD_SLOW);
SetPos(64, 24);
wprintf(L"%lc : 大奖 (+50分)\n", FOOD_BONUS);
SetPos(64, 25);
wprintf(L"%lc : 穿墙道具!!\n", FOOD_WALLPASS);
}
4.2.1KEY_PRESS
检测按键状态,封装一个宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
4.2.2蛇身移动
void SnakeMove(pSnake ps)
{
// 创建一个节点,表示蛇下一步
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("SnakeMove()::malloc()");
return;
}
// 计算下个节点,暂时先基于当前方向
int next_x = ps->_pSnake->_x;
int next_y = ps->_pSnake->_y;
switch (ps->_dir)
{
case UP: next_y -= 1; break;
case DOWN: next_y += 1; break;
case LEFT: next_x -= 2; break;
case RIGHT: next_x += 2; break;
}
// 穿墙逻辑判定
// 判断下个位置是否是墙
bool is_wall = (next_x == 0 || next_x == 56 || next_y == 0 || next_y == 26);
if (is_wall)
{
if (ps->_wall_pass_steps > 0)
{
// 如果有穿墙步数,执行传送
if (next_x == 0) next_x = 54;
else if (next_x == 56) next_x = 2;
if (next_y == 0) next_y = 25;
else if (next_y == 26) next_y = 1;
//ps->_wall_pass_steps--; // 撞墙才消耗
}
// 如果没有穿墙步数,坐标保持在墙上,KillByWall 会随后判定死亡
}
// 无论是否撞墙,每走一步都消耗穿墙时间
if (ps->_wall_pass_steps > 0)
{
ps->_wall_pass_steps--;
}
// 赋值给新节点
pNextNode->_x = next_x;
pNextNode->_y = next_y;
// 判断下一步是不是食物
if (NextIsFood(pNextNode, ps))
{
// 是食物
EatFood(pNextNode, ps);
}
else
{
// 不是食物
NoFood(pNextNode, ps);
}
// 检测蛇是否撞墙
KillByWall(ps);
// 检测蛇是否撞到自己
KillBySelf(ps);
}
4.2.3NextIsFood
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return (ps->_pFood->_x == pn->_x && ps->_pFood->_y == pn->_y);
}
4.2.4EatFood
void EatFood(pSnakeNode pn, pSnake ps)
{
// 头插挂食物
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
// 释放下一个位置的节点
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;
switch (ps->_food_type)
{
case TYPE_SPEED: // 吃了加速食物,休息时间变短
ps->_sleep_time -= 30;
if (ps->_sleep_time < 50) ps->_sleep_time = 50; // 上限
break;
case TYPE_SLOW: // 吃了减速食物,休息时间变长
ps->_sleep_time += 30;
if (ps->_sleep_time > 350) ps->_sleep_time = 350; // 下限
break;
case TYPE_WALLPASS: // 吃到穿墙果实,增加步数
ps->_wall_pass_steps += 30;
break;
case TYPE_BONUS: // 奖励食物不影响速度
case TYPE_NORMAL:
default:
break;
}
// 重新创建食物
CreateFood(ps);
}
4.2.5NoFood
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);
// 把倒数第二个节点的地址置为空
cur->next = NULL;
}
4.2.6检测蛇是否撞墙
void KillByWall(pSnake ps)
{
// 如果穿墙逻辑生效,这里通常不会被触发
// 因为SnakeMove里已经把坐标修正到地图内
// 但作为双重保险保留
if (ps->_pSnake->_x == 0 || ps->_pSnake->_x == 56 ||
ps->_pSnake->_y == 0 || ps->_pSnake->_y == 26)
{
ps->_status = KILL_BY_WALL;
}
}

4.2.7检测蛇是否撞到自己
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;
}
}
4.2.8穿墙模式
void GameRun(pSnake ps)
{
// 打印帮助信息
PrintHelpInfo();
do
{
// 总得分和食物分值
SetPos(64, 10);
printf("总得分:%d\n", ps->_score);
SetPos(64, 11);
printf("目标:");
switch (ps->_food_type)
{
case TYPE_NORMAL: printf("普通食物(★)"); break;
case TYPE_SPEED: printf("加速陷阱(▲)"); break;
case TYPE_SLOW: printf("减速福利(▼)"); break;
case TYPE_BONUS: printf("超级大奖(◆)"); break;
case TYPE_WALLPASS: printf("穿墙果实(◎)"); break;
}
SetPos(64, 12);
printf("当前速度延迟:%d ms", ps->_sleep_time);
// 显示穿墙状态
SetPos(64, 13);
if (ps->_wall_pass_steps > 0)
{
// 红色高亮显示,增加紧迫感
printf("穿墙状态:剩余 %2d 步! ", ps->_wall_pass_steps);
}
else
{
printf("穿墙状态:未激活 ");
}
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->_sleep_time > 80)
{
ps->_sleep_time -= 20;
}
}
else if (KEY_PRESS(VK_F4))
{
// 减速
if (ps->_food_weight < 350)
{
ps->_sleep_time += 20;
}
}
// 蛇走一步
SnakeMove(ps);
Sleep(ps->_sleep_time);
} while (ps->_status == OK);
}
4.3游戏结束
void GameEnd(pSnake ps)
{
SetPos(16, 12);
switch (ps->_status)
{
case END_NOMAL:
printf("已主动结束游戏,下次见\n");
break;
case KILL_BY_SELF:
printf("很遗憾,不小心撞到自己了,游戏结束\n");
break;
case KILL_BY_WALL:
printf("很遗憾,不小心撞到墙上了,游戏结束\n");
break;
}
SetPos(0, 27);
// 释放蛇身链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
5.测验
#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):");
while (1)
{
ch = getchar();
if (ch == '\n' || ch == ' ') continue;
while (getchar() != '\n');
if (ch == 'Y' || ch == 'y' || ch == 'N' || ch == 'n') {
break;
}
SetPos(20, 15);
printf("输入错误,请重新输入(Y/N): \b\b\b\b"); // \b是退格
}
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
}
int main()
{
// 适配本地环境
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
test();
return 0;
}
本章完。