零基础入门C语言之贪吃蛇的实现

在阅读本文之前,建议读者优先阅读本专栏内前面的文章。


目录

前言

一、前置知识

二、游戏开始前初始化

三、游戏运行过程

四、游戏结束

总结


前言

贪吃蛇是久负盛名的游戏,与俄罗斯方块、扫雷等游戏共同位列经典游戏行列。本篇文章主要介绍如何使用C语言及数据结构实现简单的贪吃蛇游戏。


一、前置知识

那么我们要实现的贪吃蛇要有如下基本的功能:贪吃蛇地图的绘制、蛇吃食物的功能、蛇撞墙或者自身死亡、计算得分、蛇身加速和减速、暂停游戏。目前来说,它需要我们掌握C语言的函数、枚举、结构体、动态内存管理、预处理指令、链表和Win32 API等。

上面的部分我们基本都学习过,除了上面最后一个部分,我们接下来来介绍一下这个部分。Windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为Application Programming Interface,简称API函数。WIN32 API也就是Microsoft Windows 32位平台的应用程序编程接口。

首先我们来介绍一下控制台窗口,那么什么是控制台窗口呢?其实我们平时运行程序输出结果的窗口就是控制台窗口,我们可以通过win+R后输入cmd和回车调出它。如下:

我们可以通过cmd命令来设置控制台窗口的长宽,比如说我们输入如下的代码,来将控制台窗口大小设置为30行100列:

bash 复制代码
mode con cols=100 lines=30

可以看到其结果:

同时我们也可以使用如下代码控制这个控制台的名字:

bash 复制代码
title 贪吃蛇

其运行结果如下:

但是如果说我们作为用户的话,不了解这个控制台机理的话,这么操作就很不方便,所以我们可以想办法在代码中去实现上面的步骤,那就需要使用C语言中的system函数,我们之前写扫雷程序的时候也应用过这个函数。例如如下代码:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
int main()
{
	//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
	system("mode con cols=100 lines=30");
	//设置cmd窗⼝名称
	system("title 贪吃蛇"); 
	return 0;
}

但是由于这个代码会直接运行结束,所以我们无法观测到命令行上面名称的改变,所以我们可以在后面加上一个getchar函数,让程序不直接运行结束,运行结果如下:

然后我们需要了解一下控制台屏幕上的坐标COORD,这是一个Windows API中定义的一个结构体,表示一个字符在控制台屏幕的坐标。其定义形式如下:

cpp 复制代码
typedef struct _COORD {
    SHORT X;
    SHORT Y;
} COORD, *PCOORD;

它的坐标定义规则是什么样子的呢?我们画个图出来方便描述:

如果说我们想给坐标赋值,就按照结构体的方式去赋值就可以了。然后我们需要了解的是GetStdHandle函数,它是用于从一个特定的标准设备(标准输入、标准输出、标准错误)中取得一个句柄,使这个句柄可以操作设备。其定义如下:

cpp 复制代码
HANDLE WINAPI GetStdHandle(
  _In_ DWORD nStdHandle
);

其中,它参数的取值只能为以下几种:

我们可以键入如下代码来完成对这个函数的使用:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main()
{
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
}

然后是对控制台屏幕缓冲区光标大小和可见性信息进行检索的GetConsoleCursor函数,我们还是首先来看看它的函数定义:

cpp 复制代码
BOOL WINAPI GetConsoleCursorInfo(
  _In_  HANDLE               hConsoleOutput,
  _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

其中的PCONSOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO结构的指针,这个结构主要接收主机游标的信息。我们可以通过以下代码实现应用:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main()
{
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);
}

但是我们这里面还涉及到了一个CONSOLE_CURSOR_INFO结构,这个东西具体是什么样子的呢?它的定义如下:

cpp 复制代码
typedef struct _CONSOLE_CURSOR_INFO {
 DWORD dwSize;
 BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

那这里面的结构变量都是什么意思啊?dwSize是指由光标填充的字符单元格的百分比,此值介于1到100之间,光标外观会变化,范围从完全填充单元格到单元底部的水平线条;bVisible则是游标的可见性,如果光标可见,则此成员为TRUE。那么也就是说,如果我们想实现光标的隐藏,我们就可以用下面这种定义方式:

cpp 复制代码
CursorInfo.bVisible = false; //隐藏控制台光标

我们其实可以试着打印一下刚才定义的cursorinfo中第一个变量的信息,也就是dwSize的值。那么其运行结果如下:

为什么结果是25呢,我们知道dwSize是指由光标填充的字符单元格的百分比,它这个25指的其实就是1号占2号的比例,也就是其实是说1号是2号的25%,所以才会显示25。

但实际上,我们没法直接通过结构体的方式去修改它,把它的值直接进行更改。什么意思呢?就是说如果说我想把它的值从25改成50,使用下面的方式是没法完成修改的。

cpp 复制代码
CursorInfo.dwSize = 50;

那如果说我们想完成修改的话,我们该怎么去做呢?这里就需要用到一个新的函数了。这个函数就是SetConsoleCursorInfo函数,它是专门用于设置指定控制台屏幕缓冲区光标的大小和可见性的。这个函数的定义如下:

cpp 复制代码
BOOL WINAPI SetConsoleCursorInfo(
  _In_       HANDLE              hConsoleOutput,
  _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

我们可以键入如下的代码,这段代码的含义就是来将光标改为隐藏状态:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
int main()
{
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(houtput, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(houtput, &CursorInfo);
}

我们运行代码,其结果如下:

可以看到的的确确,这个光标消失了,也就是被隐藏起来了。我们也可以通过这个函数来实现改变光标大小,比如说我们改为50就可以让光标变为原来的一半了。但是我这里遇到了一个问题,就是我无论如何都无法修改光标大小,直到我使用管理员身份运行这个生成的可执行程序,他才成功更改:

但实际上这个函数是不需要使用管理员权限的,这个原因暂时不清楚是什么。

我们再来看看下面这个更改光标位置的SetConsoleCursorPosition函数,它的函数声明如下:

cpp 复制代码
BOOL WINAPI SetConsoleCursorPosition(
  _In_ HANDLE hConsoleOutput,
  _In_ COORD  dwCursorPosition
);

它能实现出的功能就是我们可以将想设置的坐标信息放在COORD类型的pos中,然后调用SetConsoleCursorPosition函数将这个光标设置到我们想要指定的位置。所以我们可以键入如下的代码:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
int main()
{
	COORD pos = { 10, 5 };
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

我们运行一下这个代码,发现其结果如下所示:

需要注意的是,我们设置的光标位置并非是现在正在闪烁的的这个,而是我使用箭头指出的这个位置。那么如果说我们每回都这么去实现更改光标位置的话是十分麻烦的,我们就可以通过封装一个函数来实现,请读者思考一下该如何实现,我这里给出一个示例代码:

cpp 复制代码
void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE hOutput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(hOutput, pos);
}

还是传入和刚才一样的参数,然后运行这段代码,其运行的结果如下:

然后是我们要了解的最后一个函数GetAsyncKeyState,它的声明如下:

cpp 复制代码
SHORT GetAsyncKeyState(
 int vKey
);

它的作用是将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。GetAsyncKeyState的返回值是short类型,在上一次调用GetAsyncKeyState函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。 如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。

那么想要理解上面这个函数,我们首先需要了解什么是虚拟键码?虚拟键码,也叫虚拟键值,是在Windows编程中用于表示键盘或鼠标按钮的常数。它们是操作系统提供的一种抽象,使得开发者可以不必关心硬件的具体细节而处理键盘事件。虚拟键码通常在处理键盘输入时使用,例如在响应WM_KEYDOWN或WM_KEYUP消息时。

下面这个网站为我们详细列举出了虚拟键码的对应关系,我们这里不多赘述:

Virtual-Key 代码 (Winuser.h) - Win32 apps | Microsoft Learn

为了实现这个功能,我们可以将其封装为一个宏,如下:

cpp 复制代码
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

那么到这里我们贪吃蛇的前置知识部分就结束了。然后我们就可以尝试来开始实现贪吃蛇了。我们要实现的贪吃蛇是什么样子的呢?

在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★。普通的字符是占一个字节的,这类宽字符是占用2个字节。 这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。 C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这一段。 至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536个符号。 后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入宽字符的类型wchar_t和宽字符的输入和输出函数,加入<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

<locale.h > 提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。在标准中,依赖地区的部分有以下几项:数字量的格式、货币量的格式、字符集、日期和时间的表示形式。通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以 C 语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:LC_COLLATE(影响字符串比较函数strcoll和strxfrm等)、LC_CTYPE(影响字符处理函数的行为)、LC_MONETARY(影响货币格式)、LC_NUMERIC(影响printf数字格式)、LC_TIME(影响时间格式函数strftime和wcsftime)、LC_ALL(针对所有类项修改,将以上所有的类别设置为给定的语言环境)。

当然,如果有兴趣详细了解,也可以访问下面的链接:

%> | Microsoft Learn

如果说我们想要改变类项的话,就要借助setlocale函数,其函数声明如下:

cpp 复制代码
char* setlocale (int category, const char* locale);

setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是 LC_ALL,就会影响所有的类项。C 标准给第二个参数仅定义了 2 种可能取值:"C" 和 ""。在任意程序执行开始,都会隐藏式执行调用:

cpp 复制代码
setlocale(LC_ALL, "C");

当地区设置为 "C" 时,库函数按正常方式执行,小数点是一个点。当程序运行起来后想改变地区,就只能显示调用setlocale函数。用 "" 作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。

cpp 复制代码
setlocale(LC_ALL, " ");

那么如果想在屏幕上打印宽字符的话,该如何操作呢?我们可以键入如下的代码:

cpp 复制代码
#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"%lc\n", ch1);
 wprintf(L"%lc\n", ch2);
 wprintf(L"%lc\n", ch3);
 wprintf(L"%lc\n", ch4);
 return 0;
}

但是不知道为什么,我这个控制台窗口显示不出这个字体,只有当提升权限为管理员的时候才能实现:

当然,实际上这个函数也可以用来查询当前的地区,只要将第二个参数设置为NULL即可。从输出的结果来看,我们发现⼀个普通字符占一个字符的位置但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。普通字符和宽字符打印出的宽度展示如下:

我们假设实现一个27行,58列的棋盘,再围绕地图画出墙如下:

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

二、游戏开始前初始化

接下来我们来实现以下整个过程,首先对于整个游戏我们要有一个大纲,我的想法如下:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <locale.h>

void test() {
	//创建贪吃蛇

	//初始化游戏

	//运行游戏

	//结束游戏
}

int main() {
	//设置适配本地环境
	setlocale(LC_ALL, "");
	
	test();
	
	return 0;
}

可以画个流程图:

首先我们先来设想一下如何实现蛇这个结构,相对来说这个结构使用链表的形式来实现是比较合适的,那么我们就可以来定义一个结构体来进行实现:

cpp 复制代码
#pragma once

//类型声明
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, *pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;

上面只是定义了一个蛇内部的节点,但是实际上蛇在动的过程中,还需要很多其他的状态参数,比如说方向、目前分数、存活状态等等等等。所以说我们就需要再定义一个蛇的结构:

cpp 复制代码
//贪吃蛇
typedef struct Snake
{
	pSnakeNode pSnake; //指向蛇头的指针
	pSnakeNode pFood; //指向食物的指针
	enum Direction dir; //蛇的方向
	enum GAME_STATUS status; //游戏状态
	int food_weight; //一个食物分数
	int score; //分数
	int sleep_time; //休息时间
}Snake;

其中的蛇的方向选择和存在状态我们都是可以穷举出来的,所以说我们就可以使用枚举自定义变量来进行实现:

cpp 复制代码
//蛇的方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//蛇的状态
enum GAME_STATUS
{
	OK, //正常
	KILL_BY_WALL, //撞墙
	KILL_BY_SELF, //撞到自己
	END_NORMAL //正常退出
};

那整个游戏大纲就大概变为如下的形式了:

cpp 复制代码
void test() {
	//创建贪吃蛇
	Snake snake = { 0 };
	//初始化游戏
	//0.设置窗口,光标隐藏
    //1.打印环境界面
	//2.功能介绍
	//3.绘制地图
	//4.创建蛇
	//5.创建食物
	//6.设置游戏相关信息
	GameStart(&snake);
	//运行游戏
	GameRun();
	//结束游戏
	GameEnd();
}

我们按照上面分析出的步骤一步一步来,首先就是设置窗口然后隐藏光标,这一步正好是我们前置知识中涉及到的部分,读者可先行思考,我这里给出示例代码:

cpp 复制代码
GameStart(pSnake ps)
{
	//0.设置窗口,然后光标隐藏
	
	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);

	system("pause");
}

这里面最后一行的代码只是方便我们调试,避免调试窗口直接运行结束,我们运行之后结果如下:

在此之后我们就可以打印环境界面并且介绍功能了,这部分主要是表示对玩家的欢迎并且介绍规则,所以主要就是使用wprintf函数和光标定位函数即可,当然我比较推荐直接使用我们之前封装好的坐标设置函数。请读者思考实现方法,我这里给出示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

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"加速得到更高分数");
	SetPos(42, 20);
	system("pause");
	system("cls");
}

void GameStart(pSnake ps)
{
	//0.设置窗口,然后光标隐藏
	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();
	
}

运行之后进入如下界面:

此时我们可以随意按键进行跳转到如下界面:

那么到这,我们的环境界面和规则介绍就结束了,然后我们可以思考一下如何实现地图的绘制。我们还是需要使用wprintf函数进行打印,然后借助■这个字符作为墙壁,然后后通过循环打印来圈出对应区域即可,请读者思考如何实现,我这里给出示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

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"加速得到更高分数");
	SetPos(42, 20);
	system("pause");
	system("cls");
}

void CreateMap()
{
	//上
	for (int i = 0; i < 29; i++) {
		wprintf(L"%lc", L'■');
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i < 29; i++) {
		wprintf(L"%lc", L'■');
	}
	//左
	for (int i = 1; i < 26; i++) {
		SetPos(0, i);
		wprintf(L"%lc", L'■');
	}
	//右
	for (int i = 1; i < 26; i++) {
		SetPos(56, i);
		wprintf(L"%lc", L'■');
	}
	SetPos(0, 27);
	system("pause");
}

void GameStart(pSnake ps)
{
	//0.设置窗口,然后光标隐藏
	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();
    
}

其运行结果如下:

然后我们就可以来去设计蛇了,首先就是我们先要去申请存放蛇身节点的空间,然后把这些节点通过链表的头插方法链接在一起,然后通过打印特定字符的方式让玩家可以在界面上看到蛇的出现,再去设置蛇的其他属性信息。请读者思考如何实现上述思路,我这里给出示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

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"加速得到更高分数");
	SetPos(42, 20);
	system("pause");
	system("cls");
}

void CreateMap()
{
	//上
	for (int i = 0; i < 29; i++) {
		wprintf(L"%lc", L'■');
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i < 29; i++) {
		wprintf(L"%lc", L'■');
	}
	//左
	for (int i = 1; i < 26; i++) {
		SetPos(0, i);
		wprintf(L"%lc", L'■');
	}
	//右
	for (int i = 1; i < 26; i++) {
		SetPos(56, i);
		wprintf(L"%lc", L'■');
	}
	SetPos(0, 27);
	system("pause");
}

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	for (int 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;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
	}

	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;
	ps->_status = OK;

	getchar();
}

void GameStart(pSnake ps)
{
	//0.设置窗口,然后光标隐藏
	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);
	
}

需要注意的是,这里面最后一行的getchar()也是为了防止直接运行结束,由于我们在前面设置的SetPos函数是将光标设置在最左边的节点,所以如果这里使用system函数的pause参数,就会导致后面四个节点被汉字字符串给掩盖掉,故而使用这种方法。其运行结果如图:

那么接下来我们来想一下如何实现创建食物。首先我们要保证食物的出现是随机的,并且食物的横坐标只能是2~54中的一个偶数,纵坐标只能是1~25中的一个数,所以说这个生成方法其实和我们之前写扫雷的时候随机生成雷的方法是相同的。当然,在生成这个食物之后,我们必须检测这个食物是不是与蛇的身体重合了。如果重合了,就必须重新生成。最后,把光标重置到食物的坐标处,然后打印出供玩家观测的特殊字符即可。请读者思考如何实现上述思路,我这里给出示例代码如下:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

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"加速得到更高分数");
	SetPos(42, 20);
	system("pause");
	system("cls");
}

void CreateMap()
{
	//上
	for (int i = 0; i < 29; i++) {
		wprintf(L"%lc", L'■');
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i < 29; i++) {
		wprintf(L"%lc", L'■');
	}
	//左
	for (int i = 1; i < 26; i++) {
		SetPos(0, i);
		wprintf(L"%lc", L'■');
	}
	//右
	for (int i = 1; i < 26; i++) {
		SetPos(56, i);
		wprintf(L"%lc", L'■');
	}
	SetPos(0, 27);
	system("pause");
}

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	for (int 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;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
	}

	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;
	ps->_status = OK;
}

void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;

again:
	x = (rand() % 27 + 1) * 2;
	y = rand() % 25 + 1;

	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;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	SetPos(pFood->x, pFood->y);
	wprintf(L"%lc", L'★');
    ps->_pFood = pFood;
}

void GameStart(pSnake ps)
{
	//0.设置窗口,然后光标隐藏
	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);
	getchar();
}

其运行结果如下:

三、游戏运行过程

我们首先现在界面的右边打印一些帮助信息,来帮助玩家时刻铭记相关规则,这个打印相对来说十分简单,我们在前面已经使用过很多次了。请读者思考如何实现,我这里给出示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

void PrintHelpInfo()
{
	SetPos(64, 10);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
	SetPos(64, 11);
	wprintf(L"%ls", L"用↑,↓,←,→来分别控制蛇的移动");
	SetPos(64, 12);
	wprintf(L"%ls", L"按F3为加速,按F4为减速");
	SetPos(64, 13);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
	SetPos(64, 14);
	wprintf(L"%ls", L"苏浙制作");
}

void GameRun(pSnake ps)
{
	//1.打印帮助信息
	PrintHelpInfo();
	getchar();
}

其运行结果如下:

然后我们需要完成打印总分数,和当前食物分值,以及检测按键的操作。打印分数适合上面相似的操作,而检测按键就需要用到我们前置知识介绍的那个宏,同时在检测之后我们要实现对其运动方向变量的修改,请读者思考如何实现,我这里给出示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

void PrintHelpInfo()
{
	SetPos(64, 10);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
	SetPos(64, 11);
	wprintf(L"%ls", L"用↑,↓,←,→来分别控制蛇的移动");
	SetPos(64, 12);
	wprintf(L"%ls", L"按F3为加速,按F4为减速");
	SetPos(64, 13);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
	SetPos(64, 14);
	wprintf(L"%ls", L"苏浙制作");
}

void Pause()
{
	while (1) {
		Sleep(200);
		if (KEY_PRESS(VK_SPACE)) {
			break;
		}
	}
}

void GameRun(pSnake ps)
{
	//1.打印帮助信息
	PrintHelpInfo();
	do {
	//2.打印总分数和食物分值
		SetPos(64, 18);
		printf("总分: %d", ps->_score);
		SetPos(64, 19);
		printf("当前食物分: %d", 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 = END_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;
			}
		}

	} while (ps->_status == OK);
	
}

这个代码暂时没办法检测,因为我们还没有让蛇动起来。那么现在我们就可以添加让蛇移动的函数了,其实蛇的移动本质上就是前面链表的头插和尾部节点的删除,然后需要判断我们是否吃到食物,吃到的话就增加长度,否则就减少长度,请读者思考上述思路如何实现,我给出示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;

again:
	x = (rand() % 27 + 1) * 2;
	y = rand() % 25 + 1;

	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;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	SetPos(pFood->x, pFood->y);
	wprintf(L"%lc", L'★');
	ps->_pFood = pFood;

}


void PrintHelpInfo()
{
	SetPos(64, 10);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
	SetPos(64, 11);
	wprintf(L"%ls", L"用↑,↓,←,→来分别控制蛇的移动");
	SetPos(64, 12);
	wprintf(L"%ls", L"按F3为加速,按F4为减速");
	SetPos(64, 13);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
	SetPos(64, 14);
	wprintf(L"%ls", L"苏浙制作");
}


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->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;
	ps->_score += ps->_food_weight;
	
	free(pn);
	pn = NULL;

	pSnakeNode cur = ps->_pSnake;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
	}

	CreateFood(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", L'●');
		cur = cur->next;
	}

	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	free(cur->next);
	cur->next = NULL;
}

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;
	}

	if (NextIsFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}
	else {
		NoFood(pNextNode, ps);
	}
}

void GameRun(pSnake ps)
{
	//1.打印帮助信息
	PrintHelpInfo();
	do {
	//2.打印总分数和食物分值
		SetPos(64, 18);
		printf("总分: %d", ps->_score);
		SetPos(64, 19);
		printf("当前食物分: %d", 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 = END_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;
			}
		}

		//3.蛇走一步的过程
		SnakeMove(ps);
		Sleep(ps->_sleep_time);
	} while (ps->_status == OK);
	
}

其运行结果如下:

由于这个动图不太好展示,我就故意让他撞墙,根据我们的代码,它在经过之后,就会将这部分重置为两个空格,故而墙壁会消失,即可得到证明。在此之后,我们就可以继续完成撞墙和咬到自己部分函数的处理。

那么撞墙检测其实就是检测蛇头的横纵坐标其中一个是否与墙的有所重合,咬自己的检测则是遍历蛇头之后的节点看看是否与蛇头重合,请读者思考上述思路实现,我给出示例代码:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;

again:
	x = (rand() % 27 + 1) * 2;
	y = rand() % 25 + 1;

	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;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	SetPos(pFood->x, pFood->y);
	wprintf(L"%lc", L'★');
	ps->_pFood = pFood;

}

void PrintHelpInfo()
{
	SetPos(64, 10);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
	SetPos(64, 11);
	wprintf(L"%ls", L"用↑,↓,←,→来分别控制蛇的移动");
	SetPos(64, 12);
	wprintf(L"%ls", L"按F3为加速,按F4为减速");
	SetPos(64, 13);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
	SetPos(64, 14);
	wprintf(L"%ls", L"苏浙制作");
}


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->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;
	ps->_score += ps->_food_weight;
	
	free(pn);
	pn = NULL;

	pSnakeNode cur = ps->_pSnake;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
	}

	CreateFood(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", L'●');
		cur = cur->next;
	}

	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	free(cur->next);
	cur->next = NULL;
}

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;
	}

	if (NextIsFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}
	else {
		NoFood(pNextNode, ps);
	}

	KillByWall(ps);
	KillBySelf(ps);
}

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 = 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 GameRun(pSnake ps)
{
	//1.打印帮助信息
	PrintHelpInfo();
	do {
	//2.打印总分数和食物分值
		SetPos(64, 18);
		printf("总分: %d", ps->_score);
		SetPos(64, 19);
		printf("当前食物分: %d", 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 = END_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;
			}
		}

		//3.蛇走一步的过程
		SnakeMove(ps);
		Sleep(ps->_sleep_time);
	} while (ps->_status == OK);
	
}

这里不多做演示,我们在最后一起演示。

四、游戏结束

结束这个游戏可以按照读者各自的想法来实现,我这里给出我全部的实现代码:

snake.h:

cpp 复制代码
#pragma once
#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
#define POS_X 24
#define POS_Y 5

//类型声明

//蛇的方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//蛇的状态
enum GAME_STATUS
{
	OK, //正常
	KILL_BY_WALL, //撞墙
	KILL_BY_SELF, //撞到自己
	END_NORMAL //正常退出
};

//蛇身节点
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, *pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;

//贪吃蛇
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 CreateFood(pSnake ps);
//游戏运行逻辑
void GameRun(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
//暂停游戏
void Pause();
//蛇移动一步
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);

snake.c:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"

void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE houtput = NULL;
	//获取标准输出的句柄(⽤来标识不同设备的数值)
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);
}

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"加速得到更高分数");
	SetPos(42, 20);
	system("pause");
	system("cls");
}

void CreateMap()
{
	//上
	for (int i = 0; i < 29; i++) {
		wprintf(L"%lc", L'■');
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i < 29; i++) {
		wprintf(L"%lc", L'■');
	}
	//左
	for (int i = 1; i < 26; i++) {
		SetPos(0, i);
		wprintf(L"%lc", L'■');
	}
	//右
	for (int i = 1; i < 26; i++) {
		SetPos(56, i);
		wprintf(L"%lc", L'■');
	}
	SetPos(0, 27);
	system("pause");
}

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	for (int 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;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
	}

	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;
	ps->_status = OK;
}

void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;

again:
	x = (rand() % 27 + 1) * 2;
	y = rand() % 25 + 1;

	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;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	SetPos(pFood->x, pFood->y);
	wprintf(L"%lc", L'★');
	ps->_pFood = pFood;

}

void GameStart(pSnake ps)
{
	//0.设置窗口,然后光标隐藏
	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);
}

void PrintHelpInfo()
{
	SetPos(64, 10);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");
	SetPos(64, 11);
	wprintf(L"%ls", L"用↑,↓,←,→来分别控制蛇的移动");
	SetPos(64, 12);
	wprintf(L"%ls", L"按F3为加速,按F4为减速");
	SetPos(64, 13);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
	SetPos(64, 14);
	wprintf(L"%ls", L"苏浙制作");
}


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->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;
	ps->_score += ps->_food_weight;
	
	free(pn);
	pn = NULL;

	pSnakeNode cur = ps->_pSnake;
	while (cur) {
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
	}

	CreateFood(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", L'●');
		cur = cur->next;
	}

	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	free(cur->next);
	cur->next = NULL;
}

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;
	}

	if (NextIsFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}
	else {
		NoFood(pNextNode, ps);
	}

	KillByWall(ps);
	KillBySelf(ps);
}

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 = 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 GameRun(pSnake ps)
{
	//1.打印帮助信息
	PrintHelpInfo();
	do {
	//2.打印总分数和食物分值
		SetPos(64, 18);
		printf("总分: %d", ps->_score);
		SetPos(64, 19);
		printf("当前食物分: %2d", 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 = END_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;
			}
		}

		//3.蛇走一步的过程
		SnakeMove(ps);
		Sleep(ps->_sleep_time);
	} while (ps->_status == OK);
}

void GameEnd(pSnake ps)
{
	SetPos(24, 12);
	switch (ps->_status)
	{
	case END_NORMAL:
		printf("已主动结束游戏");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了,游戏结束");
		break;
	case KILL_BY_SELF:
		printf("您咬到自己了,游戏结束");
		break;
	}
	pSnakeNode cur = ps->_pSnake;
	while (cur) {
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

test.c:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <locale.h>
#include "snake.h"

void test() {
	int ch = 0;
	do {
		system("cls");
		//创建贪吃蛇
		Snake snake = { 0 };
		//初始化游戏
		//0.设置窗口,光标隐藏
		//1.打印环境界面
		//2.功能介绍
		//3.绘制地图
		//4.创建蛇
		//5.创建食物
		//6.设置游戏相关信息
		GameStart(&snake);
		//运行游戏
		GameRun(&snake);
		//结束游戏
		GameEnd(&snake);
		SetPos(20, 15);
		printf("再来一局吗?(Y/N): ");
		ch = getchar();
		while (getchar() != '\n');
		system("cls");
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}

int main() {
	//设置适配本地环境
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	test();
	
	return 0;
}

那么到这里,我们的贪吃蛇就算彻底完成了。


总结

本文详细介绍了如何用C语言实现经典的贪吃蛇游戏。主要内容包括:使用Win32 API进行控制台窗口设置;通过链表结构实现蛇身的动态增长;处理键盘输入控制蛇移动方向;实现食物生成与碰撞检测逻辑;游戏状态管理及计分系统。文章完整呈现了从初始化到游戏运行再到结束的完整开发流程,涵盖窗口控制、输入处理、动态内存管理等关键技术点。该实现可作为学习C语言和游戏开发的实践案例。

相关推荐
化作星辰2 小时前
java 给鉴权kafka2.7(sasl)发送消息权限异常处理
java·大数据·开发语言·kafka
无极小卒2 小时前
如何在三维空间中生成任意方向的矩形内部点位坐标
开发语言·算法·c#
FMRbpm2 小时前
链表中出现的问题
数据结构·c++·算法·链表·新手入门
克里斯蒂亚诺更新2 小时前
微信小程序 点击某个marker改变其大小
开发语言·前端·javascript
Alberta ゙3 小时前
C++初阶
开发语言·c++
the白勺3 小时前
RabbitMQ-基础-总结
开发语言·c#
弘毅 失败的 mian4 小时前
编译和链接
c语言·经验分享·笔记·编程入门
Dev7z4 小时前
基于Matlab多目标粒子群优化的无人机三维路径规划与避障研究
开发语言·matlab·无人机
沐知全栈开发4 小时前
HTML 脚本:基础、应用与未来趋势
开发语言