C语言实现贪吃蛇小游戏

贪吃蛇游戏简介

贪吃蛇是一款经典的电子游戏,起源于20世纪70年代的街机游戏。游戏的核心玩法简单而富有趣味性,要求玩家控制一条不断移动的蛇,吃掉屏幕上随机出现的食物,每吃掉一个食物,蛇的身体就会变长一段。游戏的挑战在于蛇不能碰到自己的身体或游戏边界,一旦碰撞,游戏就会结束。

技术要点

本篇博客实现的C语言小游戏是通过控制台输出的,涉及到的C语言知识点如下:

  1. C语言函数
  2. 枚举
  3. 结构体
  4. 动态内存管理
  5. 预处理指令
  6. 链表
  7. win32API

具体实现与分析

(完整实现代码请跳转:Snake: C语言实现贪吃蛇小游戏

游戏的初始化

对控制台窗口进行设置

将控制台窗口大小设置为宽为100高为30,调用C语言函数system来执行相关操作

cpp 复制代码
	system("mode con cols=100 lines=30");

将 控制台窗口标题设置为"贪吃蛇"

cpp 复制代码
	system("title 贪吃蛇");

打印欢迎界面

设置光标位置

控制台坐标通常指的是在控制台应用程序中,用于定位光标或指定文本输出位置的一组数值。这些坐标通常基于控制台的字符网格,其中每个字符都有一个唯一的坐标。以下是对控制台坐标的详细解释:

  • 原点:控制台坐标系统的原点(0,0)通常位于控制台的左上角。
  • X轴:水平方向,向右为正方向,通常表示字符的列数。
  • Y轴:垂直方向,向下为正方向,通常表示字符的行数。

在C语言中,控制控制台窗口的光标位置通常依赖于特定平台的API。对于Windows操作系统,你可以使用Windows API函数SetConsoleCursorPosition来实现,通过包含<windows.h>头文件来使用Windows API。以下是一个示例代码,展示如何设置控制台光标的位置:

cpp 复制代码
#include <stdio.h>
#include <windows.h>

void setCursorPosition(int x, int y) {
    // 获取标准输出句柄
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

    // 设置光标位置
    COORD position = {x, y};
    SetConsoleCursorPosition(hConsole, position);
}

int main() {
    // 移动光标到(10, 5)位置
    setCursorPosition(10, 5);
    printf("Hello, World!\n");

    return 0;
}

在这个例子中,SetConsoleCursorPosition函数用于将光标移动到指定的(x, y)坐标。请注意,这里的坐标是基于控制台窗口的缓冲区,而不是屏幕像素。

打印宽字符

在C语言中,宽字符(wide character)通常用于支持国际化(i18n)和本地化(l10n),以处理不同语言的字符集,特别是那些需要超过一个字节来表示的字符(如汉字、日文假名等)。宽字符类型在C标准库中定义为wchar_t

以下是一个简单的例子,展示了如何在C语言中打印宽字符:

cpp 复制代码
#include <wchar.h>
#include <locale.h>

int main() {
    // 设置程序的locale,以便正确处理宽字符(可选,但推荐)
    setlocale(LC_ALL, "");

    // 宽字符字符串
    wchar_t *wide_string = L"你好,世界!";

    // 打印宽字符字符串
    wprintf(L"%ls\n", wide_string);

    return 0;
}

在这个例子中:

  • setlocale(LC_ALL, ""):这行代码尝试将程序的locale设置为环境变量指定的默认locale。这对于正确处理宽字符和本地化格式是必要的。如果不需要完全本地化,这行代码可以省略,但可能会影响宽字符的正确显示。

  • wchar_t *wide_string = L"你好,世界!";:宽字符字符串字面量使用L前缀来表示。

  • wprintf(L"%ls\n", wide_string);wprintf函数用于打印宽字符字符串。%ls是宽字符字符串的格式说明符。

知道了以上知识,我们就可以在控制台指定位置输出游戏的欢迎界面信息。

cpp 复制代码
//打印菜单
void menu()
{
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	SetPos(32, 13);
	printf("*********贪吃蛇**********");
	SetPos(32, 14);
	printf("**1.进入游戏 0.退出游戏**");
	SetPos(32, 15);
	printf("*************************");
}
cpp 复制代码
//打印欢迎界面
void WelcomeToGame()
{
	SetPos(40, 14);
	//输出宽字符前,要在主调函数中使程序本地化,否则无法输出宽字符的中文
	wprintf(L"欢迎来到贪吃蛇小游戏\n");//打印宽字符(即两个字节大小的字符)
	SetPos(40, 20);
	system("pause");//暂停
	system("cls");//清屏

	SetPos(40, 14);
	wprintf(L"用↑↓←→来控制贪吃蛇的移动\n");
	SetPos(40, 15);
	wprintf(L"F3加速,F4减速\n");
	SetPos(40, 16);
	wprintf(L"加速能够获得更高的分数\n");
	SetPos(40, 20);
	system("pause");
	system("cls");
}

绘制地图

设置一个大小为,58*23的地图,通过控制台界面创建一个简单的边界框

cpp 复制代码
#define WALL L'□'

void CreateMap()
{
	//上
	SetPos(20, 2);
	for (int i = 0; i < 29; i++)//29个
	{
		wprintf(L"%lc",WALL);
	}

	//左
	for (int i = 3; i <= 25; i++)//23个
	{
		SetPos(20, i);
		wprintf(L"%lc", WALL);
	}

	//右
	for (int i = 3; i <= 25; i++)
	{
		SetPos(76, i);
		wprintf(L"%lc", WALL);
	}

	//下
	SetPos(20, 26);
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
}

初始化蛇

使用单链表创建一条蛇,并且设置游戏的相关状况

cpp 复制代码
#define BODY L'●'

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

//游戏的运行状态
enum GAME_STATUS
{
	OK,//正常
	KILL_BY_WALL,//撞到墙
	KILL_BY_SELF,//撞到自己
	END_NORMAL//正常结束esc
};

//定义蛇身节点类型,蛇是一个单链表
struct SnakeNode
{
	//坐标
	int x;
	int y;
	//蛇身的下一个节点的位置
	struct SnakeNode* next;
};
typedef struct SnakeNode SnakeNode;
typedef struct SnakeNode* pSnakeNode;

//贪吃蛇
struct Snake
{
	pSnakeNode _pSnake;//指向蛇头的指针
	pSnakeNode _pFood;//指向食物的指针,食物类型的节点和蛇身节点的结构相同
	enum DIRECTION _dir;//蛇的方向
	enum GAME_STATUS _status;//游戏的状态
	int _food_weight;//每个食物的积分
	int _score;//总分数
	int _sleep_time;//运动的快慢,时间越短越快,反之则越慢
};
typedef struct Snake Snake;
typedef struct Snake* pSnake;

使用单链表创建一条蛇

cpp 复制代码
#define POS_X 24
#define POS_Y 5

//初始化蛇

void InitSnake(pSnake ps)
{
	//默认给一条长度为5的蛇
	pSnakeNode cur = NULL;

	for (int i = 0; i < 5; i++)
	{
		//申请一个节点
		pSnakeNode newnode = (pSnakeNode)malloc(sizeof(SnakeNode));

		//设置默认的蛇的坐标
		newnode->x = POS_X + 2 * i;
		newnode->y = POS_Y;
		newnode->next = NULL;

		if (newnode == NULL)
		{
			perror("InitSnake()::malloc");
			return;
		}

		//使用头插法
		if (ps->_pSnake == NULL)//此时链表为空
		{
			ps->_pSnake = newnode;
		}
		else//此时链表非空
		{
			newnode->next = ps->_pSnake;
			ps->_pSnake = newnode;
		}
	}

	//打印蛇
	cur = ps->_pSnake;
	//遍历打印
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//设置贪吃蛇的属性
	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
	ps->_sleep_time = 200;//200ms
	ps->_status = OK;
}

生成食物

在C语言中,生成随机数通常依赖于标准库中的rand()函数。然而,rand()函数生成的随机数序列在每次程序运行时都是相同的,除非你使用srand()函数来设置随机数生成的种子(seed)。以下是如何在C语言中生成随机数的步骤:

  1. 包含头文件

    你需要包含<stdlib.h>头文件,它声明了rand()srand()函数。

  2. 设置随机数种子

    使用srand()函数来设置随机数生成的种子。通常,种子的值来自于系统时间,这样每次程序运行时都能得到不同的随机数序列。你可以使用<time.h>头文件中的time()函数来获取当前时间(以秒为单位),并将其作为种子。

  3. 生成随机数

    调用rand()函数来生成随机数。rand()函数返回一个在0到RAND_MAX之间的整数,其中RAND_MAX<stdlib.h>中定义的一个常量,表示rand()函数能返回的最大值。

以下是一个简单的例子,展示了如何在C语言中生成随机数:

cpp 复制代码
#define FOOD L'★'

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    // 设置随机数种子为当前时间
    srand(time(0));

    // 生成并打印5个随机数
    for (int i = 0; i < 5; i++) {
        int random_number = rand(); // 生成随机数
        printf("%d\n", random_number); // 打印随机数
    }

    return 0;
}

如果你需要生成一定范围内的随机数(例如,0到99之间的整数),你可以对rand()函数的返回值进行模运算(取余数):

cpp 复制代码
int random_number = rand() % 100; // 生成0到99之间的随机数

请注意,由于rand()函数生成的随机数序列是伪随机的(基于算法),所以它们并不完全等同于真正的随机数。然而,对于大多数应用程序来说,rand()函数生成的伪随机数已经足够好了。如果你需要更高质量的随机数(例如,用于加密或模拟),你可能需要使用更复杂的随机数生成算法或库。

知道了以上信息,我们可以将生成的随机坐标赋给食物,然后打印在屏幕上

cpp 复制代码
//创建食物
void CreateFood(pSnake ps)
{
	//随机生成坐标
	int x;
	int y;

	again:
	do
	{
		//x = rand() % 53 + 2;//范围是2-54
		//y = rand() % 25 + 1;//范围是1-25
		int range_x = 74 - 22 + 1;
		int range_y = 25 - 3 + 1;
		x = rand() % range_x + 22;//范围是22-74
		y = rand() % range_y + 3;//高是22,范围是3-25
	} while (x % 2 != 0);//x必须是2的倍数

	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (cur->x == x)
		{
			goto again;
		}
		cur = cur->next;
	}

	//创建一个节点
	pSnakeNode food = (pSnakeNode)malloc(sizeof(SnakeNode));
	food->x = x;
	food->y = y;
	food->next = NULL;
	ps->_pFood = food;

	SetPos(x, y);
	wprintf(L"%lc", FOOD);
}

游戏运行逻辑的实现

获取按键情况

Windows API中的GetAsyncKeyState函数用于检测特定按键的状态。

GetAsyncKeyState 函数用于确定指定虚拟键的当前状态。

函数原型如下:

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

返回值是一个SHORT值,其高位(最高位)表示键是否被按下(1表示按下,0表示未按下),如果最低位被置为1,则说明按键被按过,最低位为0则说明没有被按过。

cpp 复制代码
#include <stdio.h>
#include <windows.h>

int main() {
    while (1) {
        // 检查空格键是否被按过
        if (GetAsyncKeyState(VK_SPACE) & 0x1) {
            printf("空格键被按下!\n");
            break; // 或者你可以在这里添加其他逻辑
        }
        // 可以添加Sleep函数来避免循环过于频繁地检查按键状态
        // Sleep(100); // 等待100毫秒
    }
    return 0;
}

知道了上述知识,我们可以设计不同按键被按过后贪吃蛇的不同状态

当贪吃蛇的状态为OK时,循环代码

cpp 复制代码
//游戏暂停
void Pause()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)

//判断按键
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;
	}
}

判断贪吃蛇运动到下一个节点时是否吃到食物

如果下一个节点是食物,则将指向蛇头的指针指向该食物。

cpp 复制代码
//下一个位置是食物,就吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{
	//头插
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	//释放pn,因为头插用的是pFood,pFood=pn
	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);
}

如果下一个节点不是食物,则将指向蛇头的指针指向该节点(即将该节点挂载到蛇身上),再释放蛇的最后一个节点,并将最后一个节点用空格覆盖,以保持原先蛇的长度。

cpp 复制代码
//下一个位置不是食物
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;
}

判断贪吃蛇死亡情况

如果蛇撞到墙体,或者撞到自身,将蛇的status改变即可

cpp 复制代码
//撞到墙
void KillByWall(pSnake ps)
{
	//判断蛇头的坐标是否和墙的坐标重合
	if (ps->_pSnake->x == 20 || ps->_pSnake->x == 76
		|| ps->_pSnake->y == 2 || 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;
	}
}

释放游戏资源

由于蛇身节点都是通过动态开辟空间得来的,因此,在游戏结束后,要将资源释放还给操作系统。(即单链表的释放)

cpp 复制代码
//释放蛇身
pSnakeNode cur = ps->_pSnake;
while (cur)
{
	pSnakeNode del = cur;
	cur = cur->next;
	free(del);
}
相关推荐
QQ同步助手31 分钟前
Javascript 网页设计案例:进阶交互与动态效果
开发语言·javascript·ecmascript
多方通行838 分钟前
关于Ubuntu的server版本登录无法输入password问题
linux·开发语言·ubuntu·编辑器·bug
乌鸦94441 分钟前
《数据结构之美-- 单链表》
数据结构
程序猿阿伟1 小时前
《探索C++在3D重建中的算法与技术要点》
开发语言·c++·自然语言处理
charlie1145141911 小时前
嵌入式Linux应用层开发——调试专篇(关于使用GDB调试远程下位机开发板的应用层程序办法 + VSCode更好的界面调试体验提升)
linux·c语言·开发语言·vscode·imx6ull·嵌入式linux·调试技术
白露与泡影1 小时前
2024最新最全面Java复习路线(含P5-P8),已收录 GitHub
java·开发语言·github
狄加山6751 小时前
C语言(指针基础练习)
java·c语言·算法
hjxxlsx1 小时前
Swift 的起源与发展历程:从诞生到繁荣
开发语言·ios·swift
TANGLONG2222 小时前
【初阶数据结构和算法】八大排序算法之插入排序(直接插入排序、希尔排序及其对比)
java·c语言·数据结构·c++·算法·面试·排序算法
y25082 小时前
《时间和空间复杂度》
java·数据结构·算法