ゲーム開発ラボ

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

テトリスを作る-5 : テトリスブロックを積む

前回はブロックがフィールド外にはみ出ないように,ブロックの移動を制限しました
processing-p5.hateblo.jp

今回はブロックの積み上げの機能を実装していきます
下図が今回のプログラムの様子です
f:id:filopodia:20201128111043g: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<=2||j>=row_num+3){
        field[i][j] = true; // 両端と底面の枠縁の部分をtrueにする
      }
      else {
        field[i][j] = false;
        rect(i*c_w, j*c_h, c_w, c_h); // フィールドの描画
      }
    }
  }
  // テトリミノの初期化
  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();}
  
  // ブロックが固定されるまでの時間をカウント
  if(tetra1.isMoving){time=15;} 
  else{time--;}
  
  // カウント0でブロックを固定し,新しいブロックを生成する
  if(time<0){
    tetra1.stack();
    char type = t_type[floor(random(7))];
    tetra1 = new Tetrimino(type);
    time = 15;
  }
}


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; // ブロックの情報をfield配列にコピー
          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;
  }  
}


テトリスでは,ブロックが着地してから固定されるまでの「遊び時間」があり,この遊び時間がなくなるとブロックが固定され,新しいブロックが上部から出現します
「遊び時間」の定義はゲームメーカーによってそれぞれ異なりますが,今回は以下のように定義することにしました

・ブロックが回転,移動しているときは遊び時間のカウントを開始しない
・「遊び時間」の長さはブロックの回転,移動が終了してから1秒間(=15フレーム)とする

上記のコード中では,グローバル変数timeを「遊び時間」として定義しています
ブロックが移動,回転の途中であるか否かについては,Tetriminoクラスのクラス変数isMovingで判断しています

int time; // ブロックが固定されるまでの「遊び時間」

void draw(){
  // ブロックが固定されるまでの時間をカウント
  if(tetra1.isMoving){time=15;} 
  else{time--;}
  
  // カウント0でブロックを固定し,新しいブロックを生成する
  if(time<0){
    tetra1.stack();
    char type = t_type[floor(random(7))];
    tetra1 = new Tetrimino(type);
    time = 15;
  }
}

class Tetrimino{
  boolean isMoving; // ブロックは動いているか

  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; // ブロックの情報をfield配列にコピー
          f_color[x+i-2][y+j-1] = c;
        }
      }
    }    
  }


ブロックが着地し,そして「遊び時間」が経過したのちに,ブロックの情報は新しいブロックのものに書き換えられてしまいます
そのため,着地し固定したブロックの情報が失われる前に,この情報をフィールドの情報に保存しなければなりません
この一連の作業は,クラス関数stack()の中で行われています
ブロックが着地したときの配置にしたがって,フィールド上のセルにtrueの値を書き込んでいきます

次回は列がすべて埋まった段を消去する機能を実装していきます