JavaScript でテトリスを開発する その 2
前回の続きです。
前回は仕様 ① 画面部分の作成を行いました。今回は仕様 ② ブロックの描画とテトロミノの描画を行っていきましょう
① 画面サイズは縦 20 ブロック分、横 10 ブロック分である ⇒その1参照
② 4 つの正方形ブロックで構成されるテトリミノがある
③ テトリミノは 7 種類ある
④ テトリミノは画面内でかつ他ブロックに干渉しなければ自由に動かせる
⑤ テトリミノは画面内でかつ他ブロックに干渉しなければ自由に回転できる
⑥ テトリミノは画面最下部または他ブロックの上部に面したとき動かせなくなる
⑦ テトリミノは画面上部に生成される
⑧ テトリミノは一定間隔で下方向に移動する
⑨ テトリミノはランダムに生成される
⑩ 横 10 ブロック分がそろった場合、そろった列は消えて上の列が下りてくる
⑪ テトリミノ生成場所にブロックがある場合、ゲームが終了する
ブロックを描画しよう
まずはブロックの描画を行いましょう。
コード
・省略・
// テトリスプレイ画面描画処理
const drawPlayScreen = () => {
// 背景色を黒に指定
canvas2d.fillStyle = '#000';
// キャンバスを塗りつぶす
canvas2d.fillRect(0, 0, canvasWidth, canvasHeight);
// 塗りに赤を設定
canvas2d.fillStyle = '#E33';
// x,y =100, 100の場所に30×30のブロックを描画
canvas2d.fillRect(100, 100, blockSize, blockSize);
・省略・
};
解説
基本的に画面の描画時と考え方は同じです。fillRect を使って、描画する位置と範囲を決めてきます。
ここで注意すべきなのが、第1,2引数です。これらは x 軸の開始地点と y 軸の開始地点を指定するものですが、 canvas 要素内の左上を(0,0)としています。実際に引数に(0,0,blockSize,blockSize)と入れてみると分かりやすいでしょう。
第 3,4 引数の blockSize は前回決めた 1 ブロックのサイズであることもわすれないでおきましょう。
下記の描画がされていればOKです。
テトロミノを描画しよう
次は 4 つの正方形ブロックで構成されるテトリミノ を実際に描画してみましょう。
ちなみにテトロミノとは、テトリスで出てくるブロック群たちの事です。今回は Z 型テトロミノを描画します。
さて、ここであなたならどのようにテトロミノを描画しますか?先ほどの fillRect を 4 回分使って下のように記載するのはどうでしょうか?
// Z型の描画
CANVAS_2D.fillRect(100, 100, BLOCK_SIZE, BLOCK_SIZE);
CANVAS_2D.fillRect(130, 100, BLOCK_SIZE, BLOCK_SIZE);
CANVAS_2D.fillRect(130, 130, BLOCK_SIZE, BLOCK_SIZE);
CANVAS_2D.fillRect(160, 130, BLOCK_SIZE, BLOCK_SIZE);
たしかに描けなくはないです。ただし仕様 ④⑤⑦ のことを考えると、xy 座標の開始地点を動的に変化させる必要があります。さらにテトリミノは 7 種類あることも考慮するととんでもなく長いコードが必要となるでしょう。 ・・・ 課題 2
まずはこれまでの情報を整理します。
1ブロックにつき縦横30pxあります。つまり、上下左右の隣のブロックまでの距離は30pxになります。
となると、30px単位で色を塗るか塗らないかを判断すれば良さそうです。つまり、塗りたいブロックの位置を特定するには(縦Nブロック目×30px, 横Mブロック目×30px)で求められるということが分かります。
とすると、塗りたいマスの始点がどこにあるのか1マスずつ塗る必要があるかチェックすればよいということが分かります。「繰り返し」「チェックする」となると「for/while」と「if」を使えばいいと見えてきます。きっと次のようなロジックになりそうです
for(let = Y軸; Y軸 < Y軸の最大値; Y軸++) {
for(let = X軸; X軸 < X軸の最大値; X軸++){
if(塗るか塗らないかの判定) {
塗る処理
}
}
}
上から確かめていきましょう。
まず、対応しなければならないのが「Y軸の最大値」です。今回はあくまでテトリミノの描画です。要件②を思い出しましょう。さて、20ブロック分すべてチェックする必要はあるのでしょうか。
結論、なさそうです。なぜならテトリミノは4つのブロックだけで構成されているからです。そしてテトリミノの中でY軸方向に最も長くなるのは、先ほど紹介した 7 種のテトリミノの中のI型ブロックです。この時のY軸の長さは 4 ブロック一列となります。つまりテトリミノを描画するうえでのY軸の最大値は4ブロックまででよいということが分かります。
X軸はどうでしょうか。Y軸と考え方は同じです。I型が横になった時が最大ブロック数となるので、こちらも4ブロックが最大となります。つまり次のようなロジックになりそうです。
for(let = y; y < 4; y++) {
for(let = x; x < 4; x++){
if(塗るか塗らないかの判定) {
塗る処理
}
}
}
次に塗るか塗らないかの判定です。4×4マスの正方形の中で塗る場所と塗らない場所が分かればOKとなります。
上の図でいうと「1-0」や「2-2」が塗るべき場所となります。つまり「forループ内のyの値が1」でかつ「forループ内のxの値が0」のとき「true」であるという条件が書ければ行けそうです。
上記の条件にぴったりな方法があります。二次元配列です。
二次元配列とは下記のような配列の中に配列があるものでした。
let sdarrays = [
[1,2,3,4],
[5,6,7,8],
]
上記の配列内の値「6」にアクセスした場合は次のような指定をします。
let sdarrays = [
[1,2,3,4],
[5,6,7,8],
]
console.log(sdarrays[1][1]) // 6
となると、この二次元配列を使って4×4を再現できそうであることがわかりますね。そして塗りたい場所だけをtrueになるようにすればうまくif条件に当てはめられそうです。
let tet = [
[false,false,false,false],
[true,true,false,false],
[false,true,true,false],
[false,false,false,false],
]
そしてjavascript では、0 は false 0 以外は true と判定される仕様があります。つまり上のコードは次のような形に書き換えられそうです
let tet = [
[0, 0, 0, 0],
[1, 1, 0, 0],
[0, 1, 1, 0],
[0, 0, 0, 0],
]
ここまでを整理すると次のようになります
for (let y = 0; y < 4; y++) {
for (let x = 0; x < 4; x++) {
if (tet[y][x]) {
塗る処理
}
実際に塗る処理はfillRectを使えばよかったですね。
さて上記ロジックの実装として次のようなコードを描きます。
コード
・省略・
// キャンバスサイズ(=プレイ画面のサイズ)
const CANVAS_WIDTH = BLOCK_SIZE * PLAY_SCREEN_WIDTH;
const CANVAS_HEIGHT = BLOCK_SIZE * PLAY_SCREEN_HEIGHT;
CANVAS.width = CANVAS_WIDTH;
CANVAS.height = CANVAS_HEIGHT;
// テトリミノの1辺の最長
const TET_SIZE = 4;
// Z型のミノ
let tet = [
[0, 0, 0, 0],
[1, 1, 0, 0],
[0, 1, 1, 0],
[0, 0, 0, 0],
];
// テトリスプレイ画面描画処理
const drawPlayScreen = () => {
// 背景色を黒に指定
CANVAS_2D.fillStyle = '#000';
// キャンバスを塗りつぶす
CANVAS_2D.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 塗りに赤を設定
CANVAS_2D.fillStyle = '#E33';
// x,y =100, 100の場所に30×30のブロックを描画
CANVAS_2D.fillRect(100, 100, BLOCK_SIZE, BLOCK_SIZE);
// テトリミノを描画する
for (let y = 0; y < TET_SIZE; y++) {
for (let x = 0; x < TET_SIZE; x++) {
if (tet[y][x]) {
CANVAS_2D.fillRect(
x * BLOCK_SIZE,
y * BLOCK_SIZE,
BLOCK_SIZE,
BLOCK_SIZE
);
}
}
}
};
・省略・
解説
さきほどの復習もかねてあらためて解説していきます。
仕様 ⑤ テトリミノは画面内でかつ他ブロックに干渉しなければ自由に回転できる より、縦にも横にもなることから縦横 4 ブロック分が最長となります。なので4ブロックを定数TET_SIZEとしておきます。面積 16 マス分の正方形の中でテトリミノは描画されることになります。
次に二次元配列 tet を使って 16 マスの正方形を表現します。ここで 1 が入っている箇所をよく見てみましょう。Z 型のテトリミノの形担っている事が分かると思います。つまり、1 のある部分だけ色付けを行えばよい ということが分かります。
では、どうやって色付けを行うのか。それが最後の for ループの処理です 。1 度目の for ループ処理 for (let y = 0; y < TET_SIZE; y++) では 16 マス正方形の Y 軸方向、2 度目の for ループ for (let x = 0; x < TET_SIZE; x++) では X 軸方向を見ています。
そして、中の if (tet[y][x]) では該当するマスに 1 がある時に内部処理が走るようになっています。例えば上の画像でいうと (0-0) = tet[0][2] の値は 0 なので色付けはしない。(2-1)= tet[2][1]の値は 1 なので色付けが行われます。・・・☆javascript では、0 は false 0 以外は true と判定される仕様があります。
中はいつも通り、fillRect で塗っていますが、第1,2引数に注意してください。マスの位置に合わせて色を塗る場所を動的に変える必要があります。当然、1 マス= 1 ブロック= 30px となるので、1 マスずれるたびに 30px 分(=ブロックサイズ分)掛け算する必要があります。
まとめ
今回は仕様 ① にあたる、ブロックとテトリミノの描画を行いました。ポイントは下記の通りです。
① テトリミノの 1 辺あたりの最大サイズを考える
② テトリミノを二次元配列を使って表現する
③ 描画のする/しない は 1 と 0 で表現する
特に ③ はコンピューターにおける基本の考え方になるのでしっかり押さえておきましょう。また、最後の for ループの考え方は今後何回も出てきます。必ず現段階でどのような動きになっているのか理解しておきましょう。理解するために実際に配列の 0 と 1 の組み合わせなんかを変えてあげるといいかもしれないですね。
さて、次回は残りの 6 種類のテトリミノを作っていきます。
これまでのコード
// 1ブロックの大きさ
const BLOCK_SIZE = 30;
// フィールドのサイズ
const PLAY_SCREEN_WIDTH = 10;
const PLAY_SCREEN_HEIGHT = 20;
// キャンバスIDの取得
const CANVAS = document.getElementById('canvas');
// 2dコンテキストの取得
const CANVAS_2D = CANVAS.getContext('2d');
// キャンバスサイズ(=プレイ画面のサイズ)
const CANVAS_WIDTH = BLOCK_SIZE * PLAY_SCREEN_WIDTH;
const CANVAS_HEIGHT = BLOCK_SIZE * PLAY_SCREEN_HEIGHT;
CANVAS.width = CANVAS_WIDTH;
CANVAS.height = CANVAS_HEIGHT;
// テトリミノの1辺の最長
const TET_SIZE = 4;
// Z型のミノ
let tet = [
[0, 0, 0, 0],
[1, 1, 0, 0],
[0, 1, 1, 0],
[0, 0, 0, 0],
];
// テトリスプレイ画面描画処理
const drawPlayScreen = () => {
// 背景色を黒に指定
CANVAS_2D.fillStyle = '#000';
// キャンバスを塗りつぶす
CANVAS_2D.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// 塗りに赤を設定
CANVAS_2D.fillStyle = '#E33';
// x,y =100, 100の場所に30×30のブロックを描画
CANVAS_2D.fillRect(100, 100, BLOCK_SIZE, BLOCK_SIZE);
// テトリミノを描画する
for (let y = 0; y < TET_SIZE; y++) {
for (let x = 0; x < TET_SIZE; x++) {
if (tet[y][x]) {
CANVAS_2D.fillRect(
x * BLOCK_SIZE,
y * BLOCK_SIZE,
BLOCK_SIZE,
BLOCK_SIZE
);
}
}
}
};
// 画面を真ん中にする
const CONTAINER = document.getElementById('container');
CONTAINER.style.width = CANVAS_WIDTH + 'px';
// 初期化処理
const init = () => {
drawPlayScreen();
};