では前回の続きを作っていこう。
ボードの作成
テトリスは落下し終わったテトリミノは下部に積み上がっていく。この盤面に保存されているテトリミノの管理をするボードを作成しよう。以下のようにハイライト部を追記修正する。
<!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();
};
実行
スペースキーで回転すれば成功だ。
ポイント解説
回転前の配列oldを以下と仮定しよう。
1000
2000
3000
4000
これを時計周りに90度回し以下の配列を作る処理を作成する。(new)
4321
0000
0000
0000
new[0][0]にはold[3][0]の値が入る
new[0][1]にはold[2][0]の値が入る
new[0][2]にはold[1][0]の値が入る
new[0][3]にはold[0][0]の値が入る
この関係から
new[a][b]=old[3-b][a]
の関係があることが予測される。
すべての値に対して有効なのか,もう1列考えてみる。
[old]
0100
0200
0300
0400
[new]
0000
4321
0000
0000
new[a][b]にはold[3-b][a]
この関係が成り立つとして、aに1,bに0を入れてみる
new[1][0] -> 4
old[3][1]->4
一致している。その他
aに1,bに1
aに1,bに2
aに1,bに3
を入れたときも一致している。
以上から時計回りに90度回す処理は
new[a][b]=old[3-b][a]
と表せることがわかる。
3というのは配列要素数-1なので
列と行の数の等しい2次元配列を時計回りに90度回転させた新しい配列をつくる処理は一般的に以下でかける。
const arr = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
const arr2 = [];
for (let a = 0; a < arr.length; a++) {
arr2[a] = [];
for (let b = 0; b < arr[a].length; b++) {
arr2[a][b] = arr[arr.length - 1 - b][a];
}
}
console.log(arr2);
考察してみよう
もし反時計周りに90度回す処理や180度回す処理だった場合どのような関係になるだろうか?
ぜひ考察してみてもらいたい。
[反時計回り90度]
わかりやすいように
もとの並びを
[old]
4321
0000
0000
0000
回転後の並びを
[new]
1000
2000
3000
4000
と考える。
同じように共通している部分を抜き出す
new[0][0] = old[0][3]
new[1][0]=old[0][2]
new[2][0]=old[0][1]
new[3][0]=old[0][0]
以上から
new[a][b]=old[b][3-a]
一般項として
new[a][b]=old[b][old.length-1-a]
の関係があることがわかる
[180度回転]
[old]
0000
0000
0050
4321
[new]
1234
0500
0000
0000
new[0][0] = old[3][3]
new[0][1]=old[3][2]
new[0][2]=old[3][1]
new[0][3]=old[3][0]
new[a][b]=old[3-a][3-b]
aとbに1を入れて一致するか確認
new[1][1] =>5
new[2][2]=>5
new[a][b]=old[old.length-1-a][old.length-1-b]
回転にも制限をつける
現状回転することはできたのだが、すり抜け等が発生している。これを防止していこう。
//指定された方向に移動できるか?(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>
コメント