贪吃蛇小游戏---古法编程

VIP 从原理图到GDSII:Virtuoso+Pcell实战避坑指南(以TSMC65nm振荡器为例) 本文以TSMC65nm工艺下的振荡器设计为例,详细解析了Virtuoso环境中Pcell的最佳实践方案,涵盖变量管理、版图设计、Calibre验证等关键环节。通过实战案例和避坑指南,帮助工程师高效完成从原理图到GDSII的转化,特别针对LVS验证和参数化单元设计提供了实用解决方案。 开通可读全文·896 阅读·25 点赞·15 收藏 阅读全文

1. 简介

贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。

在编程语⾔的教学中,我们以贪吃蛇为例,从设计到代码实现来提升学⽣的编程能⼒和逻辑能⼒。

我们现在已经可以通过已学的知识来实现简易版的贪吃蛇小游戏。

2. 目标

使⽤C语⾔在Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇。

实现基本的功能:

  • 贪吃蛇地图绘制
  • 蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)
  • 蛇撞墙死亡
  • 蛇撞自身死亡
  • 计算得分
  • 蛇身加速、减速
  • 暂停游戏

3. 技术要点

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

这些知识我除了Win32API没讲其他的我都讲过,顺便我们可以通过这个小游戏来巩固一下我们的知识。

4. Win32API

4.1 Win32API概念

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

4.2 控制台程序

我们游戏的实现是在控制台上进行实现的,但是VS2022默认的是使用终端来执行程序,我们就要改成控制台执行。

终端就是这样的,我们该如何将终端改成控制台呢?

这里我们直接将终端换成控制台,记得保存!

之后我们执行程序就是显示控制台了。

我们怎么像这样子将控制台的名字改成贪吃蛇呢?

//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
system("mode con cols=100 lines=30");
//设置cmd窗口名称
system("title 贪吃蛇");

我们只需要执行这串代码即可,system翻译过来就是系统的意思,我们执行代码就是直接在系统中执行语句才能更改我们控制台的名称,我们使用system要使用" "将要执行的内容框起来,注意·这个行和列的顺序,千万不要搞混了,其次我们这个行和列的比列是不一样的,你们可以将行和列改成一样的试试。

你会发现我们将两个都改成30,并不是我们想的是个正方形。

4.3 控制台屏幕上的坐标COORD

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

COORD类型的声明:

typedef struct _COORD {
	SHORT X;
	SHORT Y;
} COORD, *PCOORD;

这里的x和y并不是我们学的坐标那样,

起始点是右上角。

4.4 GetStdHandle

GetStdHandle 是⼀个WindowsAPI函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准 错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。

这些函数都是在Windows官网都是可以看到的。

HANDLE hOutput = NULL;

//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

4.5 GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息。

实例使用:

HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

CONSOLE_CURSOR_INFO CursorInfo;

GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

4.6 CONSOLE_CURSOR_INFO

这个结构体,包含有关控制台光标的信息。

语法:

#include <windows.h>

int main()
{
    HANDLE hOutput = NULL;
    hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_CURSOR_INFO CursorInfo;
    GetConsoleCursorInfo(hOutput, &CursorInfo);
    CursorInfo.bVisible = FALSE;  // 设置为不可见
    SetConsoleCursorInfo(hOutput, &CursorInfo); // 应用设置
    return 0;
}

dwSize,光标填充的字符单元格的百分比。此值介于 1 到 100 之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。

bVisible,游标的可见性。如果光标可见,则此成员为 TRUE。

隐藏光标:

CursorInfo.bVisible = false; //隐藏控制台光标

4.7 SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的⼤小和可见性。

我们结合上面的各个函数就可以实现光标的隐藏了。

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

4.8 SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调 ⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置

实例使用:

COORD pos = { 10, 5 };
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);

我们为了后续的光标定位方便,我们可以直接封装一个函数,用于定位光标。

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

//设置光标的坐标
void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE hOutput = NULL;
	//获取标准输出的句柄(用来标识不同设备的数值)
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(hOutput, pos);
}

4.9 GetAsyncKeyState

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

如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。

#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

5. 贪吃蛇游戏设计和分析

5.1 地图的绘制

这是我们要达到的预期的地图效果,就是使用“■” 框出一块方格,食物和蛇我们先不看。

这里我们要知道我们打印的■是宽字符,我们平常打印的字符abc等都是只占一个字节,但是我们宽字符是占两个字节的。

这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道 该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。

控制台窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。

但是我们使用宽字符并不是可以直接使用的,我们要适配本地环境。

5.1.1 <local.h>本地化

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

在标准中,依赖地区的部分有以下几项:

  • 数字量的格式
  • 货币量的格式
  • 字符集
  • 日期和时间的表示形式

5.1.2 类项

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

  • LC_COLLATE:影响字符串比较函数 strcoll () 和 strxfrm ()。
  • LC_CTYPE:影响字符处理函数的行为。
  • LC_MONETARY:影响货币格式。
  • LC_NUMERIC:影响 printf () 的数字格式。
  • LC_TIME:影响时间格式 strftime () 和 wcsftime ()。
  • LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境。

5.1.3 setlocale函数

函数语法:

char* setlocale (int category, const char* locale);

etlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。

setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参 数是LC_ALL,就会影响所有的类项。

C标准给第⼆个参数仅定义了2种可能取值:"C"(正常模式)和""(本地模式)。

当地区设置为"C"时,设置为C语⾔默认的模式,这时库函数按正常⽅式执⾏。

当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。

⽤""作为第2个参数,调⽤setlocale函 数就可以切换到本地模式,这种模式下程序会适应本地环境。

切换到我们的本地模式后就⽀持宽字符(汉字)的输出等:

setlocale(LC_ALL, ""); //切换到本地环境

5.1.4 宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?

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

void PrintHelpInfo()
{
    SetPos(64, 13);
    wprintf(L"%ls", L"不能撞墙,不能咬到自己\n");
    SetPos(64, 14);
    wprintf(L"%ls", L"用 ↑ . ↓ . ← . → 来控制蛇的移动\n");
    SetPos(64, 15);
    wprintf(L"%ls", L"按F3加速,按F4减速\n");
    SetPos(64, 16);
    wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏\n");
}

5.2 地图坐标

我们加入实现一个棋盘27行,58列的棋盘,我们该怎么打印棋盘呢?

我们打印宽字符是占两个字符的位置,所以我们在打印列时需要除以2。

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 <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", L'■');
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", L'■');
	}
}

5.3 蛇身和食物的绘制

同样,我们的食物和蛇身也是使用宽字符,所以我们一定要确保其中的x坐标是2的倍数,否则可能出现一半卡在墙中的现象,其中食物是随机刷新的,我们一定要确保食物不予蛇身重合,蛇身的移动我们就直接使用链表的头插尾删即可完成。

5.4 数据结构设计

在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信 息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏, 所以蛇节点结构如下:

typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

要管理整条贪吃蛇的各个元素,我们再封装⼀个Snake的结构来维护整条贪吃蛇:

typedef struct Snake
{
	pSnakeNode _pSnake;  //指向蛇头的指针
	pSnakeNode _pFood;   //指向食物的指针
	enum DIRECTION _dir;   //使用枚举来列举蛇的方向
	enum GAME_STATUS _status;  //游戏的状态
	int _food_weight;      //一个食物的分数
	int _score;             //总分数
	int _sleep_time;         //休息时间
}Snake,*pSnake;

其中,蛇的方向和游戏的状态我们可以直接使用枚举的方式来完成。

蛇的方向:

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

蛇的状态:

//游戏的状态
enum GAME_STATUS
{
	OK,            //正常运行
	KILL_BY_WALL,  //撞墙
	KILL_BY_SELF,  //自己撞自己
	EMD_NORMAL    //正常退出
};

6. 核心逻辑实现分析

6.1 主逻辑

程序开始就设置程序支持本地模式,然后进入游戏的主逻辑。

主逻辑分为 3 个过程:

  • 游戏开始(GameStart)完成游戏的初始化
  • 游戏运行(GameRun)完成游戏运行逻辑的实现
  • 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
//完成游戏的测试逻辑
void test()
{
	int ch = 0;
	int c = 0;
	do
	{
		system("cls");
		//创建贪吃蛇
		Snake snake = { 0 };
		//初始化游戏
		//1. 打印欢迎界面
		//2. 功能介绍
		//3. 绘制地图
		//4. 创建蛇
		//5. 创建食物
		GameStart(&snake);

		//运行游戏
		GameRun(&snake);
		//结束游戏--善后工作
		GameEnd(&snake);

		SetPos(20, 15);
		printf("再来一局吗?(Y/N):");
		ch = getchar();
		//清行尾 + 防 EOF 死循环
		while ((c = getchar()) != '\n' && c != EOF);
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}
int main()
{
	//设置适配本地环境,这样我们就可以打印宽字符。
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	test();
	return 0;
}

这里我们设置了一个随机数种子,可以保证食物的随机性。

6.2 游戏开始

这个模块完成游戏的初始化任务:

  • 控制台窗口大小的设置
  • 控制台窗口名字的设置
  • 鼠标光标的隐藏
  • 打印欢迎界面
  • 创建地图
  • 初始化蛇
  • 创建第一个食物
void GameStart(pSnake ps)
{
	//0. 设置窗口的大小
	//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
	system("mode con cols=100 lines=30");
	//设置cmd窗口名称
	system("title 贪吃蛇");

	//1. 光标隐藏
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);   //创建句柄
	CONSOLE_CURSOR_INFO CursorInfo;    //创建光标结构体
	GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false; //隐藏控制台光标
	SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态

	//2. 打印欢迎界面和功能介绍
	WelcomeToGame();

	//3. 地图绘制
	CreateMap();

	//4. 初始化蛇身
	InitSnake(ps);

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

这其中的光标的隐藏部分是知道就信,没有需要完全掌握的必要。

6.2.1 打印欢迎界面

我们要实现这两个欢迎界面:

void WelcomeToGame()
{
	SetPos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	SetPos(42, 15);
	system("pause");  //暂停
	system("cls");   //清空屏幕
	SetPos(25, 14);
	wprintf(L"用↑.↓.←.→来控制蛇的移动,按F3加速,按F4减速\n");
	SetPos(25, 15);
	wprintf(L"加速能够得到更高的分数\n");
	SetPos(42, 20);
	system("pause");
	system("cls");
}

这里我们使用了将之前光标定位时封装的函数拿过来了,这里的pause就是停下等待,等我们按下任意一个键之后,程序就可以继续往下执行了。但是要引进头文件stdlib.h;cls就是清空屏幕,这个就是为了可以下一次打印。这样我们就完成了欢迎界面的打印,注意:这个光标定位,你们可以根据自己的审美来调整打印语句的位置。

6.2.2 创建地图

这个创建地图我们刚刚已经提到了,但是我们一定要好好思考一下,为什么我们的坐标是这样的。

易错点:就是坐标的计算 上: (0,0) 到 (56,0) 下: (0,26) 到 (56,26) 左: (0,1) 到 (0,25) 右: (56,1) 到 (56,25)

我们为了能够更加方便的更改地图的边框图形,我们可以使用一个宏定义来方便更改。

#define WALL L'□'
void CreateMap()
{
	//上
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

6.2.3 初始化蛇身

我们打印蛇身同样使用宏定义的方式来定义蛇身的图案。

蛇最开始长度为 5 节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。

创建 5 个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。

  • 蛇的初始位置从 (24,5) 开始。

再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。

  • 游戏状态是:OK
  • 蛇的移动速度:200 毫秒
  • 蛇的默认方向:RIGHT
  • 初始成绩:0
  • 每个食物的分数:10
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 + 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->_sleep_time = 200;//毫秒
	ps->_food_weight = 10;
	ps->_score = 0;
	ps->_status = OK;
	ps->_dir = RIGHT;
}

6.2.4 创建第一个食物

  • 先随机生成食物的坐标
    • x 坐标必须是 2 的倍数
    • 食物的坐标得在墙体内部
    • 食物的坐标不能和蛇身每个节点的坐标重复
  • 创建食物节点,打印食物

我们食物的宽字符也使用宏的定义的方式。

void CreateFood(pSnake ps)
{
	//先确定范围
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	//蛇身和食物不冲突
	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;
	}
	else
	{
		pFood->x = x;
		pFood->y = y;
		SetPos(pFood->x, pFood->y);
		wprintf(L"%c", FOOD);
		ps->_pFood = pFood;
	}
}

6.3 游戏运行

游戏运⾏期间,右侧打印帮助信息,提⽰玩家,坐标开始位置(64,15)

根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。

如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。

我们检测按键需要使用到一些虚拟按键:

  • 上:VK_UP
  • 下:VK_DOWN
  • 左:VK_LEFT
  • 右:VK_RIGHT
  • 空格:VK_SPACE
  • ESC:VK_ESCAPE
  • F3:VK_F3
  • F4:VK_F4

确定了蛇的⽅向和速度,蛇就可以移动了。

打印帮助信息:

void PrintHelpInfo()
{
	
	SetPos(64, 13);
	wprintf(L"%ls", L"不能撞墙,不能咬到自己\n");
	SetPos(64, 14);
	wprintf(L"%ls", L"用↑.↓.←.→来控制蛇的移动\n");
	SetPos(64, 15);
	wprintf(L"%ls", L"按F3加速,按F4减速\n");
	SetPos(64, 16);
	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏\n");
}
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	//按键状态
	do
	{
		SetPos(64, 10);
		printf("总得分:%d", ps->_score);
		SetPos(64, 11);
		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 = EMD_NORMAL;
			break;
		}
		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->_food_weight -= 2;
				ps->_sleep_time += 30;
			}
		}
		Sleep(ps->_sleep_time);//休息时间
		SnakeMove(ps);//走一步

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

这里我们将检测按键状态,封装一个宏:

#define KEY_PRESS(VK)  ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

6.3.1 蛇身的移动

我们蛇身的移动本质上就是蛇的头插尾删。

所以先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。

确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理(EatFood),如果不是食物则做前进一步的处理(NoFood)。

蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。

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

判断是否是食物:

int NextIsFood(pSnakeNode pNextNode, pSnake ps)
{
	if (ps->_pFood->x == pNextNode->x && ps->_pFood->y == pNextNode->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

吃掉食物:

void EatFood(pSnakeNode pNextNode, pSnake ps)
{
	//头插法
	pNextNode->next = ps->_pSnake;
	ps->_pSnake = pNextNode;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_score += ps->_food_weight;
	//释放食物结点
	free(ps->_pFood);
	ps->_pFood = NULL;
	//创建新的食物
	CreateFood(ps);
}

撞墙:

void KILLBYWALL(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	if (cur->x == 0 || cur->x == 56 || cur->y == 0 || cur->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 NoFood(pSnakeNode pNextNode, pSnake ps)
{
	//头插
	pNextNode->next = ps->_pSnake;
	ps->_pSnake = pNextNode;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		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;

}

易错点:这⾥最容易错误的是,释放最后⼀个结点后,还得将倒数第2个节点的next指针改为NULL, 保证蛇尾打印可以正常结束,不会越界访问。

判断蛇头的坐标是否和墙的坐标冲突:

void KILLBYWALL(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	if (cur->x == 0 || cur->x == 56 || cur->y == 0 || cur->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;
	}
}

6.4 游戏结束

游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇⾝节点。

void GameEnd(pSnake ps)
{
	SetPos(24, 14);
	switch (ps->_status)
	{
	case KILL_BY_SELF:
		printf("你撞到自己了,游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("你撞到墙了,游戏结束\n");
		break;
	case EMD_NORMAL:
		printf("你主动结束游戏\n");
		break;
	}

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

7. 总结

至此这个贪吃蛇小游戏就结束了,通过这个小游戏,我们可以对之前学过的知识进行一个比较全面的复习,同时我们也真正的应用上了链表,后续我还会出一期使用opencode,实现贪吃蛇小游戏,那个就比较精美了,我们这个使用的是简单的宽字符没够美观,但是基本实现了,小游戏的运行逻辑,如果你们需要完全的代码可以去我的GitHub上找完整代码。

8. GitHub

https://github.com/PeterYuanCoder/Snake-game.git

delphi fmx android 自动更新(一) 所以,如果是http 则,manifest.xml application 下,https无需添加。增加android:usesCleartextTraffic="true",不然无法使用http下载东西。1,REQUEST_INSTALL_PACKAGES 权限。4,WRITE_EXTERNAL_STORAGE 权限。3,READ_EXTERNAL_STORAGE 权限。一般是从http下载安装包 apk。2,INTERNET 权限。sFile为apk包路径。 542 阅读·0 点赞·1 收藏 阅读详情

相关推荐

保姆级教程:用STM32F103C8T6和MAX6675搞定K型热电偶测温(附模拟/硬件SPI双版本代码)

本文提供STM32F103C8T6与MAX6675实现K型热电偶高精度测温的保姆级教程,涵盖硬件连接、SPI驱动实现及模拟SPI方案,并附双版本代码。详细解析温度数据读取、处理及常见问题排查,助力工业控制、实验室设备等场景的稳定测温系统搭建。

weixin_30765505的博客 579

贪吃蛇 - Canvas 版

本文从零手写控制台版贪吃蛇,不依赖游戏引擎,仅用循环、列表与条件判断,深入解析状态管理、队列模拟、碰撞检测与方向控制等核心编程概念。通过“读取输入—更新状态—渲染画面”的主循环,揭示游戏本质是状态机的持续演进,为理解前端框架与游戏开发奠定坚实基础。

weixin_39245324的博客 378

动态知识图谱构建:从本体论到工程实践

知识图谱作为语义网和人工智能领域的核心技术,旨在通过结构化的方式表示和组织知识。其核心原理在于将实体、概念及其关系建模为图结构,从而支持复杂的语义推理和关联查询。这一技术为机器理解数据间的深层联系提供了基础,在智能推荐、企业知识管理和学术研究等领域具有重要价值。通过结合自然语言处理(NLP)和图数据库技术,系统能够从非结构化数据中自动抽取知识,并构建动态演化的认知网络。本文以“本体论章鱼”项目为例,探讨了如何实现从静态本体到动态知识网络的范式转变,并详细解析了其分布式感知、适应性学习的架构设计,为构建灵活可

weixin_30731305的博客 655

RDMA-Verbs-API 和数据结构(三)

libibverbs 向应用提供一套操作 RDMA 设备的 Verbs API。应用先打开设备,创建 PD、MR、CQ 和 QP,再用 WR 描述要提交的工作,用 SGE 指定本地数据范围,最后从 CQ 取得 WC,判断工作结果并回收缓冲区。公开 API 层、libibverbs 通用处理与分发层、provider 接口层。公开层包含动态库导出函数、头文件 inline 函数和宏适配入口。通用层处理参数适配、资源公共字段、操作表选择,以及事件计数和确认状态;

非专业业余程序员 381

使用TinyProxy搭建代理服务器

背景 办公电脑所在网络无法连接外网,机房服务器可以连接外网,通过服务器代理上网; 一、CentOS下TinyProxy安装–搭建代理服务器 TinyProxy是个轻量级简单的HTTP/HTTPS代理程序,GitHub和官网,安装方式这里主要介绍两种 CentOS下yum安装,参考:TinyProxy安装 Docker安装 先安装docker环境yum install docker -y 二、Win10下使用 ...

椰汁菠萝 1万+

15、SQL Server查询优化与CPU负载管理

本文深入探讨了SQL Server查询优化与CPU负载管理的关键技术,包括强制连接类型的使用、键查找问题的解决、索引设计的最佳实践、高CPU负载的排查与优化,以及并行性的利弊与调整策略。通过优化查询逻辑、合理设计索引、监控与管理CPU资源,可以显著提升SQL Server的性能和稳定性。适用于数据库开发人员和DBA进行性能调优参考。

pz8901234的博客 72

缓存、索引与数据结构的工程选择

工程中,算法不只是“选一个复杂度更低的数据结构”:还要看查询类型、更新频率、内存成本、结果是否会过期以及能否保持一致。本文用商品目录演示三种常见选择:用字典加速 SKU 精确查询,用有序索引加速价格区间查询,用有界缓存复用重复查询结果,并通过可运行代码观察更新与缓存失效的关系。

m0_62221377的博客 447

OpenClaw 股票自动化交易实践指南:从原理到实战的完整技术路径

量化交易策略的开发是一个系统工程,需要综合运用金融知识、编程技能和数据分析能力。OpenClaw通过引入提示词工程技术,大大降低了策略开发的技术门槛,使得更多投资者能够参与到量化交易中来。但技术只是工具,真正决定策略成败的是对市场的深刻理解和严格的风险管理。本章系统介绍了OpenClaw环境下的策略开发方法,从提示词驱动到传统代码实现,从参数优化到回测验证,涵盖了策略开发的完整流程。提示词驱动方式适合快速验证交易想法,传统代码方式适合实现复杂的定制逻辑,两种方式可以协同使用,发挥各自的优势。

lovely_yoshino的博客 5979

《嵌入式工程师的软硬件修仙之路》第 3 期|数据结构筑基:环形缓冲区与链表——嵌入式最常用的两种数据结构

上一期我们用指针看透了内存的本质。本期从串口中断数据痛点出发,逐步推导环形缓冲区(Ring Buffer)的读写公式与空满判断,再讲链表指针操作 O(1) 原理,以及 FreeRTOS 就绪队列、Linux list_head 背后的内核哲学。

坐看风云变幻 340

HyperLynx仿真(一)LineSim简单介绍

可以选择设置信号线与信号线之间默认的距离和信号线与同平面的平面之间的距离,这些设置根据工程师项目上经常使用的情况而设置,也可以选择不设置,在每次仿真时根据需要再对每一个仿真单独设置。仿真属性的设置对话框包含一般设置、 显示颜色设置、LineSim 设置、BoardSim 设置、 层叠默认设置、焊盘类型设置、仿真示波器默认设置、电路仿真器默认设置、电源完整性设置及高级设置。第一次使用HyperLynx软件时需要设置仿真相关环境,包括仿真软件的基本设置、模型库路径设置、仿真控制器控制、单位等。...

小幽余生不加糖 8936

Python 爬虫实战:爬虫与 ECharts 结合 —— 前端可视化展示

本文介绍了爬虫技术与ECharts可视化结合的完整实现流程。首先使用Python爬取京东商品数据(价格、评价数等),通过BeautifulSoup解析和pandas处理转换为JSON格式。然后构建前端页面,利用ECharts实现价格分布柱状图、价格-评价数散点图和店铺占比饼图三种可视化效果。文章详细解析了爬虫数据提取、格式转换和前端图表配置等关键技术点,并提供了完整的代码示例。最后提出了基于API接口、数据库存储等扩展方向,为数据采集到可视化展示提供了完整的解决方案。

2503_91057718的博客 628

C++——【哈希表】&unordered_map和unordered_set

C++——哈希表,开放定址法和链地址法实现;unordered_map,unordered_set封装哈希表

2402_87037360的博客 292

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

笔记-论数据结构1

在【向心】角度的看待下,数据可以被理解为多个不同角度的概念;例如信息的载体,其他各种各样的理解角度,在科技水平越来越发达的今日,你所触碰的一切东西可能不是数据,但它们正在朝着数据化的方向去发展。

fish_zzzz的博客 264

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

Redis++ 缓存实战:五种数据结构、pipeline/事务/watch 与二次封装

本文基于redis-plus-plus实现C++缓存实践,通过五种数据结构(string、hash、list、set、zset)精准匹配不同业务场景,结合pipeline提升批量操作效率,利用事务保证原子性,以watch实现乐观锁防并发冲突。采用“配置+工厂”模式统一管理连接池,遵循缓存读取三段式(查、回源、回填),确保缓存命中率与数据一致性,有效缓解数据库压力。

syagain_zsx的博客 310

【数据结构|贪心算法】哈夫曼编码(Huffman Coding)万字详解|原理、构造、Java 实现、压缩实战、面试考点

在数据压缩领域,哈夫曼编码是最经典的无损变长前缀编码算法,也是数据结构、算法面试、软考必考知识点。gzip、zip、PNG、JPEG、MP3 底层都离不开哈夫曼编码的身影。为什么它是最优前缀编码?为什么不能直接随便写变长编码,一定要前缀编码?本文从场景引入、等长编码痛点、哈夫曼树构造、手算案例、完整 Java 编码 + 解码代码、WPL 计算、优缺点、面试高频问题一次性讲清楚。等长编码:ASCII 就是典型,每个字符固定 8bit。优点:解码简单;缺点:高频字符也要占用同样 bit,压缩效率低。

m0_63990694的博客 635

《Java版数据结构 & 集合类剖析》Map & Set & 反射:“从哈希碰撞到红黑树转换,反射帮你扒开 HashMap 的桶数组看个究竟”

本文系统剖析Java中Map与Set的数据结构基础——二叉搜索树(BST),涵盖其插入、查找、删除等核心操作,并通过代码实现展示其实现逻辑。重点分析了BST的性能瓶颈及其在极端情况下的退化问题,引出平衡二叉树(如红黑树)的重要性。进一步阐述了TreeMap与TreeSet底层基于红黑树的实现机制,揭示其高效动态查找能力。最后结合键值对模型,说明Map与Set在实际应用中如何支持高效的动态搜索,为理解Java集合框架中的高级数据结构奠定基础。

2401_88739751的博客 243

数据结构 --- 无锁队列(Lock-Free Queue)

在高并发系统中,无锁队列通过原子操作(如CAS)实现线程安全,避免传统互斥锁带来的阻塞与上下文切换开销。其核心在于满足线性化语义与锁无关进展保证(Lock-Free),而非绝对无等待。根据生产者/消费者数量(SPSC、MPSC等)和结构选择(环形缓冲区或链表),可优化性能与复杂度。正确使用C++内存模型(如acquire/release)确保数据同步顺序,防止ABA问题并实现安全内存回收。工程上应优先选用适配场景的简化模型,而非盲目追求通用性。

MzKyle的博客 459

栈、队列、链表分别适合什么场景?嵌入式数据结构选型指南

栈、队列、链表是嵌入式系统最基础、最高频、最核心的三种线性数据结构,覆盖了状态管理、数据缓存、消息调度、设备通信、日志存储等绝大多数场景。很多新手开发者照搬 PC 端的编程习惯,乱用数组、链表、动态内存,最终导致设备卡顿、堆栈溢出、死机、内存泄漏等疑难问题。在嵌入式开发中,数据结构的选型从来不是 “随心所欲” 的语法选择,而是资源、实时性、稳定性、内存开销的综合博弈。3.表达式解析、语法校验 嵌入式脚本解析、串口指令校验、括号匹配、进制转换运算,利用栈的嵌套特性完成逻辑校验。

teach2004的博客 223

数据结构7-二叉树

与其内耗自己,不如责备他人

景画的博客 265

数据结构(1)

顺序表的底层结构是数组,是对数组的封装,实现了常用的增删改查等接口。就像“苍蝇馆子”的炒西蓝花是基础食材,而“米其林餐厅”的绿野仙踪(西蓝花+料汁+小饰品+摆盘)就是顺序表(数组+增加数据+删除数据+修改数据+查找数据+……线性表是一种在实际中广泛使用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串等。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。依次存储数据元素的线性结构,一般情况下采用数组存储。上并不一定是连续的。

likexuec的博客 197

数据结构>>优先级队列(堆)

本文详解优先级队列与堆的原理,重点讲解大根堆的结构特性、下标映射规则及建堆、插入、删除、修改等操作。基于数组实现完全二叉树逻辑,通过向下/向上调整维护堆序性质。配套完整Java代码,涵盖大根堆构建与优先级队列核心功能,适用于数据结构复习与实战应用。

Ma410782的博客 329

数据结构:单链表(C语言)

简单介绍链表的定义和分类,具体讲解链表的创建和维护方法,最后总结链表的特点和局限性

lei_jiale的博客 383

[C++高阶数据结构] 图的定义 | 邻接矩阵 | 邻接表 | 图的遍历方式

图是由顶点集与边集构成的数据结构,记作 $ G = (V, E) $。与树不同,图可有环、不连通,且两点间路径不唯一。分为无向图与有向图,区别在于边的方向性,影响度、连通性及矩阵对称性。完全图边数分别为 $ \frac{n(n-1)}{2} $(无向)和 $ n(n-1) $(有向)。存储结构上,邻接矩阵适合稠密图,支持快速判断连通性,但空间复杂度高;邻接表更适合稀疏图,便于遍历邻接点,节省空间。

Tairitsu_青云 467

【数据结构】快速排序专题

快速排序基于前序遍历递归骨架,通过划分将数组分为小于、等于、大于基准值的三部分。主流实现包括Hoare双指针法、挖坑法与前后指针法,三者在重复元素处理上表现各异:Hoare与前后指针落点不同,易导致不平衡递归;三路划分可高效处理重复数据,提升性能。优化策略包括三数取中选基准、小区间转插入排序、递归转栈非递归及内省排序(深度超限时切换堆排),有效避免最坏情况。平均时间复杂度为 $O(N \log N)$,空间复杂度 $O(\log N)$,是实际应用中最高效的排序算法之一。

这里是Cocatrice的博客,感谢大家一直以来的支持和鼓励,如果对C/C++方向感兴趣的话,可以通过主页的微信名片加Cocatrice的好友,也可以通过邮箱:Chen31980094@163.com 联系,一同加油! 745

【数据结构】排序_OJ题(二):划分与选择

本文系统对比四种快排写法在排序数组(912)中的性能表现,揭示划分策略在重复值、有序数据下的退化问题。通过三路划分与内省排序优化,显著提升处理极端数据的效率。结合75、324、973等题,阐明划分的三大用途:全排、选第k小、定边界。强调实际应用中应根据数据特征选择算法,避免盲目使用经典快排。

这里是Cocatrice的博客,感谢大家一直以来的支持和鼓励,如果对C/C++方向感兴趣的话,可以通过主页的微信名片加Cocatrice的好友,也可以通过邮箱:Chen31980094@163.com 联系,一同加油! 158

数据结构 06|图:度怎么数、拓扑序列怎么列、Dijkstra 与关键路径怎么手算

本文讲解 408 图论考点。先说明无向图与有向图度的计算及邻接矩阵、邻接表两类存储的取舍,再讲广度优先与深度优先遍历的序列判定,随后覆盖最小生成树的两种算法、拓扑序列的列法与计数、Dijkstra 最短路径的推演,以及 AOE 网关键路径与时间余量的计算。逐题核对 2009—2025 年共 31 道真题,附考频表与文末速查表。

m0_56226131的博客 168

计算机软考补码数据结构2026年-东方仙盟

符号位:0 正 1 负,n 位字长真题坑:8 位补码最小是 - 128,原码最小只能 - 127;定点小数只有补码可以表示 - 1。

cybersnow精通 28 门计算机语言,凭借其超凡的技术能力,成功开发过上万个应用,广泛涉及政府、商业、个人等众多领域,甚至在检察院、环保局、公安局等专业场景中也大放异彩。不仅熟练掌握单片机和物联网开发,在软件架构设计方面更是独树一帜,自创了跨平台软件 432
上一篇: 深入理解函数栈帧的创建与销毁——从汇编视角彻底搞懂函数调用
下一篇: 一文通关-----栈和队列

双11狂欢大促

盲盒赢 定制羽绒服

开启惊喜盲盒,抽取你的专属好礼

— 惊喜奖品抢先看 —
YSL070124
博客等级 码龄1年 322粉丝 30原创
评论 28
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值