ゲーム開発ラボ

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

テトリスを作る-2 : ブロックを回転する

前回はフィールドと,テトリスのブロック(Tetrimino)の基本的な設定を行いました
processing-p5.hateblo.jp

今回はTetriminoの回転についてコードに記述していきます
出来上がるプログラムの様子は以下となります
f:id:filopodia:20201125161020g:plain

現時点でのコードは以下の通りです

int col_num=10, row_num=20; // セルの数
float c_w, c_h; // セルの幅と高さ

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

Tetrimino tetra1;

void setup(){
  frameRate(15);
  background(100);
  size(200, 400);
  c_w = width/(col_num+2.0);
  c_h = height/(row_num+2.0);
  translate(c_w, c_h);
  
  // フィールドの初期化
  stroke(200);
  fill(255);
  for(int i=0; i<col_num; i++){ 
    for(int j=0; j<row_num; j++){
      field[i][j] = false;
      rect(i*c_w, j*c_h, c_w, c_h); // フィールドの描画
    }
  } 
  // テトリミノの初期化
  tetra1 = new Tetrimino('l');  
}

void draw(){
  fill(255);
  stroke(200);
  translate(c_w, c_h);
  for(int i=0; i<col_num; i++){ 
    for(int j=0; j<row_num; j++){
      rect(i*c_w, j*c_h, c_w, c_h); // フィールドの描画
    }
  } 
  tetra1.display();
}

void keyPressed(){ // 方向キーを押したときの動作
  tetra1.keyPressed();
}


class Tetrimino{
  char type; // i, o, s, z, j, l, t
  int x, y; // poition
  boolean[][] layout = new boolean[4][4];
  
  // constructor
  Tetrimino(char _type){
    type = _type;
    x = 5; y = 5;
    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;}
        break;
      case 'o':
        layout[1][1]=layout[1][2]=layout[2][1]=layout[2][2]=true;
        break;
      case 's':
        layout[1][2]=layout[2][1]=layout[2][2]=layout[3][1]=true;
        break;
      case 'z':
        layout[1][1]=layout[2][1]=layout[2][2]=layout[3][2]=true;
        break;
      case 'j':
        layout[1][1]=layout[2][1]=layout[3][1]=layout[3][2]=true;
        break;
      case 'l':
        layout[1][1]=layout[2][1]=layout[3][1]=layout[1][2]=true;
        break;
      case 't':
      default:
        layout[1][1]=layout[2][1]=layout[3][1]=layout[2][2]=true;
        break;
    }    
  }
  void display(){
    // 種類ごとに描画
    for(int i=0; i<4; i++){
      for(int j=0; j<4; j++){
        if(layout[i][j]){stroke(0);fill(200);
          rect((x+i-2)*c_w, (y+j-2)*c_h, c_w, c_h);}
        }
     }
  }
  
  void keyPressed(){
    switch(keyCode){
      case UP   : rotate(); break; // ブロックの回転
      case LEFT : x--; break;
      case RIGHT: x++; break;
      case DOWN : y++; break;
    }    
  }
  
  void rotate(){ // Super Rotation System
    boolean[][] tmp = new boolean[4][4]; // layoutの情報を一時的に格納
    for(int i=0; i<4;i++){ // deep copy
      for(int j=0; j<4; j++){
        tmp[i][j] = layout[i][j];
      }
    }
    switch(type){ // 右回転
      case 'i': // I型
      case 'o': // O型
        for(int i=0; i<4; i++){ 
          for(int j=0; j<4; j++){
            layout[i][j] = tmp[j][3-i];
          }
        }
        break;
      default: // その他
        for(int i=1; i<4; i++){ 
          for(int j=0; j<3; j++){
            layout[i][j] = tmp[j+1][3-i];
          }
        }
        break;
    }     
  }
}


前回のコードから変化した点について詳しくみていきましょう
一番の変更点は,Tetriminoクラスの中に,クラス関数としてrotate()を定義していることです
rotate()関数ではTetriminoブロックの形状を右方向に回転しています

class Tetrimino{

  void rotate(){ // Super Rotation Systemに基づき、ブロックを右回転
    boolean[][] tmp = new boolean[4][4]; // layoutの情報を一時的に格納
    for(int i=0; i<4;i++){ // deep copy
      for(int j=0; j<4; j++){
        tmp[i][j] = layout[i][j];
      }
    }
    switch(type){ // ブロックの種類ごとに配列を回転
      case 'i': // I型
      case 'o': // O型
        for(int i=0; i<4; i++){ 
          for(int j=0; j<4; j++){
            layout[i][j] = tmp[j][3-i];
          }
        }
        break;
      default: // その他
        for(int i=1; i<4; i++){ 
          for(int j=0; j<3; j++){
            layout[i][j] = tmp[j+1][3-i];
          }
        }
        break;
    }     
  }
}


Tetriminoの回転の仕方については,ゲーム会社によってルールが異なるようです
今回は,Super Rotation Systemと呼ばれる回転のルールに則ることにしました
f:id:filopodia:20201125173803p:plain
任天堂はこのSuper Rotation Systemに基づいたルールを採用しているそうですが,セガなどはSega Rotation Systemという別のルールを採用しているようです

上記コードでは,ブロック形状のデータを格納している4x4のBoolean型配列layoutについて,まずtmpという配列にコピーをとったあとに,配列の回転操作を行なっています
なお,layoutのデータをtmpにコピーするときには注意が必要です
配列やオブジェクトのコピーにはshallow copy(浅いコピー)とdeep copy(深いコピー)というものが存在していますが,今回のコードではdeep copyである必要があります

以下がdeep copyを行なっている部分です

    for(int i=0; i<4;i++){ // deep copy
      for(int j=0; j<4; j++){
        tmp[i][j] = layout[i][j];
      }
    }


これをもし以下のように記述してしまうとshallow copyとなり,ブロックの形状がくずれてしまいます

  // shallow copy
  tmp = layout; 

配列をdeep copyした場合,同じ要素を持った配列の完全なコピーができあがります。このとき,片方の配列の要素を変化させても,もう片方の配列には影響はありません
deep copyとは,瓜二つの存在を作り出す操作のことです
これに対して,配列をshallow copyした場合,名前は異なるけれど要素を共有しているペアの配列ができあがります。このとき片方の配列の要素を変化させると,もう片方の配列にも同様の変化が発生します
shallow copyとは,名前は異なるけれど本体は1つしかない,別名のペアを作り出す操作のことです


コードを記述する際には,deep copyshallow copyの違いを頭の片隅に置いておき,これらをうまく活用しましょう
次回はブロックと枠組みの衝突判定について実装していきます