`
444878909
  • 浏览: 638430 次
文章分类
社区版块
存档分类
最新评论

VC写的双人版俄罗斯方块

 
阅读更多

以前写过C++版的俄罗斯方块,后来老师让写双人版,刚开始大家都认为是把所有的代码copy一遍就行了,但实际并不是这样,这样做并不能实现双人版。

在老师的提示下,我们写出来C++版的俄罗斯方块游戏,现在把它移植到VC下,让界面更好看些。

一. 功能需求:

1. 能进行双人模式:

这是程序的主要功能。

2. 能进行下一砖块的预测:

这也是俄罗斯方块游戏的基本功能,可以在这个基础上进行扩展其他功能,如不同于经典模式的砖块显示等来增加难度。

二. 总体设计计:

1.矩形框类的设计:

设计CBin类描述Tetris游戏的矩形框,用image的二维数组来描述这个矩形框。设置不同的值显示不同颜色的矩形,若没有砖块则为0。

2.砖块的设计:

设计CBrick抽象类来设计砖块,应用多态性的原理,其他不同类型的砖块类继承CBrick,来设计不同的砖块。

3.砖块在面板中的显示设计:

在视图类中设计并显示砖块。

三. 详细设计:

1.矩形框类的设计:

设计CBin类描述Tetris游戏的矩形框,用image的二维数组来描述这个矩形框。设置不同的值显示不同颜色的矩形,若没有砖块则为0。

CBin类的五个成员函数:

(1) CBin(unsigned int unsigned int h)

构造函数,用来初始化数据成员,width和height,并为image分配空间并初始化。

(2) ~CBin()

析构函数,删除在构造函数中为image分配的空间。

(3) Void getImage(unsigned char*destImage)

将image的数据拷贝到destImage,你可以假设destimage指向的空间足够容纳image的空间。

(4) Void setImage(unsigned char**srcImage)

把srcImage中的数据拷贝到image,你可以假设srcImage是一个合法的指针。

(5) Unsigned int removeFullLines()

检查image,如果任何一行完全填满,则删除这一行,并让上面一行的数据下移,返回删除的总行数。

设计代码如下:

class CBin {
    private:
        unsigned char** image;
        unsigned int width;
        unsigned int height;
        
    public:
        CBin(unsigned int w, unsigned int h);
        ~CBin();
       
        unsigned int getWidth() { return width; };
        unsigned int getHeight() { return height; };
        
        void getImage(unsigned char** destImage);
        void setImage(unsigned char** srcImage);
        
        unsigned int removeFullLines();
};
 CBin::CBin(unsigned int w, unsigned int h)
{
	width=w;
	height=h;
	image = new unsigned char* [height];
    for (unsigned int i = 0; i<height; i++) 
	{
        image[i] = new unsigned char [width]; 
		for (unsigned int j = 0; j<width; j++) 
		    image[i][j]=0;
    }
}
CBin::~CBin()
{
	for (unsigned int i=0; i<height; i++) {
        delete image[i];
    }
    delete[] image;
}
void CBin::getImage(unsigned char** destImage)
{
     for (unsigned int i = 0; i<height; i++) 
    	 for (unsigned int j = 0; j<width; j++) 
		    destImage[i][j]=image[i][j];
}
void CBin::setImage(unsigned char** srcImage)
{
	   for (unsigned int i = 0; i<height; i++) 
    	 for (unsigned int j = 0; j<width; j++) 
		    image[i][j]=srcImage[i][j];

}
unsigned int CBin::removeFullLines()
{
	unsigned int flag,EmptyLine=0;
    unsigned int i,j,m;
	for (i=0; i<height; i++) 
	{
		flag=0;
		for (j=0; j<width; j++) 
		{
			if (image[i][j]==0 )
				flag=1;
		}
        //一行完全被填满
		if(flag==0)  
		{
			for (j=0; j<width; j++) 
			{
				image[i][j]=0;   
			}
			//消除整行
			for(m=i; m>0; m--)
            {
				for (j=0; j<width; j++) 
				{
				   image[m][j]=image[m-1][j];
				}
			}
            for (j=0; j<width; j++) 
			{
				   image[0][j]=0;
			}
            EmptyLine++;  //记录消的行数

			i--;
		
		}
		
	}
     return EmptyLine;   
}


2.砖块的设计:

设计CBrick抽象类来设计砖块,应用多态性的原理,其他不同类型的砖块类继承CBrick,来设计不同的砖块。

CBrick类的成员函数:

(1) int shiftLeft(CBin *bin

将砖块在游戏的矩形框内左移一位。注意座椅可能产生冲突,左边有赚快活碰到墙,如果不能左移返回0值,如果能左移返回1值。

(2) int shiftRight(CBin *bin)

同shiftLeft,右移。

(3) int shiftDown(CBin *bin)

同shiftLeft,下移。

(4) int rotateClockwise(CBin *bin)

同shiftLeft,顺时针旋转。

(5) int checkCollision(CBin* bin)

检查移动或旋转过程中是否有冲突,有冲突返回0,否则返回1。

(6) int checkCollision(CBin* bin)

重载运算符>>,通过设置映射到游戏矩形的二维数组binImage,设置砖块的颜色,这里假设binimage是一个合法的大侠合适的二维数组。

(7) void operator>>(unsigned char *binimage)

(8) putAtTop(unsigned int newOrient,unsigned int newPosX)

通过设置砖块特定点的Y坐标(posY)是的砖块位于矩形的最上方,砖块的X坐标和砖块的状态由newPosX和newOrient确定。注意posY的值依赖于砖块的状态

(9) putAtMid(unsigned int newOrient,unsigned int newPosX)

与putAtTop一致,只是将坐标设置在面板中间。

class CCBrick : public CBrick {
    public:
        int shiftLeft(CBin* bin);
        int shiftRight(CBin* bin);
        int shiftDown(CBin* bin);
        int rotateClockwise(CBin* bin);
        
        int checkCollision(CBin* bin);
        
        void operator>>(unsigned char** binImage);
        void putAtTop(int newOrient, int newPosX);
		void putAtMid(int newOrient, int newPosX);
};
/////////////////////////
int CCBrick::shiftLeft(CBin* bin)
{
	int posX;

	posX=getPosX();
    setPosX(posX-1);
    if (checkCollision(bin)== 0) {
        setPosX(posX);
        return 0;
    }
    return 1;
}
int CCBrick::shiftDown(CBin* bin)
{
	int posY;
     
	posY=getPosY();
    setPosY(posY+1);
    if (checkCollision(bin)== 0) {
		setPosY(posY);
        return 0;
    }
    return 1;
}
int CCBrick::shiftRight(CBin* bin)
{
	int posX;
	posX=getPosX();
    setPosX(posX+1);
    if (checkCollision(bin)==0) {
		setPosX(posX);
        return 0;
    }
    return 1;
}
int CCBrick::rotateClockwise(CBin* bin)
{
	int orientation,oldOrientation;
    orientation=getOrientation();
	oldOrientation=orientation;
	if (orientation==3)
		orientation=0;
	else
		orientation=orientation+1;
    setOrientation(orientation);
	if (checkCollision(bin)== 0) {
        setOrientation(oldOrientation);
        return 0;
    }
    return 1;
}

int CCBrick::checkCollision(CBin* bin)
{
	int width;
    int height;
    unsigned char** image;
	int orientation;
	int posX;
	int posY;
	width=bin->getWidth();
	height=bin->getHeight();
	image = new unsigned char* [height];
    for (int i = 0; i<height; i++) 
	{
        image[i] = new unsigned char [width]; 
    }
	bin->getImage(image);
    orientation=getOrientation();
    posX=getPosX();
	posY=getPosY();
	if (orientation==0) 
	{
		if((posX<0)||
			((posX+1)>width-1)||
			(posY<1)||
			(posY+1>height-1))
		  return 0;
		if ( (image[posY-1][posX]!=0)||
			 (image[posY-1][posX+1]!=0)||
             (image[posY][posX]!=0)||
			 (image[posY+1][posX]!=0)||
			 (image[posY+1][posX+1]!=0))
             return 0;
	}
	if (orientation==1) 
	{
		if( (posX<1)||
			((posX+1)>width-1)||
			(posY<0)||
			((posY+1)>height-1))
		  return 0;
		if ( (image[posY+1][posX+1]!=0)||
			 (image[posY][posX+1]!=0)||
             (image[posY][posX]!=0)||
			 (image[posY][posX-1]!=0)||
			 (image[posY+1][posX-1]!=0))
             return 0;
	}
	if (orientation==2) 
	{
		if((posX<1)||
			(posX>width-1)||
			(posY<1)||
			((posY+1)>height-1))
		  return 0;
		if ( (image[posY-1][posX-1]!=0)||
			 (image[posY-1][posX]!=0)||
             (image[posY][posX]!=0)||
			 (image[posY+1][posX]!=0)||
			 (image[posY+1][posX-1]!=0))
             return 0;
	}
	if (orientation==3) 
	{
		if( (posX<1)||
			(posX+1>width-1)||
			(posY<1)||
			((posY)>height-1))
	    return 0;
		if ( (image[posY-1][posX+1]!=0)||
			 (image[posY][posX+1]!=0)||
             (image[posY][posX]!=0)||
			 (image[posY][posX-1]!=0)||
			 (image[posY-1][posX-1]!=0))
             return 0;
	}
    return 1;
}

void CCBrick::operator>>(unsigned char** binImage)
{
	int orientation;
	int posX;
	int posY;
	unsigned char colour;

  
	posX=getPosX();
	posY=getPosY();
	orientation=getOrientation();
	colour=getColour();
	if (orientation==0) 
	{
		binImage[posY-1][posX+1]=colour;
        binImage[posY-1][posX]=colour;
		binImage[posY][posX]=colour;
		binImage[posY+1][posX]=colour;
		binImage[posY+1][posX+1]=colour;
    }
	if (orientation==1) 
	{
		binImage[posY+1][posX+1]=colour;
        binImage[posY][posX+1]=colour;
		binImage[posY][posX]=colour;
		binImage[posY][posX-1]=colour;
		binImage[posY+1][posX-1]=colour;
    }	
	if (orientation==2) 
	{
		binImage[posY-1][posX-1]=colour;
        binImage[posY][posX]=colour;
		binImage[posY-1][posX]=colour;
		binImage[posY+1][posX]=colour;
		binImage[posY+1][posX-1]=colour;
    }
	if (orientation==3) 
	{
        binImage[posY-1][posX+1]=colour;
        binImage[posY][posX+1]=colour;
		binImage[posY][posX]=colour;
		binImage[posY][posX-1]=colour;
		binImage[posY-1][posX-1]=colour;
    }	

}

void CCBrick::putAtTop(int newOrient, int newPosX)
{
    setPosX(newPosX);
	setOrientation(newOrient);
    switch(newOrient)
	{
	case 0: setPosY(1); break;
	case 1: setPosY(0); break;
	case 2: setPosY(1); break;
	case 3: setPosY(1); break;
	}
}
void CCBrick::putAtMid(int newOrient, int newPosX)
{
    setPosX(newPosX-3);
	setOrientation(newOrient);
	switch(newOrient)
	{
	case 0: setPosY(9); break;
	case 1: setPosY(8); break;
	case 2: setPosY(9); break;
	case 3: setPosY(9); break;
	
	}
}


3.砖块在面板中的显示设计:

在视图类中设计并显示砖块。

(1).定义相关的变量并在构造函数中初始化。

(2).添加DrawImage(CBin*bin,unsigned char**image,CDC*pDC)函数,用来绘制游戏砖块。

这其中要绘制四个面板,传递四个参数。两个主面板,两个与侧面板。

部分代码如下:

 void CNewTerisView::DrawImage(CBin *bin,CBin *bin1,CBin *bin2,CBin *bin3, unsigned char **image, unsigned char **image1, unsigned char **image2, unsigned char **image3,CDC *pDC)
    CRect rect;
     //创建背景位图画刷
	 CBitmap bitmap;
	 bitmap.LoadBitmap(IDB_BITMAP24);
	 CBrush brush;
	 brush.CreatePatternBrush(&bitmap);

	 GetClientRect(&rect);
     pDC->FillRect(rect,&brush); 
     //右边玩家分数的输出
	 CRect re;
	 pDC->FillSolidRect(re,RGB(210,255,255));
	 char buf[100];
	 sprintf(buf,"  %d  ",numLines*10);
	 pDC->TextOut(330,90,buf);
     //左边玩家分数的输出
	 CRect re1;
	 pDC->FillSolidRect(re1,RGB(230,255,100));
	 char buf1[100];
	 sprintf(buf1,"  %d  ",numLines1*10);
	 pDC->TextOut(440,90,buf1);

	 CRect rc,rc1,rc2,rc3;
	 //将载入的位图资源放在数组中
	 int Bit[8]={IDB_BITMAP11,IDB_BITMAP12,IDB_BITMAP13,IDB_BITMAP14,IDB_BITMAP15,IDB_BITMAP16,IDB_BITMAP17,IDB_BITMAP18};
	 int b,b1,b2;	 
    
	  for(i=0;i<height1;i++)
	 {
		 for(j=0;j<width1;j++)
		 {
			  rc1=CRect(j*nSize+300,i*nSize,(j+1)*nSize+300,(i+1)*nSize);		//矩形的区域	 
			 if(image1[i][j]!=0)
			 {			 
				 b=image1[i][j];
				 b1=b;
				 CBitmap bitmap1; //左边玩家预测砖块的位图填充
	             bitmap1.LoadBitmap(Bit[b]);
	             bitmap1.SetBitmapDimension(20,20);
	             CBrush brush1;
	             brush1.CreatePatternBrush(&bitmap1);
				 pDC->FillRect(rc1,&brush1);
			 }
		 }
	 }
	 for(i=0;i<height;i++)
	 {
		 for(j=0;j<width;j++)
		 {
			 rc=CRect(j*nSize+80,i*nSize,(j+1)*nSize+80,(i+1)*nSize);			 
			 if(image[i][j]!=0)
			 {
				 b=image[i][j];    //左边玩家主面板砖块的位图填充
				 CBitmap bitmap1;
	             bitmap1.LoadBitmap(Bit[b]);
	             bitmap1.SetBitmapDimension(20,20);
	             CBrush brush1;
	             brush1.CreatePatternBrush(&bitmap1);
				 pDC->FillRect(rc,&brush1);
			 }
		
		 }
	 }


(3).在菜单中添加“开始”、“难度”、“暂停”、“游戏模式”等菜单,

A.为“开始”菜单添加消息响应函数。

初始化面板,并设置定时器。

代码如下:

void CNewTerisView::OnGameStart() 
{
	// TODO: Add your command handler code here
	gameOver=0;
	brickInFlight=0;
	numLines=0;
	//------------
	gameOver1=0;
	brickInFlight1=0;
	numLines1=0;
	for(unsigned int i=0;i<20;i++)
	{
		for(unsigned int j=0;j<10;j++)
		{
			outputImage[i][j]=0;
			outputImage1[i][j]=0;
			outputImage2[i][j]=0;
			outputImage3[i][j]=0;
		}
	}
	bin->setImage(outputImage);
	bin1->setImage(outputImage1);
	bin2->setImage(outputImage2);
	bin3->setImage(outputImage3);
	/////////////加背景音乐
    res=FindResource(::AfxGetApp()->m_hInstance,MAKEINTRESOURCE(IDR_WAVE1),"WAVE");
	hSound1=LoadResource(::AfxGetApp()->m_hInstance,res);
	lpSound1=(LPSTR)LockResource(hSound1);
	sndPlaySound(lpSound1,SND_LOOP|SND_ASYNC|SND_MEMORY);
	//
	SetTimer(0,difficulty,NULL);

}

B.“难度”菜单中有容易、中等与高级三个等级。通过设置砖块下落的速度来设置难度。

C.“暂停”菜单中设置标志变量,是程序执行时,不执行shiftDown()函数即可。

void CNewTerisView::OnPause() //暂停
{
	// TODO: Add your command handler code here
	if(!p)   //原先没有暂停则暂停
	{
	Pause1=TRUE;  //设置暂停为真
	Pause2=TRUE;
	p=TRUE;
	}
	else   //若原先暂停,按下后继续游戏
	{
	Pause1=FALSE;
	Pause2=FALSE;
	p=FALSE;
	}
}

D.“游戏模式”菜单可以设置游戏为“经典模式”或“奇幻模式”。

用于设定两大不同类的砖块。

(4).为视图类添加WM_TIMER的消息响应函数。(此相当于C++中所执行的主函数中的代码)

A在此函数中,随机产生不同的砖块,并显示出来,还要判断是否冲突或游戏是否结束,若游戏没结束也不冲突则砖块下移一格,之后还要判断是否应消行等。

B.预测功能的实现,是将预测面板上一次随机产生的砖块类型值赋给本次的主程序面板。

C.双人模式的实现很简单,只是重新绘制了一个矩形框(CBin),并绘制一个面板,将其显示在不同的区域。预测与此类似,只不过预测没有判断冲突等,只是显示即可。

具体代码如下:

if(!brickInFlight&&!gameOver)  //判断左边游戏是否结束及砖块是否在飞
	{
	
		if(!RandomFlag)  //若是第一次产生主界面砖块类型,则执行下面语句(第一次产生的砖块不是预测的)下面语句只执行一次
		{
		  brickType=((unsigned int)rand()%NUM_BRICK_TYPES)+1;
		  initOrientation=(unsigned int)(rand()%4);
		  RandomFlag=TRUE;
		}
        else //第二次以后,主界面的砖块都是上一次预测砖块的类型
		{
			brickType=brickType1;
			initOrientation=initOrientation1;
		}
	    CTime time=CTime::GetCurrentTime(); //预测砖块类型的产生
		int q=int(time.GetSecond());
		brickType1=(q%NUM_BRICK_TYPES)+1;
		initOrientation1=(unsigned int)(q%4);

        if(!modal)    //若不是经典模式,则为奇幻模式
		{
		if(brickType==1)      //左边主界面砖块的动态产生
			activeBrick=new CtBrick;
		else if(brickType==2)
			activeBrick=new CsBrick;
		else if(brickType==3)
			activeBrick=new CCBrick;
		else if(brickType==4)
		    activeBrick=new CPointBrick;
		else if(brickType==5)
			activeBrick=new CiBrick;
        else if(brickType==6)
			activeBrick=new C10Brick;
		else if(brickType==7)
			activeBrick=new C7Brick;
        /////////000000
		if(brickType1==1)
			activeBrick1=new CtBrick;//左边预测界面砖块的动态产生
		else if(brickType1==2)
			activeBrick1=new CsBrick;
		else if(brickType1==3)
			activeBrick1=new CCBrick;
		else if(brickType1==4)
		    activeBrick1=new CPointBrick;
		else if(brickType1==5)
			activeBrick1=new CiBrick;
        else if(brickType1==6)
			activeBrick1=new C10Brick;
		else if(brickType1==7)
			activeBrick1=new C7Brick;
		}
		else           //若不是奇幻模式,则为经典模式
		{
           if(brickType==1)
			activeBrick=new CTBrick;
		else if(brickType==2)
			activeBrick=new CABrick;
		else if(brickType==3)
			activeBrick=new CSBrick;
		else if(brickType==4)
		    activeBrick=new CLBrick;
		else if(brickType==5)
			activeBrick=new COBrick;
        else if(brickType==6)
			activeBrick=new CIBrick;
		else if(brickType==7)
			activeBrick=new CJBrick;
          /////////////////
		if(brickType1==1)
			activeBrick1=new CTBrick;
		else if(brickType1==2)
			activeBrick1=new CABrick;
		else if(brickType1==3)
			activeBrick1=new CLBrick;
		else if(brickType1==4)
		    activeBrick1=new CSBrick;
		else if(brickType1==5)
			activeBrick1=new COBrick;
        else if(brickType1==6)
			activeBrick1=new CIBrick;
		else if(brickType1==7)
			activeBrick1=new CJBrick;
		}
		activeBrick->setColour((unsigned char)brickType); //左边主界面设置颜色
		activeBrick->putAtTop(initOrientation,binWidth/2);//将产生的砖块置顶
		//-----
		activeBrick1->setColour((unsigned char)brickType1); //左边预测砖块设置颜色
		activeBrick1->putAtMid(initOrientation1,binWidth1/2);//将预测产生的砖块放置在面板中间
		///******************************************
		notCollide=activeBrick->checkCollision(bin); //判断是否冲突
		if(notCollide)        //判断是否冲突
		{
			brickInFlight=1;
			bin->getImage(outputImage);
			activeBrick->operator>>(outputImage);
			bin1->getImage(outputImage1);
			activeBrick1->operator>>(outputImage1);
	        Invalidate(FALSE);
		}
		else          //冲突则游戏设置游戏结束变量,删除new的东西
		{
			gameOver=1;
			delete activeBrick;
			delete activeBrick1;
			brickInFlight=0;
		}
	}
	////////////////////////////////////////////////////
	if(brickInFlight&&!gameOver) 
	{
		if(!Pause1)
		notCollide=activeBrick->shiftDown(bin); //砖块下移一格
		if(notCollide){
			bin->getImage(outputImage);         //不冲突获得画面
			activeBrick->operator>>(outputImage); //显示画面
		}
		else 
		{
			brickInFlight=0;
			bin->getImage(outputImage);
			
			activeBrick->operator>>(outputImage);
			bin->setImage(outputImage);
	
			Invalidate(FALSE);
			numLines=numLines+bin->removeFullLines();  //消行分数增加
			bin->getImage(outputImage);
		
		}
		Invalidate(FALSE);
	}
	//////////////////////////////
	if(gameOver){           //左边游戏是否结束
		KillTimer(0);
		res1=FindResource(::AfxGetApp()->m_hInstance,MAKEINTRESOURCE(IDR_WAVE2),"WAVE");
	    hSound2=LoadResource(::AfxGetApp()->m_hInstance,res1);
	    lpSound2=(LPSTR)LockResource(hSound2);
	    sndPlaySound(lpSound2,SND_ASYNC|SND_MEMORY);
		if(MessageBox("左边的兄弟,得加油啊!还玩吗?","提示",MB_YESNO)==IDYES)
			OnGameStart();
		else
			exit(0);
	}

(1) 在OnDraw()函数中调用DrawImage()函数,即可显示面板。

(2) 当然还得添加WM_KEYDOWON的消息响应函数,以响应用户按键。

代码如下:

void CNewTerisView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
	// TODO: Add your message handler code here and/or call default
	////////左边玩家的控制键
	if(nChar==VK_NUMPAD0)
      OnPause();
	if(nChar==VK_RIGHT)
        activeBrick->shiftRight(bin);
	else if(nChar==VK_LEFT)
		activeBrick->shiftLeft(bin);
	else if(nChar==VK_UP)
		activeBrick->rotateClockwise(bin);
	else if(nChar==VK_DOWN)
		activeBrick->shiftDown(bin);
	//////////右边玩家的控制键
	if(nChar==VK_NUMPAD6)
        activeBrick2->shiftRight(bin2);
	else if(nChar==VK_NUMPAD4)
		activeBrick2->shiftLeft(bin2);
	else if(nChar==VK_NUMPAD8)
		activeBrick2->rotateClockwise(bin2);
	else if(nChar==VK_NUMPAD5)
		activeBrick2->shiftDown(bin2);
	Invalidate(FALSE);
	CView::OnKeyDown(nChar, nRepCnt, nFlags);
}


四.测试与实现:

经典模式:

奇幻模式:

单人模式:

游戏结束:

五. 总结:

通过本次双人版俄罗斯方块游戏的开发,使我对VC可视化编程产生了更大的兴趣,也激发了我开发游戏的强烈兴趣。以前在dos环境下编写的俄罗斯方块游戏与此相比,能够充分体现出可视化编程的优势,界面比dos下的好看多了。而且其操作简便,用户体验很好。

这次的开发过程没有前几次那么曲折,可能是我的编程能力真的有了提高,当然,这也主要是因为老师已经把俄罗斯方块的实现写好了,我们只是做一些修改。你想要修改成自己想要的东西,首先,你得看懂程序,否则你不知道在那里修改,怎么修改。通过修改程序,实现双人版俄罗斯方块,在这一过程中,我们也学到了很多东西,有技巧性的代码,也有思维上的模式与创新等。只有你不断的改,不断地练习,你的编程能力在不知不觉中提高。

通过自己的修修改改,不知不觉就实现了你所想要的东西。当然,这其中一定会有挫折,会有BUG,当你真的编出你所想要的东西时,这种成就感将以前的所有痛苦、bug都化为乌有。就是在不断的出现问题,解决问题的过程中学习。

很多人都知难而退,也许只有坚持到底,你才会解决解决所遇到的困难,你才会成功。

本程序的优点:具有较好的界面,实现了双人版俄罗斯方块的同时,实现了预测及不同于经典模式砖块的选择,还加了音乐。

本程序的缺点:游戏的控制不够灵活,反应较慢,还有很多有待提高的地方。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics