ゲーム開発ラボ

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

テトリスを作る-1

テトリスは「落ちもの系パズル」の一種で,フィールド上部から落下してくるブロックを並べて消去するゲームです
今回はテトリスを再現していきたいと思います

下図が今回のアプリの様子です
f:id:filopodia:20201125091036g:plain

今回はフィールドの設定と, Tetriminoと呼ばれるブロックの実装を行います
ブロックどうしの衝突判定や,ブロックが揃った段を消去する,といった機能は実装していません

コードは以下のようになっています

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('t');  
}

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();
  if(frameCount%15==0){tetra1.y++;}
}

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 = 1;
    initiate();
  }
  
  // methods
  void initiate(){ // テトリミノの初期設定
    for(int i=0; i<4; i++){
      for(int j=0; j<4; j++){
        layout[i][j] = false;
      }
    }
    // 種類ごとにlayoutを決定
    switch(type){
      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);
        }
     }
   }
}


上のコードのうち,まずはフィールドの設定に関する箇所を確認します
テトリスのフィールドは,横10マス,縦20マスが公式の設定となっています
そこで,まずはキャンバスを10x20の四角形(セル)に分割し,フィールドを描画することから始めます
なお以下のコードでは,フィールドの周囲に1マス分ずつ余白を設けるようにしています

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

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++){
      rect(i*c_w, j*c_h, c_w, c_h); // フィールドの描画
    }
  }  
}

上のコードを実行すると,まっさらなフィールドが作成されます
f:id:filopodia:20201125095507p:plain
さて,テトリスゲームが開始されると,このフィールド上にブロックが出現し,セルが次々にブロックで埋められていくことになります
そこで,フィールド上のセルがブロックで占められているかどうかを判断する配列を用意しましょう

以下のコードは,boolean型を格納する配列fieldを宣言,初期化するコードを含んだものです
boolean型配列fieldは,フィールド上の各セル上にブロックが存在するかどうかの情報を格納するためのもので,セル上にブロックが存在するときはtrue,セル上にブロックが存在しないときはfalseを格納します

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

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

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); // フィールドの描画
    }
  } 
}

ひとまずフィールドの設定が完了したので,次にテトリスのブロックTetriminoの設定を行いましょう
Tetriminoは全部で7種類存在し,それぞれに名前が付いています

f:id:filopodia:20201125102057p:plain

上図の上段左からI型,J型,L型,O型,下段左からS型,T型,Z型となります
それぞれのTetriminoをコード上で実装するにあたって,まずはTetriminoのクラスを定義します
Tetriminoクラスは,クラス変数としてブロックの種類と座標の情報を持つようにします
以下がTetriminoクラスを定義するコードとなります

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 = 1;
    initiate();
  }
  
  // methods
  void initiate(){ // テトリミノの初期設定
    for(int i=0; i<4; i++){
      for(int j=0; j<4; j++){
        layout[i][j] = false;
      }
    }
    // 種類ごとにlayoutを決定
    switch(type){
      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);
        }
     }
   }
}


Tetriminoクラスのインスタンス生成時にはchar型のデータをコンストラクタ引数として渡すようにします
アルファベットの 'i' が引数として渡されたときはI型ブロックが,'z' のときはZ型が,というようにインスタンスが生成されます

上記のコードではTetriminoを描画するにあたって,まず4x4のboolean型配列layoutを用意しています
このbool型配列layoutには,trueとfalseの値をブロックの形状になるように格納します
たとえば,T型ブロックの場合は下図の灰色の部分に相当する配列要素にtrueの値を格納し,白色の部分に相当する配列要素にfalseの値を格納します
f:id:filopodia:20201125133444p:plain
この処理は,コード内では次のように表記しています

 layout[1][1]=layout[2][1]=layout[3][1]=layout[2][2]=true;


4x4のboolean型配列に対して,trueの値を各Tetraminoブロックの形状に相当するように格納していき,
次に,上記配列についてtrueの値が格納されている配列要素に対してのみ,四角形を描画する処理をクラス関数display()で行なっています

  void initiate(){ // テトリミノの初期設定
    for(int i=0; i<4; i++){
      for(int j=0; j<4; j++){
        layout[i][j] = false;
      }
    }
    // 種類ごとにlayoutを決定
    switch(type){
      case 'i':
        for(int i=0; i<4;i++){layout[i][1]=true;} // I型ブロックの形状にtrueを格納
        break;
      case 'o':
        layout[1][1]=layout[1][2]=layout[2][1]=layout[2][2]=true; // O型ブロック
        break;
      case 's':
        layout[1][2]=layout[2][1]=layout[2][2]=layout[3][1]=true; // S型ブロック
        break;
      case 'z':
        layout[1][1]=layout[2][1]=layout[2][2]=layout[3][2]=true; // Z型ブロック
        break;
      case 'j':
        layout[1][1]=layout[2][1]=layout[3][1]=layout[3][2]=true; // J型ブロック
        break;
      case 'l':
        layout[1][1]=layout[2][1]=layout[3][1]=layout[1][2]=true; // L型ブロック
        break;
      case 't':
      default:
        layout[1][1]=layout[2][1]=layout[3][1]=layout[2][2]=true; // T型ブロック
        break;
    }    
  }
  void display(){
    for(int i=0; i<4; i++){
      for(int j=0; j<4; j++){
        if(layout[i][j]){ // trueの値が格納されている配列要素に対して,四角形を描画
          stroke(0);fill(200);
          rect((x+i-2)*c_w, (y+j-2)*c_h, c_w, c_h);
        }
     }
   }


以上でフィールドおよびTetriminoブロックの基本的な記述をすることができました
次回以降は,Tetriminoブロックの回転や衝突判定,段の消去といった機能を実現していきたいと思います