前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

Tetris

作者头像
ttony0
发布2022-12-26 18:40:15
6580
发布2022-12-26 18:40:15
举报

引言

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

一、初步构思

基于这学期学习的东西,我打算用 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、代码目录

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

2、print.h

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

3、piece.h

代码语言:javascript
复制
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

代码语言:javascript
复制
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、宏定义与全局常量

代码语言:javascript
复制
#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),调用后可以进行打印、擦除等操作,通过句柄实现。

代码语言:javascript
复制
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()函数,不用多说,看一下就知道用来干什么。

代码语言:javascript
复制
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 //高亮

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

代码语言:javascript
复制
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];可将每种方块都定义成一种确定的颜色。

代码语言:javascript
复制
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()函数同理,只是改成了擦除。

代码语言:javascript
复制
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<<"■"; 
			}
		}
	}
}
代码语言:javascript
复制
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即可。

代码语言:javascript
复制
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成员,调用了两个函数用来进行游戏。

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

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

代码语言:javascript
复制
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()或者开始游戏。

代码语言:javascript
复制
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()与开始界面类似,打印出游戏规则,各坐标通过不断调整得到,接收到键盘输入信号则返回。

代码语言:javascript
复制
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()函数生成一开始的游戏界面,包括地图、分数、显示下一个方块的框,并对成员变量进行了初始化。

代码语言:javascript
复制
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类的更新函数就好了。

代码语言:javascript
复制
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)

代码语言:javascript
复制
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,其中需要判断是否重合,而且加入了在低于某个高度后无法切换的限制。

代码语言:javascript
复制
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控制地图的刷新速率。

代码语言:javascript
复制
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()实现。

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

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

代码语言:javascript
复制
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可结束整个程序,从而调整返回值。

代码语言:javascript
复制
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;
			}
		}
	}
}
代码语言:javascript
复制
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的值,以改变下落速度。

代码语言:javascript
复制
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()为比较核心的一个函数,方块每自动下落一格便调用一次这个函数,逻辑判断如下面的流程图所示。

代码语言:javascript
复制
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生成流程图真的特!别!好!用!(破音

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



本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-08-09 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 一、初步构思
  • 二、实现代码
    • 1、代码目录
      • 2、print.h
        • 3、piece.h
          • 4、tetris.h
            • 5、宏定义与全局常量
            • 6、print.h
            • 7、piece.h
            • 8、tetris.h
            • 后记
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档