使用C++一步步实现俄罗斯方块后续

时间:2022-01-28 08:31:21

一、实验简介

1.1 实验内容

本节实验我们将实现俄罗斯方块主要函数的设计,完成基本功能并运行。

1.2 实验知识点

窗口的绘制
方块类的设计
旋转算法
移动、消除函数

1.3 实验环境

xface 终端
g++ 编译器
ncurses 库

1.4 编译程序

编译命令要加上 -l 选项引入 ncurses 库:

g++ main.c -l ncurses

1.5 运行程序

./a.out

1.6 运行结果

使用C++一步步实现俄罗斯方块后续

二、实验步骤

2.1 头文件

首先包含头文件以及定义一个交换函数和随机数函数,后面用到(交换函数用来做方块的旋转,随机数用来设置方块的形状)

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <sys/time.h>
#include <sys/types.h>
#include <stdlib.h>
#include <ncurses.h>
#include <unistd.h>
 
/* 交换a和b */
void swap(int &a, int &b){
 int t=a;
 a = b;
 b = t;
}
 
/* 得到一个(min,max)区间的随机整数
int getrand(int min, int max)
{
 return(min+rand()%(max-min+1));
}

2.2 定义类

由于程序内容相对简单,这里只定义了一个 Piece 类

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Piece
 {
 public:
  int score;  //得分
  int shape;  //表示当前方块的形状
  int next_shape;  //表示下一个方块的形状
 
  int head_x;  //当前方块首个box的位置,标记位置
  int head_y;
 
  int size_h;  //当前方块的size
  int size_w;
 
  int next_size_h;  //下一个方块的size
  int next_size_w;
 
  int box_shape[4][4]; //当前方块的shpe数组 4x4
  int next_box_shape[4][4];  //下一个方块的shpe数组 4x4
 
  int box_map[30][45];  //用来标记游戏框内的每个box
 
  bool game_over;  //游戏结束的标志
 
 public:
  void initial();  //初始化函数
  void set_shape(int &cshape, int box_shape[][4],int &size_w, int & size_h);  //设置方块形状
 
  void score_next();  //显示下一个方块的形状以及分数
  void judge();  //判断是否层满
  void move(); //移动函数 通过 ← → ↓ 控制
  void rotate(); //旋转函数
  bool isaggin(); //判断下一次行动是否会越界或者重合
  bool exsqr(int row); //判断当前行是否空
 
 
 };

2.3 设置方块形状

这里通过 case 语句定义了7种方块的形状,在每次下一个方块掉落之前都要调用以设置好它的形状以及初始位置

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
void Piece::set_shape(int &cshape, int shape[][4],int &size_w,int &size_h)
{
 /*首先将用来表示的4x4数组初始化为0*/
 int i,j;
 for(i=0;i<4;i++)
  for(j=0;j<4;j++)
   shape[i][j]=0;
 
 /*设置7种初始形状并设置它们的size*/
 switch(cshape)
 {
  case 0:
   size_h=1;
   size_w=4;
   shape[0][0]=1;
   shape[0][1]=1;
   shape[0][2]=1;
   shape[0][3]=1;
   break;
  case 1:
   size_h=2;
   size_w=3;
   shape[0][0]=1;
   shape[1][0]=1;
   shape[1][1]=1;
   shape[1][2]=1;
   break;
  case 2:
   size_h=2;
   size_w=3;
   shape[0][2]=1;
   shape[1][0]=1;
   shape[1][1]=1;
   shape[1][2]=1;
   break;
  case 3:
   size_h=2;
   size_w=3;
   shape[0][1]=1;
   shape[0][2]=1;
   shape[1][0]=1;
   shape[1][1]=1;
   break;
 
  case 4:
   size_h=2;
   size_w=3;
   shape[0][0]=1;
   shape[0][1]=1;
   shape[1][1]=1;
   shape[1][2]=1;
   break;
 
  case 5:
   size_h=2;
   size_w=2;
   shape[0][0]=1;
   shape[0][1]=1;
   shape[1][0]=1;
   shape[1][1]=1;
   break;
 
  case 6:
   size_h=2;
   size_w=3;
   shape[0][1]=1;
   shape[1][0]=1;
   shape[1][1]=1;
   shape[1][2]=1;
   break;
 }
 
 //设置完形状以后初始化方块的起始位置
 head_x=game_win_width/2;
 head_y=1;
 
 //如果刚初始化就重合了,游戏结束~
 if(isaggin()) /* GAME OVER ! */
  game_over=true;
 
}

2.4 旋转函数

这里用了一个比较简单的算法对方块进行旋转,类似于矩阵的旋转,先将 shape 数组进行斜对角线对称化,再进行左右对称,便完成了旋转,需要注意的是要判断旋转后方块是否出界或重合,如果是,则取消本次旋转。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void Piece::rotate()
 {
  int temp[4][4]={0}; //临时变量
  int temp_piece[4][4]={0}; //备份用的数组
  int i,j,tmp_size_h,tmp_size_w;
 
  tmp_size_w=size_w;
  tmp_size_h=size_h;
 
  for(int i=0; i<4;i++)
   for(int j=0;j<4;j++)
    temp_piece[i][j]=box_shape[i][j]; //备份一下当前的方块,如果旋转失败则返回到当前的形状
 
 
  for(i=0;i<4;i++)
   for(j=0;j<4;j++)
    temp[j][i]=box_shape[i][j]; //斜对角线对称
  i=size_h;
  size_h=size_w;
  size_w=i;
  for(i=0;i<size_h;i++)
   for(j=0;j<size_w;j++)
    box_shape[i][size_w-1-j]=temp[i][j]; //左右对称
 
 
  /*如果旋转以后重合,则返回到备份的数组形状*/
  if(isaggin()){
   for(int i=0; i<4;i++)
    for(int j=0;j<4;j++)
     box_shape[i][j]=temp_piece[i][j];
   size_w=tmp_size_w; //记得size也要变回原来的size
   size_h=tmp_size_h;
  }
 
  /*如果旋转成功,那么在屏幕上进行显示*/
  else{
   for(int i=0; i<4;i++)
    for(int j=0;j<4;j++){
     if(temp_piece[i][j]==1){
      mvwaddch(game_win,head_y+i,head_x+j,' '); //移动到game_win窗口的某个坐标处打印字符
      wrefresh(game_win);
     }
    }
   for(int i=0; i<size_h;i++)
    for(int j=0;j<size_w;j++){
     if(this->box_shape[i][j]==1){
      mvwaddch(game_win,head_y+i,head_x+j,'#');
      wrefresh(game_win);
     }
   }
 
  }
}

2.5 移动函数

如果玩家没有按下任何按键,方块需要慢速下落,所以我们不能够因为等待按键输入而阻塞在 getch() ,这里用到了 select() 来取消阻塞。

?
1
2
3
4
5
6
/* 这里只是截取了程序的一部分,具体实现请参考源码 */
struct timeval timeout;
 timeout.tv_sec = 0;
 timeout.tv_usec= 500000;
 
if (select(1, &set, NULL, NULL, &timeout) == 0)

timeout 就是我们最多等待按键的时间,这里设置了 500000us,超过这个时间就不再等待 getch() 的输入,直接进行下一步。

如果在 timeout 时间内检测到按键,则下面的 if 语句为真,得到输入的 key 值,通过判断不同的 key 值进行向左、右、下、旋转等操作。

if (FD_ISSET(0, &set))
    while ((key = getch()) == -1) ;
向左、右、下移动的函数处理方式基本相同,这里只拿向下移动的函数进行说明

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* 这里只是截取了程序的一部分,具体实现请参考源码 */
 
/* 如果输入的按键是 ↓ */
if(key==KEY_DOWN){
  head_y++; //方块的y坐标+1
  if(isaggin()){ //如果重合或出界,则取消这次移动
   head_y--;
 
   /*既然停下来了,那么把地图上对应的box设置为已被占用,用1表示,0表示未被占用
   for(int i=0;i<size_h;i++)
    for(int j=0;j<size_w;j++)
     if(box_shape[i][j]==1)
      box_map[head_y+i][head_x+j]=1;
 
   score_next(); //显示分数以及提示下一个方块
 
  }
 
  /*如果能够向下移动,那么取消当前方块的显示,向下移动一行进行显示,这里注意for循环的行要从下往上
  else{
   for(int i=size_h-1; i>=0;i--)
    for(int j=0;j<size_w;j++){
     if(this->box_shape[i][j]==1){
      mvwaddch(game_win,head_y-1+i,head_x+j,' ');
      mvwaddch(game_win,head_y+i,head_x+j,'#');
 
     }
    }
   wrefresh(game_win);
}

2.6 重复函数

每次移动或旋转之后要进行判断的函数,函数返回真则不能行动,返回假则可以进行下一步。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool Piece::isaggin(){
 for(int i=0;i<size_h;i++)
  for(int j=0;j<size_w;j++){
   if(box_shape[i][j]==1){
    if(head_y+i > game_win_height-2) //下面出界
     return true;
    if(head_x+j > game_win_width-2 || head_x+i-1<0) //左右出界
     return true;
    if(box_map[head_y+i][head_x+j]==1) //与已占用的box重合
     return true ;
   }
  }
 return false;
}

2.7 层满函数

最后一个很重要的功能是对方块已满的行进行消除,每当一个方块向下移动停止后都需要进行判断。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void Piece::judge(){
 int i,j;
 int line=0; //用来记录层满的行数
 bool full;
 for(i=1;i<game_win_height-1;i++){ //除去边界
  full=true;
  for(j=1;j<game_win_width-1;j++){
   if(box_map[i][j]==0) //存在未被占用的box
    full=false; //说明本层未满
  }
  if(full){ //如果该层满
   line++; //行满+1
   score+=50; //加分~
   for(j=1;j<game_win_width-1;j++)
    box_map[i][j]=0; //把该层清空(标记为未被占用)
  }
 }
 
 /*上面判断完后 看line的值,如果非 0 说明有层已满需要进行消除*/
 if(line!=0){
 for(i=game_win_height-2;i>=2;i--){
  int s=i;
  if(exsqr(i)==0){
   while(s>1 && exsqr(--s)==0); //查找存在方块的行,将其下移
   for(j=1;j<game_win_width-1;j++){
    box_map[i][j]=box_map[s][j]; //上层下移
    box_map[s][j]=0; //上层清空
   }
  }
 }
 
 /*清空和移动标记完成以后就要屏幕刷新了,重新打印game_win*/
 for(int i=1;i<game_win_height-1;i++)
   for(int j=1;j<game_win_width-1;j++){
    if(box_map[i][j]==1){
     mvwaddch(game_win,i,j,'#');
     wrefresh(game_win);
    }
    else{
     mvwaddch(game_win,i,j,' ');
     wrefresh(game_win);
    }
   }
 }
}

三、实验总结

到这里几个关键函数的介绍也就完成了,搞明白这些函数的功能并实现,再参考源码补全其他函数以及main函数就可以运行啦!当然俄罗斯方块的实现方法还有很多,每个人的思路和方法可能会不一样,或许你写出来的俄罗斯方块更简洁、更流畅! Enjoy it !:)

原文链接:https://www.hzl-fj.com/1243.html