基于Win32 API控制台的贪吃蛇游戏:从设计到C语言实现详解


🔥@晨非辰Tong: 个人主页
👀专栏:《数据结构与算法入门指南》《C++学习之旅》
💪学习阶段:C/C++、数据结构与算法初学者
⏳"人理解迭代,神理解递归。"


文章目录

  • 引用
  • 一、游戏背景
  • 二、游戏效果演示
  • 三、项目预期目标
    • [3.1 包含的基本功能](#3.1 包含的基本功能)
    • [3.2 项目涉及技术要点](#3.2 项目涉及技术要点)
  • [四、Win32 API介绍](#四、Win32 API介绍)
    • [4.1 控制台程序(Console)](#4.1 控制台程序(Console))
    • [4.2 控制台屏幕上的坐标COORD](#4.2 控制台屏幕上的坐标COORD)
    • [4.3 获取句柄:GetStdHandle](#4.3 获取句柄:GetStdHandle)
    • [4.4 获取光标信息:GetConsoleCursorInfor](#4.4 获取光标信息:GetConsoleCursorInfor)
      • [4.4.1 接收光标:CONSOLE_CURSOR_INFO](#4.4.1 接收光标:CONSOLE_CURSOR_INFO)
    • [4.5 设置光标信息:SetConsoleCursorInfo](#4.5 设置光标信息:SetConsoleCursorInfo)
    • [4.6 设置光标位置:SetConsoleCursorPosition](#4.6 设置光标位置:SetConsoleCursorPosition)
    • [4.7 获取按键情况:GetAsyncKeyState](#4.7 获取按键情况:GetAsyncKeyState)
  • 五、游戏设计与分析
    • [5.1 地图设计](#5.1 地图设计)
      • [5.1.1 <locale.h>本地化](#5.1.1 <locale.h>本地化)
      • [5.1.2 类项](#5.1.2 类项)
      • [5.1.3 setlocale函数](#5.1.3 setlocale函数)
      • [5.1.4 宽字符的打印](#5.1.4 宽字符的打印)
    • [4.2 蛇身和食物](#4.2 蛇身和食物)
    • [4.3 结构设计](#4.3 结构设计)
    • [4.4 总览:游戏流程](#4.4 总览:游戏流程)
  • 五、核心逻辑实现
    • [5.1 游戏主逻辑](#5.1 游戏主逻辑)
    • [5.2 板块:游戏初始化](#5.2 板块:游戏初始化)
      • [5.2.1 欢迎界面](#5.2.1 欢迎界面)
      • [5.2.2 绘制地图](#5.2.2 绘制地图)
      • [5.2.3 创建蛇](#5.2.3 创建蛇)
      • [5.2.4 创建食物](#5.2.4 创建食物)
    • [5.3 板块:游戏运行](#5.3 板块:游戏运行)
      • [5.3.1 提示信息](#5.3.1 提示信息)
      • [5.3.2 蛇身的移动](#5.3.2 蛇身的移动)
    • [5.4 板块:游戏结束](#5.4 板块:游戏结束)
  • 六、源码完整参考
  • 总结

引用

掌握C语言的语法只是第一步,如何将分散的知识点融会贯通,构建出一个完整的项目,才是检验学习成效的关键。

贪吃蛇游戏,正是这样一个经典的"试金石"。它逻辑清晰、结构完整,几乎涵盖了C语言初级阶段的所有核心概念:从数据存储、流程控制,到函数封装与模块化设计。

本项目将引导你,使用C语言和Win32控制台API,从零开始实现。

这不仅是一次编程实践,更是一次对C语言学习成果的系统性检验与升华。让我们开始吧。


一、游戏背景

贪吃蛇是久负盛名的游戏,与俄罗斯方块、扫雷等游戏位列经典之位。

在C语言的学习中,将会以实现贪吃蛇项目来为C语言的学习进行收尾,也是借此检验掌握程度。(不会的赶紧去复习!!)


二、游戏效果演示

简易版贪吃蛇


三、项目预期目标

使用C语言在Windows环境的控制台中模拟实现简易版的贪吃蛇游戏。

3.1 包含的基本功能

  1. 贪吃蛇游戏地图绘制;
  2. 蛇吃食物(由上、下、左、右方向键控制蛇的功能);
  3. 蛇撞墙死亡;
  4. 蛇撞自身死亡;
  5. 计算得分;
  6. 蛇身的加速、减速;
  7. 暂停游戏;

3.2 项目涉及技术要点

友情提醒:实现贪吃蛇项目需要 C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API 等相关知识。


四、Win32 API介绍

Win32 API是Windows操作系统为应用程序提供的编程接口。它如同一个核心服务中心,应用程序通过调用这些预置的函数,便能请求系统完成窗口管理、图形绘制、文件操作等底层任务。

本游戏将在控制台环境中,运用其中几个关键API,实现光标控制、键盘响应等核心交互功能。

4.1 控制台程序(Console)

在电脑上打开一个叫 "命令提示符" 的应用(cmd)就是控制台程序。

在这个黑框框中,可以使用指令来设置控制台窗口的大小(长宽):通过设置行数、列数改变大小。

  • 设置窗口大小
bash 复制代码
mode con cols=120 lines=35

【详情参考:mode指令

  • 重命名窗口名称
bash 复制代码
title 名称

【详情参考:title命令

原版:

bash 复制代码
title 贪吃蛇_雾忱星

改版:

当然,游戏实现指令肯定不是在cmd中一个一个输,而是在VS2022中去实现。

想要在VS2022上实现和命名控制台一样的作用,要做出以下设置(看情况!)

  • 运行后,界面为如下显示,按步骤设置
  • 设置效果为:

使用system函数,包含<stdlib.h>头文件

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
	//设置窗口大小
	system("mode con cols=30 lines=30");
	
	//重命名
	system("title 贪吃蛇");

	getchar();//使用原因:防止程序直接结束,看不到效果
	return 0;
}

4.2 控制台屏幕上的坐标COORD

【详情参考:COORD

COORD是Windows API中定义的一个结构体,表示一个字符在控制台屏幕缓冲区的坐标,坐标系的原点(0,0)位于缓冲区顶部左侧单元格。

下面控制台的黑色光标就是一个坐标

COORD类型声明: 包含<windows.h> 头文件

c 复制代码
typedef struct _COORD 
{
	SHORT X;
	SHORT Y;
} 
  • 坐标赋值:
c 复制代码
int main()
{
	COORD pos = { 10, 10};
	system("pause");

	return 0;
}

但是,运行不会产生任何效果,因为还需要其他的内容配合进行,继续看。

4.3 获取句柄:GetStdHandle

【详情参考:GetStdHandle】包含<windows.h> 头文件

GetStdHandle是Windows API函数。用于从特定的标准设备(输入、输出、错误)中获取一个**句柄**(标识不同设备数值),使句柄可以操作设备。

我们可以将 "句柄"理解为"钥匙" ,可以用"钥匙"来操作相应的设备。前提是获取到相应设备的信息

  • 语法:
c 复制代码
HANDLE GetStdHandle(DWORD nStdHandle);
  • 参数:
含义
STD_INPUT_HANDLE ((DWORD)-10) 标准输入设备。 最初,这是输入缓冲区CONIN$的控制台。
STD_OUTPUT_HANDLE ((DWORD)-11) 标准输出设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$
STD_ERROR_HANDLE ((DWORD)-12) 标准错误设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$
  • 获取信息
  1. 获取标准输出设备的句柄
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

//获取标准输出设备的句柄
int main()
{
	HANDLE houtput = NULL; 
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	return 0;
}

4.4 获取光标信息:GetConsoleCursorInfor

【详情参考 :GetConsoleCursorInfor】包含<windows.h> 头文件

作用:检索有关指定控制台屏幕缓冲区光标大小、可见性信息。

c 复制代码
BOOL WINAPI GetConsoleCursorInfo(
	HANDLE               hConsoleOutput,
  PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
  • 参数:
hConsoleOutput 控制台屏幕缓冲区的句柄。
lpConsoleCursorInfo 指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关控制台游标的信息。

4.4.1 接收光标:CONSOLE_CURSOR_INFO

是一个结构体,存放控制台光标的信息。

【详情参考:CONSOLE_CURSOR_INFO】包含<windows.h> 头文件

  • 语法:
c 复制代码
typedef struct _CONSOLE_CURSOR_INFO 
{
  DWORD dwSize;
  BOOL  bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
  • 参数:
dwSize 由游标填充的字符单元格的百分比。 介于 1 和 100 之间。 光标外观变化,从完全填充单元格到显示为单元格底部的水平线。
bVisible 游标的可见性。 如果游标可见,则此成员为 TRUE。
  • 应用举例:
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
int main()
{
	HANDLE houtput = NULL; 
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//获取光标信息
	CONSOLE_CURSOR_INFO CursorInfo;//结构体变量
	GetConsoleCursorInfo(houtput, &CursorInfo);
	
	printf("%d\n", CursorInfo.dwSize);
	return 0;
}


输出25代表光标占比,为完全显示的25%。

那么知道了如何获得信息,就要开始设置光标信息!

4.5 设置光标信息:SetConsoleCursorInfo

【详情参考:SetConsoleCursorInfo】包含<windows.h> 头文件

  • 语法:
c 复制代码
BOOL WINAPI SetConsoleCursorInfo
(
  HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
  • 参数:
hConsoleOutput 控制台屏幕缓冲区的句柄。 该句柄必须具有 GENERIC_READ 访问权限。
lpConsoleCursorInfo 指向 CONSOLE_CURSOR_INFO 结构的指针,该结构为控制台屏幕缓冲区的光标提供新的规范。
  • 应用举例:
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
int main()
{
	//获取标准输出设备句柄
	HANDLE houtput = NULL; 
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//获取光标信息
	CONSOLE_CURSOR_INFO CursorInfo;//结构体变量
	GetConsoleCursorInfo(houtput, &CursorInfo);
	
	printf("%d\n", CursorInfo.dwSize);

	//设置光标信息
	CursorInfo.dwSize = 50;//将光标占比改为50
	SetConsoleCursorInfo(houtput, &CursorInfo);
	printf("%d\n", CursorInfo.dwSize);
	return 0;
}
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>

int main()
{
	//获取标准输出设备句柄
	HANDLE houtput = NULL; 
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//定义光标信息结构体变量
	CONSOLE_CURSOR_INFO CursorInfo;

	//获取和句柄相关的控制台上的光标信息,存在结构体中
	GetConsoleCursorInfo(houtput, &CursorInfo);
	printf("%d\n", CursorInfo.dwSize);

	//设置光标显示
	CursorInfo.bVisible = false;//不显示
	//设置和句柄相关的控制台上的光标信息,存在结构体中
	SetConsoleCursorInfo(houtput, &CursorInfo);
	return 0;
}

改变显示true/false,包含<atdbool.h>头文件


4.6 设置光标位置:SetConsoleCursorPosition

【详情参考:SetConsoleCursorPosition】包含<windows.h> 头文件

  • 语法:
c 复制代码
BOOL WINAPI SetConsoleCursorPosition
(
  HANDLE hConsoleOutput,
  COORD  dwCursorPosition
);
  • 参数:
hConsoleOutput 控制台屏幕缓冲区的句柄
dwCursorPosition 指定新光标位置(以字符为单位)的 COORD 结构。 坐标是屏幕缓冲区字符单元的列和行。 坐标必须位于控制台屏幕缓冲区的边界以内
  • 应用举例:
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main()
{
	//获取标准输出设备句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//设置坐标
	COORD pos = { 10, 5 };
	SetConsoleCursorPosition(houtput, pos);
	
	getchar();
	return 0;
}
  • 封装成函数:
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
//封装成函数
void set_pos(short x, short y)
{
	//获取标准输出设备句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//设置坐标
	COORD pos = {x, y};
	SetConsoleCursorPosition(houtput, pos);
}

int main()
{
	set_pos(10, 20);
	getchar();
	return 0;
}

4.7 获取按键情况:GetAsyncKeyState

【详情参考:GetAsyncKeyState】包含<windows.h> 头文件

  • 语法:
c 复制代码
SHORT GetAsyncKeyState(int vKey);

将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态

  • 返回值:

GetAsyncKeyState的返回值是short类型(2字节),在上⼀次调用GetAsyncKeyState函数后,如果返回的16位的short数据中:最高位(当前状态) 是1,说明按键的状态是按下,如果是0,说明按键的状态是抬起;最低位(历史状态) 被置为1则说明,该按键被按过,否则为0

  • 应用示例:
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
//获取按键情况
//虚拟键码
//宏函数
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)

int main()
{
	while (1)
	{
		if (KEY_PRESS(0x60))
		{
			printf("0\n");
		}

		else if (KEY_PRESS(0x61))
		{
			printf("1\n");
		}

		else if (KEY_PRESS(0x62))
		{
			printf("2\n");
		}

		else if (KEY_PRESS(0x63))
		{
			printf("3\n");
		}

		else if (KEY_PRESS(0x64))
		{
			printf("4\n");
		}

		else if (KEY_PRESS(0x65))
		{
			printf("5\n");
		}

		else if (KEY_PRESS(0x66))
		{
			printf("6\n");
		}

		else if (KEY_PRESS(0x67))
		{
			printf("7\n");
		}

		else if (KEY_PRESS(0x68))
		{
			printf("8\n");
		}

		else if(KEY_PRESS(0x69))
		{
			printf("9\n");
		}
	}
	return 0;
}

五、游戏设计与分析

5.1 地图设计

(放图片)

在地图,设置墙体用宽字符 :□,打印蛇用宽字符 :●,打印食物用宽字符:★。

在这里 "宽字符"占2个字节 ,它的出现是 C语言为适应国际化做出的改变 。(C语言最初假定字符都是单字节,但是对于非英语国家就不适用)

C语言加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数

5.1.1 <locale.h>本地化

<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分

标准中依赖地区有以下几个部分:

  1. 数字量格式;
  2. 货币量格式;
  3. 字符集;
  4. 日期、时间表示形式;

5.1.2 类项

通过修改地区,程序可以改变它的行为来适应不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能不希望被修改 。所以C语言支持针对不同的类项进行修改,下面的⼀个宏,指定⼀个类项:

  • LC_COLLATE:影响字符串比较函数strcol()/strxfrm()
  • LC_CTYPE:影响字符处理函数的行为。
  • LC_MONETARY:影响货币格式。
  • LC_NUMERIC:影响printf()的数字格式。
  • LC_TIME:影响时间格式strftime()/wcsftime
  • LC_ALL:针对所有类项设置成特定语言环境。

每个类项详情,参考

5.1.3 setlocale函数

【详情参考:setlocale

函数用于修改当前地区,可以针对一个类项或者全部。

  • 语法:
c 复制代码
char* setlocale(int category, const char* locale);
  • 参数:
category 为前面展示的类项
locale 可以是"C"(正常模式)/""(本地模式)

(任意程序执行都默认为setlocale(LC_ALL, "C");

  • 返回值: 返回值是⼀个字符串指针,表示已经设置好的格式。如果调用失败,则返回空指针NULL
c 复制代码
setlocale(LC_ALL, NULL)

上面可以用来查询当前地区,第二个参数为NULL

5.1.4 宽字符的打印

宽字符的字面量 必须加上前缀L ,否则C语言会把字面量当作窄字符类型处理。前缀L在单引号前面,表示宽字符,宽字符的打印使用wprintf,对应占位符%lc;在双引号前,表示宽字符串,对应占位符为%ls

  • 宽字符类型: wchar_t
c 复制代码
#include <locale.h>

int main()
{
	//设置地区
	setlocale(LC_ALL, "");

	wchar_t ch1 = L'●';
	wchar_t ch2 = L'汉';
	wchar_t ch3 = L'字';

	printf("%c%c\n", 'a', 'b');
	
	//注意打印时也要加L
	wprintf(L"%lc\n", ch1);
	wprintf(L"%lc\n", ch2);
	wprintf(L"%lc\n", ch3);

	return 0;
}

发现⼀个普通字符占⼀个字符的位置,但是⼀个汉字字符,占2个字符的,那么如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。

4.2 蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的⼀个坐标处,连续5个节点。

注意: 蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有一半出现在墙体中,另外一般在墙外的现象,坐标不好对齐。

关于食物,在墙体内随机生成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇身重合,然后打印★。

4.3 结构设计

1.设计蛇身

可以知道蛇就是一个移动的链表,蛇身由节点组成。在地图中,蛇身以坐标形式定位且节点也要指向下一个节点,所以用结构体实现。

c 复制代码
//蛇身节点
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;

	struct SnakeNode* next;//指向下一个节点
}SnakeNode, * pSnakeNode;
  1. 维护贪吃蛇
    对于蛇身,需要对以下几点维护:
维护整条蛇的指针(头) 维护食物的指针 蛇的走向(上下左右)
游戏状态(正常、撞墙、撞自己、正常退出) 每个食物的分数 游戏总分数
每走一步休眠的时间

可见以上都是描述蛇的,所以用结构体实现:

c 复制代码
/贪吃蛇数据
typedef struct Snake 
{
	pSnakeNode _pSnake;//维护整条蛇的指针
	pSnakeNode _pFood;//维护食物的指针
	enum DIRECTION _dir;//走向
	enum GAME_STATUES _statues;//状态
	int _food_weight;//一个食物的分数
	int _score;//总分数
	int _sleep_time;//休息时间,越短越快
}Snake, * pSnake;
  • 对于方向、状态,清楚有什么,可以用枚举:
c 复制代码
//枚举_蛇的走向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//枚举_游戏状态
enum GAME_STATUES
{
	OK,//正常
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//撞自己
	END_NOMAL//退出
};

4.4 总览:游戏流程


五、核心逻辑实现

5.1 游戏主逻辑

在开始,先设置本地区域setlocale(LC_ALL, "");,再进入总体实现:

  • 游戏初始化:GameInit();,完成相关数据初始化;
  • 游戏运行:GameRunt();,实现运行游戏的主体函数;
  • 游戏结束:GameEnd();,善后工作。

5.2 板块:游戏初始化

在这里需要完成:

  • 控制台窗口大小、名称设置;
  • 光标的隐藏;
  • 打印欢迎界面、操作信息;
  • 绘制地图;
  • 创建蛇、食物。
c 复制代码
void GameInit(pSnake ps)
{
	//设置控制台窗口大小:110列40行大小
	system("mode con cols=110 lines=40");
	system("title @雾忱星的贪吃蛇");//命名程序

	//隐藏光标
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获句柄
	CONSOLE_CURSOR_INFO CursorInfo;//接受信息
	GetConsoleCursorInfo(houtput, &CursorInfo);//获取光标信息	
	CursorInfo.bVisible = false;//不可见
	SetConsoleCursorInfo(houtput, &CursorInfo);//设置信息

	//1.打印欢迎界面、操作信息
	WelcomeToGame();

	//2.打印地图
	CreateMap();

	//3.创建蛇
	InitSnake(ps);

	//4.创建食物
	CreateFood(ps);
}

5.2.1 欢迎界面

进行简单的介绍以及必要的操作指引。

  • 效果展示:

c 复制代码
//1.欢迎界面
void WelcomeToGame()
{
	//打印欢迎信息
	SetPos(43, 16);
	wprintf(L"【欢迎来到贪吃蛇小游戏】\n" );
	SetPos(47, 20);
	system("pause");//暂停
	system("cls");//清屏

	//操作说明
	SetPos(47, 11);
	wprintf(L"【游戏操作手册】\n");
	SetPos(30, 15);
	wprintf(L"1.【移动】:使用键盘上的↑ . ↓ . ← . → 分别控制蛇的移动\n");
	SetPos(30, 17);
	wprintf(L"2.【速度】:F3为加速,F4为减速\n");
	SetPos(30, 19);
	wprintf(L"3.【注意】:加速将会得到更高的分数!\n");

	SetPos(30, 21);
	system("pause");//暂停
	system("cls");//清屏
}
  • 注解: (注意宽字符的输出格式)
    只需要找好合适位置,定位光标,将信息输出即可。
    下方的"请按任意键继续. . ."的提示,由暂停指令system("pause");//暂停完成。
    界面的切换,由清屏指令system("cls");//清屏完成。

5.2.2 绘制地图

打印宽字符,为贪吃蛇划分出一块地方运行。

格外注意坐标的计算:地图大小不能超过窗口大下、x坐标跨度为2、y坐标跨度为1。

  • 效果展示:
c 复制代码
//2.打印地图
//控制台窗口大小:110列40行大小

//注意两种打印格式:
//wprintf(L"%lc", L'□');正确,为宽字符
//wprintf(L"□");错误,为窄字符
#define WALL L'□' 

void CreateMap()
{
	//上边界
	for (int i = 0; i < 39; i++)
	{
		wprintf(L"%lc", WALL);
	}

	//下边界
	SetPos(0, 35);
	for (int i = 0; i < 39; i++)
	{
		wprintf(L"%lc", WALL);
	}

	//左边界
	for (int i = 1; i <= 34; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右边界
	for (int i = 1; i <= 34; i++)
	{
		SetPos(76, i);
		wprintf(L"%lc", WALL);
	}
}
  • 注解: 其中关键为宽字符的打印格式
    可能习惯了printf()的格式,直接写了wprintf(L"□");这是不对的,输出的是窄字符。

5.2.3 创建蛇

基本信息在前面已经给过,现在将 使用链表 将节点进行连接并初始化各种属性。

  • 效果展示:
c 复制代码
//3.创建蛇
#define POS_X 24
#define POS_Y 5
#define BODY L'●'

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	
	//创建蛇身,假定初始为5节点
	for (int i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}

		//设置节点坐标
		cur->x = POS_X + (i * 2);//x坐标,因为宽字符占连个字符
		cur->y = POS_Y;
		cur->next = NULL;

		//头插法_链接蛇身
		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->_food_weight = 10;
	ps->_score = 0;
	ps->_sleep_time = 200;//ms
	ps->_statues = OK;
}
  • 注解:

生成节点需要申请对应的空间malloc,随便将蛇设置一个坐标,这里不要忘记将next置空,否则运行不成功。

这里用 "头插法" 将节点进行链接(当然尾插也可以),接着就在设置的坐标打印蛇身,并设置属性。

5.2.4 创建食物

随机生成食物坐标,但是不超过地图,不与蛇重合。

  • 效果展示:
c 复制代码
//4.创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;

	//判断x是否是2的倍数
again:
	do
	{
		x = rand() % 37 + 2;
		y = rand() % 34 + 1;
	} while (x % 2 != 0);

	//食物坐标不能和蛇坐标冲突
	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;
		SetPos(x, y);
		wprintf(L"%lc", FOOD);//打印食物
		ps->_pFood = pFood;//蛇的食物属性
	}
}
  • 注解:

使用函数srand((unsigned int)time(NULL));生成随机数,利用数学知识,保证在地图内。

a % b 的结果是 a 除以 b 的余数余数的范围永远是 0 到 b-1

循环判断是否与蛇重合,使用关键字goto,again回溯修正,保证正确性。

5.3 板块:游戏运行

此板块,先在地图右侧打印相关信息,提示玩家游戏状态。游戏根据蛇的状态,决定是否继续游戏。游戏中,根据按键情况做出相应操作。

  • 虚拟键:
VK_UP VK_DOWN VK_LEFT VK_RIGHT
暂停VK_SPACE 正常退出VK_ESCAPE 加速VK_SHIFT 减速VK_CAPITAL
c 复制代码
void GameRun(pSnake ps)
{
	//打印窗口右侧信息
	PrintHelpIno();

	//蛇的操控
	do
	{
		SetPos(80, 23);//得分
		printf("【目前总分】:%d分", ps->_score);
		SetPos(80, 25);
		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_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_SHIFT))
		{
			//加速
			if (ps->_sleep_time > 80)
			{
				ps->_sleep_time -= 30;
				ps->_food_weight += 2;
			}
		}
		else if (KEY_PRESS(VK_CAPITAL))
		{
			//减速
			if (ps->_food_weight > 2)
			{
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;
			}
		}

		SnakeMove(ps);//蛇移动一次的过程
		Sleep(ps->_sleep_time);

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

5.3.1 提示信息

  • 效果展示:
c 复制代码
void PrintHelpIno()
{
	SetPos(93, 8);
	printf("【请注意:】");
	SetPos(80, 10);
	printf("1.【状态】:蛇不能穿墙,不能要到自己!");
	SetPos(80, 12);
	wprintf(L"%ls", L"2.【移动】:使用键盘上的↑ . ↓ . ← . → ");
	SetPos(80, 14);
	wprintf(L"%ls", L"3.【速度】:SHIFT 为加速,Capslock 为减速");
	SetPos(80, 16);
	printf("4.【ESC】:退出游戏 ·【space】:暂停游戏");
	SetPos(80, 20);
	printf("【@雾忱星制作】");
}

5.3.2 蛇身的移动

对于移动函数,选择在下一位置提前创建节点,之后根据下一位置的情况进行分情况分析:

(节点的坐标根据位置关系进行加减)

c 复制代码
//下一步食物?
//pSnakeNode pn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(pSnakeNode pn, pSnake ps)
{
	return (pn->x == ps->_pFood->x) && (pn->y == ps->_pFood->y);
}


//吃食物
//pSnakeNode pn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
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;//总分上涨
	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", BODY);
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);//定位到最后节点
	printf("  ");//空格覆盖身体
	free(cur->next);
	cur->next = NULL;//倒数第二节点置空
}

//检测是否撞墙
void KillByWall(pSnake ps)
{
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 76 || 
		ps->_pSnake->y == 0 || ps->_pSnake->y == 35)
	{
		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 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);
}
  • 注解: 必须注意坐标的计算

判断下一步是否是食物: 节点坐标和创建的食物节点坐标对比

  1. 是食物:
    链接食物节点,因为节点重复 ,将创建的下一节点释放、置空,再次打印蛇。
  2. 不是食物:
    链接创建的下一节点,并且将最后一个节点释放、置空,这里要注意-->用空白字符将身体覆盖。

判断下一步是否撞墙: 节点坐标和墙的边界坐标对比,撞墙就将状态改为KILL_BY_WALL,游戏结束。

判断下一步是否撞自己: 遍历蛇身,将下一个节点坐标和蛇身的坐标对比,撞自己就将状态改为KILL_BY_SELF,游戏结束。


5.4 板块:游戏结束

当状态不再是OK时,在屏幕上打印结束信息,并释放所有节点。再询问是否再来一把游戏。

  • 效果展示:
c 复制代码
//结束游戏_善后
void GameEnd(pSnake ps)
{
	SetPos(33, 14);
	switch (ps->_status)
	{
	case END_NOMAL:
		printf("【您主动结束了游戏!】");
		break;
	case KILL_BY_WALL:
		printf("【蛇撞到了墙!】");
		break;
	case KILL_BY_SELF:
		printf("【蛇撞到了自己!】");
		break;
	}

	SetPos(0, 36);

	//释放链表
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = NULL;
		del = cur;
		cur = cur->next;
		free(del);
		del->next = NULL;
	}
}

六、源码完整参考

【源码存放:】https://gitee.com/tian-aochen/test_118/tree/master/test_12.6_贪吃蛇改/Snake


总结

html 复制代码
🍓 我是晨非辰Tong!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:

本项目通过Win32控制台API,用C语言成功实现了经典的贪吃蛇游戏。

核心收获在于将零散的知识点------如结构体、数组、函数、循环控制------组织成一个可运行的整体。

从坐标处理到键盘响应,每一个步骤都强化了对底层基础的理解与控制力。这不仅巩固了C语言的学习效果,更重要的是建立起从构思到实现的完整项目开发思维。

相关推荐
百胜软件@百胜软件2 小时前
CTO Wow Club 上海研讨会成功举办,百胜软件深度分享零售AI智能体实战之道
大数据·人工智能·零售
Victor3562 小时前
Netty(27)Netty的拦截器和过滤器是什么?如何使用它们?
后端
Dingdangcat862 小时前
基于RetinaNet的仙人掌品种识别与分类:Gymnocalycium与Mammillaria属10品种自动识别
人工智能·数据挖掘
ASD123asfadxv2 小时前
柑橘果实表面病害与虫害智能检测与分类 YOLO11-Seg-GhostHGNetV2实现
人工智能·分类·数据挖掘
Serendipity_Carl2 小时前
京东手机销售数据分析: 从数据清洗到可视化仪表盘
python·数据分析·pandas·pyecharts
希艾席帝恩2 小时前
数字孪生正在悄然改变交通管理方式
大数据·人工智能·数字孪生·数据可视化·数字化转型
大千AI助手2 小时前
Kaldi:开源语音识别工具链的核心架构与技术演进
人工智能·机器学习·架构·开源·语音识别·kaldi·大千ai助手
龙腾AI白云2 小时前
基于Tensorflow库的RNN模型预测实战Tensorflow库简介循环神经网络简介
人工智能·fastapi
码界奇点2 小时前
基于Go语言的Web管理面板系统设计与实现
开发语言·后端·golang·毕业设计·web·go语言·源代码管理