テトリスもどきゲーム

「テトリスもどき」ゲームのプログラムを紹介します。

  1. ゲームのルール

     正方形4個から構成されるピースを、回転や左右に移動しながら、適当な場所に誘導して落とします。落ちたピースを構成する4角(ダイス)が、 左壁から右まで連続するとその行は削除されます。削除出来ないピースが上まで積み上がると、ゲームオーバーになります。

     アプレット上でクリックし、sキーを押すとゲームが開始します。左右の矢印キーで左右に、下キーで下に早く落ちます。 スペースキーでピースを回転します。横の線上をピースで埋めるとそのラインが消去(クリア)されます。一定以上をクリアすると、自然落下の速度が速くなります。

  2. プログラムの構造

    1. データ構造

      以下のような配列を利用しています。
      int area[][] = new int [14][24]; //背景のピース記憶領域
      int piece[][] = new int [5][5]; //ブィールド生成領域
      int bx,by;   //ピースの座標軸

      int nline;    //消したラインの総数
      int nscore;   //現在のスコア
      int nlevel;    //現在のレヴェル
      int nfast=500;  //ピースの落下速度

    2. ゲームのフィールドの表現

       ゲームのフィールド(落下する空間)は 14*24 の格子で構成します。これを配列 area[][] で記録します。フィールドの左右2列(area[0][]、area[1][]、area[12][]、area[13][])と下の2行を(area[][22]、area[][23])に -1 を記録し、ピースが入り込めない「壁」とします。それ以外の、中央部ではピースのないフィールドを 0,ピースのあるフィールドにピースの番号(1..7)を記録します。落ちるモノをここではピースと呼んでいます。

    3. ピースのデータ構造

       このゲームで工夫がいるのはピースの回転です。この回転を容易にするピースの表現方法が重要です。回転で広い領域を使うのは下図の直線型のピースで、赤の位置を中心として右回転させます。


       そこで、ピースの形状を5*5の配列 piece[i][j] で記憶し、ピースを構成する小さな四角(ダイスと呼びます)のない要素を0、ダイスのある位置を n(ピースの番号)で表現します。piece[0][0]は左上隅の点とします。ピースの位置は、中央の赤いダイスの位置とし、変数 bx by に記録します。
    4. ピースの作成

       bpをピースの種類を表す番号とします。中心と、その下にはかならずダイス(4角)があるものとします。
       piece[2][2] = bp;
       piece[2][3] = bp;
      他の二つを、ピースの種別で次のように設定します。
            case 2:
              piece[3][1] = bp;
              piece[3][2] = bp;
              break;
            case 3:
              piece[1][2] = bp;
              piece[1][3] = bp;
              break;
    5. ピースの回転

       一般に原点(0,0)を中心とする右回転は(x,y)座標を(y,x)座標に入れ替え、適当に符号を変更すれば、回転が表現できます。ただし、ここでは原点は配列の左上隅ですから、j の符号の入れ替えは -j でなく、4-j とします。たとえば、(2,4)のダイスは右回転で(0,2)に移動します。したがって、
            copy_piece[4-j][i] = piece[i][j]; 
      で右回転が表現できます。
    6. ピースの移動、可能性

      5*5 のピースを配置する中心位置を (bx,by) とします。各、i ,j について
           (piece[i][j] >= 1 && area[bx + i - 2][by + j - 1] == 0)
      であれば、つまり、ピースのダイスがある位置のフィールドが空白であれば、その位置にピースを移動することができます。bx + i - 2 の -2 はピースの中心から左端までの距離です。by + j - 1 の -1 は、ピースを一つ下に落としたときの、中心から上端までの距離になります。このチェックを checkPiece(piece,x,y) で行います。
       ピースを落とす(byを増加する)ことが出来なくなったら、ピースの番号を対応するフィールドの対応位置(area[][])にコピーします。
    7. 行の消去判断と詰め込み

      消去可能かどうかの判定は簡単です。area[][]のi行にすべてに 0 のマスがなければ、i番目の行を消去可能ですから
            ld=1;
           for(j=2; j<12; j++) ld = ld * area[j][i];
      で、ld が 0 でなければ、横方向にすべてダイスがありますから、削除可能です。行を削除するには、上の行を1行下げれば良いので、
          for(k=i-1;k>0;k--)
            for(s=2;s<12;s++) area[s][k+1]=area[s][k];
      となります。
    8. ゲームの得点とレベル制御

       消した行数で、得点が決まります。また、消した行の平方根でレベルを定め、レベルが高くなると、落下時間の間隔を短くしています。
    9. キーイベント処理

       キーイベントを処理するには、 keyPressed(KeyEvent e)を利用します。 押されたキーの種類は keyCode で知ることができます。方向キーは LETF、RIGHT などで指定できます。
    10. 音処理

      コメントにしてありますが、minim ライブラリーを利用して、行クリア時などに効果音を出す機能です
  3. ゲームプログラム

     サンプル的なプログラムです。
    //テトリスもどき
    //フォントが必要 MS-Gothic-48.vlw
    //音用ライブラリ
    //import ddf.minim.*;
    //Minim minim;//音のクラス
    //AudioSample sound_roll, sound_clear;
    
    PImage backImage;
    PFont font;
    
    int area[][] = new int[14][24];//ピース記憶領域
    int piece[][] = new int[5][5];//ブロック生成領域
    int piece_next[][] = new int[5][5];//次のピース
    int copy_piece[][] = new int[5][5];
    
    int preFrame;//前の時刻
    int startTime = 0;//時間
    int playTime;//利用時間
    int nline = 0;//消したラインの総数
    
    int nlevel = 1;//現在のレベル
    int nfast = 800;//落下時間間隔
    
    int bx, by;//落下中のピースの座標
    
    int x = 2, y = 0;
    int z = 0;
    boolean rs;
    
    void setup() {
    
      size(320, 440);
      frameRate(45);
    
      //minim = new Minim(this);
      //sound_roll = minim.loadSample("meka_ta_sui01.mp3");
      //sound_clear = minim.loadSample("magic23.mp3");
    
      backImage = createBackgroundImage();
    
      font = loadFont("MS-Gothic-48.vlw");
    
      bx = 7; 
      by = 2;
      getNewPiece(piece_next);
      updatePiece();
    
      for (int i = 2;i < 12;i++)
        for (int j = 0;j < 22;j++) area[i][j] = 0;
    
      startTime = millis();
      preFrame = startTime;
    
      //0 : ピースが存在しない、-1 :壁
      //左右の壁
      for (int i = 0;i < 22;i++) {
        area[1][i] = -1;
        area[12][i] = -1;
      }//for i
      //下の壁
      for (int i = 2;i < 12;i++) {
        area[i][22] = -1; 
        area[i][23] = -1;
      }//for i
    }//setup
    
    void draw() {
      int i, j, count;
    
      //背景フィールド再描画 
      image(backImage, 0, 0);
    
      //落とす時間がチェック
      int nowFrame = millis();
      int frameSpan = nowFrame - preFrame;
      if (frameSpan >= nfast) {
        if (by <= 22)
          by++;//ピースを落とす
        preFrame = nowFrame;
      }//if frame
      //利用時間
      playTime = (int)((nowFrame - startTime) / 1000);
    
      //ピースをひとつ下に落下が可能かどうか?
      if (!checkPiece(piece, bx, by+1)) {
        //落下は不可:ピースをフィールドにおく
        for (i = 0;i < 5;i++)
          for (j = 0;j < 5;j++)
            if (piece[i][j] >= 1) area[bx + i - 2][by + j - 2] = piece[i][j];
    
        Check_Clear();//行クリアできたか
        updatePiece();//次のピース作成
        bx = 7; 
        by = 2;//最初の位置
      }//if count
    
      //落下中のピースを描く
      for (i = 0;i < 5;i++)
        for (j = 0;j < 5;j++) Draw_Block(piece[i][j], i + bx - 3, j + by - 3);
      //エリアのピースを描く
      for (i = 0;i < 12;i++)
        for (j = 2;j < 22;j++) Draw_Block(area[i][j], i - 1, j - 2);
      //次のピースを描画
      for (i = 1;i < 5;i++)
        for (j = 1;j < 5;j++) Draw_Block(piece_next[i][j], i + 11, j + 3);
    
      if (area[7][2] >= 1) {
        //ゲームオーバー
        noLoop();//停止
    
        fill(255, 0, 0);
        textFont(font, 32);
        textAlign(CENTER, CENTER);
        text("GameOver", 120, height / 2 - 10);
      }//if gameover
    
      //数字の描画 
      textFont(font, 18);
      fill(0);
      textAlign(RIGHT);
    
      text(int(playTime), 294, 48);//時間
      text(int(  nlevel), 294, height - 80);//現在のレベル
      text(int(   nline), 294, height - 26);//消したラインの数
    }//draw
    
    void stop() {
      super.stop();
    
      //sound_roll.close();
      //sound_clear.close();
      //minim.stop();
    }//stop
    
    void keyPressed() {
    
      if (keyCode==LEFT) {
        Move_Left();
      }//if left
      else if (keyCode == RIGHT) {
        Move_Right();//右移動
      }//if right
      else if (keyCode == DOWN) {
        Move_Down();//下移動
      }//if down
      else if (keyCode == UP) {
        Roll_piece();
      }//if up
    
      // repaint();
    }//keypress
    
    //次ぎに落とすピースを作成
    void updatePiece() {
      for (int i = 0;i < 5;i++)
        for (int j = 0;j < 5;j++) piece[i][j] = piece_next[i][j];
      getNewPiece(piece_next);
    }//createBlock
    
    //ピースの要素を描画
    void Draw_Block(int blockID, int addressX, int addressY) {
      if (blockID >= 1) {
        stroke(64);
        //番号から色を決定
        fill(0 + blockID * 22, 150 + blockID % 2 * 70, 100 + blockID * 25);
        rect(addressX * 20, 20 + addressY * 20, 20, 20);
      }//if blockID
    }//draw_block
    
    public int[][] getNewPiece(int[][] newPiece) {
      //int[][] newPiece = new int[5][5];
      int pieceID = (int)(Math.random() * 140000) % 7 + 1;
    
      for (int i = 0;i < 5;i++)
        for (int j = 0;j < 5;j++) newPiece[i][j] = 0;
    
      newPiece[2][2] = pieceID;
      newPiece[2][3] = pieceID;
      switch(pieceID) {
      case 1:
        newPiece[1][1] = pieceID;
        newPiece[1][2] = pieceID;
        break;
      case 2:
        newPiece[3][1] = pieceID;
        newPiece[3][2] = pieceID;
        break;
      case 3:
        newPiece[1][2] = pieceID;
        newPiece[1][3] = pieceID;
        break;
      case 4:
        newPiece[2][1] = pieceID;
        newPiece[3][3] = pieceID;
        break;
      case 5:
        newPiece[2][1] = pieceID;
        newPiece[1][3] = pieceID;
        break;
      case 6:
        newPiece[1][3] = pieceID;
        newPiece[3][3] = pieceID;
        break;
      case 7://I 型ピース
        newPiece[2][1] = pieceID;
        newPiece[2][4] = pieceID;
        break;
      }//switch
      return newPiece;
    }//getnewpiece
    
    public void Check_Clear() {
      //ピースが消えるかどうかを判定・ピースの消去
      int i, j, s, k;
      int ld;
      boolean cleared = false;
    
      //ラインが消せるかどうか判定
      for (i = 21;i > 0;i--) {
        ld = 1;
        for (j = 2;j < 12;j++)ld = ld * area[j][i];
    
        if (ld != 0) {
          //一行が揃った
          cleared = true;
          //上からプレスする
          for (k = i - 1;k > 0;k--)
            for (s = 2;s < 12;s++) area[s][k + 1] = area[s][k];
          i++;
          nline++;
        }//if ld
      }//for i
    
      if (cleared) {
        //レベル上げの処理
        delay(100);
        //level up
        nlevel=(int)sqrt(nline+10);
        nfast = 850 - (50 * nlevel);
        //最低でも0.1秒
       if (nfast <= 100) nfast = 100;
      }//if cleared
    }//check_clear
    
    public void Move_Down() {
      if (checkPiece(piece, bx, by+1)) by++;
    }//down  
    
    void Move_Right() {
      if (checkPiece(piece, bx+1, by)) bx++;
    }//right
    
    void Move_Left() {
      if (checkPiece(piece, bx-1, by)) bx--;
    }//left
    
    public void Roll_piece() {
      int i, j, count;
      count = 0;
      //仮回転体生成
      for (i=0;i<5;i++)
        for (j=0;j<5;j++) copy_piece[4 - j][i] = piece[i][j];
    
      //回転可能なら回転する
      if (checkPiece(copy_piece, bx, by)) {
        //生成したピースを回転後の形に更新
        for (i = 0;i < 5;i++)
          for (j = 0;j < 5;j++) piece[i][j] = copy_piece[i][j];
        //sound_roll.trigger();
      }//if
    }//roll
    
    boolean checkPiece(int[][] piece, int bx, int by) {
      int count=0;
      for (int i = 0;i < 5;i++) {
        for (int j = 0;j < 5;j++)
          if (piece[i][j] >= 1 && area[bx + i - 2][by + j - 2] == 0) count++;
      }
      if (count==4) return true;
      else return false;
    }
    
    
    PImage createBackgroundImage() {
    
      PFont ft = createFont("MS Gothic", 14);
      fill(232, 240, 255);//background
      noStroke();
      rect(0, 0, width, height);
    
      //white background
      fill(255);
      stroke(208);
      rect( 20, 20, 200, height - 40);//game field
      rect(240, 10, 60, 46);//time back
      rect(240, 70, 60, 114);//next back
      rect(240, height - 123, 60, 46);//level back
      rect(240, height -  67, 60, 46);//line back
    
      //label background
      fill(32, 80, 128);
      noStroke();
      rect(241, 11, 59, 15);//time
      rect(241, 71, 59, 15);//next
      rect(241, height - 123, 59, 16);//level
      rect(241, height -  67, 59, 16);//line
    
      //text
      textFont(ft);
      fill(0, 6, 16);
      textAlign(RIGHT, BASELINE);
      text("←→:move ↑:roll ↓:fall", width, height - 2);//guide
    
      fill(250);
      textAlign(LEFT, BASELINE);
      text( "TIME", 246, 24);
      text( "NEXT", 246, 84);
      text("LEVEL", 246, height - 109);
      text( "LINE", 246, height -  53);
    
      return get();
    }

  4. 発展

     さまざまなゲームが出ていますが、このゲームはゲームを創作するときの良いヒントになるかと思います。