C语言编程项目--俄罗斯方块

1. 俄罗斯方块原理

1.1 延时函数和清屏函数

Linux系统函数
include <stdlib.h>
usleep(50000); //50毫秒 1/20 秒
system(“clear”);//system 函数调用系统命令

实现刷帧的效果:

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

int main()
{
	int i;
	for(i = 0; i < 10;i++)
	{
		printf("%d\n",i);
		usleep(1000*1000);
		system("clear");
	}
	return 0;
}

1.2 清空缓存

输入缓存:stdin
输出缓存:stdout
fflush(stdout); //清缓存

1.3 非阻塞输入

scanf getchar gets 阻塞性输入,不输入程序就一直等待
不输入程序也不等待,叫非阻塞输入。

getChar :非阻塞输入

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

#define TTY_PATH "/dev/tty"
#define STTY_US "stty raw -echo -F"
#define STTY_DEF "stty -raw echo -F"

//非阻塞输入:
int getChar()
{
	fd_set rfds; //用于表示文件描述符集合
	struct timeval tv; //用于设置超时时间
	int ch = 0;
	//函数将文件描述符集合rfds清空,并将标准输入文件描述符0添加到集合中
	FD_ZERO(&rfds);
	FD_SET(0,&rfds);
	//设置超时时间tv的秒数为0,微秒数为10
	/*
	这意味着在调用select函数时,
	如果在10微秒内有可读取的输入,select函数将返回大于0的值
	*/
	tv.tv_sec = 0;
	tv.tv_usec = 10;
	/*
	函数调用select函数,传递参数1(最大文件描述符值加1)
	文件描述符集合rfds作为读取集合,NULL作为写入集合和错误集合,以及超时时间tv。
	如果select函数返回的值大于0,表示在超时时间内有可读取的输入。
	在这种情况下,函数调用getchar函数来获取一个字符,并将其赋值给变量ch
	*/
	if(select(1,&rfds,NULL,NULL,&tv) > 0)
	{
		ch = getchar();
	}
	return ch;
}

int main()
{
	//屏蔽掉终端的正常输入
	system(STTY_US TTY_PATH);// "stty raw -echo -F""/dev/tty"
	while(1)
	{
		printf("hehe\n"); //屏幕会一直输出hehe
		char c = getChar();//非阻塞输入
		printf("%c\n", c); //可以输入一个字符,打印到终端,终端继续输出hehe
		//由于屏蔽了终端的正常输入,ctrl + c已经不能结果程序了
		//我们需要自己判断输入ctrl + c之后的功能,不是必须使用ctrl + c,任意ASCII表中的字符都可以
		if(c == 3) // 输入ctrl + c 获得到的ascii值是3
		{
			//让终端恢复正常
			system(STTY_DEF TTY_PATH);//"stty -raw echo -F""/dev/tty"
			break;
		}
		usleep(1000*1000);
	}
	return 0;
}

1.4 在屏幕上移动一个方块

左上角是 0 0 点
X轴向右递增,Y轴向下递增
void drawPoint(int x,int y) //根据x,y值输出一个方块
{
printf(“\033[%d;%dH”,y+1,x*2+1);
printf(“\033[1;36m■ \033[0m”);
}

移动方块的本质是移动一对坐标 x,y(通过asdw)

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

#define TTY_PATH "/dev/tty"
#define STTY_US "stty raw -echo -F"
#define STTY_DEF "stty -raw echo -F"

int getChar();//非阻塞输入

void drawPoint(int x,int y);//在x y位置画一个方块

int x = 5,y = 5; //图形坐标  全局变量

int main()
{
	//屏蔽掉终端的正常输入
	system(STTY_US TTY_PATH);// "stty raw -echo -F""/dev/tty"
	while(1)
	{
		int ctrl = getChar();//输入
		if(ctrl == 3)
		{
			//让终端恢复正常
			system(STTY_DEF TTY_PATH);//"stty -raw echo -F""/dev/tty"
			break;
		}
		switch(ctrl)
		{
			case 'a':
				x--;
				break;
			case 's':
				y++;
				break;
			case 'd':
				x++;
				break;
			case 'w':
				y--;
				break;
		}
		drawPoint(x,y);
		fflush(stdout);//清缓存,可以马上看到结果
		//刷帧
		usleep(1000*1000);
		system("clear");
	}

	return 0;
}

int getChar()
{
	fd_set rfds;
	struct timeval tv;
	int ch = 0;
	FD_ZERO(&rfds);
	FD_SET(0,&rfds);
	tv.tv_sec = 0;
	tv.tv_usec = 10;
	if(select(1,&rfds,NULL,NULL,&tv) > 0)
	{
		ch = getchar();
	}
	return ch;
}

void drawPoint(int x,int y)
{
	printf("\033[%d;%dH",y+1,x*2+1);
	printf("\033[1;36m■ \033[0m");
}
	

1.5 在屏幕输出 L 方块

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

#define TTY_PATH "/dev/tty"
#define STTY_US "stty raw -echo -F"
#define STTY_DEF "stty -raw echo -F"

int getChar();//非阻塞输入

void drawPoint(int x,int y);//在x y位置画一个方块

int x = 5,y = 5; //图形坐标  全局变量
struct Point
{
	int x;
	int y;
};

// 数组中的数据是图形中各个点的偏移量
struct Point shapi[4] = {{0.0},{0,-1},{0,-2},{1,0}};
int main()
{
	//屏蔽掉终端的正常输入
	system(STTY_US TTY_PATH);// "stty raw -echo -F""/dev/tty"
	while(1)
	{
		int ctrl = getChar();//输入
		if(ctrl == 3)
		{
			//让终端恢复正常
			system(STTY_DEF TTY_PATH);//"stty -raw echo -F""/dev/tty"
			break;
		}
		switch(ctrl)
		{
			case 'a':
				x--;
				break;
			case 's':
				y++;
				break;
			case 'd':
				x++;
				break;
			case 'w':
				y--;
				break;
		}
		int j;
		for(j = 0; j < 4;j++)
		{
				drawPoint(x + shapi[j].x, y + shapi[j].y);
		}
		fflush(stdout);//清缓存,可以马上看到结果
		//刷帧
		usleep(1000*1000);
		system("clear");
	}

	return 0;
}

int getChar()
{
	fd_set rfds;
	struct timeval tv;
	int ch = 0;
	FD_ZERO(&rfds);
	FD_SET(0,&rfds);
	tv.tv_sec = 0;
	tv.tv_usec = 10;
	if(select(1,&rfds,NULL,NULL,&tv) > 0)
	{
		ch = getchar();
	}
	return ch;
}

void drawPoint(int x,int y)
{
	printf("\033[%d;%dH",y+1,x*2+1);
	printf("\033[1;36m■ \033[0m");
}

在这里插入图片描述

1.6 自动变形

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

#define TTY_PATH "/dev/tty"
#define STTY_US "stty raw -echo -F"
#define STTY_DEF "stty -raw echo -F"

int getChar();//非阻塞输入

void drawPoint(int x,int y);//在x y位置画一个方块

int x = 5,y = 5; //图形坐标  全局变量
struct Point
{
	int x;
	int y;
};
// 数组中的数据是图形中各个点的偏移量,二维数组的第一个角标是用来选择图形的
struct Point shapi[4][4] = {
	{{0.0},{0,-1},{0,-2},{1,0}},
	{{0.0},{0,1},{1,0},{2,0}},
	{{0.0},{-1,0},{0,1},{0,2}},
	{{0.0},{0,-1},{-1,0},{-2,0}}
};



int main()
{
	//屏蔽掉终端的正常输入
	system(STTY_US TTY_PATH);// "stty raw -echo -F""/dev/tty"
	while(1)
	{
		int ctrl = getChar();//输入
		if(ctrl == 3)
		{
			//让终端恢复正常
			system(STTY_DEF TTY_PATH);//"stty -raw echo -F""/dev/tty"
			break;
		}
		switch(ctrl)
		{
			case 'a':
				x--;
				break;
			case 's':
				y++;
				break;
			case 'd':
				x++;
				break;
			case 'w':
				y--;
				break;
		}
		int i,j;
		system("clear");
		for(i = 0; i < 4;i++) //改变i值实现变形
		{
			for(j = 0; j < 4;j++)
			{
				drawPoint(x + shapi[i][j].x, y + shapi[i][j].y);

			}
			fflush(stdout);//清缓存,可以马上看到结果
			//刷帧
			usleep(1000*1000);
			system("clear");
		}
		
	}

	return 0;
}

int getChar()
{
	fd_set rfds;
	struct timeval tv;
	int ch = 0;
	FD_ZERO(&rfds);
	FD_SET(0,&rfds);
	tv.tv_sec = 0;
	tv.tv_usec = 10;
	if(select(1,&rfds,NULL,NULL,&tv) > 0)
	{
		ch = getchar();
	}
	return ch;
}

void drawPoint(int x,int y)
{
	printf("\033[%d;%dH",y+1,x*2+1);
	printf("\033[1;36m■ \033[0m");
}

1.7 消行

1.7.1 一维数组

移动一维数组,判断是否为0,是0向前移动,将所有0都放到前面。
在这里插入图片描述

#include <stdio.h>

int main()
{
	int a[10] = {5,2,0,3,0,0,1,0,6,0};
	int i;
	int count = 0;
	for(i = 9;i >= 0; i--)
	{
		if(a[i] == 0)
		{
			count++;
		}
		else if(count > 0)
		{
			a[i+count] = a[i];
			a[i] = 0;
		}
	}
	
	for(i = 0; i < 10;i++)
	{
		printf("%d ",a[i]);
	}
	printf("\n");
	return 0;
}
1.7.2 二维数组

移动二维数组,将全为1的,去掉,其余的往下放
在这里插入图片描述

#include <stdio.h>

int isFull(int* arr, int len); //判断数组里面的数值是否全部为1
void moveline(int* dst,int* src, int len);//将src数组赋值到dst数组里面
void clearline(int* src,int len); //将数组值全部变为0

int main()
{
	int a[10][3] = {
		{1,0,1},
		{1,0,0},
		{1,1,1},
		{1,0,1},
		{1,1,1},
		{1,1,1},
		{0,1,0},
		{1,1,1},
		{0,1,1},
		{1,1,1}
	};
	int i;
	int count = 0;
	for(i = 9;i >= 0; i--)
	{
		//a[i]是int类型的一维数组,判断数组值是否全部为1
		if(isFull(a[i],3))
		{
			count++;
			clearline(a[i],3); //将数组值全部为1的数组,置0
		}
		else if(count > 0)
		{
			moveline(a[i+count],a[i],3); //将数组值不全部为1的,往后移动
		}
	}
	int j;
	//遍历打印二维数组
	for(i = 0; i < 10;i++)
	{
		for(j = 0; j < 3;j++)
		{
			printf("%d ",a[i][j]);
		}
		printf("\n");
	}
	printf("\n");
	return 0;
}

int isFull(int* arr, int len)
{
	int i;
	for(i = 0;i < len;i++)
	{
		if(arr[i] != 1)
		{
			return 0;
		}
	}
	return 1;
}

void moveline(int* dst,int* src, int len)
{
	int i;
	for(i = 0; i < len; i++)
	{
		dst[i] = src[i];
		src[i] = 0;
	}
}

void clearline(int* src,int len)
{
	int i;
	for(i = 0; i < len; i++)
	{
		src[i] = 0;
	}
}

根据二维数组的值画图

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

int getChar();//非阻塞输入

int isFull(int* arr, int len); //判断数组里面的数值是否全部为1
void moveline(int* dst,int* src, int len);//将src数组赋值到dst数组里面
void clearline(int* src,int len); //将数组值全部变为0
void show();
void drawPoint(int x,int y);//在x y位置画一个方块

int a[10][3] = {
		{1,0,1},
		{1,0,0},
		{1,1,1},
		{1,0,1},
		{1,1,1},
		{1,1,1},
		{0,1,0},
		{1,1,1},
		{0,1,1},
		{1,1,1}
	};

int main()
{
	system("clear");
	show();
	int i;
	int count = 0;
	for(i = 9;i >= 0; i--)
	{
		//a[i]是int类型的一维数组,判断数组值是否全部为1
		if(isFull(a[i],3))
		{
			count++;
			clearline(a[i],3); //将数组值全部为1的数组,置0
		}
		else if(count > 0)
		{
			moveline(a[i+count],a[i],3); //将数组值不全部为1的,往后移动
		}
	}
	getChar();//按任意键显示结果
	system("clear");
	show();
	printf("\n");
	return 0;
}

int isFull(int* arr, int len)
{
	int i;
	for(i = 0;i < len;i++)
	{
		if(arr[i] != 1)
		{
			return 0;
		}
	}
	return 1;
}

void moveline(int* dst,int* src, int len)
{
	int i;
	for(i = 0; i < len; i++)
	{
		dst[i] = src[i];
		src[i] = 0;
	}
}

void clearline(int* src,int len)
{
	int i;
	for(i = 0; i < len; i++)
	{
		src[i] = 0;
	}
}

void show()
{
	int i,j;
	for(i = 0; i < 10;i++)
	{
		for(j = 0; j < 3;j++)
		{
			if(a[i][j] == 1)
			{
				drawPoint(j,i);
			}	
		}
	}
}

void drawPoint(int x,int y)
{
	printf("\033[%d;%dH",y+1,x*2+1);
	printf("\033[1;36m■ \033[0m");
}

int getChar()
{
	fd_set rfds;
	struct timeval tv;
	int ch = 0;
	FD_ZERO(&rfds);
	FD_SET(0,&rfds);
	tv.tv_sec = 0;
	tv.tv_usec = 10;
	if(select(1,&rfds,NULL,NULL,&tv) > 0)
	{
		ch = getchar();
	}
	return ch;
}

在这里插入图片描述

2. 俄罗斯方块的实现

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

#define TTY_PATH "/dev/tty"
#define STTY_US "stty raw -echo -F"
#define STTY_DEF "stty -raw echo -F"

#define WIDTH 20
#define HEIGHT 20
#define SPEED 8

int getChar();//非阻塞输入

void drawPoint(int x,int y);//在x y位置画一个方块

int x, y;//图形的坐标
int currentShap;//表示当前图形的角标,它是shaps数组的第一个角标
//表示屏幕的二维数组,元素值1表示有图形,0表示没有图形,2表示边框
int screen[HEIGHT][WIDTH] = {0};


void initScreen(); //初始化屏幕二维数组
void draw(); //画图形
void createShap();//创建图形
int userCtrl(); //用户控制
void changeShap();//变形
int collideChange();
//碰撞
int collideLeft();
int collideRight();
int collideDown();

void moveDown();//向下移动
void clearShap();//消行

struct Point
{
	int x;
	int y;
};

struct Point shaps[19][4] = {
{{0,0},{-1,0},{1,0},{2,0}},//横条
{{0,0},{0,-1},{0,1},{0,2}},//竖条
{{0,0},{-1,-1},{-1,0},{0,-1}},//方块
{{0,0},{0,-1},{0,-2},{1,0}},//正L1
{{0,0},{0,1},{1,0},{2,0}},//正L2
{{0,0},{-1,0},{0,1},{0,2}},//正L3
{{0,0},{0,-1},{-1,0},{-2,0}},//正L4
{{0,0},{-1,0},{0,-1},{0,-2}},//反L1
{{0,0},{0,-1},{1,0},{2,0}},//反L2
{{0,0},{1,0},{0,1},{0,2}},//反L3
{{0,0},{-1,0},{-2,0},{0,1}},//反L4
{{0,0},{-1,0},{1,0},{0,-1}},//T1
{{0,0},{0,1},{0,-1},{1,0}},//T2
{{0,0},{-1,0},{1,0},{0,1}},//T3
{{0,0},{-1,0},{0,-1},{0,1}},//T4
{{0,0},{1,0},{0,-1},{-1,-1}},//正Z1
{{0,0},{1,-1},{0,1},{1,0}},//正Z2
{{0,0},{1,-1},{-1,0},{0,-1}},//反z1
{{0,0},{-1,-1},{-1,0},{0,1}}//反Z2
};

int main()
{
	srand(time(0));//设置随机值
	//屏蔽掉终端的正常输入
	system(STTY_US TTY_PATH);// "stty raw -echo -F""/dev/tty"
	initScreen();//初始化屏幕二维数组
	createShap();//创建图形
	int moveDownCD = 0;
	//游戏的主逻辑循环
	while(1)
	{
		draw(); //画图形
		//返回值0,执行break结束程序
		if(!userCtrl()) //用户控制  !userCtrl() 等价于   userCtrl() == 0
		{
			break;
		}
		if(++moveDownCD == 10)//向下移动的CD计时,循环每执行10次移动一次
		{
			moveDown();
			moveDownCD = 0;
		}
		//刷帧
		usleep(50*1000);
	}

	return 0;
}

//将屏幕二维数组的边界的元素置2
void initScreen()
{
	/*
	screen[0][] 上 screen[HEIGHT-1][] 下
	screen[][0] 左 screen[][WIDTH-1] 右
	*/
	int x, y;
	//上下
	for(x = 0; x < WIDTH;x++)
	{
		screen[0][x] = 2;
		screen[HEIGHT-1][x] = 2;
	}
	//左右
	for(y = 0; y < HEIGHT;y++)
	{
		screen[y][0] = 2;
		screen[y][WIDTH-1] = 2;
	}

}

void createShap()
{
	x = WIDTH/2;
	y = 2;
	currentShap = rand()%19; //在0-18之间的随机数
}

void draw()
{
	system("clear"); //清空之前的图像
	/*
	画图形分为两部分,一部分是我们可以控制移动的图形,叫动态图形;
	另一部分是边界和已经落下的图,是我们不能控制的图形
	*/
	//动态图形
	int i;
	for(i = 0;i < 4;i++)
	{
		/*
		 shaps[currentShap]运算得到shaps数组中的一个一维数组,这个一维数组表示一个图形的4个点
		 当前的循环就是在遍历 shaps[currentShap]数组的4个点

		
		*/
		int lx = shaps[currentShap][i].x + x;
		int ly = shaps[currentShap][i].y + y;
		drawPoint(lx,ly);
	}

	//边界和已经落下的图
	//画屏幕二维数组
	int x, y;
	//遍历二维数组,判断每个元素的值,只要不是0就需要画方块
	for(y = 0; y < HEIGHT;y++)
	{
		for(x = 0;x < WIDTH;x++)
		{
			if(screen[y][x] != 0)
			{
				drawPoint(x,y);
			}
		}
	}
}

//用户的控制,返回1 表示继续  0表示结束
int userCtrl()
{
	int ctrl = getChar();

	switch (ctrl)
	{
	case 'a': //左
		if(collideLeft())  //不写花括号,遇到第一个;就结束
			x--;
		break;
	case 'd': //右
		if(collideRight()) 
			x++;
		break;
	case 's': //加速
		if(collideDown()) 
			y++;
		break;
	case 'w': //变形
		changeShap();
		break;
	case 3:
		//让终端恢复正常
		system(STTY_DEF TTY_PATH);//"stty -raw echo -F""/dev/tty"
		return 0;
	}
	return 1;
}


void changeShap()
{
	int tshap = currentShap;
	switch (tshap)
	{
	case 1: //竖条
		tshap = 0;
		break;
	case 2: //方块
		break;
	case 6: //正 L 4
		tshap = 3;
		break;
	case 10: //反 L 4
		tshap = 7;
		break;
	case 14: //T4
		tshap = 11;
		break;
	case 16: //正 Z 2 
		tshap = 15;
		break;
	case 18: //反 Z 2
		tshap = 17;
		break;
	default:
		tshap++;
	}
	if(collideChange(tshap))
		currentShap = tshap; //改变currentShap的值完成变形
}

//碰撞不允许变形
int collideChange(int tshap)
{
	int i;
	for(i = 0;i < 4; i++)
	{
		int lx = shaps[currentShap][i].x + x; //将要变形的位置的x值
		int ly = shaps[currentShap][i].y + y;//将要变形的位置的y值
		//判断screen二维数组的ly lx的位置有没有图形或者边界
		if(screen[ly][lx] != 0)
		{
			return 0; //发生碰撞
		}
	}
	return 1;//没有发生碰撞
}

//左移碰撞 返回0发生碰撞  返回1没有碰撞
int collideLeft()
{
	//计算当前图形的4个点的位置,然后将每个点的x值-1,能算出假设移动之后4个点的位置
	//然后遍历假设移动后的4个点,用这4个点的坐标去screen数组中找到对应的元素,只有元素的值不为0就说明发生了碰撞。
	int i;
	for(i = 0;i < 4; i++)
	{
		int lx = shaps[currentShap][i].x + x - 1; //将要移动的位置的x值
		int ly = shaps[currentShap][i].y + y;
		//判断screen二维数组的ly lx的位置有没有图形或者边界
		if(screen[ly][lx] != 0)
		{
			return 0; //发生碰撞
		}
	}
	return 1;//没有发生碰撞
}

//右移碰撞
int collideRight()
{
	int i;
	for(i = 0;i < 4; i++)
	{
		int lx = shaps[currentShap][i].x + x + 1; //将要移动的位置的x值
		int ly = shaps[currentShap][i].y + y;
		//判断screen二维数组的ly lx的位置有没有图形或者边界
		if(screen[ly][lx] != 0)
		{
			return 0; //发生碰撞
		}
	}
	return 1;//没有发生碰撞
}

//下移碰撞
int collideDown()
{
	int i;
	for(i = 0;i < 4; i++)
	{
		int lx = shaps[currentShap][i].x + x; //将要移动的位置的x值
		int ly = shaps[currentShap][i].y + y + 1;
		//判断screen二维数组的ly lx的位置有没有图形或者边界
		if(screen[ly][lx] != 0)
		{
			return 0; //发生碰撞
		}
	}
	return 1;//没有发生碰撞
}

//将动态图形添加到screen数组中
void addToScreen()
{
	int i;
	for(i = 0;i < 4;i++)
	{
		int lx = shaps[currentShap][i].x+x;
		int ly = shaps[currentShap][i].y+y;
		screen[ly][lx] = 1;
	}
}

//清空line行
void clearLine(int line)
{
	int x;
	for(x = 1;x < WIDTH-1;x++)
	{
		screen[line][x] = 0;
	}
}

//使用src行复制到dst行
void moveLine(int dst, int src)
{
	int x;
	for(x = 1;x < WIDTH-1;x++)
	{
		screen[dst][x] = screen[src][x];
		screen[src][x] = 0;
	}
}

//判断line行是否全是1
int isFull(int line)
{
	int x;
	for(x = 1;x < WIDTH-1;x++)
	{
		if(screen[line][x] == 0)
		{
			return 0;
		}
	}
	return 1;
}

//消行
void clearShap()
{
	int y;
	int count = 0;
	for(y = HEIGHT-2;y > 0;y--)
	{
		if(isFull(y))//y行全是1
		{
			count++;
			//清空y行
			clearLine(y);
		}
		else if(count > 0)
		{
			//将y行复制到y+count行
			moveLine(y+count, y);
		}
	}
}

//方块自动下降
void  moveDown()
{
	if(collideDown())
		y++;
	else
	{
		addToScreen();//将动态图形添加到screen数组
		clearShap();//消行
		createShap();//生成新图形
	}
}

void drawPoint(int x,int y)
{
	printf("\033[%d;%dH",y+1,x*2+1);
	printf("\033[1;36m■ \033[0m");
}

int getChar()
{
	fd_set rfds;
	struct timeval tv;
	int ch = 0;
	FD_ZERO(&rfds);
	FD_SET(0,&rfds);
	tv.tv_sec = 0;
	tv.tv_usec = 10;
	if(select(1,&rfds,NULL,NULL,&tv) > 0)
	{
		ch = getchar();
	}
	return ch;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

努力跟上的码农小酥

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

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

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

打赏作者

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

抵扣说明:

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

余额充值