ゲーム開発ラボ

視覚的に楽しいアプリやゲーム開発をしながら,Javaやjavascriptを楽しく学んでいきます

テトリスを作る-6 : 揃った段を消す

前回はテトリスブロックを積み重ねる機能を追加しました
processing-p5.hateblo.jp

今回はブロックが揃った段を消す機能を実装していきます
下図が今回のプログラムの様子です
f:id:filopodia:20201128193146g:plain

コードは以下のようになりました

int col_num=10, row_num=20; // セルの数
float c_w, c_h; // セルの幅と高さ
char[] t_type = {'i', 'o', 's', 'z', 'j', 'l', 't'}; // Tetriminoの種類
int time; // 遊び時間

boolean[][] field = new boolean[col_num+6][row_num+6]; // フィールド上のセルにブロックが存在しているか
color[][] f_color = new color[col_num+6][row_num+6]; // フィールドの色

Tetrimino tetra1; // Tetriminoブロック

void setup(){
  frameRate(15);
  background(100);
  size(260, 460);
  c_w = width/(col_num+6.0);
  c_h = height/(row_num+6.0);
  
  // フィールドの初期化
  stroke(200);
  fill(255);
  for(int i=0; i<col_num+6; i++){ 
    for(int j=0; j<row_num+6; j++){
      f_color[i][j] = color(255,255,255);
      
      if(i<=2||i>=col_num+3||j>=row_num+3){
        field[i][j] = true; // 両端と底面の枠縁の部分をtrueにする        
      }
      else {
        field[i][j] = false;
      }
    }
  }
  // テトリミノの初期化
  char type = t_type[floor(random(7))];
  tetra1 = new Tetrimino(type); 
}

void draw(){
  background(100);
  stroke(200);
  for(int i=3; i<col_num+3; i++){ 
    for(int j=3; j<row_num+3; j++){
      if(!field[i][j]){fill(255);}
      else{fill(f_color[i][j]);alpha(50);}
      rect(i*c_w, j*c_h, c_w, c_h); // フィールドの描画
    }
  } 
  tetra1.display();
  if(frameCount%15==0){tetra1.fall();} // 15フレームごとにブロック落下
  
  if(tetra1.isMoving){time=15;}
  else{time--;}
  
  if(time<0){ // ブロックが着地した場合,新しいブロックを生成
    tetra1.stack();
    char type = t_type[floor(random(7))];
    tetra1 = new Tetrimino(type);
    time = 15;
  }
  
  lineClear(); // 揃った段を消去
}

void lineClear(){
  for(int j=row_num+2; j>=3; j--){
    boolean gotLine = true;
    for(int i=2; i<=col_num+2; i++){
      gotLine &= field[i][j];
    }
    if(gotLine){
      for(int jj=j; jj>=3; jj--){
        for(int i=2; i<=col_num+2; i++){
          field[i][jj] = field[i][jj-1];
          f_color[i][jj] = f_color[i][jj-1];
        }
      }
      gotLine = false;
    }
  }
}

void keyPressed(){
  tetra1.keyPressed();
}


class Tetrimino{
  char type; // i, o, s, z, j, l, t
  color c;
  int x, y; // poition
  boolean isMoving;
  boolean[][] layout = new boolean[4][4]; // ブロックの形状
  
  // constructor
  Tetrimino(char _type){
    type = _type;
    x = 8; y = 3; 
    isMoving = true;
    initiate();
  }
  
  // methods
  void initiate(){
    for(int i=0; i<4; i++){
      for(int j=0; j<4; j++){
        layout[i][j] = false;
      }
    }
    switch(type){  // 種類ごとにlayoutを決定
      case 'i':
        for(int i=0; i<4;i++){layout[i][1]=true;}
        c = color(170, 202, 255); // 水色
        break;
      case 'o':
        layout[1][1]=layout[1][2]=layout[2][1]=layout[2][2]=true;
        c = color(249, 234, 100); // 黄色
        break;
      case 's':
        layout[1][2]=layout[2][1]=layout[2][2]=layout[3][1]=true;
        c = color(0, 255, 0); // 緑色
        break;
      case 'z':
        layout[1][1]=layout[2][1]=layout[2][2]=layout[3][2]=true;
        c = color(255, 0, 0); // 赤色
        break;
      case 'j':
        layout[1][1]=layout[2][1]=layout[3][1]=layout[3][2]=true;
        c = color(0, 0, 255); // 青色
        break;
      case 'l':
        layout[1][1]=layout[2][1]=layout[3][1]=layout[1][2]=true;
        c = color(221, 90, 48); // オレンジ色
        break;
      case 't':
      default:
        layout[1][1]=layout[2][1]=layout[3][1]=layout[2][2]=true;
        c = color(121, 78, 157); // 紫色
        break;
    }    
  }
  void display(){ // ブロックの描画
    for(int i=0; i<4; i++){
      for(int j=0; j<4; j++){
        if(layout[i][j]){stroke(0);fill(c);
          rect((x+i-2)*c_w, (y+j-1)*c_h, c_w, c_h);}
        }
     }
  }
  
  void fall(){ // ブロックの落下
    if(!isCollide(layout, x, y+1)) y++; // 衝突しない場合落下
    else isMoving = false; // これ以上ブロックが落下できない場合,「ブロックが停止した」とみなす
  }
  
  void stack(){ // ブロックを積み重ねる
    for(int i=0; i<4; i++){
      for(int j=0; j<4; j++){
        if(layout[i][j]){
          field[x+i-2][y+j-1] = true;
          f_color[x+i-2][y+j-1] = c;
        }
      }
    }    
  }
  
  void keyPressed(){
    switch(keyCode){
      case SHIFT: 
        if(!isCollide(rotate(), x, y)){layout=rotate(); isMoving = true;}// 回転しても衝突しない場合
        else{ // 回転した場合衝突してしまうが,座標移動すると回転できる場合(kick)
          if(!isCollide(rotate(), x-1, y)){layout=rotate(); x--; isMoving = true;}
          else if(!isCollide(rotate(), x+1, y)){layout=rotate(); x++; isMoving = true;}
          else if(!isCollide(rotate(), x, y-1)){layout=rotate(); y--; isMoving = true;}
        }
        break; // ブロックの回転
      case LEFT :
        if(!isCollide(layout, x-1, y)){x--; isMoving = true;} 
        break;
      case RIGHT:
        if(!isCollide(layout, x+1, y)){x++; isMoving = true;}
        break;
      case DOWN :
        fall(); break;
    }    
  }
  
  boolean[][] rotate(){ // Super Rotation System
    boolean[][] tmp = new boolean[4][4]; // layoutの情報を一時的に格納
    switch(type){ // 右回転
      case 'i': // I型
      case 'o': // O型
        for(int i=0; i<4; i++){ 
          for(int j=0; j<4; j++){
            tmp[j][3-i] = layout[i][j];
          }
        }
        break;
      default: // その他
        for(int i=1; i<4; i++){ 
          for(int j=0; j<3; j++){
             tmp[j+1][3-i] = layout[i][j];
          }
        }
        break;
    }
    return tmp;
  }
  
  boolean isCollide(boolean[][] _tmp, int _x, int _y){ // ブロックが壁や他のブロックに衝突しているか
    boolean result = false;    
    boolean[][] tmp = _tmp;
    int xx = _x;
    int yy = _y;
    
    for(int i=0; i<4; i++){
      for(int j=0; j<4; j++){
        if(tmp[i][j]&&field[xx+i-2][yy+j-1]){
          result=true;
        }
      }
    }
    return result;
  }  
}


前回のコードから追加されたのは,関数lineClear()です
lineClear()では,field配列の各行について,要素がすべてtrue(=ブロックが揃っている)の行を調べています
もし要素がすべてtrueの場合,1つ前の行の要素をコピーしていくことで「段を消す」ことを実現します

void lineClear(){
  for(int j=row_num+2; j>=3; j--){ // 行を下から精査
    boolean gotLine = true;
    for(int i=2; i<=col_num+2; i++){
      gotLine &= field[i][j]; // 行の各要素について論理積をとる
    }
    if(gotLine){ // 論理積がtrueのとき
      for(int jj=j; jj>=3; jj--){
        for(int i=2; i<=col_num+2; i++){
          field[i][jj] = field[i][jj-1]; // 上の行の内容をコピー
          f_color[i][jj] = f_color[i][jj-1];
        }
      }
      gotLine = false;
    }
  }
}


今回まででテトリスの基本的なゲーム要素を実装することができました
次に行うべきは,テトリスのゲームバランスの調整となります
これは例えば,ブロックの落下速度の変化であったり,次に現れるブロックの予告,ブロックの保留などです

また,ブロックの移動・回転や段消去のアニメーションにも改善の余地が多々ありますので,これらについても随時調整をしていきたいと思います