テトリスを作る - 3 衝突判定
次のステップは、テトリスゲームの核心部分である衝突判定と、プレイヤーがブロックを操作するためのキー入力処理の実装です。これが機能しないと、ブロックは画面外に永遠に落ちていってしまいます。
今回は、まず衝突判定のロジックを実装し、そのあとで落下処理やキー入力に組み込みます。
ステップ 5: 衝突判定(最重要ロジック)の実装
テトリミノが移動しようとする先に、①壁、②底、または③固定されたブロックがあるかをチェックします。
衝突判定関数 checkCollision の作成
ゲーム盤の枠と、落下して固定されたブロックに当たると、Trueとする衝突判定を組み込み、それ以上動かないようにします。
具体的には左右の壁、底、ブロックにあたれば判定をするようにします。
/**
* 指定された位置 (offset) にテトリミノを移動または回転させたときに、
* ボードの境界や固定されたブロックと衝突するかどうかを判定する。
* @param {object} piece - 現在のテトリミノオブジェクト (matrix, pos)
* @param {number} offsetX - X軸方向の移動量 (通常 -1, 0, 1)
* @param {number} offsetY - Y軸方向の移動量 (通常 0, 1)
* @returns {boolean} 衝突する場合は true、しない場合は false
*/
function checkCollision(piece, offsetX, offsetY) {
const matrix = piece.matrix;
const currentX = piece.pos.x;
const currentY = piece.pos.y;
for (let y = 0; y < matrix.length; y++) {
for (let x = 0; x < matrix[y].length; x++) {
// ブロックのマス(値が0ではない)のみをチェック
if (matrix[y][x] !== 0) {
// 移動後のボード上の座標を計算
const nextX = currentX + x + offsetX;
const nextY = currentY + y + offsetY;
// 衝突条件 1: ボードの左右の壁、または底を超えてしまう
if (nextX < 0 || nextX >= BOARD_WIDTH || nextY >= BOARD_HEIGHT) {
return true; // 衝突 (壁または底)
}
// 衝突条件 2: ボードの上端を超えてしまう (上端に生成されたばかりのピースは除く)
// nextY < 0 の場合は、落下中であれば通常衝突とは見なさない。
if (nextY < 0) {
continue; // 処理をスキップ
}
// 衝突条件 3: 既に固定されたブロック (board[nextY][nextX] が 0 ではない) と重なる
if (board[nextY] && board[nextY][nextX] !== 0) {
return true; // 衝突 (固定ブロック)
}
}
}
}
return false; // 衝突なし
}ステップ 6: 落下処理の修正(衝突判定の組み込み)
ステップ4で作成した落下処理の部分について衝突判定を組み込みます。
gameLoop()の中の
if (currentPiece) {
currentPiece.pos.y++;
}を
pieceDrop()に変更します。
mergePieceToBoard の作成
着地したテトリミノを、データとしてボード配列に書き込む関数です。
着地したタイミングで
(value !== 0)
の場合としてテトリミノの2次元配列の中で数字が入っている部分を判定してboardの二次元配列のマスのに数字を入れます。
この部分が2つの二次元配列が重なる部分の判定となるので少し分かりにくいところです、
数字が入ることで色がつくきブロックが固定されたように見えるようになります。
/**
* 現在のテトリミノをボード(2次元配列)に固定する
* @param {object} piece - 現在のテトリミノ
*/
function mergePieceToBoard(piece) {
piece.matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
// テトリミノのブロックがあれば、そのボード上の座標を value で上書き
const boardY = piece.pos.y + y;
const boardX = piece.pos.x + x;
// 既に board[y] が存在することを確認(念のため)
if (board[boardY]) {
// valueはブロックの色を表すTETROMINOS配列のインデックスを使用しても良い
board[boardY][boardX] = value;
}
}
});
});
}pieceDrop関数の作成(落下処理)
ブロックが下に移動しようとして衝突する場合、それは着地を意味します。着地したら(checkCollisionで判定)、そのブロックをボードに固定し、新しいブロックを生成して上部に配置します。
// テトリミノを1マス下に移動させる関数
function pieceDrop() {
if (!currentPiece) return;
// 衝突判定: 1マス下に移動したら衝突するか? (offsetX=0, offsetY=1)
if (checkCollision(currentPiece, 0, 1)) {
// 衝突する場合 (着地)
// ブロックをボードに固定する関数を呼び出す(次のセクションで作成)
mergePieceToBoard(currentPiece);
// 新しいテトリミノを生成
currentPiece = createPiece();
}
} else {
// 衝突しない場合
currentPiece.pos.y++; // 1マス下に移動
}
}ここまでで、ブロックは着地すると止まり、新しいブロックが出現するようになりました。
これまでの全コード。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>JavaScript Tetris</title>
<style>
/* CSSは次のセクションで定義します */
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
#tetris-canvas {
border: 5px solid #333; /* 枠線をつけてゲームボードを分かりやすくします */
}
</style>
</head>
<body>
<canvas id="tetris-canvas"></canvas>
<script>
// ブロック1つあたりのピクセルサイズ
const BLOCK_SIZE = 30;
// テトリス盤面のサイズ(縦20マス、横10マスが一般的)
const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
// キャンバスのサイズを決定
const CANVAS_WIDTH = BLOCK_SIZE * BOARD_WIDTH;
const CANVAS_HEIGHT = BLOCK_SIZE * BOARD_HEIGHT;
// キャンバスを取得
const canvas = document.getElementById('tetris-canvas');
const ctx = canvas.getContext('2d');
// キャンバスにサイズを適用
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
// ゲームボードを2次元配列で作成し、すべて0(空白)で初期化
let board = [];
for (let y = 0; y < BOARD_HEIGHT; y++) {
// 1行につき BOARD_WIDTH の要素を持つ配列を作成
board[y] = new Array(BOARD_WIDTH).fill(0);
}
function drawBoard() {
for (let y = 0; y < BOARD_HEIGHT; y++) {
for (let x = 0; x < BOARD_WIDTH; x++) {
if (board[y][x] !== 0) {
// ボード上のマスが空白(0)でなければ描画
ctx.fillStyle = 'blue'; // 仮の色
ctx.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
// ブロックの境界線を描画
ctx.strokeStyle = '#000';
ctx.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
}
}
}
// テトリミノの形状と色を定義する配列
// 1以上の数字はブロックの種類(色)を示す
const TETROMINOS = [
// 0: I型 (Cyan)
{
matrix: [
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
color: '#00FFFF' // シアン
},
// 1: J型 (Blue)
{
matrix: [
[2, 0, 0],
[2, 2, 2],
[0, 0, 0],
],
color: '#0000FF' // 青
},
// 2: L型 (Orange)
{
matrix: [
[0, 0, 3],
[3, 3, 3],
[0, 0, 0],
],
color: '#FFA500' // オレンジ
},
// 3: O型 (Yellow)
{
matrix: [
[4, 4],
[4, 4],
],
color: '#FFFF00' // 黄色
},
// 4: S型 (Green)
{
matrix: [
[0, 5, 5],
[5, 5, 0],
[0, 0, 0],
],
color: '#00FF00' // 緑
},
// 5: T型 (Purple)
{
matrix: [
[0, 6, 0],
[6, 6, 6],
[0, 0, 0],
],
color: '#800080' // 紫
},
// 6: Z型 (Red)
{
matrix: [
[7, 7, 0],
[0, 7, 7],
[0, 0, 0],
],
color: '#FF0000' // 赤
}
];
// 現在操作中のテトリミノを保持するオブジェクト
let currentPiece = null;
function createPiece() {
// 0からTETROMINOSの数-1までの乱数を生成
const randIndex = Math.floor(Math.random() * TETROMINOS.length);
const pieceData = TETROMINOS[randIndex];
// テトリミノの情報をオブジェクトとして返す
return {
matrix: pieceData.matrix,
color: pieceData.color,
// 開始位置 (ボードの上端中央)
pos: { x: Math.floor(BOARD_WIDTH / 2) - Math.floor(pieceData.matrix[0].length / 2), y: 0 }
};
}
function drawPiece(piece) {
// テトリミノのブロックを描画
piece.matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
// ボード上の座標に変換
const drawX = (piece.pos.x + x) * BLOCK_SIZE;
const drawY = (piece.pos.y + y) * BLOCK_SIZE;
ctx.fillStyle = piece.color;
ctx.fillRect(drawX, drawY, BLOCK_SIZE, BLOCK_SIZE);
// ブロックの境界線を描画
ctx.strokeStyle = '#000';
ctx.strokeRect(drawX, drawY, BLOCK_SIZE, BLOCK_SIZE);
}
});
});
}
function gameLoop() {
// 画面のクリア
ctx.clearRect(0, 0, canvas.width, canvas.height);
// if (currentPiece) {
// currentPiece.pos.y++;
// }
pieceDrop()
// 固定されたブロックと落下中のブロックを描画
if (currentPiece) {
drawPiece(currentPiece);
}
drawBoard();
}
/**
* 指定された位置 (offset) にテトリミノを移動または回転させたときに、
* ボードの境界や固定されたブロックと衝突するかどうかを判定する。
* @param {object} piece - 現在のテトリミノオブジェクト (matrix, pos)
* @param {number} offsetX - X軸方向の移動量 (通常 -1, 0, 1)
* @param {number} offsetY - Y軸方向の移動量 (通常 0, 1)
* @returns {boolean} 衝突する場合は true、しない場合は false
*/
function checkCollision(piece, offsetX, offsetY) {
const matrix = piece.matrix;
const currentX = piece.pos.x;
const currentY = piece.pos.y;
for (let y = 0; y < matrix.length; y++) {
for (let x = 0; x < matrix[y].length; x++) {
// ブロックのマス(値が0ではない)のみをチェック
if (matrix[y][x] !== 0) {
// 移動後のボード上の座標を計算
const nextX = currentX + x + offsetX;
const nextY = currentY + y + offsetY;
// 衝突条件 1: ボードの左右の壁、または底を超えてしまう
if (nextX < 0 || nextX >= BOARD_WIDTH || nextY >= BOARD_HEIGHT) {
return true; // 衝突 (壁または底)
}
// 衝突条件 2: ボードの上端を超えてしまう (上端に生成されたばかりのピースは除く)
// nextY < 0 の場合は、落下中であれば通常衝突とは見なさない。
if (nextY < 0) {
continue; // 処理をスキップ
}
// 衝突条件 3: 既に固定されたブロック (board[nextY][nextX] が 0 ではない) と重なる
if (board[nextY] && board[nextY][nextX] !== 0) {
return true; // 衝突 (固定ブロック)
}
}
}
}
return false; // 衝突なし
}
// テトリミノを1マス下に移動させる関数
function pieceDrop() {
if (!currentPiece) return;
// 衝突判定: 1マス下に移動したら衝突するか? (offsetX=0, offsetY=1)
if (checkCollision(currentPiece, 0, 1)) {
// 衝突する場合 (着地)
// ブロックをボードに固定する関数を呼び出す(次のセクションで作成)
mergePieceToBoard(currentPiece);
// 新しいテトリミノを生成
currentPiece = createPiece();
}
} else {
// 衝突しない場合
currentPiece.pos.y++; // 1マス下に移動
}
}
/**
* 現在のテトリミノをボード(2次元配列)に固定する
* @param {object} piece - 現在のテトリミノ
*/
function mergePieceToBoard(piece) {
piece.matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
// テトリミノのブロックがあれば、そのボード上の座標を value で上書き
const boardY = piece.pos.y + y;
const boardX = piece.pos.x + x;
// 既に board[y] が存在することを確認(念のため)
if (board[boardY]) {
// valueはブロックの色を表すTETROMINOS配列のインデックスを使用しても良い
board[boardY][boardX] = value;
}
}
});
});
}
// --- ゲーム開始 ---
setInterval(gameLoop,1000)
currentPiece = createPiece(); // 最初のテトリミノを生成
</script>
</body>
</html>


コメント