テトリスを作る - 4 キー入力
着地して固定されるようになったので、次はテトリスをゲームとして成立させるための重要な要素、**プレイヤーによる操作(キー入力)**を実装します。
ステップ 7: キー入力による操作の実装
キーボードの矢印キーを使って、テトリミノを左右に移動させ、また下方向へ瞬時に落とす処理を実装します。
キーイベントリスナーの追加
まず、コードの末尾に、キーボードの入力を監視するイベントリスナーを追加します。
// キーボードイベントの監視を設定
document.addEventListener('keydown', handleKeyInput);キー入力処理関数 handleKeyInput の作成
function handleKeyInput(event) {
if (!currentPiece) return;
switch (event.key) {
case 'ArrowLeft':
// 左に移動 (offsetX: -1, offsetY: 0)
pieceMove(-1, 0);
break;
case 'ArrowRight':
// 右に移動 (offsetX: 1, offsetY: 0)
pieceMove(1, 0);
break;
case 'ArrowDown':
// 落下を速める (ソフトドロップ)
pieceMove(0, 1);
// 落下を速めたら、ドロップカウンターをリセットしてすぐに次の落下を促す
dropCounter = 0;
break;
case ' ': // スペースキー (ハードドロップ)
// ハードドロップ(一気に底まで落とす)
hardDrop();
break;
case 'ArrowUp':
case 'x':
// 回転処理(次のステップで実装)
pieceRotate();
break;
}
}この関数内で、押されたキーに応じてテトリミノを動かします。テトリミノがない条件では動きません。
if (!currentPiece) return;
動かしたい方向のキーボードを押すと"pieceMove(-1, 0);"関数を使って座標の移動をします。
case 'ArrowLeft':
pieceMove(-1, 0);
break;
テトリミノを移動させる関数 pieceMove の作成
左右の移動とソフトドロップ(速い落下)は、この汎用的な関数で行います。
/**
* テトリミノを指定されたオフセット分移動させる
* @param {number} offsetX - X軸方向の移動量
* @param {number} offsetY - Y軸方向の移動量
*/
function pieceMove(offsetX, offsetY) {
// 衝突判定を行い、移動可能かチェック
if (!checkCollision(currentPiece, offsetX, offsetY)) {
// 衝突しない場合のみ、位置を更新
currentPiece.pos.x += offsetX;
currentPiece.pos.y += offsetY;
} else if (offsetY === 1) {
// 下への移動(ソフトドロップ)で衝突した場合は、着地したと見なす
// pieceDrop()が既に衝突判定を持っているので、ここでは単に終了
}
}衝突判定して衝突していない条件で座標を変更します。
if (!checkCollision(currentPiece, offsetX, offsetY))
キーボードを押した分の"offsetX"を受けてその分追加します。
currentPiece.pos.x += offsetX;
ハードドロップ(一瞬で着地)のロジック
スペースキーを押されたとき、ブロックが着地するまで繰り返し下に移動させます。
function hardDrop() {
if (!currentPiece) return;
// 衝突するまで繰り返し1マスずつ下に移動させる
while (!checkCollision(currentPiece, 0, 1)) {
currentPiece.pos.y++;
}
// 衝突したら、強制的に着地処理(固定と新規生成)を行う
mergePieceToBoard(currentPiece);
// checkRowClear(); // 次のステップで実装
currentPiece = createPiece();
// ゲームオーバー判定
if (checkCollision(currentPiece, 0, 0)) {
console.log("ゲームオーバー!");
// ゲームループを停止する処理などを追加
}
}これで、キーボードを使ってブロックを左右に動かし、ソフトドロップやハードドロップができるようになりました。
テトリミノの回転の実装
行列を回転させる関数 ・・・ rotateMatrix(matrix)
テトリミノの形状を表す行列(配列の配列)を受け取り、それを90度右に回転させた新しい行列を返す関数
回転のロジック: [y][x] の要素を [x][size - 1 - y]
/**
* 行列を90度右に回転させる
* (y, x) の要素は (x, size - 1 - y) に移動する
* @param {Array<Array<number>>} matrix - 回転させたい行列
* @returns {Array<Array<number>>} 回転後の新しい行列
*/
function rotateMatrix(matrix) {
const size = matrix.length;
// 新しい行列を初期化(すべて0)
const newMatrix = Array(size).fill(0).map(() => Array(size).fill(0));
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
// 回転のロジック: [y][x] の要素を [x][size - 1 - y] に移動
newMatrix[x][size - 1 - y] = matrix[y][x];
}
}
return newMatrix;
}実際に回転操作する関数 ・・・ pieceRotate
キー入力で呼び出される関数です。回転後の形状で衝突判定を行い、問題なければテトリミノを回転させます。
// キー入力処理関数 (handleKeyInput) で 'ArrowUp' または 'x' が押された時に呼び出す
function pieceRotate() {
if (!currentPiece) return;
// 1. 回転後の行列を取得
const rotatedMatrix = rotateMatrix(currentPiece.matrix);
// 2. 一時的にテトリミノの行列を回転後のものに置き換え
const originalMatrix = currentPiece.matrix;
currentPiece.matrix = rotatedMatrix;
// 3. 回転後の位置で衝突判定を行う
if (checkCollision(currentPiece, 0, 0)) {
// 衝突する場合: 元に戻す (Wall Kickは次のステップで考慮)
currentPiece.matrix = originalMatrix;
}
// 衝突しない場合: currentPiece.matrix は rotatedMatrix のまま確定
}これまでの全コード
<!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;
}
}
});
});
}
function handleKeyInput(event) {
if (!currentPiece) return;
switch (event.key) {
case 'ArrowLeft':
// 左に移動 (offsetX: -1, offsetY: 0)
pieceMove(-1, 0);
break;
case 'ArrowRight':
// 右に移動 (offsetX: 1, offsetY: 0)
pieceMove(1, 0);
break;
case 'ArrowDown':
// 落下を速める (ソフトドロップ)
pieceMove(0, 1);
// 落下を速めたら、ドロップカウンターをリセットしてすぐに次の落下を促す
dropCounter = 0;
break;
case ' ': // スペースキー (ハードドロップ)
// ハードドロップ(一気に底まで落とす)
hardDrop();
break;
case 'ArrowUp':
case 'x':
// 回転処理(次のステップで実装)
pieceRotate();
break;
}
}
/**
* テトリミノを指定されたオフセット分移動させる
* @param {number} offsetX - X軸方向の移動量
* @param {number} offsetY - Y軸方向の移動量
*/
function pieceMove(offsetX, offsetY) {
// 衝突判定を行い、移動可能かチェック
if (!checkCollision(currentPiece, offsetX, offsetY)) {
// 衝突しない場合のみ、位置を更新
currentPiece.pos.x += offsetX;
currentPiece.pos.y += offsetY;
} else if (offsetY === 1) {
// 下への移動(ソフトドロップ)で衝突した場合は、着地したと見なす
// pieceDrop()が既に衝突判定を持っているので、ここでは単に終了
}
}
function hardDrop() {
if (!currentPiece) return;
// 衝突するまで繰り返し1マスずつ下に移動させる
while (!checkCollision(currentPiece, 0, 1)) {
currentPiece.pos.y++;
}
// 衝突したら、強制的に着地処理(固定と新規生成)を行う
mergePieceToBoard(currentPiece);
// checkRowClear(); // 次のステップで実装
currentPiece = createPiece();
// ゲームオーバー判定
if (checkCollision(currentPiece, 0, 0)) {
console.log("ゲームオーバー!");
// ゲームループを停止する処理などを追加
}
}
/**
* 行列を90度右に回転させる
* (y, x) の要素は (x, size - 1 - y) に移動する
* @param {Array<Array<number>>} matrix - 回転させたい行列
* @returns {Array<Array<number>>} 回転後の新しい行列
*/
function rotateMatrix(matrix) {
const size = matrix.length;
// 新しい行列を初期化(すべて0)
const newMatrix = Array(size).fill(0).map(() => Array(size).fill(0));
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
// 回転のロジック: [y][x] の要素を [x][size - 1 - y] に移動
newMatrix[x][size - 1 - y] = matrix[y][x];
}
}
return newMatrix;
}
// キー入力処理関数 (handleKeyInput) で 'ArrowUp' または 'x' が押された時に呼び出す
function pieceRotate() {
if (!currentPiece) return;
// 1. 回転後の行列を取得
const rotatedMatrix = rotateMatrix(currentPiece.matrix);
// 2. 一時的にテトリミノの行列を回転後のものに置き換え
const originalMatrix = currentPiece.matrix;
currentPiece.matrix = rotatedMatrix;
// 3. 回転後の位置で衝突判定を行う
if (checkCollision(currentPiece, 0, 0)) {
// 衝突する場合: 元に戻す (Wall Kickは次のステップで考慮)
currentPiece.matrix = originalMatrix;
}
// 衝突しない場合: currentPiece.matrix は rotatedMatrix のまま確定
}
// キーボードイベントの監視を設定
document.addEventListener('keydown', handleKeyInput);
// --- ゲーム開始 ---
setInterval(gameLoop,1000)
currentPiece = createPiece(); // 最初のテトリミノを生成
</script>
</body>
</html>