贪吃蛇代码实现与剖析(C语言)


首先说明:
1.
这个贪吃蛇代码只有在Windows中执行才会起效果
我用的是Windows系统中的VS2019编译器
2.
我们先给出贪吃蛇的完整代码,是为了让大家提起接下来往后仔细看完这篇博客的热情
3.
这个贪吃蛇代码的前置知识:
1.C语言:函数,结构体,枚举,指针,动态内存管理(free,malloc…),宏
2.数据结构:链表

1.温馨提示

想要执行这个代码,在VS2019中需要调整一下控制台的属性
我们先在VS2019中随意跑一段简单的hello world调出控制台来进行属性的调整

默认情况下:我们的控制台是这个样子的
在这里插入图片描述
我们需要修改一下这个控制台的属性
在这里插入图片描述
在这里插入图片描述
然后就会出现这个
在这里插入图片描述
只有这样,我们才可以更好的实现这个窗口
否则,同样的代码在这个控制台窗口下就会出现这种样子
在这里插入图片描述
而我们修改了之后的样子是这样的
在这里插入图片描述
所以我们才要去修改这个控制台窗口的属性

2.最终实现版本的样子

1.游戏开始-欢迎界面

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

2.游戏运行界面

在这里插入图片描述

3.游戏结束界面

在这里插入图片描述

4.选择是否继续玩

1.选择继续

在这里插入图片描述
输入Y/y并按下回车即可继续玩
在这里插入图片描述
然后回到游戏最开始
在这里插入图片描述

2.选择退出游戏

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

3.完整代码

大家可以先在自己的VS中执行一下玩一玩
1.Snake.h

#pragma once
#include <stdio.h>
#include <Windows.h>
#include <locale.h>
#include <stdbool.h>
#include <stdlib.h>
#include <time.h>
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)

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

#define INIT_X 24
#define INIT_Y 6

typedef struct SnakeNode
{
	struct SnakeNode* next;
	int x;
	int y;
}SNode;

enum Direction
{
	UP,
	DOWN,
	LEFT,
	RIGHT
};

enum GameState
{
	OK,
	EXIT_NORMAL,
	KILL_BY_WALL,
	KILL_BY_SELF
};

typedef struct Snake
{
	SNode* _pSnake;//蛇头节点
	SNode* _pFood;//食物
	enum Direction _dir;//蛇移动的方向
	enum GameState _state;//当前游戏状态
	int _score;//当前得分
	int _foodWeight;//每个食物的分数
	int _sleepTime;//蛇的休息时间,影响加速和减速和暂停

}Snake;


void SetPos(short x, short y);
void GameStart(Snake* ps);
void WelcomeToGame();
void CreateMap();
void InitSnake(Snake* ps);
void CreateFood(Snake* ps);

void GameRun(Snake* ps);
void PrintHelpInfo();
void SnakeMove(Snake* ps);
//判断是否撞墙
void IfKillByWall(Snake* ps,int x, int y);
//判断是否咬到自己
void IfKillBySelf(Snake* ps, int x, int y);

void EatFood(SNode* pNextNode, Snake* ps);
void NoFood(SNode* pNextNode, Snake* ps);
//暂停函数
void pause();

void GameEnd(Snake* ps);

2.Snake.c

#include "Snake.h"
//设置控制台光标位置的函数
void SetPos(short x, short y)
{
	COORD pos = { x,y };
	HANDLE handle = NULL;
	//获取标准输出的句柄(用来表示不同设备的数值),使用这个句柄可以操作这个设备
	handle = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的光标信息放在COORD类型的pos中
	//调用SetConsoleCursorPosition函数将光标位置设置到指定的位置
	SetConsoleCursorPosition(handle, pos);
}

//system("mode con cols=120 lines=35");

void WelcomeToGame()
{
	SetPos(45, 12);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(45, 18);
	system("pause");
	system("cls");
	SetPos(45, 12);
	printf("用↑.↓.←.→ 分别控制蛇的移动,F1为加速,F2为减速");
	SetPos(45, 13);
	printf("加速能够得到更高的分数");
	SetPos(45, 18);
	system("pause");
	system("cls");
}

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

void InitSnake(Snake* ps)
{
	//初始化蛇身
	for (int i = 0; i < 5; i++)
	{
		SNode* newnode = (SNode*)malloc(sizeof(SNode));
		if (newnode == NULL)
		{
			perror("InitSnake():: malloc fail");
			exit(-1);
		}
		newnode->next = NULL;
		newnode->x = INIT_X + 2 * i;
		newnode->y = INIT_Y;
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = newnode;
		}
		else
		{
			newnode->next = ps->_pSnake;
			ps->_pSnake = newnode;
		}
	}
	//打印蛇身
	SNode* cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//初始化其他属性
	ps->_dir = RIGHT;
	ps->_state = OK;
	ps->_foodWeight = 10;
	ps->_score = 0;
	ps->_sleepTime = 200;
}

void CreateFood(Snake* ps)
{
	//创建食物
	while (1)
	{
		//保证初始化到墙内
		//x:2~54
		int x = rand() % 53 + 2;//0~52+2  ->  2~54
		//y:1~25
		int y = rand() % 25 + 1;//0~24+1  ->  1~25

		//保证初始化的x必须为偶数
		if (x % 2 != 0)
		{
			continue;
		}

		//保证初始化时不跟蛇身重合
		SNode* cur = ps->_pSnake;
		bool flag = false;
		while (cur)
		{
			//跟蛇身重合
			if (cur->x == x && cur->y == y)
			{
				flag = true;
				break;
			}
			cur = cur->next;
		}
		//没有跟蛇身重合
		if (!flag)
		{
			SNode* newnode = (SNode*)malloc(sizeof(SNode));
			if (newnode == NULL)
			{
				perror("CreateFood():: malloc fail");
				exit(-1);
			}
			newnode->next = NULL;
			newnode->x = x;
			newnode->y = y;
			ps->_pFood = newnode;
			break;
		}
	}
	//打印食物
	SetPos(ps->_pFood->x, ps->_pFood->y);
	wprintf(L"%lc", FOOD);
}

void GameStart(Snake* ps)
{
	WelcomeToGame();
	CreateMap();
	InitSnake(ps);
	CreateFood(ps);
}

void PrintHelpInfo()
{
	SetPos(65, 17);
	printf("不能穿墙,不能咬到自己");
	SetPos(65, 18);
	printf("用↑.↓.←.→ 分别控制蛇的移动");
	SetPos(65, 19);
	printf("F1为加速,F2为减速");
	SetPos(65, 20);
	printf("Esc: 退出游戏  space:暂停游戏");

	SetPos(65, 22);
	printf("编写者:wzs");
}

void EatFood(SNode* pNextNode, Snake* ps)
{
	pNextNode->next = ps->_pSnake;
	ps->_pSnake = pNextNode;
	//打印蛇身
	SNode* cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x,cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//释放食物节点
	free(ps->_pFood);
	ps->_pFood = NULL;
	//加分
	ps->_score += ps->_foodWeight;
	//创建新食物
	CreateFood(ps);
}

void NoFood(SNode* pNextNode, Snake* ps)
{
	pNextNode->next = ps->_pSnake;
	ps->_pSnake = pNextNode;
	//释放最后一个节点
	SNode* 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;
}

//判断是否撞墙
void IfKillByWall(Snake* ps,int x,int y)
{
	if (x == 0 || x == 56 || y == 0 || y == 26)
	{
		ps->_state = KILL_BY_WALL;
	}
}
//判断是否咬到自己
void IfKillBySelf(Snake* ps, int x, int y)
{
	SNode* cur = ps->_pSnake->next;
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_state = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

void SnakeMove(Snake* ps)
{
	//1.根据蛇头的坐标和方向,计算下一个节点的坐标
	int x = ps->_pSnake->x;
	int y = ps->_pSnake->y;
	switch (ps->_dir)
	{
	case UP:
		y--;
		break;
	case DOWN:
		y++;
		break;
	case LEFT:
		x -= 2;
		break;
	case RIGHT:
		x += 2;
		break;
	}

	//创建下一个节点
	SNode* pNextNode = (SNode*)malloc(sizeof(SNode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove():: malloc fail");
		exit(-1);
	}
	pNextNode->x = x;
	pNextNode->y = y;
	pNextNode->next = NULL;

	//判断下一个是不是食物
	if (x == ps->_pFood->x && y == ps->_pFood->y)
	{
		//下一个位置有食物
		EatFood(pNextNode, ps);
	}
	//下一个位置没有食物
	else
	{
		NoFood(pNextNode, ps);
	}
	//判断是否撞墙
	IfKillByWall(ps, x, y);
	//判断是否咬到自己
	IfKillBySelf(ps, x, y);
}
//暂停函数
void pause()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

void GameRun(Snake* ps)
{
	PrintHelpInfo();
	do
	{
		SetPos(65, 10);
		printf("得分: %d , 每个食物得分: %d ", ps->_score, ps->_foodWeight);
		//调整方向
		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();
		}
		//Esc退出
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_state = EXIT_NORMAL;
			break;
		}
		//加速
		else if (KEY_PRESS(VK_F1))
		{
			//防止一直加速导致sleepTime<0出现bug
			if (ps->_sleepTime >= 50)
			{
				ps->_sleepTime -= 30;
				ps->_foodWeight += 2;//加速时食物的分值会增加
			}
		}
		//减速
		else if (KEY_PRESS(VK_F2))
		{
			//防止一直减速导致程序运行太慢出现卡顿影响用户体验
			if (ps->_sleepTime < 350)
			{
				ps->_sleepTime += 30;
				ps->_foodWeight -= 2;
				//防止太慢时食物得分减为负数
				if (ps->_sleepTime == 350)
				{
					ps->_foodWeight = 1;
				}
			}
		}
		//蛇每次移动都要有一定的休眠时间,时间越短,蛇移动的速度就越快
		Sleep(ps->_sleepTime);
		SnakeMove(ps);
	} while (ps->_state == OK);
}

void GameEnd(Snake* ps)
{
	SetPos(65, 24);
	switch (ps->_state)
	{
	case EXIT_NORMAL:
		printf("玩家选择退出,游戏结束");
		break;
	case KILL_BY_WALL:
		printf("很遗憾,撞墙了,游戏结束");
		break;
	case KILL_BY_SELF:
		printf("很遗憾,咬到自己了,游戏结束");
		break;
	default:
		break;
	}
	//释放蛇身的节点
	SNode* cur = ps->_pSnake;
	while (cur)
	{
		SNode* del = cur;
		cur = cur->next;
		free(del);
	}
}

3.test.c文件

#include "Snake.h"

//初始化光标信息等
void Init()
{
	HANDLE handle = NULL;
	//获取标准输出的句柄(用来表示不同设备的数值),使用这个句柄可以操作这个设备
	handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;//CONSOLE_CURSOR_INFO 这个结构体包含有关控制台光标的信息
	GetConsoleCursorInfo(handle, &CursorInfo);//检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
	CursorInfo.bVisible = false;//隐藏控制台光标
	SetConsoleCursorInfo(handle, &CursorInfo);//设置指定控制台屏幕缓冲区的光标的大小和可见性
}

int main()
{
	setlocale(LC_ALL, "");
	system("mode con cols=120 lines=35");
	system("title 贪吃蛇");
	Init();
	char input = 0;
	do
	{
		Snake snake = { 0 };//p_Snake=0;而NULL指针的本质就是(void(*)0),在数值上0跟NULL是相等的,这里可以认为p_Snake==NULL
		srand((unsigned int)time(NULL));
		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);
		SetPos(65, 26);
		printf("要在玩一局吗?(Y/N)");
		input = getchar();
		getchar();//清理'\n'
		system("cls");//清屏
		SetPos(45,12);
		if (input == 'n' || input == 'N')
		{
			printf("欢迎再次在玩");
		}
		else if (input == 'Y' || input == 'y')
		{
			printf("游戏即将开始,祝您玩的开心");
			SetPos(45, 14);
			system("pause");
			system("cls");//清屏
		}
	} while (input == 'y' || input == 'Y');
	SetPos(32, 0);
	return 0;
}

一.Win32相关API的介绍

1.首先我们先介绍一下:什么是API?
在这里插入图片描述
也就是说我们Window系统给我们提供了很多函数,让我们可以通过调用这些函数去完成一些我们目前想要完成的任务
而这些函数服务的对象是应用程序

因此这些函数被称为API

1.控制台程序

1.什么是控制台程序

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

就是这个Microsoft Visual Studio 调试控制台
在这里插入图片描述
在我们的Windows系统中,就有一个叫做命令提示符的工具
这个也是控制台程序
我们可以在Window系统中搜索cmd
在这里插入图片描述
然后打开,这个命令提示符就是控制台
在这里插入图片描述
我们要介绍的是:

2.命令提示符中设置控制台窗口的大小

我们可以设置控制台窗口的长宽:
比方说我现在想要让这个命令提示符的行数和列数设置为:
10行,50列

mode con cols=50 lines=10

在这里插入图片描述
我们输入这个命令,按下回车(就像是在Linux系统中输入命令行相同)
在这里插入图片描述
然后发现这个命令提示符变得特别小了
因此这就证明了我们是可以手动设置控制台大小的

3.控制台行和列的注意事项

然后我想让他变成一个正方形呢?
我们输入:让它行和列都变成30吧

mode con cols=30 lines=30

在这里插入图片描述
在这里插入图片描述
为什么不是一个正方形呢?
明明我输的是行30,列30啊
为什么会这样呢?
因为控制台中行的长度的基本单位和列的长度的基本单位不同

其实:
我们可以简单理解为:

控制台中每一行的长度==每一列的长度*2

那么我们想要构建一个正方形就可这样做了:

mode con cols=60 lines=30

在这里插入图片描述
这就是一个正方形了
在这里插入图片描述

4.VS2019中设置控制台窗口的大小

比方说我们想要一个15行,30列的一个正方形控制台窗口

mode con cols=30 lines=15

只需要包含Windows.h头文件
并且使用system函数

system("mode con cols=30 lines=15");

在这里插入图片描述

5.设置控制台名称

我们这个个界面还有一个贪吃蛇的名称
这个怎么设置呢?
跟刚才一样
只需要在VS代码里面加上

system("title 贪吃蛇");

在这里插入图片描述
不过我这里一开始的时候是无法修改这个控制台的名称
等到我写完贪吃蛇代码之后
控制台的名称就自然而然好了

所以大家如果在这一步无法修改名称的话,请先继续往后看

2.控制台屏幕坐标

COORD是Windows API中定义的一个结构体
它表示一个字符在控制台屏幕上的坐标
这个坐标系是这样的
在这里插入图片描述

typedef struct _COORD
{
  SHORT X;//X轴上的坐标
  SHORT Y;//Y轴上的坐标
}COORD,*PCOORD;

如果我们想要给这个控制台坐标赋值的话:
比方说我们给它的坐标赋值为:x轴:20,y轴:10
COORD pos = {20,10};
那么pos就是这个控制台上的对应位置的点

我们现在已经清楚了这个控制的坐标系的规则
但是还有一个问题:
在这里插入图片描述
这个控制台上的光标去哪了?
其实这个光标被我们隐藏了
那么我们该怎么样去隐藏这个光标呢?
别急,我们先来介绍一个函数:GetStdHandle

3.GetStdHandle

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

这是微软官方给的API的使用手册,大家可以看一下
Windows API索引
这个GetStdHandle函数的手册网址:GetStdHandle函数的手册网址
在这里插入图片描述

4.设置控制台光标状态

1.GetConsoleCursorInfo

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

2.CONSOLE_CURSOR_INFO

在这里插入图片描述

3.SetConsoleCursorInfo

在这里插入图片描述
也就是说我们想要隐藏光标,需要这样:

#include <stdio.h>
#include <Windows.h>
#include <stdbool.h>
int main()
{
	system("mode con cols=60 lines=20");
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//让handle具有能够操作控制台标准输出设备的能力/权限
	CONSOLE_CURSOR_INFO CursorInfo;//这个结构体就是定义光标信息的结构体
	GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false;//隐藏控制台光标的操作
	SetConsoleCursorInfo(handle, &CursorInfo);//设置控制台光标状态
	return 0;
}

不要忘了在C语言中使用bool类型的话需要包含stdbool.h头文件
在这里插入图片描述
这样我们就成功隐藏光标了
在这里插入图片描述
可是你这个控制台还能够在任意位置打印数据啊,
这肯定是通过设置光标位置做到的,那么如何才能设置光标位置呢?

4.SetConsoleCursorPosition

在这里插入图片描述
实例:

 COORD pos = {30, 10};
 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
 //设置标准输出上光标的位置为pos
 SetConsoleCursorPosition(handle, pos);

在这里插入图片描述
成功在指定位置打印了hello world

5.SetPos函数的实现

那么既然我们需要很多次调整光标位置以便能够在任意位置写入数据
那么我们不妨设计一个函数SetPos来实现调整光标位置的操作呢?
于是我们就可以写出这样的函数

void SetPos(short x, short y)
{
	COORD pos = { x,y };
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(handle, pos);
}

6.GetAsyncKeyState

下面的问题来了:
我们想要玩这个游戏,就一定要能够接收我们的输入
我们既然是在电脑上玩,那就需要使用键盘去玩
那么就一定需要编译器能够在游戏运行的时候获取按键情况

因此微软WIN32API中就给了这么一个函数GetAsyncKeyState
作用是:获取按键情况

SHORT GetAsyncKeyState(
 int vKey
);

在这里插入图片描述
因此我们就可以让这个返回值跟1进行按位与

如果得出来的值是1:那么就代表这个值的最低位是1,也就是说这个键被按过

如果得出来的值是0:那么就代表这个值的最低位是0,也就是说这个键没有被按过
因此我们可以写出如下的宏

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

7.打印宽字符的实现

我们现在能够在屏幕上的任意位置打印数据,还能够隐藏光标,还能检测哪些键是否被按过
那不就可以了吗?
我们还漏了一点:
在这里插入图片描述
这个黑色原点:也就是蛇身
这个黑色五角星,也就是食物
这个白色方块:也就是墙体
这个是怎么打印出来的呢?
键盘上也没有啊

我们可以通过
搜狗输入法->输入方式->符号大全->里面就有
在这里插入图片描述
然后那不就行了吗?
是不行的
因为这三个字符属于宽字符(一个宽字符占2个字节,一个普通字符占1个字节),在VS的默认情况下我们是无法单独打印这些字符的
在这里插入图片描述
那么如何才能打印呢?
在这里插入图片描述
在这里插入图片描述
我们的准备工作终于做完了
下面就可以开始我们贪吃蛇游戏的具体实现了

二.贪吃蛇的游戏流程分析

这是我们贪吃蛇的整个游戏流程的分析
在这里插入图片描述

1.游戏窗口的实现

1.界面的初始化

根据我们刚才API部分的学习,我们已经写出了Init函数
可以用来隐藏屏幕光标
在这里插入图片描述
然后我们可以把打印宽字符,设置窗口大小,窗口名称的代码在main函数中去写
我们在这里将控制台的大小设置为宽:35行,列:120列
所以我们就可以在main函数当中这样去写

int main()
{
	setlocale(LC_ALL, "");
	system("mode con cols=120 lines=35");
	system("title 贪吃蛇");
	Init();
	return 0;
}

这样就完成了
在这里插入图片描述

2.欢迎界面的打印

我们在前面已经实现了SetPos函数
在这里插入图片描述
然后我们就可以通过Setpos去定位光标,然后打印欢迎信息
在这里插入图片描述

3.窗口布局(地图坐标)

这里要实现的是CreateMap函数
我们在这里实现的是一个27行,58列的棋盘
所以我们就需要去通过SetPos定位光标,然后打印这个墙
在这里插入图片描述
其次,为了便于打印,我们宏定义了墙,蛇身,食物的字符

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

在这里插入图片描述
注意:这里最后打印下面的墙是因为测试时当我们打印完成之后
程序运行结束就会打印:

C:\Users\23119\Desktop\C++_learn_code\cpp_learn_code\Snake_review\Debug\Snake_review.exe (进程 6000)已退出,代码为 0。
按任意键关闭此窗口. . .

如果我们最后打印的不是下面的墙
那样就会出现这种情况:
因为打印完下面的墙之后又打印了左边和右边的墙
导致程序结束时下面的墙被这句话覆盖了
在这里插入图片描述
其实我们打印的过程是这样的
在这里插入图片描述
我们调试看一下过程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.蛇身结构体的创建与初始化

1.蛇身节点的结构体

在这里插入图片描述

2.食物节点的结构体

在这里插入图片描述

3.蛇身结构体的创建

在这里插入图片描述
因此我们就可以定义出下面的结构体
在这里插入图片描述

4.蛇身的初始化

定义好蛇身节点,食物节点和蛇的结构体之后
下面我们要初始化这条蛇
怎么初始化呢?
在这里插入图片描述
在这里插入图片描述
因此我们就可以写出这样的代码

这两个宏定义是Snake.h文件中的
#define INIT_X 24
#define INIT_Y 6
void InitSnake(Snake* ps)
{
	//初始化蛇身
	for (int i = 0; i < 5; i++)
	{
		SNode* newnode = (SNode*)malloc(sizeof(SNode));
		if (newnode == NULL)
		{
			perror("InitSnake():: malloc fail");
			exit(-1);
		}
		newnode->next = NULL;
		newnode->x = INIT_X + 2 * i;
		newnode->y = INIT_Y;
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = newnode;
		}
		else
		{
			newnode->next = ps->_pSnake;
			ps->_pSnake = newnode;
		}
	}
	//打印蛇身
	SNode* cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//初始化其他属性
	ps->_dir = RIGHT;
	ps->_state = OK;
	ps->_foodWeight = 10;
	ps->_score = 0;
	ps->_sleepTime = 200;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
后面两块是打印蛇身和初始化其他属性的注意事项
在这里插入图片描述

5.食物的初始化

在这里插入图片描述
注意:rand()%53生成的随机数的范围是:0~52

void CreateFood(Snake* ps)
{
	//创建食物
	while (1)
	{
		//保证初始化到墙内
		//x:2~54
		int x = rand() % 53 + 2;//0~52+2  ->  2~54
		//y:1~25
		int y = rand() % 25 + 1;//0~24+1  ->  1~25
		//保证初始化的x必须为偶数
		if (x % 2 != 0)
		{
			continue;//这里是continue while(1){...}这个循环,这次循环不再执行下面的语句,直接跳转到下一次while(1){...}
		}
		//保证初始化时不跟蛇身重合
		SNode* cur = ps->_pSnake;
		bool flag = false;
		while (cur)
		{
			//跟蛇身重合,重新通过rand函数设置x和y
			if (cur->x == x && cur->y == y)
			{
				flag = true;
				break;//这里是break出while(cur){...}这个循环
			}
			cur = cur->next;
		}
		//没有跟蛇身重合,就可以创建食物节点了
		if (!flag)
		{
			SNode* newnode = (SNode*)malloc(sizeof(SNode));
			if (newnode == NULL)
			{
				perror("CreateFood():: malloc fail");
				exit(-1);
			}
			newnode->next = NULL;
			newnode->x = x;
			newnode->y = y;
			ps->_pFood = newnode;
			break;
		}
	}
	//打印食物
	SetPos(ps->_pFood->x, ps->_pFood->y);
	wprintf(L"%lc", FOOD);
}

3.GameStart部分的完整代码

1.重点说明一下main函数

在这里插入图片描述

2.完整代码

1.Snake.h

#pragma once
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#include <stdio.h>
#include <Windows.h>
#include <stdbool.h>
#include <locale.h>
#include <stdlib.h>
#include <time.h>
#define INIT_X 24
#define INIT_Y 6
typedef struct SnakeNode
{
	struct SnakeNode* next;
	int x;
	int y;
}SNode;

enum Direction
{
	UP,
	DOWN,
	LEFT,
	RIGHT
};

enum GameState
{
	OK,
	EXIT_NORMAL,
	KILL_BY_WALL,
	KILL_BY_SELF
};

typedef struct Snake
{
	SNode* _pSnake;//蛇头节点
	SNode* _pFood;//食物
	enum Direction _dir;//蛇移动的方向
	enum GameState _state;//当前游戏状态
	int _score;//当前得分
	int _foodWeight;//每个食物的分数
	int _sleepTime;//蛇的休息时间,影响加速和减速和暂停
}Snake;

void Init();
void SetPos(short x, short y);

void GameStart(Snake* ps);

void WelcomeToGame();
void CreateMap();

void InitSnake(Snake* ps);
void CreateFood(Snake* ps);

2.Snake.c

#include "Snake.h"
void Init()
{
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//让handle具有能够操作控制台标准输出设备的能力/权限
	CONSOLE_CURSOR_INFO CursorInfo;//这个结构体就是定义光标信息的结构体
	GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false;//隐藏控制台光标的操作
	SetConsoleCursorInfo(handle, &CursorInfo);//设置控制台光标状态
}

void SetPos(short x, short y)
{
	COORD pos = { x,y };
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(handle, pos);
}

void WelcomeToGame()
{
	SetPos(45, 12);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(45, 18);
	system("pause");
	system("cls");
	SetPos(45, 12);
	printf("用↑.↓.←.→ 分别控制蛇的移动,F1为加速,F2为减速");
	SetPos(45, 13);
	printf("加速能够得到更高的分数");
	SetPos(45, 18);
	system("pause");
	system("cls");
}

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

void InitSnake(Snake* ps)
{
	//初始化蛇身
	for (int i = 0; i < 5; i++)
	{
		SNode* newnode = (SNode*)malloc(sizeof(SNode));
		if (newnode == NULL)
		{
			perror("InitSnake():: malloc fail");
			exit(-1);
		}
		newnode->next = NULL;
		newnode->x = INIT_X + 2 * i;
		newnode->y = INIT_Y;
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = newnode;
		}
		else
		{
			newnode->next = ps->_pSnake;
			ps->_pSnake = newnode;
		}
	}
	//打印蛇身
	SNode* cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//初始化其他属性
	ps->_dir = RIGHT;
	ps->_state = OK;
	ps->_foodWeight = 10;
	ps->_score = 0;
	ps->_sleepTime = 200;
}

void CreateFood(Snake* ps)
{
	//创建食物
	while (1)
	{
		//保证初始化到墙内
		//x:2~54
		int x = rand() % 53 + 2;//0~52+2  ->  2~54
		//y:1~25
		int y = rand() % 25 + 1;//0~24+1  ->  1~25

		//保证初始化的x必须为偶数
		if (x % 2 != 0)
		{
			continue;
		}

		//保证初始化时不跟蛇身重合
		SNode* cur = ps->_pSnake;
		bool flag = false;
		while (cur)
		{
			//跟蛇身重合
			if (cur->x == x && cur->y == y)
			{
				flag = true;
				break;
			}
			cur = cur->next;
		}
		//没有跟蛇身重合
		if (!flag)
		{
			SNode* newnode = (SNode*)malloc(sizeof(SNode));
			if (newnode == NULL)
			{
				perror("CreateFood():: malloc fail");
				exit(-1);
			}
			newnode->next = NULL;
			newnode->x = x;
			newnode->y = y;
			ps->_pFood = newnode;
			break;
		}
	}
	//打印食物
	SetPos(ps->_pFood->x, ps->_pFood->y);
	wprintf(L"%lc", FOOD);
}

void GameStart(Snake* ps)
{
	WelcomeToGame();
	CreateMap();
	InitSnake(ps);
	CreateFood(ps);
}

3.test.c

#include "Snake.h"
//初始化光标信息等
int main()
{
    setlocale(LC_ALL, "");
    system("mode con cols=120 lines=35");
    system("title 贪吃蛇");
    Init();
    Snake snake = { 0 };//将snake结构体变量的内容全都初始化为0
    //(这里主要是为了初始化p_Snake头节点的指针,为了防止头插法创建蛇身链表时出现野指针的非法访问问题)
    //p_Snake=0;而NULL指针的本质就是(void(*)0),在数值上0跟NULL是相等的,这里可以认为p_Snake==NULL
    srand((unsigned int)time(NULL));//设置随机数种子,防止每一次运行rand生成的随机数都是一样的
    GameStart(&snake);
    SetPos(0, 30);//这里我们要定位一下光标,
    //防止最后打印的那条包含:"返回值为0"的语句因为光标最后处于打印食物位置的下一行
    //而导致覆盖我们的墙体
    return 0;
}

3.最终实现情况:

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

4.游戏运行

我们的蛇身结构体和食物都已经初始化好了,游戏的开始工作结束
下面开始实现游戏运行的代码了

1.GameRun函数的整体框架

在这个GameRun函数中我们要实现的整体框架是:
在这里插入图片描述
因此我们可以写出这样的代码框架在这里插入图片描述

2.帮助信息的打印

经过了前面打印欢迎界面之后,这个帮助信息的打印对我们来说就轻而易举了
在这里插入图片描述

3.获取按键情况

我们之前在API中提到过这个获取按键情况的宏
在这里插入图片描述

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

如果我们按了这个键,这个宏对应于这个键的值就是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();
}
//Esc退出
else if (KEY_PRESS(VK_ESCAPE))
{
	ps->_state = EXIT_NORMAL;
	break;
}
//加速
else if (KEY_PRESS(VK_F1))
{
	//防止一直加速导致sleepTime<0出现bug
	if (ps->_sleepTime >= 50)
	{
		ps->_sleepTime -= 30;
		ps->_foodWeight += 2;//加速时食物的分值会增加
	}
}
//减速
else if (KEY_PRESS(VK_F2))
{
	//防止一直减速导致程序运行太慢出现卡顿影响用户体验
	if (ps->_sleepTime < 350)
	{
		ps->_sleepTime += 30;
		ps->_foodWeight -= 2;
		//防止太慢时食物得分减为负数
		if (ps->_sleepTime == 350)
		{
			ps->_foodWeight = 1;
		}
	}
}

Sleep是C语言的库函数,可以让程序休息对应的时间
单位是ms
这里的pause是暂停函数:
在这里插入图片描述
所以我们就可以完善一下我们的GameRun函数了

void GameRun(Snake* ps)
{
	PrintHelpInfo();
	do
	{
		SetPos(65, 10);
		printf("得分: %d , 每个食物得分: %d ", ps->_score, ps->_foodWeight);
		//调整方向
		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();
		}
		//Esc退出
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_state = EXIT_NORMAL;
			break;
		}
		//加速
		else if (KEY_PRESS(VK_F1))
		{
			//防止一直加速导致sleepTime<0出现bug
			if (ps->_sleepTime >= 50)
			{
				ps->_sleepTime -= 30;
				ps->_foodWeight += 2;//加速时食物的分值会增加
			}
		}
		//减速
		else if (KEY_PRESS(VK_F2))
		{
			//防止一直减速导致程序运行太慢出现卡顿影响用户体验
			if (ps->_sleepTime < 350)
			{
				ps->_sleepTime += 30;
				ps->_foodWeight -= 2;
				//防止太慢时食物得分减为负数
				if (ps->_sleepTime == 350)
				{
					ps->_foodWeight = 1;
				}
			}
		}
		//蛇每次移动都要有一定的休眠时间,时间越短,蛇移动的速度就越快
		Sleep(ps->_sleepTime);
		SnakeMove(ps);
	} while (ps->_state == OK);
}
//暂停函数
void pause()
{
        while (1)
        {
            Sleep(200);
            if (KEY_PRESS(VK_SPACE))
            {
                break;
            }
        }
}

4.蛇身的移动

1.整体框架

在这里插入图片描述

void SnakeMove(Snake* ps)
{
	//1.根据蛇头的坐标和方向,计算下一个节点的坐标
	int x = ps->_pSnake->x;
	int y = ps->_pSnake->y;
	switch (ps->_dir)
	{
	case UP:
		y--;
		break;
	case DOWN:
		y++;
		break;
	case LEFT:
		x -= 2;
		break;
	case RIGHT:
		x += 2;
		break;
	}

	//2.创建下一个节点
	SNode* pNextNode = (SNode*)malloc(sizeof(SNode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove():: malloc fail");
		exit(-1);
	}
	pNextNode->x = x;
	pNextNode->y = y;
	pNextNode->next = NULL;

	//3.判断下一个是不是食物
	if (x == ps->_pFood->x && y == ps->_pFood->y)
	{
		//下一个位置有食物
		EatFood(pNextNode, ps);
	}
	//下一个位置没有食物
	else
	{
		NoFood(pNextNode, ps);
	}
	//判断是否撞墙
	IfKillByWall(ps, x, y);
	//判断是否咬到自己
	IfKillBySelf(ps, x, y);
}

下面我们就要实现一下下面的这4个函数,那么SnakeMove函数就成功完成了

2.EatFood和NoFood函数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
所以我们就可以写出这样的代码
在这里插入图片描述

3.IfKillByWall和IfKillBySelf函数

这两个函数的返回值类型可以是void
因为我们可以直接在这两个函数当中修改游戏当前状态
也就是ps->_state
因此我们可以这样写:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.GameRun部分的完整代码

这里只写了这一部分的完整代码
需要再加上GameStart部分的完整代码才可以正常运行

1.完整代码

Snake.h

#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)
void pause();
void GameRun(Snake* ps);
void PrintHelpInfo();

void EatFood(SNode* pNextNode, Snake* ps);
void NoFood(SNode* pNextNode, Snake* ps);
void IfKillByWall(Snake* ps, int x, int y);
void IfKillBySelf(Snake* ps, int x, int y);
void SnakeMove(Snake* ps);

Snake.c

//暂停函数
void pause()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

void GameRun(Snake* ps)
{
	PrintHelpInfo();
	do
	{
		SetPos(65, 10);
		printf("得分: %d , 每个食物得分: %d ", ps->_score, ps->_foodWeight);
		//调整方向
		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();
		}
		//Esc退出
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_state = EXIT_NORMAL;
			break;
		}
		//加速
		else if (KEY_PRESS(VK_F1))
		{
			//防止一直加速导致sleepTime<0出现bug
			if (ps->_sleepTime >= 50)
			{
				ps->_sleepTime -= 30;
				ps->_foodWeight += 2;//加速时食物的分值会增加
			}
		}
		//减速
		else if (KEY_PRESS(VK_F2))
		{
			//防止一直减速导致程序运行太慢出现卡顿影响用户体验
			if (ps->_sleepTime < 350)
			{
				ps->_sleepTime += 30;
				ps->_foodWeight -= 2;
				//防止太慢时食物得分减为负数
				if (ps->_sleepTime == 350)
				{
					ps->_foodWeight = 1;
				}
			}
		}
		//蛇每次移动都要有一定的休眠时间,时间越短,蛇移动的速度就越快
		Sleep(ps->_sleepTime);
		SnakeMove(ps);
	} while (ps->_state == OK);
}

void PrintHelpInfo()
{
	SetPos(65, 17);
	printf("不能穿墙,不能咬到自己");
	SetPos(65, 18);
	printf("用↑.↓.←.→ 分别控制蛇的移动");
	SetPos(65, 19);
	printf("F1为加速,F2为减速");
	SetPos(65, 20);
	printf("Esc: 退出游戏  space:暂停游戏");

	SetPos(65, 22);
	printf("编写者:wzs");
}

void EatFood(SNode* pNextNode, Snake* ps)
{
	pNextNode->next = ps->_pSnake;
	ps->_pSnake = pNextNode;
	//打印蛇身
	SNode* cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//释放食物节点
	free(ps->_pFood);
	ps->_pFood = NULL;
	//加分
	ps->_score += ps->_foodWeight;
	//创建新食物
	CreateFood(ps);
}

void NoFood(SNode* pNextNode, Snake* ps)
{
	pNextNode->next = ps->_pSnake;
	ps->_pSnake = pNextNode;
	//释放最后一个节点
	SNode* 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;
}

//判断是否撞墙
void IfKillByWall(Snake* ps, int x, int y)
{
	if (x == 0 || x == 56 || y == 0 || y == 26)
	{
		ps->_state = KILL_BY_WALL;
	}
}
//判断是否咬到自己
void IfKillBySelf(Snake* ps, int x, int y)
{
	SNode* cur = ps->_pSnake->next;
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_state = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

void SnakeMove(Snake* ps)
{
	//1.根据蛇头的坐标和方向,计算下一个节点的坐标
	int x = ps->_pSnake->x;
	int y = ps->_pSnake->y;
	switch (ps->_dir)
	{
	case UP:
		y--;
		break;
	case DOWN:
		y++;
		break;
	case LEFT:
		x -= 2;
		break;
	case RIGHT:
		x += 2;
		break;
	}

	//2.创建下一个节点
	SNode* pNextNode = (SNode*)malloc(sizeof(SNode));
	if (pNextNode == NULL)
	{
		perror("SnakeMove():: malloc fail");
		exit(-1);
	}
	pNextNode->x = x;
	pNextNode->y = y;
	pNextNode->next = NULL;

	//3.判断下一个是不是食物
	if (x == ps->_pFood->x && y == ps->_pFood->y)
	{
		//下一个位置有食物
		EatFood(pNextNode, ps);
	}
	//下一个位置没有食物
	else
	{
		NoFood(pNextNode, ps);
	}
	//判断是否撞墙
	IfKillByWall(ps, x, y);
	//判断是否咬到自己
	IfKillBySelf(ps, x, y);
}

test.c

int main()
{
    setlocale(LC_ALL, "");
    system("mode con cols=120 lines=35");
    system("title 贪吃蛇");
    Init();
    Snake snake = { 0 };//将snake结构体变量的内容全都初始化为0
    //(这里主要是为了初始化p_Snake头节点的指针,为了防止头插法创建蛇身链表时出现野指针的非法访问问题)
    //p_Snake=0;而NULL指针的本质就是(void(*)0),在数值上0跟NULL是相等的,这里可以认为p_Snake==NULL
    srand((unsigned int)time(NULL));//设置随机数种子,防止每一次运行rand生成的随机数都是一样的
    GameStart(&snake);
    GameRun(&snake);
    SetPos(0, 30);//这里我们要定位一下光标,
    //防止最后打印的那条包含:"返回值为0"的语句因为光标最后处于打印食物位置的下一行
    //而导致覆盖我们的墙体
    return 0;
}

2.最终实现情况

在这里插入图片描述
咬到自己:
在这里插入图片描述
撞墙:
在这里插入图片描述

6.游戏结束后的处理

在这里插入图片描述

1.代码实现

在这里插入图片描述

2.Y/N 是否再来一局

int main()
{
	setlocale(LC_ALL, "");
	system("mode con cols=120 lines=35");
	system("title 贪吃蛇");
	Init();
	char input = 0;
	do
	{
		Snake snake = { 0 };//p_Snake=0;而NULL指针的本质就是(void(*)0),在数值上0跟NULL是相等的,这里可以认为p_Snake==NULL
		srand((unsigned int)time(NULL));
		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);
		SetPos(65, 26);
		printf("要在玩一局吗?(Y/N)");
		input = getchar();
		getchar();//清理'\n'
		system("cls");//清屏
		SetPos(45,12);
		if (input == 'n' || input == 'N')
		{
			printf("欢迎再次在玩");
		}
		else if (input == 'Y' || input == 'y')
		{
			printf("游戏即将开始,祝您玩的开心");
			SetPos(45, 14);
			system("pause");
			system("cls");//清屏
		}
	} while (input == 'y' || input == 'Y');
	SetPos(32, 0);
	return 0;
}

三.总结

上面就是我们贪吃蛇代码的整体分析和梳理
其实我们的整体思路就是这个图片所展现的
我们只需要先把大概的框架全部完成
具体的函数先声明出来
然后我们剩下的任务就只有去把那些函数一一实现即可
在这里插入图片描述

以上就是贪吃蛇代码实现与剖析(C语言)的全部内容,希望能对大家有所帮助!

评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

program-learner

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值