テトリスを作ろう2

JavaScript

では前回の続きを作っていこう。

ボードの作成

テトリスは落下し終わったテトリミノは下部に積み上がっていく。この盤面に保存されているテトリミノの管理をするボードを作成しよう。以下のようにハイライト部を追記修正する。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>テトリス</title>
    <style>
      body {
        background: #ddf5ff;
      }
      #container {
        margin: 0 auto;
      }
    </style>
  </head>
  <body onload="init()">
    <div id="container">
      <canvas id="cvs"></canvas>
    </div>
    <script>
      //ブロック1マスの大きさ
      const blockSize = 30;
      //ボードサイズ
      const boardRow = 20;
      const boardCol = 10;
      //キャンバスの取得
      const cvs = document.getElementById('cvs');
      //2dコンテキストを取得
      const ctx = cvs.getContext('2d');
      //キャンバスサイズ
      const canvasW = blockSize * boardCol;
      const canvasH = blockSize * boardRow;
      cvs.width = canvasW;
      cvs.height = canvasH;
      //コンテナの設定
      const container = document.getElementById('container');
      container.style.width = canvasW + 'px';

      //tetの1辺の大きさ
      const tetSize = 4;
      //T型のtet
      const tet = [
        [0, 0, 0, 0],
        [0, 1, 0, 0],
        [1, 1, 1, 0],
        [0, 0, 0, 0],
      ];

      //テトリミノのオフセット量(何マス分ずれているか)
      let offsetX = 0;
      let offsetY = 0;

      //ボード本体
      const board = [];

      //描画処理
      const draw = () => {
        //塗りに黒を設定
        ctx.fillStyle = '#000';
        //キャンバスを塗りつぶす
        ctx.fillRect(0, 0, canvasW, canvasH);

        //ボードに存在しているブロックを塗る
        for (let y = 0; y < boardRow; y++) {
          for (let x = 0; x < boardCol; x++) {
            if (board[y][x]) {
              drawBlock(x, y);
            }
          }
        }

        //テトリミノの描画
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              drawBlock(offsetX+x,offsetY+y);
            }
          }
        }
      };
      //ブロック一つを描画する
      const drawBlock = (x, y) => {
        let px = x * blockSize;
        let py = y * blockSize;
        //塗りを設定
        ctx.fillStyle = '#f00';
        ctx.fillRect(px, py, blockSize, blockSize);
        //線を設定
        ctx.strokeStyle = 'black';
        //線を描画
        ctx.strokeRect(px, py, blockSize, blockSize);
      };

      document.onkeydown = (e) => {
        switch (e.keyCode) {
          case 37: //左
            offsetX--;
            break;
          case 38: //上
            offsetY--;
            break;
          case 39: //右
            offsetX++;
            break;
          case 40: //下
            offsetY++;
            break;
        }
        draw();
      };
      //
      //初期化処理
      const init = () => {
        //ボード(20*10を0埋め)
        for (let y = 0; y < boardRow; y++) {
          board[y] = [];
          for (let x = 0; x < boardCol; x++) {
            board[y][x] = 0;
          }
        }
        //テスト用
        board[3][5]=1;

        draw();
      };
    </script>
  </body>
</html>

ポイント解説

20行10列のボード用の配列をまずは0埋めし、ブロックがあるマスに1をいれていくことにする。今回はテスト用に上から4行目,右から6個目のマスに一つブロックを描画した。
drawの大まかな処理は
黒塗りつぶし->ボードの描画(既存ブロックの描画)->移動中のテトリミノの描画
という順になる。

移動の制限(canMove関数の実装)

今は矢印キーによって、範囲外に出てしまうし、既存のブロックとぶつかってもすり抜けてしまう。これを修正して行こう。以下のようにcanMove関数を実装し、移動する前に移動できるかを確認することにする。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>テトリス</title>
    <style>
      body {
        background: #ddf5ff;
      }
      #container {
        margin: 0 auto;
      }
    </style>
  </head>
  <body onload="init()">
    <div id="container">
      <canvas id="cvs"></canvas>
    </div>
    <script>
      //ブロック1マスの大きさ
      const blockSize = 30;
      //ボードサイズ
      const boardRow = 20;
      const boardCol = 10;
      //キャンバスの取得
      const cvs = document.getElementById('cvs');
      //2dコンテキストを取得
      const ctx = cvs.getContext('2d');
      //キャンバスサイズ
      const canvasW = blockSize * boardCol;
      const canvasH = blockSize * boardRow;
      cvs.width = canvasW;
      cvs.height = canvasH;
      //コンテナの設定
      const container = document.getElementById('container');
      container.style.width = canvasW + 'px';

      //tetの1辺の大きさ
      const tetSize = 4;
      //T型のtet
      let tet = [
        [0, 0, 0, 0],
        [0, 1, 0, 0],
        [1, 1, 1, 0],
        [0, 0, 0, 0],
      ];

      //テトリミノのオフセット量(何マス分ずれているか)
      let offsetX = 0;
      let offsetY = 0;

      //ボード本体
      const board = [];

      //描画処理
      const draw = () => {
        //塗りに黒を設定
        ctx.fillStyle = '#000';
        //キャンバスを塗りつぶす
        ctx.fillRect(0, 0, canvasW, canvasH);

        //ボードに存在しているブロックを塗る
        for (let y = 0; y < boardRow; y++) {
          for (let x = 0; x < boardCol; x++) {
            if (board[y][x]) {
              drawBlock(x, y);
            }
          }
        }

        //テトリミノの描画
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              drawBlock(offsetX+x,offsetY+y);
            }
          }
        }
      };
      //ブロック一つを描画する
      const drawBlock = (x, y) => {
        let px = x * blockSize;
        let py = y * blockSize;
        //塗りを設定
        ctx.fillStyle = '#f00';
        ctx.fillRect(px, py, blockSize, blockSize);
        //線を設定
        ctx.strokeStyle = 'black';
        //線を描画
        ctx.strokeRect(px, py, blockSize, blockSize);
      };

      //指定された方向に移動できるか?(x移動量,y移動量)
      const canMove = (dx, dy) => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            //その場所にブロックがあれば
            if (tet[y][x]) {
              //ボード座標に変換
              let nx = offsetX + x + dx;
              let ny = offsetY + y + dy;
              if (
                //調査する座標がボード外だったらできない
                ny < 0 ||
                nx < 0 ||
                ny >= boardRow ||
                nx >= boardCol ||
                //移動したいボード上の場所にすでに存在してたらできない
                board[ny][nx]
              ) {
                //移動できない
                return false;
              }
            }
          }
        }
        //移動できる
        return true;
      };

      document.onkeydown = (e) => {
        switch (e.keyCode) {
          case 37: //左
            if (canMove(-1, 0)) offsetX--;
            break;
          case 38: //上
            if (canMove(0, -1)) offsetY--;
            break;
          case 39: //右
            if (canMove(1, 0)) offsetX++;
            break;
          case 40: //下
            if (canMove(0, 1)) offsetY++;
            break;
        }
        draw();
      };
      //
      //初期化処理
      const init = () => {
        //ボード(20*10を0埋め)
        for (let y = 0; y < boardRow; y++) {
          board[y] = [];
          for (let x = 0; x < boardCol; x++) {
            board[y][x] = 0;
          }
        }
        //テスト用
        board[3][5]=1;

        draw();
      };
    </script>
  </body>
</html>

いざ実行!

実行してみよう。範囲外や、既存ブロックをすり抜けなくなったことがわかる。

ポイント解説

テトリミノを構成している4つのブロックそれぞれに対して指定方向に一つ移動してみてそこが範囲外や既存のブロックが存在指定場合にfalseを返すようにしている。
判定の際はテトリミノのローカル座標をoffsetを加えてボードの座標に変換しているところがポイント。

回転を考える

スペースキーが押された時の回転を考える。

時計回りに90度回転させたテトリミノの作成

まずは90度回転させたテトリミノを作成する関数を作成する。以下のように追記。

//回転
      const createRotateTet = () => {
        //新しいtetを作る
        let newTet = [];
        for (let y = 0; y < tetSize; y++) {
          newTet[y] = [];
          for (let x = 0; x < tetSize; x++) {
            //時計回りに90度回転させる
            newTet[y][x] = tet[tetSize - 1 - x][y];
          }
        }
        return newTet;
      };

      document.onkeydown = (e) => {
        switch (e.keyCode) {
          case 37: //左
            if (canMove(-1, 0)) offsetX--;
            break;
          case 38: //上
            if (canMove(0, -1)) offsetY--;
            break;
          case 39: //右
            if (canMove(1, 0)) offsetX++;
            break;
          case 40: //下
            if (canMove(0, 1)) offsetY++;
            break;
          case 32: //space
            tet = createRotateTet();
            
        }
        draw();
      };

実行

スペースキーで回転すれば成功だ。

ポイント解説

時計周りに90度回した後にこうなっている配列を仮定してみよう。
4321
0000
0000
0000

回転前の状態はこういった配列だ。
1000
2000
3000
4000

0行目の値は回転前の0列目にあることがわかる。
なのでまず以下の関係がわかる

4321   [i][]
0000
0000
0000

1000 [][i]
2000
3000
4000

次に回転後の列の値を考える。
列0の値は回転前の行3
列1の値は回転前の行2
列2の値は回転前の行1
列3の値は回転前の行0

つまり

4321   [i][j]
0000
0000
0000

1000 [3-j][i]
2000
3000
4000

の関係ということがわかる。配列の要素数をsizeと表すと

[i][j] <= [size-1-j][i]
の関係があることがわかる。

確認してみよう

もし反時計周りに90度回す処理だとするとどのような関係になるだろうか?
これがわかればこの部分は理解しているといってよい。

回転にも制限をつける

現状回転することはできたのだが、すり抜け等が発生している。これを防止していこう。

//指定された方向に移動できるか?(x移動量,y移動量,対象tet)
      const canMove = (dx, dy , nowTet = tet) => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            //その場所にブロックがあれば
            if (nowTet[y][x]) {
              //ボード座標に変換
              let nx = offsetX + x + dx;
              let ny = offsetY + y + dy;
              if (
                //調査する座標がボード外だったらできない
                ny < 0 ||
                nx < 0 ||
                ny >= boardRow ||
                nx >= boardCol ||
                //移動したいボード上の場所にすでに存在してたらできない
                board[ny][nx]
              ) {
                return false;
              }
            }
          }
        }
        return true;
      };
      //回転
      const createRotateTet = () => {
        //新しいtetを作る
        let newTet = [];
        for (let y = 0; y < tetSize; y++) {
          newTet[y] = [];
          for (let x = 0; x < tetSize; x++) {
            //時計回りに90度回転させる
            newTet[y][x] = tet[tetSize - 1 - x][y];
          }
        }
        return newTet;
      };

      document.onkeydown = (e) => {
        switch (e.keyCode) {
          case 37: //左
            if (canMove(-1, 0)) offsetX--;
            break;
          case 38: //上
            if (canMove(0, -1)) offsetY--;
            break;
          case 39: //右
            if (canMove(1, 0)) offsetX++;
            break;
          case 40: //下
            if (canMove(0, 1)) offsetY++;
            break;
          case 32: //space
            let newTet= createRotateTet();
            if(canMove(0,0,newTet)){
              tet=newTet;
            }   
        }
        draw();
      };

ポイント解説

まず、canMove関数を変更して3つ目の引数を受け取れるようにする。その際、もし3つ目の引数がなかったら現状のtetをそのまま代入されるようにデフォルト引数で設定しておく。

デフォルト引数詳細

このようにしておくことで90度回転させて作成した新しいTetに関しても検証できるようになる。

90度回して新しいTetを作ってみて、それが適切な回転なのかをcanMoveを検証している。もし、回した結果、一部が画面外に出たり、他のブロックと重なった場合はfalseを返すのでその場合は回転がなかったことにしてそのままnewTetをゴミにしている。もし、回転して問題ないようだったらそれをtetに代入してその後操作を継続できるようにしている。


自動的に落下する処理を作ろう

それではいよいよ自動的に落下する処理を作っていこう。

初期のtet出現ポイントの設定

まずは、テストで置いておいたブロックを削除し、以下のように出現ポイントを設定する関数を作成する。

      const initStartPos=()=>{
        offsetX = boardCol / 2 - tetSize / 2;
        offsetY= 0;    
      }
      //初期化処理
      const init = () => {
        //ボード(20*10を0埋め)
        for (let y = 0; y < boardRow; y++) {
          board[y] = [];
          for (let x = 0; x < boardCol; x++) {
            board[y][x] = 0;
          }
        }
        //テスト用
        //board[3][5]=1;
        initStartPos();
        
        draw();
      };

ポイント

およその左右の中点を算出し、そこからテトリミノが出現するようにしている。

setIntervalを使って自動的に落ちるようにする。

<script>
     //落下サイクル(小さい方が速い)
      const speed = 300;
      //ブロック1マスの大きさ
      const blockSize = 30;
      //ボードサイズ
      const boardRow = 20;
      const boardCol = 10;
      //キャンバスの取得
      const cvs = document.getElementById('cvs');
      //2dコンテキストを取得
      const ctx = cvs.getContext('2d');
      //キャンバスサイズ
      const canvasW = blockSize * boardCol;
      const canvasH = blockSize * boardRow;
      cvs.width = canvasW;
      cvs.height = canvasH;
      //コンテナの設定
      const container = document.getElementById('container');
      container.style.width = canvasW + 'px';

      //tetの1辺の大きさ
      const tetSize = 4;
      //T型のtet
      let tet = [
        [0, 0, 0, 0],
        [0, 1, 0, 0],
        [1, 1, 1, 0],
        [0, 0, 0, 0],
      ];

      //テトリミノのオフセット量(何マス分ずれているか)
      let offsetX = 0;
      let offsetY = 0;

      //ボード本体
      const board = [];

      //タイマーID
      let timerId = NaN;

      //描画処理
      const draw = () => {
        //塗りに黒を設定
        ctx.fillStyle = '#000';
        //キャンバスを塗りつぶす
        ctx.fillRect(0, 0, canvasW, canvasH);

        //ボードに存在しているブロックを塗る
        for (let y = 0; y < boardRow; y++) {
          for (let x = 0; x < boardCol; x++) {
            if (board[y][x]) {
              drawBlock(x, y);
            }
          }
        }

        //テトリミノの描画
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              drawBlock(offsetX + x, offsetY + y);
            }
          }
        }
      };
      //ブロック一つを描画する
      const drawBlock = (x, y) => {
        let px = x * blockSize;
        let py = y * blockSize;
        //塗りを設定
        ctx.fillStyle = '#f00';
        ctx.fillRect(px, py, blockSize, blockSize);
        //線を設定
        ctx.strokeStyle = 'black';
        //線を描画
        ctx.strokeRect(px, py, blockSize, blockSize);
      };

      //指定された方向に移動できるか?(x移動量,y移動量,対象tet)
      const canMove = (dx, dy, nowTet = tet) => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            //その場所にブロックがあれば
            if (nowTet[y][x]) {
              //ボード座標に変換(offsetX(-2~8)+x(0~3)+移動量(-1~1)
              let nx = offsetX + x + dx;
              let ny = offsetY + y + dy;
              if (
                //調査する座標がボード外だったらできない
                ny < 0 ||
                nx < 0 ||
                ny >= boardRow ||
                nx >= boardCol ||
                //移動したいボード上の場所にすでに存在してたらできない
                board[ny][nx]
              ) {
                //移動できない
                return false;
              }
            }
          }
        }
        //移動できる
        return true;
      };
      //回転
      const createRotateTet = () => {
        //新しいtetを作る
        let newTet = [];
        for (let y = 0; y < tetSize; y++) {
          newTet[y] = [];
          for (let x = 0; x < tetSize; x++) {
            //時計回りに90度回転させる
            newTet[y][x] = tet[tetSize - 1 - x][y];
          }
        }
        return newTet;
      };

      document.onkeydown = (e) => {
        switch (e.keyCode) {
          case 37: //左
            if (canMove(-1, 0)) offsetX--;
            break;
          case 38: //上
            if (canMove(0, -1)) offsetY--;
            break;
          case 39: //右
            if (canMove(1, 0)) offsetX++;
            break;
          case 40: //下
            if (canMove(0, 1)) offsetY++;
            break;
          case 32: //space
            let newTet = createRotateTet();
            if (canMove(0, 0, newTet)) {
              tet = newTet;
            }
        }
        draw();
      };
      //繰り返し行われる落下処理
      const dropTet = () => {
        //下に行けたら
        if (canMove(0, 1)) {
          //下に行く
          offsetY++;
        } else {
        }
        draw();
      };
      const initStartPos = () => {
        offsetX = boardCol / 2 - tetSize / 2;
        offsetY = 0;
      };
      //初期化処理
      const init = () => {
        //ボード(20*10を0埋め)
        for (let y = 0; y < boardRow; y++) {
          board[y] = [];
          for (let x = 0; x < boardCol; x++) {
            board[y][x] = 0;
          }
        }
        //テスト用
        //board[3][5]=1;
        initStartPos();
       //繰り返し処理
        timerId=setInterval(dropTet,speed);

        draw();
      };
    </script>

ポイント解説

自動的に落下するようになった。speedをいじることで落下速度を制御できる。
処理的には難しいことはしていない、繰り返し処理を行うことができる
setInterval関数
を使って「下に行ければ行く」という処理を行っているだけだ。
現段階ではtimerIdは使用していない。これはゲームオーバー処理の際に必要となる。

今回はここまで

無事テトリミノが落下するようになり、だんだんテトリスっぽくなってきた。
続きは次回

ここまでの全文

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>テトリス</title>
    <style>
      body {
        background: #ddf5ff;
      }
      #container {
        margin: 0 auto;
      }
    </style>
  </head>
  <body onload="init()">
    <div id="container">
      <canvas id="cvs"></canvas>
    </div>
    <script>
      //落下サイクル(小さい方が速い)
      const speed = 300;
      //ブロック1マスの大きさ
      const blockSize = 30;
      //ボードサイズ
      const boardRow = 20;
      const boardCol = 10;
      //キャンバスの取得
      const cvs = document.getElementById('cvs');
      //2dコンテキストを取得
      const ctx = cvs.getContext('2d');
      //キャンバスサイズ
      const canvasW = blockSize * boardCol;
      const canvasH = blockSize * boardRow;
      cvs.width = canvasW;
      cvs.height = canvasH;
      //コンテナの設定
      const container = document.getElementById('container');
      container.style.width = canvasW + 'px';

      //tetの1辺の大きさ
      const tetSize = 4;
      //T型のtet
      let tet = [
        [0, 0, 0, 0],
        [0, 1, 0, 0],
        [1, 1, 1, 0],
        [0, 0, 0, 0],
      ];

      //テトリミノのオフセット量(何マス分ずれているか)
      let offsetX = 0;
      let offsetY = 0;

      //ボード本体
      const board = [];

      //タイマーID
      let timerId = NaN;

      //描画処理
      const draw = () => {
        //塗りに黒を設定
        ctx.fillStyle = '#000';
        //キャンバスを塗りつぶす
        ctx.fillRect(0, 0, canvasW, canvasH);

        //ボードに存在しているブロックを塗る
        for (let y = 0; y < boardRow; y++) {
          for (let x = 0; x < boardCol; x++) {
            if (board[y][x]) {
              drawBlock(x, y);
            }
          }
        }

        //テトリミノの描画
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            if (tet[y][x]) {
              drawBlock(offsetX + x, offsetY + y);
            }
          }
        }
      };
      //ブロック一つを描画する
      const drawBlock = (x, y) => {
        let px = x * blockSize;
        let py = y * blockSize;
        //塗りを設定
        ctx.fillStyle = '#f00';
        ctx.fillRect(px, py, blockSize, blockSize);
        //線を設定
        ctx.strokeStyle = 'black';
        //線を描画
        ctx.strokeRect(px, py, blockSize, blockSize);
      };

      //指定された方向に移動できるか?(x移動量,y移動量,対象tet)
      const canMove = (dx, dy, nowTet = tet) => {
        for (let y = 0; y < tetSize; y++) {
          for (let x = 0; x < tetSize; x++) {
            //その場所にブロックがあれば
            if (nowTet[y][x]) {
              //ボード座標に変換(offsetX(-2~8)+x(0~3)+移動量(-1~1)
              let nx = offsetX + x + dx;
              let ny = offsetY + y + dy;
              if (
                //調査する座標がボード外だったらできない
                ny < 0 ||
                nx < 0 ||
                ny >= boardRow ||
                nx >= boardCol ||
                //移動したいボード上の場所にすでに存在してたらできない
                board[ny][nx]
              ) {
                //移動できない
                return false;
              }
            }
          }
        }
        //移動できる
        return true;
      };
      //回転
      const createRotateTet = () => {
        //新しいtetを作る
        let newTet = [];
        for (let y = 0; y < tetSize; y++) {
          newTet[y] = [];
          for (let x = 0; x < tetSize; x++) {
            //時計回りに90度回転させる
            newTet[y][x] = tet[tetSize - 1 - x][y];
          }
        }
        return newTet;
      };

      document.onkeydown = (e) => {
        switch (e.keyCode) {
          case 37: //左
            if (canMove(-1, 0)) offsetX--;
            break;
          case 38: //上
            if (canMove(0, -1)) offsetY--;
            break;
          case 39: //右
            if (canMove(1, 0)) offsetX++;
            break;
          case 40: //下
            if (canMove(0, 1)) offsetY++;
            break;
          case 32: //space
            let newTet = createRotateTet();
            if (canMove(0, 0, newTet)) {
              tet = newTet;
            }
        }
        draw();
      };
      //繰り返し行われる落下処理
      const dropTet = () => {
        //下に行けたら
        if (canMove(0, 1)) {
          //下に行く
          offsetY++;
        } else {
        }
        draw();
      };
      const initStartPos = () => {
        offsetX = boardCol / 2 - tetSize / 2;
        offsetY = 0;
      };
      //初期化処理
      const init = () => {
        //ボード(20*10を0埋め)
        for (let y = 0; y < boardRow; y++) {
          board[y] = [];
          for (let x = 0; x < boardCol; x++) {
            board[y][x] = 0;
          }
        }
        //テスト用
        //board[3][5]=1;
        initStartPos();
        //繰り返し処理
        timerId=setInterval(dropTet,speed);

        draw();
      };
    </script>
  </body>
</html>

コメント

タイトルとURLをコピーしました