C语言:进阶版贪吃蛇游戏(含有穿墙模式)附源码

源代码:加强版贪吃蛇游戏_多种食物种类和穿墙功能

目录

1.前言

1.1目标

1.2所需技术

2.Win32API介绍

2.1Win32API

2.2控制台程序

2.3控制台屏幕上的坐标COORD

类型声明

坐标系统:

赋值

[2.4GetStdHandle 函数](#2.4GetStdHandle 函数)

[2.5GetConsoleCursorInfo 函数](#2.5GetConsoleCursorInfo 函数)

2.5.1CONSOLE_CURSOR_INFO

2.6SetConsoleCursorInfo

2.7SetConsoleCursorPosition

SetPos:封装一个设置光标位置的函数

2.8GetAsyncKeyState

3.游戏设计与分析

3.1地图

[3.1.1 本地化](#3.1.1 本地化)

[3.1.2 类项](#3.1.2 类项)

3.1.3setlocale函数

3.1.4宽字符的打印

3.1.5地图坐标

3.2蛇身和食物

3.3数据结构设计

4.核心逻辑实现

[4.1GameStart - 初始化游戏](#4.1GameStart - 初始化游戏)

4.1.1打印开始界面以及功能介绍

4.1.2绘制地图

4.1.3绘制游戏侧方帮助信息

4.1.4初始化蛇身

4.1.5创建食物

4.2游戏运行

4.2.1KEY_PRESS

4.2.2蛇身移动

4.2.3NextIsFood

4.2.4EatFood

4.2.5NoFood

4.2.6检测蛇是否撞墙

4.2.7检测蛇是否撞到自己

4.2.8穿墙模式

4.3游戏结束

5.测验


1.前言

1.1目标

使用C语言在Windows环境的控制台中模拟实现贪吃蛇游戏,并增加多种食物和个性化穿墙模式。

实现功能:

贪吃蛇游戏的地图,可通过方向键控制贪吃蛇的方向,蛇撞墙游戏结束,蛇撞自身游戏结束,实时计算得分,控制蛇加速或减速,暂停游戏,随机刷新不同食物以及穿墙果实提供穿墙功能。

1.2所需技术

C语言函数,枚举,结构体,动态内存管理,预处理指令,链表,Win32API等。


2.Win32API介绍

2.1Win32API

Win32 APIWindows 应用程序编程接口 的核心集合,是微软 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;
}

本章完。

相关推荐
开开心心_Every4 小时前
手机PDF处理工具:支持格式转换与批注
游戏·微信·智能手机·pdf·逻辑回归·excel·语音识别
njsgcs4 小时前
ppo 游戏导航视觉 基于cnn 两个动作空间 训练120轮记录
游戏·ppo
sulikey15 小时前
Steam《妹居物语》接入deepseek API教程
游戏·api·steam·deepseek·妹居物语
诺狞猫21 小时前
思澈科技solution井字棋游戏【外置应用】
科技·游戏
德育处主任Pro1 天前
『NAS』在绿联部署一个像素风宝可梦同人游戏-pokerogue
游戏·docker·群晖·nas·绿联
华硕之声1 天前
HIFI、OWS耳机,磁轴键盘等ROG外设纷纷亮相
科技·游戏·steam
串流游戏小天才1 天前
伊莫挥爪测试上线!UU远程助力手机开启冒险之旅
游戏·智能手机·电脑
njsgcs2 天前
ai游戏画面分析+吐槽
游戏
bin91532 天前
(文后附完整代码)html+css+javascript 弓箭射击游戏项目分析
前端·javascript·css·游戏·html·前端开发