C语言--贪吃蛇

news2025/1/17 21:51:13

目录

    • 1. 实现目标
    • 2. 需掌握的技术
    • 3. Win32 API介绍
      • 控制台程序
      • 控制台屏幕上的坐标COORD
      • GetStdHandle
      • GetConsoleCursorinfo
      • CONSOLE_CURSOR_INFO
      • SetConsoleCursorInfo
      • SetConsoleCursorPosition
      • GetAsyncKeyState
    • 4. 贪吃蛇游戏设计与分析
      • 地图
      • <locale.h>本地化
      • 类项
      • setlocale函数
      • 宽字符打印
      • 地图坐标
      • 蛇身和食物
    • 5. 数据结构设计
    • 6. 游戏流程设计
    • 7. 核心逻辑实现分析
      • 游戏主逻辑
      • 游戏开始
        • 打印欢迎界面
        • 创建地图
        • 蛇初始化蛇身
        • 创建第一个食物
      • 游戏运行
        • KEY_PRESS
        • PrintHelpInfo
        • 蛇身移动
        • NextIsFood
        • EatFood
        • NoEatFood
        • KillByWall
        • KillBySelf
      • 游戏结束
    • 完整
      • test.c--贪吃蛇的测试
      • snack.h--贪吃蛇游戏中类型的声明,函数的声明
      • snake.c--函数的实现

1. 实现目标

使用C语言在Windows 环境的控制台中模拟实现经典小游戏贪吃蛇

实现的基本功能:

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

2. 需掌握的技术

C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等

3. Win32 API介绍

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

控制台程序

平常我们运行起来的程序其实就是控制台程序

我们可以可以使用cmd命令设置控制台窗口的长度:设置控制台窗口的大小,30行,100列

mode con cols=100 lines=30

可也以通过命令设置控制台窗口的名字

title 贪吃蛇

这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行

//设置控制台的显示大小、名称
int main()
{
	//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
	system("mode con cols=100 lines=30");
	//设置cmd窗⼝名称
	system("title 贪吃蛇");
	//getchar();//输入一个字符程序再往下走
	system("pause");//程序暂停
	return 0;
}

控制台屏幕上的坐标COORD

COORD是Windows API中定义的一个结构体,表示一个字符在控制台屏幕上的坐标

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

#include <windows.h>
int main()
{
	COORD pos = { 40,10 };//给坐标赋值
	return 0;
}

GetStdHandle

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

HANDLE GetStdHandle (DWORD nStdHandle);

#include <windows.h>
#include <stdbool.h>
int main()
{
	//获取标准输出的句柄(用来标识不同设备的数值)
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	return 0;
}

GetConsoleCursorinfo

检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息

BOOL WINAP GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

实例:

#include <windows.h>
#include <stdbool.h>
int main()
{
	//获取标准输出的句柄(用来标识不同设备的数值)
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);

	CONSOLE_CURSOR_INFO cursor_info = {0};
	GetConsoleCursorInfo(handle,&cursor_info);//获取控制台光标信息
	return 0;
}

CONSOLE_CURSOR_INFO

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

typedef struct _CONSOLE_CURSOR_INFO{
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

  • dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,返回从完全填充单元格到单元底部的水平线条。
  • bVisible,游标的可见性。如果光标可见,则此成员为TRUE
#include <windows.h>
#include <stdbool.h>
int main()
{
	//cursor_info.dwSize = 100;
	cursor_info.bVisible = false;//隐藏控制台光标
}

SetConsoleCursorInfo

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

BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSPOLE_CURSOR_INFO *lpConsoleCursorInfo
);

例子:

#include <windows.h>
#include <stdbool.h>
int main()
{
	CONSOLE_CURSOR_INFO cursor_info = {0};
	
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	GetConsoleCursorInfo(handle,&cursor_info);//获取控制台光标信息
	//cursor_info.dwSize = 100;
	cursor_info.bVisible = false;//隐藏控制台光标
	SetConsoleCursorInfo(handle, &cursor_info);//设置控制台光标状态
	return 0;
}

SetConsoleCursorPosition

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

BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);

#include <windows.h>
#include <stdbool.h>
int main()
{
	//获得设备句柄
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//根据句柄设置光标的位子
	COORD pos = { 20,5 };
	//设置标准输出上光标的位置为pos
	SetConsoleCursorPosition(handle, pos);
	printf("hehe");
	return 0;
}

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

#include <windows.h>
#include <stdbool.h>

SetPos(int x, int y)
{
	//获得设备句柄
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//根据句柄设置光标的位子
	COORD pos = { x,y };
	SetConsoleCursorPosition(handle, pos);
}
int main()
{
	SetPos(20, 5);
	printf("hehe");
	return 0;
}

GetAsyncKeyState

获取按键情况,GetAsyncKeyState的函数原型如下:

SHORT GetAsyncKeyState(
int vKey
);

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

GetAsyncKeyState的返回值是short类型,在上一次调用GetAsyncKeyState函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。

如果我们要判断一个键是否被按过,可以检测GetAsynKeyState返回值的最低位是否为1.

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

4. 贪吃蛇游戏设计与分析

地图

最终实现的效果:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

如果想在控制台的窗口中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍一下控制台窗口的坐标知识。

控制台窗口的坐标如下所示,横向是X轴,从左向右依次增长,纵向是Y轴,从上大下依次增长。

在这里插入图片描述

在游戏地图上,我打印墙体使用宽字符:□,打印蛇只用宽字符●,打印食物使用宽字符★

普通的字符是占一个字节的,这类宽字符是占2个字节的

为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型wchar_t和宽字符的输入和输出函数,加入和 <locale.h> 头文件,其中提供了允许程序员针对特定地区调整程序行为的函数。

<locale.h>本地化

<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
在标准可以,依赖地区的部分有以下几项

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

类项

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

  • LC_COLLATE
  • LC_CTYPE
  • LC_MONETARY
  • LC_NUMERIC
  • LC_TIME
  • LC_ALL-针对所有类项修改

setlocale函数

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

setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项

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

C标准给第二个参数仅定义了2种可能取值:“C”和“ ”。

任意程序执行开始,都会隐藏执行调用:

setlocale(LC_ALL, “C”);

当地区设置为“C”时,库函数按正常方式执行,小数点是一个点。

当程序运行起来后想改变地区,就只能显示调用setlocale函数。用“ ”作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉子)的输出等。

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

宽字符打印

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

#include<locale.h>
int main()
{
	setlocale(LC_ALL, "");
	wchar_t ch1 = L'●';
	wchar_t ch2 = L'哈';
	printf("%c%c\n", 'a', 'b');
	wprintf(L"%lc\n", ch1);
	wprintf(L"%lc\n", ch2);
	return 0;
}

在这里插入图片描述

从输出的结果来看,我们发现一个不同字符占一个字符的位置但是打印一个汉字字符,占用2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。

地图坐标

设计实现一个棋盘27行,58列
在这里插入图片描述

蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,开始出现蛇,连续5个节点。注意:蛇的每个节点的X坐标必须是2个倍数,否则可能会出现蛇的一个节点由一半出现在墙体中,另外一半出现在墙外的现象,坐标不好对齐。

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

5. 数据结构设计

在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节点其实就是hi链表的每个节点。每个节点只需要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:

//贪吃蛇,蛇身节点的定义
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

要管理整条贪吃蛇,我们要再分装一个Snake结构来维护整条贪吃蛇

//贪吃蛇
typedef struct Snake
{
	pSnakeNode pSnake;//维护整条蛇的指针
	pSnakeNode pFood;//指向食物的指针
	int Score;//当前累积的分数
	int FoodWeight;//一个食物的分数
	int SleepTime;//蛇休眠的时间,时间越短,速度越快
	enum GAME_STATUS status;//游戏的当前状态
	enum DIRECTION dir;//蛇当前走的方向
}Snake,*pSnake;

蛇的方向,可以一一列举,使用枚举

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

游戏状态,可以一一列举,使用枚举

//游戏的状态运行
enum GAME_STSTUS
{
	OK = 1,//正常运行
	ESC,//按了ESC键退出,正常退出
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF//撞自身
};

6. 游戏流程设计

GameStart–游戏开始

  1. 设置游戏窗口的大小
  2. 设置窗口的名字
  3. 隐藏屏幕光标
  4. 打印欢迎界面–WelcomeToGame
  5. 创建地图–CreateMap
  6. 初始化蛇身–InitSnake
  7. 创建食物–CreateFood

GameRun–游戏运行

  1. 右侧打印帮助信息–PrintHelpInfo
  2. 打印当前已获得分数和每个食物的分数
  3. 获取按键情况–KEY_PRESS
  4. 根据按键情况移动蛇–SnakeMove
    2~4循环,直到游戏是结束状态

SnakeMove

  1. 根据蛇头的坐标和方向,计算下一节点的坐标
  2. 判断下一节点是否是食物–NextIsFood
  3. 不是食物,吃掉植物,尾巴删除一节–NoFood
  4. 判断是否撞墙–KillByWall
  5. 判断是否撞上自己–KillBySelf

GameEnd–游戏结束

  1. 告知游戏结束的原因
  2. 释放蛇身节点

7. 核心逻辑实现分析

游戏主逻辑

#define _CRT_SECURE_NO_WARNINGS 1
#include "snack.h"


void test()
{
	
	int ch = 0;

	do
	{
		//创建贪吃蛇
		Snake snake = { 0 };
		GameStart(&snake);//游戏开始前的初始化

		GameRun(&snake);//玩游戏的过程
		GameEnd(&snake);//善后的工作

		SetPos(20,15);
		printf("再来一局吗?(Y/N):");
		ch = getchar();
		getchar();//清理\n
	} while (ch=='Y'||ch=='y');
}

int main()
{
	//修改适配本地中文环境
	setlocale(LC_ALL,"");
	test();//贪吃蛇游戏的测试
	SetPos(0,27);
	return 0;

}

游戏开始

void GameStart(pSnake ps)
{
	//设置控制台的信息,窗口大小,窗口名称
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//隐藏光标
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);
	
	
	//打印欢迎信息
	WelcomeToGame();
	
	//绘制地图
	CreateMap();

	//初始化蛇
	InitSnake(ps);

	//创建食物
	CreateFood(ps);
}
打印欢迎界面
void WelcomeToGame()
{
	//欢迎信息
	SetPos(40,10);
	printf("欢迎来到贪吃蛇小游戏\n");
	SetPos(40, 20);
	system("pause");
	system("cls");

	//功能介绍信息
	SetPos(15, 10);
	printf("用↑.↓.← .→ 来控制蛇的移动,A是加速,D是减速");
	SetPos(15, 11);
	printf("加速能够得到更高的分数");
	SetPos(40, 20);
	system("pause");
	system("cls");
}
创建地图

创建地图就是将墙体打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L打印地图的关键是要算好坐标,才能在想要的位置打印墙体。

墙体打印的宽字符:

#define WALL L’□’

创建地图函数CreateMap

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

}
蛇初始化蛇身

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

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

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

蛇身打印的宽字符:

#define BODY L’●’

初始化蛇身函数:InitSnake

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	for(int i=0;i<5;i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			printf("InitSnake():malloc() fail\n");
			return;
		}
		cur->x = POS_X + 2 * i;//
		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->FoodWeight = 10;
	ps->pFood = NULL;
	ps->Score = 0;
	ps->SleepTime = 200;
	ps->status = OK;
}
创建第一个食物
  • 先随机生成食物的坐标
    • x的坐标必须是2的倍数
    • 食物的坐标不能和蛇身每个节点的坐标重复
  • 创建食物节点,打印食物

食物打印的宽字符:

#define FOOD L’★’

创建食物的函数:CreateFood

//创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;

again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 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;
	}

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

游戏运行

游戏运行期间,右侧打印帮助信息,提示玩家

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

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

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

//游戏运行的整个逻辑
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	
	do
	{
		//当前的分数情况
		SetPos(62, 10);
		printf("总分:%5d\n", ps->Score);
		SetPos(62, 11);
		printf("食物的分值:%02d\n", ps->FoodWeight);


		//监测按键
		//上、下、左、右、ESC、空格、F3/F4
		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_ESCAPE))
		{
			ps->status = ESC;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//游戏暂停
			pause();//暂停和恢复暂停
		}
		else if (KEY_PRESS(0x41))
		{
			//加速,休眠时间变短
			if (ps->SleepTime >= 80)
			{
				ps->SleepTime -= 30;
				ps->FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(0x44))
		{
			if (ps->FoodWeight > 2)
			{
				ps->SleepTime += 30;
				ps->FoodWeight -= 2;
			}
		}
		

		//走一步
		SnakeMove(ps);

		//睡眠一下
		Sleep(ps->SleepTime);

	} while (ps->status == OK);
	
	
}
KEY_PRESS

检测按键状态,我们分装了一个宏

#define KEY_PRESS(vk)( GetAsyncKeyState(vk)&0x1 ? 1:0)
PrintHelpInfo
//打印帮助信息
void PrintHelpInfo()
{
	SetPos(62, 15);
	printf("1.不能穿墙,不能咬到自己");
	SetPos(62, 16);
	printf("2.用↑.↓.← .→ 来控制蛇的移动");
	SetPos(62, 17);
	printf("3.A是加速,D是减速");
	SetPos(62, 18);
	printf("4.ESC退出游戏,space暂停游戏");

	SetPos(62, 19);
	printf("加油噻!");
}
蛇身移动

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

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

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

/蛇移动的函数每走一步
void SnakeMove(pSnake ps)
{
	//创建一个节点
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove():malloc()");
		return;
	}
	pNext->next = NULL;

	switch (ps->dir)
	{
	case UP:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->pSnake->x - 2;
		pNext->y = ps->pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->pSnake->x + 2;
		pNext->y = ps->pSnake->y;
		break;
	}

	//下一个坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//是食物就吃掉
		EatFood(ps,pNext);
	}
	else {
		//不是食物就正常走
		NotEatFood(ps,pNext);
	}

	//监测撞墙
	KillByWall(ps);

	//监测撞自己
	KillBySelf(ps);
}

NextIsFood
//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pNext)
{
	if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)
	{
		return 1;//下一处坐标是食物
	}
	else {
		return 0;
	}
}
EatFood
//下一步要走的位置处是食物
void EatFood(pSnake ps, pSnakeNode pNext)
{
	pNext->next = ps->pSnake;
	ps->pSnake = pNext;

	pSnakeNode cur = ps->pSnake;
	//打印蛇身
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->Score += ps->FoodWeight;

	//释放旧的食物
	free(ps->pFood);
	//创建新食物
	CreateFood(ps);
}
NoEatFood

将下⼀个节点头插⼊蛇的⾝体,并将之前蛇⾝最后⼀个节点打印为空格,放弃掉蛇⾝的最后⼀个节点


//下一步要走的位置不是食物
void NotEatFood(pSnake ps, pSnakeNode pNext)
{
	//头插法
	pNext->next = ps->pSnake;
	ps->pSnake = pNext;

	//释放尾结点
	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;


}
KillByWall

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

//监测是否被撞墙
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;
	}
}
KillBySelf

判断蛇头的坐标是否和蛇⾝体的坐标冲突

//监测是否撞自己
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;
			return;
		}
		cur = cur->next;
	}
}

游戏结束

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


//游戏结束的资源释放
void GameEnd(pSnake ps)
{
	SetPos(15, 12);
	switch (ps->status)
	{

	case ESC:
		printf("主动退出游戏,正常退出\n");
		break;
	case KILL_BY_WALL:
		printf("很遗憾,撞墙了,游戏结束\n");
		break;
	case KILL_BY_SELF:
		printf("很遗憾,咬到自己了,游戏结束\n");
		break;
	}
	//释放贪吃蛇的链表资源
	pSnakeNode cur = ps->pSnake;
	pSnakeNode del = NULL;
	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}
	free(ps->pFood);
	ps = NULL;
	
}

完整

test.c–贪吃蛇的测试

#define _CRT_SECURE_NO_WARNINGS 1
#include "snack.h"


void test()
{
	
	int ch = 0;

	do
	{
		//创建贪吃蛇
		Snake snake = { 0 };
		GameStart(&snake);//游戏开始前的初始化

		GameRun(&snake);//玩游戏的过程
		GameEnd(&snake);//善后的工作

		SetPos(20,15);
		printf("再来一局吗?(Y/N):");
		ch = getchar();
		getchar();//清理\n
	} while (ch=='Y'||ch=='y');
}

int main()
{
	//修改适配本地中文环境
	setlocale(LC_ALL,"");
	test();//贪吃蛇游戏的测试
	SetPos(0,27);
	return 0;

}

snack.h–贪吃蛇游戏中类型的声明,函数的声明

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <windows.h>
#include <stdbool.h>
#include <math.h>

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

//蛇默认的起始坐标
#define POS_X 24
#define POS_Y 5

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


//贪吃蛇,蛇身节点的定义
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

//游戏的状态运行
enum GAME_STSTUS
{
	OK = 1,//正常运行
	ESC,//按了ESC键退出,正常退出
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF//撞自身
};

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


//贪吃蛇
typedef struct Snake
{
	pSnakeNode pSnake;//维护整条蛇的指针
	pSnakeNode pFood;//指向食物的指针
	int Score;//当前累积的分数
	int FoodWeight;//一个食物的分数
	int SleepTime;//蛇休眠的时间,时间越短,速度越快
	enum GAME_STATUS status;//游戏的当前状态
	enum DIRECTION dir;//蛇当前走的方向
}Snake,*pSnake;



//定位控制台的光标位置
void SetPos(int x, int y);

//游戏开始前的准备工作
void GameStart(pSnake ps);

//欢迎界面
void WelcomeToGame();

//绘制地图
void CreateMap();

//初始化贪吃蛇
void InitSnake(pSnake ps);

//创建食物
void CreateFood(pSnake ps);




//游戏运行的整个逻辑
void GameRun(pSnake ps);

//打印帮助信息
void PrintHelpInfo();

//蛇移动的函数每走一步
void SnakeMove(pSnake ps);

//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pNext);

//下一步要走的位置处是食物
void EatFood(pSnake ps, pSnakeNode pNext);

//下一步要走的位置不是食物
void NotEatFood(pSnake ps, pSnakeNode pNext);

//监测是否被撞墙
void KillByWall(pSnake ps);

//监测是否撞自己
void KillBySelf(pSnake ps);



//游戏借宿的资源释放
void GameEnd(pSnake ps);

snake.c–函数的实现

#define _CRT_SECURE_NO_WARNINGS 1
#include "snack.h"

void SetPos(int x, int y)
{
	//获得设备句柄
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//根据句柄设置光标的位子
	COORD pos = { x,y };
	SetConsoleCursorPosition(handle, pos);
}


void WelcomeToGame()
{
	//欢迎信息
	SetPos(40,10);
	printf("欢迎来到贪吃蛇小游戏\n");
	SetPos(40, 20);
	system("pause");
	system("cls");

	//功能介绍信息
	SetPos(15, 10);
	printf("用↑.↓.← .→ 来控制蛇的移动,A是加速,D是减速");
	SetPos(15, 11);
	printf("加速能够得到更高的分数");
	SetPos(40, 20);
	system("pause");
	system("cls");
}



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

}



void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	for(int i=0;i<5;i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			printf("InitSnake():malloc() fail\n");
			return;
		}
		cur->x = POS_X + 2 * i;//
		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->FoodWeight = 10;
	ps->pFood = NULL;
	ps->Score = 0;
	ps->SleepTime = 200;
	ps->status = OK;
}


//创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;

again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 24 + 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;
	}

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


void GameStart(pSnake ps)
{
	//设置控制台的信息,窗口大小,窗口名称
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//隐藏光标
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle, &CursorInfo);
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);
	
	
	//打印欢迎信息
	WelcomeToGame();
	
	//绘制地图
	CreateMap();

	//初始化蛇
	InitSnake(ps);

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





//打印帮助信息
void PrintHelpInfo()
{
	SetPos(62, 15);
	printf("1.不能穿墙,不能咬到自己");
	SetPos(62, 16);
	printf("2.用↑.↓.← .→ 来控制蛇的移动");
	SetPos(62, 17);
	printf("3.A是加速,D是减速");
	SetPos(62, 18);
	printf("4.ESC退出游戏,space暂停游戏");

	SetPos(62, 19);
	printf("加油噻!");
}


void pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}


//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pNext)
{
	if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)
	{
		return 1;//下一处坐标是食物
	}
	else {
		return 0;
	}
}

//下一步要走的位置处是食物
void EatFood(pSnake ps, pSnakeNode pNext)
{
	pNext->next = ps->pSnake;
	ps->pSnake = pNext;

	pSnakeNode cur = ps->pSnake;
	//打印蛇身
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->Score += ps->FoodWeight;

	//释放旧的食物
	free(ps->pFood);
	//创建新食物
	CreateFood(ps);
}

//下一步要走的位置不是食物
void NotEatFood(pSnake ps, pSnakeNode pNext)
{
	//头插法
	pNext->next = ps->pSnake;
	ps->pSnake = pNext;

	//释放尾结点
	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==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;
			return;
		}
		cur = cur->next;
	}
}





//蛇移动的函数每走一步
void SnakeMove(pSnake ps)
{
	//创建一个节点
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove():malloc()");
		return;
	}
	pNext->next = NULL;

	switch (ps->dir)
	{
	case UP:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->pSnake->x;
		pNext->y = ps->pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->pSnake->x - 2;
		pNext->y = ps->pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->pSnake->x + 2;
		pNext->y = ps->pSnake->y;
		break;
	}

	//下一个坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//是食物就吃掉
		EatFood(ps,pNext);
	}
	else {
		//不是食物就正常走
		NotEatFood(ps,pNext);
	}

	//监测撞墙
	KillByWall(ps);

	//监测撞自己
	KillBySelf(ps);
}





//游戏运行的整个逻辑
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	
	do
	{
		//当前的分数情况
		SetPos(62, 10);
		printf("总分:%5d\n", ps->Score);
		SetPos(62, 11);
		printf("食物的分值:%02d\n", ps->FoodWeight);


		//监测按键
		//上、下、左、右、ESC、空格、F3/F4
		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_ESCAPE))
		{
			ps->status = ESC;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			//游戏暂停
			pause();//暂停和恢复暂停
		}
		else if (KEY_PRESS(0x41))
		{
			//加速,休眠时间变短
			if (ps->SleepTime >= 80)
			{
				ps->SleepTime -= 30;
				ps->FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(0x44))
		{
			if (ps->FoodWeight > 2)
			{
				ps->SleepTime += 30;
				ps->FoodWeight -= 2;
			}
		}
		

		//走一步
		SnakeMove(ps);

		//睡眠一下
		Sleep(ps->SleepTime);

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





//游戏结束的资源释放
void GameEnd(pSnake ps)
{
	SetPos(15, 12);
	switch (ps->status)
	{

	case ESC:
		printf("主动退出游戏,正常退出\n");
		break;
	case KILL_BY_WALL:
		printf("很遗憾,撞墙了,游戏结束\n");
		break;
	case KILL_BY_SELF:
		printf("很遗憾,咬到自己了,游戏结束\n");
		break;
	}
	//释放贪吃蛇的链表资源
	pSnakeNode cur = ps->pSnake;
	pSnakeNode del = NULL;
	while (cur)
	{
		del = cur;
		cur = cur->next;
		free(del);
	}
	free(ps->pFood);
	ps = NULL;
	
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1470320.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

matlab经验模式分解的R波检测算法

1、内容简介 略 56-可以交流、咨询、答疑 2、内容说明 略 心血管疾病是威胁人类生命的主要疾病之一&#xff0c;而心电信号&#xff08;electrocardiogram, ECG&#xff09; 则是评价心脏功能的主要依据&#xff0c;因此&#xff0c;关于心电信号检测处理的研究一直为各方所…

js设计模式:状态模式

作用: 将对象的行为和状态进行分离,状态是由行为操作决定的,而不是直接控制。 同时,行为也是由状态决定的,每个状态都有自己的行为和相应的方法 行为与状态分离,可以使代码方便维护 示例: <!DOCTYPE html> <html lang"en"><head><meta cha…

基于YOLOv5+PySide6的火灾火情火焰检测系统设计深度学习

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;225火灾 获取完整源码源文件已标注的数据集&#xff08;1553张&#xff09;配置跑起来说明 可有偿49yuan一对一远程操作&#xff0c;在你电脑跑起来 效果展示&#xff1a; ​数据集在下载的文件夹&#xff1a;yolov5-5.0\…

Vue3中的select 的option是多余的?

背景&#xff1a; 通过Vue3中填充一个下拉框&#xff0c;在打开页面时要指定默认选中&#xff0c;并在选项改变时把下拉框的选中值显示出来 问题&#xff1a; 填充通常的作法是设置 <option v-for"option in cities" :value"option.value" >&a…

软件测试中的测试左移与测试右移

在软件开发的过程中&#xff0c;测试是确保软件质量的一个至关重要的环节。随着软件开发方法的不断演进&#xff0c;测试也在不断地发展和改进。其中&#xff0c;测试左移&#xff08;Shift Left Testing&#xff09;和测试右移&#xff08;Shift Right Testing&#xff09;是两…

QlikSense CyberSecurity : Configuring preferred Cipher Suites

You can rank the preferred cipher suites that Qlik License Service uses to encrypt and decrypt the signed key license.您可以对Qlik许可证服务用于加密和解密签名密钥许可证的首选密码套件进行排序。 The Qlik License Service is included in Qlik Sense Enterprise …

【微服务生态】Dubbo

文章目录 一、概述二、Dubbo环境搭建-docker版三、Dubbo配置四、高可用4.1 zookeeper宕机与dubbo直连4.2 负载均衡 五、服务限流、服务降级、服务容错六、Dubbo 对比 OpenFeign 一、概述 Dubbo 是一款高性能、轻量级的开源Java RPC框架&#xff0c;它提供了三大核心能力&#…

Redis如何修改key名称

点击上方蓝字关注我 近期出现过多次修改Redis中key名字的场景&#xff0c;本次简介一下如何修改Redis中key名称的方法。 1. 命令行方式修改在Redis中&#xff0c;可以使用rename命令来修改Key的名称。这个命令的基本语法如下&#xff1a; RENAME old_key new_key 在这里&#…

2024年5月软考考试时间及考试安排

2024年5月软考考试时间&#xff1a;为5月25日到28日 考试采取科目连考、分批次考试的方式,连考的第一个科目作答结束交卷完成后自动进入第二个科目&#xff0c;第一个科目节余的时长可为第二个科目使用。 高级资格&#xff1a;综合知识科目考试时长150分钟&#xff0c;最短作…

代码随想录第41天|● 01背包问题,你该了解这些! ● 01背包问题,你该了解这些! 滚动数组 ● 416. 分割等和子集

文章目录 背包问题背包题目解法一 ● 01背包问题-二维数组五部曲1.确定dp数组2、确定递推公式3、初始化dp数组4、循环代码&#xff1a; 解法二-01背包问题-滚动数组五部曲1&#xff1a;定义dp二、递推公式三、初始化四、循环顺序代码&#xff1a; 698. 划分为k个相等的子集题解…

Mysql5.7主从复制搭建

注意不适用Mysql8 Docker搭建Mysql主从复制 docker run -p 3307:3306 --name mysql-master \ -v /usr/local/develop/mysql-master/log:/var/log/mysql \ -v /usr/local/develop/mysql-master/data:/var/lib/mysql \ -v /usr/local/develop/mysql-master/conf:/etc/mysql/con…

开源工具和框架

目录 开源工具和框架 一、 开源工具和框架 二、开源工具和框架在现代软件开发中的角色 1、基础设施建设&#xff1a; 2、开发效率提升&#xff1a; 3、代码质量保障&#xff1a; 4、技术创新&#xff1a; 三、广泛使用的开源项目分析 3.1、Linux 3.2、Git 3.3、Docke…

高等数学(无穷小与无穷大)

目录 一、无穷小 二、无穷大 三、无穷小与无穷大的关系 四、无穷小量的阶的比较 一、无穷小 二、无穷大 三、无穷小与无穷大的关系 四、无穷小量的阶的比较

女生常用的社交app软件有哪些?分享女生用的最多的社交软件

随着科技的迅猛发展&#xff0c;社交软件也日益多样化。除了常见的社交平台&#xff0c;一些全新的社交软件如雨后春笋般涌现&#xff0c;为用户带来了更多元、更有趣的社交体验。这里为大家介绍 5 款女生用的最多的社交软件&#xff0c;它们分别是丛丛、青藤之恋、meetu、小奢…

12. Springboot集成Dubbo3(三)Dubbo-Admin

目录 1、前言 2、安装 2.1、下载Dubbo-admin 2.2、修改配置 2.3、编译前端 2.4、访问 2.5、加载自己的服务 2.6、服务测试 2.7、其他 3、小结 1、前言 Dubbo Admin是用于管理Dubbo服务的基于Web的管理工具。Dubbo Admin提供了一个用户友好的界面&#xff0c;用于在分…

进程 2月24日学习笔记

1.进程: 程序&#xff1a;存放在外存中的一段数据组成的文件 进程&#xff1a;是一个程序动态执行的过程,包括进程的创建、进程的调度、进程的消亡 2.进程相关命令: 1.top 动态查看当前系统中的所有进程信息&#xff08;根据CPU占用率排序&#xff09; PID:唯一识…

番外篇 | YOLOv5+DeepSort实现行人目标跟踪检测

前言:Hello大家好,我是小哥谈。DeepSort是一种用于目标跟踪的深度学习算法。它结合了目标检测和目标跟踪的技术,能够在视频中准确地跟踪多个目标,并为每个目标分配一个唯一的ID。DeepSort的核心思想是将目标检测和目标跟踪两个任务进行联合训练,以提高跟踪的准确性和稳定性…

(详细使用指南)Linux下交叉编译带ffmpeg的opencv并移植到RK3588等ARM端

一 问题背景 瑞芯微RK3588等嵌入式板作为边缘端设备为算法模型的部署提供了便利&#xff0c;目前很多分类或好检测模型针对边缘端做了优化或量化&#xff0c;使得在边缘端也能达到实时稳定的识别和检测效果。 但嵌入式设备普遍的flash emmc不大&#xff0c;一般在32G左…

【Flink精讲】Flink任务调度机制

Graph 的概念 Flink 中的执行图可以分成四层&#xff1a; StreamGraph -> JobGraph -> ExecutionGraph -> 物理执 行图。 StreamGraph&#xff1a;是根据用户通过 Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构。JobGraph&#xff1a; StreamGraph …

科学高效备考2024年AMC10:2000-2023年1250道AMC10真题练一练

我整理了2000-2023年的全部AMC10的AB卷真题共1250题&#xff0c;并且独家制作了多种在线练习&#xff0c;利用碎片化时间&#xff0c;一年足以通过自学在2024年AMC10竞赛中取得好成绩。 我们今天继续来随机看五道题目和解析。 2000-2023年AMC10真题练一练&#xff1a;2013年第…