引言
跟着室友搞事情系列,在室友的启发下也准备自己写点什么东西,既然室友写了个华容道,那我就写一个俄罗斯方块吧。
一、初步构思
基于这学期学习的东西,我打算用 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
,并利用switch
将 rand
转换为 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()
函数。同时这一函数也用来判断游戏是否结束,所以返回值为布尔值。
函数首先将下一个方块pre
的id、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()
函数用于切换pic
与pre
,其中需要判断是否重合,而且加入了在低于某个高度后无法切换的限制。
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
生成流程图真的特!别!好!用!(破音
所有代码已经存放到 这里 ,需要的话可以戳进去看看。