Tetris

俄罗斯方块

 Max.C     2019-08-09   16919 words    & views

引言

跟着室友搞事情系列,在室友的启发下也准备自己写点什么东西,既然室友写了个华容道,那我就写一个俄罗斯方块吧。

一、初步构思

基于这学期学习的东西,我打算用 C++ 完成这个俄罗斯方块。那么,初步的想法则是将正在下落的方块设计成一个类,再将地图(想不到其他适合的名称)也设计成一个类,暂时不考虑整行消除、游戏结束条件、计分等,先试着做一个方块能够在地图上堆叠起来的 demo 。

俄罗斯方块实际上就是一个方块在地图上进行操作(旋转、下落、移动、消除),而地图会随着方块的更新而更新(一个方块触底则置入地图中,地图其中一行填满则消除),那么我们需要考虑的就只有如何做到整个地图一个方块的交互。

首先我们进行方块的构思,一个方块类需要的成员变量有:

  • 形状
  • 颜色
  • 在地图上的坐标

介于方块有 I、J、L、O、Z、N、T 一共七种,I 型方块有四格长,那我们就用一个 4 x 4 的二维数组表示一个方块的形状(方块所在方格用 1 表示,下图绿色部分),方块的坐标用这个二维数组左上角的坐标表示(下图红色部分)。

然后是地图的构思,俄罗斯方块的地图一般是高为20、宽为10,如果加上边界则是 22 x 12 ,由于我们需要保证方块能够接触到边界(此时方块的坐标可能在边界之外,如下图),我们再在边界外侧在加上两单位的空余,最终的地图为 26 x 16 大小。

由于大部分操作都需要结合地图与方块,地图也只需要坐标信息,那我们直接设计一个游戏窗口的类,这个类的成员包含方块地图,再完善这个类就可以了。

在敲代码的过程中,游戏窗口这个类不断完善,最终确定为以下几个成员变量(2.0版本):

  • 当前正在下落的方块
  • 下一个将要下落的方块
  • 地图(已经触底的方块直接存入地图)
  • 游戏得分
  • 下落速度

只要完善这两个类,我们的俄罗斯方块也就可以完成,接下来我将慢慢解析各部分的代码。

二、实现代码

通过查阅资料和初步尝试,我先完成了如下的目录与代码框架。(由于2.0版本与1.0版本有所差异,本博客依照的是2.0版本的代码)

1、代码目录

|-- include
    |-- print.h //控制打印光标、颜色等基础操作
    |-- piece.h //方块的类,完成对方块本身的操作
        #include "print.h"
    |-- tetris.h //游戏窗口的类,实现地图与方块的交互、游戏的开始与结束等
        #include "piece.h"
|-- src
    |-- main.cpp
        #include "tetris.h"     

2、print.h

void gotoxy(int x, int y); //将命令行光标移动至(x,y)
void printTimes(string str, int n); //对一个字符打印多次
void setColor(int n); //设置打印的颜色

3、piece.h

class Piece{
	public:
		int id; //方块的类型	
		int color; //方块的颜色		
		int nx; //方块的x坐标		
		int ny; //方块的y坐标		
		Piece(int,int,int); //构造函数		
		void renewPie(int,int,int); //构造一个新的方块		
		void draw(); //打印该方块		
		void erase(); //擦除该方块		
		void span(); //旋转该方块		
};

4、tetris.h

class Tetris{
		Piece pic; //当前下落的方块		
		Piece pre; //下一个下落的方块		
		int map[WIDTH+6][HEIGHT+6]={
			{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
			{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
			{0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0},
			{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0},
			{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0},
			{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0},
			{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0},
			{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0},
			{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0},
			{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0},
			{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0},
			{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0},
			{0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0},
			{0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0},
			{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
			{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
		}; //游戏地图 10*20 大小		
		int score; //游戏得分		
		int count; //游戏速度		
	public:
		Tetris(); //构造函数	
		void welcome(); //开始界面 		
		void rule(); //游戏规则界面		
		void initMap(); //初始化地图		
		void pause(); //暂停游戏 		
		int runGame(); //开始游戏,键盘输入接口 		
		int update(); //自动下降、地图更新		
		int gameOver(); //游戏结束		
		bool renewPie(); //构造一个新的方块		
		void exchange(); //交换方块		
		bool judge(int x,int y,int id); //判断方块与地图是否重叠		
		void getScore(int rank); //更新得分		
};

5、宏定义与全局常量

#define X 8 
#define Y 4
#define P 9
#define Q 19 
#define R 40 
//坐标信息,用于调试游戏界面的坐标

#define WIDTH  10	
#define HEIGHT  20 
#define TEMP  3 
//地图长度信息

#define WHITE 0 
#define BLUE 1 
#define GREEN 2
#define RED 3
#define YELLOW 4
#define PURPLE 5
#define CYAN 6
#define PINK 7
//颜色信息,用于直观选择颜色

#define I1  0	
#define I2  1

#define O 2		

#define L1 3	
#define L2 4
#define L3 5
#define L4 6

#define J1 7	
#define J2 8 
#define J3 9
#define J4 10

#define T1 11
#define T2 12
#define T3 13
#define T4 14

#define Z1 15
#define Z2 16

#define N1 17
#define N2 18
//七种方块类型和各自的旋转状态

const int shape[19][4][4]={
	{
		{0,1,0,0},
		{0,1,0,0},
		{0,1,0,0},
		{0,1,0,0},
	},
	{
		{0,0,0,0},
		{0,0,0,0},
		{1,1,1,1},
		{0,0,0,0},
	},
	{
		{0,0,0,0},
		{0,1,1,0},
		{0,1,1,0},
		{0,0,0,0},
	},
	{
		{0,1,0,0},
		{0,1,0,0},
		{0,1,1,0},
		{0,0,0,0},
	},
	{
		{0,0,0,0},
		{0,1,1,1},
		{0,1,0,0},
		{0,0,0,0},
	},
	{
		{0,0,0,0},
		{0,1,1,0},
		{0,0,1,0},
		{0,0,1,0},
	},
	{
		{0,0,0,0},
		{0,0,1,0},
		{1,1,1,0},
		{0,0,0,0},
	},
	{
		{0,0,1,0},
		{0,0,1,0},
		{0,1,1,0},
		{0,0,0,0},
	},
	{
		{0,0,0,0},
		{0,1,0,0},
		{0,1,1,1},
		{0,0,0,0},
	},
	{
		{0,0,0,0},
		{0,1,1,0},
		{0,1,0,0},
		{0,1,0,0},
	},
	{
		{0,0,0,0},
		{1,1,1,0},
		{0,0,1,0},
		{0,0,0,0},
	},
	{
		{0,0,0,0},
		{0,1,0,0},
		{1,1,1,0},
		{0,0,0,0},
	},
	{
		{0,0,0,0},
		{0,1,0,0},
		{0,1,1,0},
		{0,1,0,0},
	},
	{
		{0,0,0,0},
		{0,0,0,0},
		{1,1,1,0},
		{0,1,0,0},
	},
	{
		{0,0,0,0},
		{0,1,0,0},
		{1,1,0,0},
		{0,1,0,0},
	},
	{
		{0,0,1,0},
		{0,1,1,0},
		{0,1,0,0},
		{0,0,0,0},
	},
	{
		{0,0,0,0},
		{1,1,0,0},
		{0,1,1,0},
		{0,0,0,0},
	},
	{
		{0,1,0,0},
		{0,1,1,0},
		{0,0,1,0},
		{0,0,0,0},
	},
	{
		{0,0,0,0},
		{0,1,1,0},
		{1,1,0,0},
		{0,0,0,0},
	},
};
const int col[19]={1,1,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,7,7};

//方块类型、旋转状态、颜色的具体实现

上面的宏定义和常量定义让代码看起来更直观,也更容易实现。接下来开始说明各个源文件中的函数定义。

6、print.h

print.h用于实现最基础的操作,一开始写完之后基本不需要改动,几个函数都参考了室友华容道的代码,再次赞美室友

gotoxy()函数用于将光标移动至(x,y),调用后可以进行打印、擦除等操作,通过句柄实现。

void gotoxy(int x, int y) {
    COORD pos = {x,y};
    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);// Getting the standard output device handle
    
    SetConsoleCursorPosition(hOut, pos); //Windows & Position
    
}

printTimes()函数,不用多说,看一下就知道用来干什么。

void printTimes(string str, int n){
	for(int i = 0; i < n; i++){
		cout << str;
	}
	return;
}

setColor()函数用于改变接下来输出的字符的颜色,加上我们对颜色的宏定义,调用这个函数就可以轻松改变颜色。例如用setColor(RED);语句就可以将输出字符改为红色。

setColor()是基于SetConsoleTextAttribute()函数实现的,函数定义在头文件<windows.h>中,第一个参数默认如下,第二个参数则为我们需要调整的颜色,参数有:

  • FOREGROUND_BLUE //蓝色
  • FOREGROUND_RED //红色
  • FOREGROUND_GREEN //绿色
  • FOREGROUND_INTENSITY //高亮

各个参数可以用 | 连接,自己搭配出适合的颜色。

void setColor(int n){
	switch (n){
		case WHITE:{
			SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
				FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN | 					FOREGROUND_INTENSITY);
			break;
		}
		case BLUE:{
			SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
				FOREGROUND_BLUE );
			break;
		}
		case GREEN:{
			SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
				FOREGROUND_GREEN | FOREGROUND_INTENSITY);
			break;
		}
		case RED:{
			SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
				FOREGROUND_RED );
			break;
		}
		case YELLOW:{
			SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
				FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
			break;
		}
		case PURPLE:{
			SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
				FOREGROUND_BLUE | FOREGROUND_RED );
			break;
		}
		case CYAN:{
			SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
				FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
			break;
		}
		case PINK:{
			SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
				FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_INTENSITY);
			break;
		}
	}
}

7、piece.h

piece.h 用于实现方块本身的操作,与地图无关。之所以没有将判断函数写进Piece类中,就是因为判断需要结合方块与地图,所以写在了Tetris类中。

首先是构造函数,由于最后我们会手动调用renewPie()刷新方块,所以构造函数默认即可(虽然是我写出来之后才发现的)。

Piece类具有id、color、nx、ny四个成员变量,renewPie函数用于更新这四个变量,根据宏定义,我们可以用一个 0 ≤ id < 19 表示一种方块。

但是函数的第一个参数 rand 并不表示 id,而且0 ≤ rand < 7 ,并利用switchrand 转换为 id。这是因为如果直接用随机数生成小于19的数,那些只有一个旋转状态的方块(例如O)比有多个旋转状态的方块(例如Z)出现的概率小得多,由于一开始我忽视了这一点,只能用switch做一个简单的转换,让各个方块出现的概率相同。

根据全局常量const int col[19]={1,1,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,7,7};,代码color=col[id];可将每种方块都定义成一种确定的颜色。

void Piece::renewPie(int rand=0,int x=16,int y=1){
	switch(rand){
		case 0:{
			id=0;
			break;
		}
		case 1:{
			id=2;
			break;
		}
		case 2:{
			id=3;
			break;
		}
		case 3:{
			id=7;
			break;
		}
		case 4:{
			id=11;
			break;
		}
		case 5:{
			id=15;
			break;
		}
		case 6:{
			id=17;
			break;
		}
	}
	ny=y;
	nx=x;
	color=col[id];
}

draw()函数用于将当前方块打印出来,这里需要注意的是gotoxy(X+2*(nx+i),Y+ny+j);,由于一个字符是占两个空格大小的,所以在x轴坐标进行了x2的调整。

erase()函数同理,只是改成了擦除。

void Piece::draw(){
	setColor(color);
	for(int i=0;i<4;i++){
		for(int j=0;j<4;j++){
			if(shape[id][i][j]==1){
				gotoxy(X+2*(nx+i),Y+ny+j);
				cout<<"■"; 
			}
		}
	}
}
void Piece::erase(){
	for(int i=0;i<4;i++){
		for(int j=0;j<4;j++){
			if(shape[id][i][j]==1){
				gotoxy(X+2*(nx+i),Y+ny+j);
				cout<<"  ";  
			}
		}
	}
}

span()函数用于旋转方块,实现方式也十分简单,只需改变成员变量id即可。

void Piece::span(){
	switch(id)
	{
	case I1: id = I2; break;	
	case I2: id = I1; break;

	case O: id = O; break;

	case L1: id = L2; break;
	case L2: id = L3; break;
	case L3: id = L4; break;
	case L4: id = L1; break; 

	case J1: id = J2; break;
	case J2: id = J3; break;
	case J3: id = J4; break;
	case J4: id = J1; break;
	
	case T1: id = T2; break;
	case T2: id = T3; break;
	case T3: id = T4; break;
	case T4: id = T1; break;

	case Z1: id = Z2; break;
	case Z2: id = Z1; break;	

	case N1: id = N2; break;
	case N2: id = N1; break;
	}
	return;
}

piece.h 的函数实现都挺简单的,接下来就开始实现Tetris类完成整个游戏界面。

8、tetris.h

tetris.h要实现的函数比较多,我们可以先看到main.cpp中,直接创建了一个Tetris成员,调用了两个函数用来进行游戏。

int main(){
	system("cls"); //清屏
	system("title 俄罗斯方块"); //游戏窗口标题	
	system("mode con cols=60 lines=28"); //游戏窗口大小	
	Tetris game;
	game.welcome(); //开始界面
	while(game.runGame()){ //游戏界面
	}
    return 0;
}

Tetris类的所有成员函数如下:

Tetris(); //构造函数
void welcome(); //开始界面 
void rule(); //游戏规则界面
void initMap(); //初始化地图 
void pause(); //暂停游戏 
int runGame(); //开始游戏,键盘输入接口 
int update(); //自动下降、地图更新
int gameOver(); //游戏结束
bool renewPie(); //构造一个新的方块
void exchange(); //交换方块
bool judge(int x,int y,int id); //判断方块与地图是否重叠
void getScore(int rank); //更新得分

首先是构造函数,由于在initMap()中我们对所有成员都进行了初始化,所以依旧没必要写。(我依旧是在写完之后才发现)

welcome()用于形成开始界面,首先通过setColor()gotoxy()打印游戏界面,再利用kbhit()getch()接收键盘的输入信号,选择跳转至规则界面rule()或者开始游戏。

void Tetris::welcome(){
	system("cls");
	setColor(WHITE);
	gotoxy(X+15,Y-1);
	cout<<"俄罗斯方块";
	Piece cub1(10,2,2);
	cub1.draw();
	Piece cub2(17,6,2);
	cub2.draw();
	Piece cub3(15,10,2);
	cub3.draw();
	Piece cub4(4,14,1);
	cub4.draw();
	Piece cub5(2,4,6);
	cub5.draw();
	Piece cub6(0,8,6);
	cub6.draw();
	Piece cub7(12,12,6);
	cub7.draw();
	setColor(WHITE);
	gotoxy(Q,Y+13);
	cout<<"开始游戏\t空格";
	gotoxy(Q,Y+15);
	cout<<"规则操作\tR键";
	
	//开始界面
	while(1){
		if(kbhit()){	
			char ch;
			ch=getch();
			switch(ch){
				case ' ':{
					return;
				}
				case 'r':case 'R':{
					rule();
					gotoxy(Q,Y+13);
					cout<<"开始游戏\t空格";
					gotoxy(Q,Y+15);
					cout<<"规则操作\tR键";
					break;
				}
			}
		}
	}
} 

规则界面rule()与开始界面类似,打印出游戏规则,各坐标通过不断调整得到,接收到键盘输入信号则返回。

void Tetris::rule(){
	setColor(WHITE);
	gotoxy(P+2,Y+12);
	cout<<"W";
	gotoxy(P,Y+13);
	cout<<"A S D";
	gotoxy(P+2,Y+15);
	cout<<"J";
	gotoxy(P,Y+17);
	cout<<"空格键";
	gotoxy(Q+7,Y+12);
	cout<<"旋转";
	gotoxy(Q,Y+13);
	cout<<"  左移 下落 右移";
	gotoxy(Q,Y+15);
	cout<<"切换(下落不超过3格)";
	gotoxy(Q+5,Y+17);
	cout<<"开始/暂停";
	gotoxy(R,Y+12);
	cout<<"Line\tScore";
	gotoxy(R,Y+14);
	cout<<"1\t10";
	gotoxy(R,Y+15);
	cout<<"2\t40";
	gotoxy(R,Y+16);
	cout<<"3\t80";
	gotoxy(R,Y+17);
	cout<<"4\t160";
	gotoxy(X+12,Y+20);
	cout<<"请按R键返回主选单";
	while(1){
		if(getch()=='r' || getch()=='R'){
		for(int i=0;i<10;i++){
			gotoxy(P,Y+12+i);
			printTimes(" ",50);
		}
		break;
		} 
	}
	return;
}

initMap()函数生成一开始的游戏界面,包括地图、分数、显示下一个方块的框,并对成员变量进行了初始化。

void Tetris::initMap(){
	system("cls"); 
	gotoxy(X-2,Y-1);
	printTimes("■",12);
	for(int i=0;i<20;i++){
		gotoxy(X-2,Y+i);
		cout<<"■";
		printTimes("  ",10);
		cout<<"■";
	} 
	gotoxy(X-2,Y+20);
	printTimes("■",12); 
	for(int i=0;i<HEIGHT;i++){
		for(int j=0;j<WIDTH;j++){ 
			map[TEMP+j][TEMP+i]=0;
		}
	}
	score=0;
	count=500;
	gotoxy(X+WIDTH*2+8,Y+14);
	cout<<"Score:\t"<<score;
	gotoxy(X+28,Y-1);
	printTimes("■",8);
	for(int i=0;i<6;i++){
		gotoxy(X+28,Y+i);
		cout<<"■";
		printTimes("  ",6);
		cout<<"■";
	} 
	gotoxy(X+28,Y+6);
	printTimes("■",8);
}

由于2.0版本引入了两个方块,所以更新方块的方式比较复杂,所以直接写成了renewPie()函数。同时这一函数也用来判断游戏是否结束,所以返回值为布尔值。

函数首先将下一个方块preid、color赋值给当前方块pic,将pic的位置初始化,判断方块是否与上边界重合并不断下移:如果不重合且位置在最上方,则将这个方块打印出来,新的方块就紧贴上边界显示出来;如果不重合的位置不在最上方if(pic.ny>0),则表示已经无法添加新的方块,游戏结束。逻辑如图所示。

完成对pic的操作后,就可以更新pre,利用随机数与Piece类的更新函数就好了。

bool Tetris::renewPie(){
	pic.id=pre.id;
	pic.nx=3;
	pic.ny=-2;
	pic.color=pre.color;
	while(!judge(pic.nx,pic.ny,pic.id)){
		pic.ny++;
	}
	if(pic.ny>0) return 1;//return true则游戏结束 	
	pic.draw();
	pre.erase();
	srand((int)time(NULL)); 
	pre.renewPie(rand()%7);
	pre.draw();
	return 0;
} 

judge()函数用于判断方块是否与地图重合,由于只需要坐标、ID就可以确定一个方块的具体位置,函数形参就直接选择了(int x,int y,int id)

bool Tetris::judge(int x,int y,int id){
	for(int i=0;i<4;i++){
		for(int j=0;j<4;j++){
			if(shape[id][i][j]==1 && map[x+TEMP+i][y+TEMP+j]>0){
				return false; 
			}
		}
	}
	return true;
}

exchange()函数用于切换picpre,其中需要判断是否重合,而且加入了在低于某个高度后无法切换的限制。

void Tetris::exchange(){
	if(judge(pic.nx,pic.ny,pre.id) && pic.ny<2){
		pic.erase();
		pre.erase();
		int temp=pic.id;
		pic.id=pre.id;
		pre.id=temp;
		temp=pic.color;
		pic.color=pre.color;
		pre.color=temp;
		pic.draw();
		pre.draw();
	}
	return;
}

runGame()是界面与键盘交互的最直接的接口,与键盘的交互用了以下格式,保证了读取键盘输入的及时性,并且通过变量count控制地图的刷新速率。

while(1){
		if(i>=count){
			i=0;
			//更新地图			
		}
		if(kbhit()){	
			char ch=getch();
			switch(ch){//读取键盘信息进行操作			
			}
		}
		Sleep(1);
		i++;
	}	

函数接收键盘输入W、A、S、D、J、空格并进行相对的操作,如果游戏结束,返回1可重新执行该函数从而重新开始游戏,返回0可结束整个程序,具体实现方式通过gameOver()update()实现。

左移、右移、旋转、切换的操作相对比较简单,先判断移动之后的方块是否与背景重合,如果不重合,擦除-更新坐标-打印即可。

暂停操作则仅需调用暂停函数进入暂停界面,与开始界面、规则界面实现方法类似。

int Tetris::runGame(){//键盘输入接口 
	initMap();
	Sleep(200);
	if(renewPie()) return gameOver();
	int i=0;
	while(1){
		if(i>=count){
			i=0;
			if(update()) return gameOver();
		}
		if(kbhit()){	
			char ch;
			ch=getch();
			switch(ch){
				case 'a':case 'A':{//左移 
					if(judge(pic.nx-1,pic.ny,pic.id)){
						pic.erase();
						pic.nx--;
						pic.draw();
					}
					break;
				}
				case 'd':case 'D':{//右移 
					if(judge(pic.nx+1,pic.ny,pic.id)){
						pic.erase();
						pic.nx++;
						pic.draw();
					}
					break;
				}
				case 'w':case 'W':{//选择 
					Piece temp=pic;
					temp.span();
					if(judge(temp.nx,temp.ny,temp.id)){
						pic.erase();
						pic.span();
						pic.draw();
					}
					break;
				}
				case 's':case 'S':{//下移 
					if(judge(pic.nx,pic.ny+1,pic.id)){
						pic.erase();
						pic.ny++;
						pic.draw();
					}
					break;
				}
				case ' ':{
					pause();
					break;
				}
				case 'j':case 'J':{
					exchange();
					break;
				}
				default:
					break;
			}
		}
		Sleep(1);
		i++;
		gotoxy(X+WIDTH*2+2,Y+HEIGHT);
	}
}

pause()gameOver()的实现方式与上面同理,在调试中注意停止/开始的过程中什么需要被擦除、需要被打印,慢慢完善代码即可。

gameOver()同样注意返回1可重新执行runGame()函数从而重新开始游戏,返回0可结束整个程序,从而调整返回值。

void Tetris::pause(){
	setColor(WHITE);
	for(int i=0;i<HEIGHT;i++){
		gotoxy(X,Y+i);
		for(int j=0;j<WIDTH;j++){ 
			cout<<"■"; 
		}
	} 
	for(int i=0;i<6;i++){
		gotoxy(X+30,Y+i);
		for(int j=0;j<6;j++){ 
			cout<<"■"; 
		}
	} 
	gotoxy(X+WIDTH*2+2,Y+HEIGHT);
	while(1){
		if(kbhit()){	
			char ch;
			ch=getch();
			if(ch==' '){
				for(int i=0;i<HEIGHT;i++){
					gotoxy(X,Y+i);
					for(int j=0;j<WIDTH;j++){
						if(map[TEMP+j][TEMP+i]>0){
							setColor(map[TEMP+j][TEMP+i]);
							cout<<"■"; 
						}
						else cout<<"  "; 
					}
				}
				for(int i=0;i<6;i++){
					gotoxy(X+30,Y+i);
					for(int j=0;j<6;j++){ 
						cout<<"  "; 
					}
				} 
				setColor(pic.color);
				pic.draw();
				setColor(pre.color);
				pre.draw();
				return;
			}
		}
	}
}
int Tetris::gameOver(){
	setColor(WHITE);
	for(int i=0;i<HEIGHT;i++){
		gotoxy(X,Y+i);
		for(int j=0;j<WIDTH;j++){ 
			cout<<"■"; 
		}
	}
	gotoxy(X+WIDTH*2+8,Y+8);
	cout<<"Game Over";
	gotoxy(X+WIDTH*2+8,Y+10);
	cout<<"请按R键重新开始";
	gotoxy(X+WIDTH*2+8,Y+12);
	cout<<"或按空格退出游戏";
	while(1){
		if(kbhit()){	
			char ch;
			ch=getch();
			switch(ch){
				case ' ':{
					return 0;
				}
				case 'r':case 'R' :{
					return 1;
				}
			}
		}
	}
}

getScore()用于更新得分,形参rank为一次性消除的行数,根据行数更新得分,同时更新得分后判断是否修改count的值,以改变下落速度。

void Tetris::getScore(int rank){
		switch(rank){
			case 1:{
				score+=10;
				break;
			}
			case 2:{
				score+=40;
				break;
			}
			case 3:{
				score+=80;
				break;
			}
			case 4:{
				score+=160; 
				break;
			}
		}
		setColor(WHITE);
		gotoxy(X+WIDTH*2+8,Y+14);
		cout<<"Score:\t"<<score;
		if(count>300 && score>200)count=300;
		else if(count>200 && score>500)count=200;
		else if(count>100 && score>800)count=100;//更改速度 	
}

update()为比较核心的一个函数,方块每自动下落一格便调用一次这个函数,逻辑判断如下面的流程图所示。

int Tetris::update(){
	if(judge(pic.nx,pic.ny+1,pic.id)){//若下方有空位,自动下降   
		pic.erase();
		pic.ny++;
		pic.draw();
	}
	else{
		for(int i=0;i<4;i++){
			for(int j=0;j<4;j++){
				if(shape[pic.id][i][j]==1){
					map[pic.nx+TEMP+i][pic.ny+TEMP+j]=pic.color;
				}
			}
		}
		//方块触底 ,加入背景中 
		Sleep(100);
		int rank=0;
		for(int j=0;j<HEIGHT;j++){
			int flag=0;
			for(int i=0;i<WIDTH;i++){
				if(map[TEMP+i][TEMP+j]==0){
					break;
				}
				flag++;
			}
			if(flag==10){//一整行消除 
				rank++;
				for(int t=j;t>0;t--){
					for(int i=0;i<WIDTH;i++){
						map[TEMP+i][TEMP+t]=map[TEMP+i][TEMP+t-1];
					}
				} 
			}
		}
		if(rank>0) getScore(rank); 
		for(int i=0;i<HEIGHT;i++){
					gotoxy(X,Y+i);
					for(int j=0;j<WIDTH;j++){
						if(map[TEMP+j][TEMP+i]>0){
							setColor(map[TEMP+j][TEMP+i]);
							cout<<"■"; 
						}
						else cout<<"  "; 
					}
			} 	
		return renewPie();	
		//增加新方块 
	} 
	return 0; 
}

至此,我们的所有代码就完成了!

后记

事实上,相比起搭建这个博客,写俄罗斯方块的时间其实比我预想中的短。由于将方块封装成一个类,写起来感觉特别简洁,调试也没有花多少时间,基本上在打代码的过程中如果在哪里卡住了,调试个两三回就可以找出问题所在。花时间比较多的反而是一开始的构思,确定方块和地图如何表示,学习如何和命令行进行交互之类的。写这个程序也学到了不少Windows API的用法(虽然都是特别基础的),完成之后也是很有成就感的。

而且,通过这几天的写博客,markdown的语法也熟悉了不少,熟悉了之后用起来特别清爽。上面的流程图也是用markdown生成的(只不过这个博客模板好像不支持生成流程图,所以我在本地写完后保存成了截图),用markdown生成流程图真的特!别!好!用!(破音

所有代码已经存放到 这里 ,需要的话可以戳进去看看。