ブラウザで動くシンプルなテトリス風ゲームの作り方¶
ウェブブラウザで動作するシンプルなテトリス風ゲームを作成するチュートリアルです。JavaScriptの基本知識があれば、このガイドに沿って自分だけのブロック落下ゲームを開発できます。HTML、CSS、そしてJavaScriptを使った実践的なゲーム開発を通して、プログラミングの楽しさを体験しましょう!
目次¶
- はじめに
- 開発環境の準備
- ゲームの基本構造
- ゲーム盤の作成
- ブロックの設計
- ゲームのロジック
- キーボード制御の実装
- ゲームステータスと得点システム
- スタイリングとアニメーション
- 拡張とカスタマイズ
- まとめと次のステップ
はじめに¶
このチュートリアルについて¶
このチュートリアルでは、JavaScriptを使用してブラウザで動くテトリス風ゲームを一から作成します。シンプルな構造でありながらも、ゲーム開発の基本的な概念や技術を学ぶことができます。
必要な知識¶
- HTML/CSSの基礎
- JavaScriptの基礎(変数、関数、イベントなど)
- DOMの基本的な操作方法
初心者向け注意点
JavaScriptの経験が少ない方は、事前に基本的な概念を復習しておくことをお勧めします。特に配列操作とイベント処理についての理解があると役立ちます。
開発環境の準備¶
必要なツール¶
テトリス風ゲームの開発に必要なツールは以下のとおりです:
- テキストエディタ(VS Code、Sublime Text、Atomなど)
- ウェブブラウザ(Chrome、Firefox、Edgeなど)
- ローカルサーバー(オプション)
プロジェクトの構成¶
以下のようなファイル構成で作業を進めます:
tetris-game/
│
├── index.html # HTMLメインファイル
├── style.css # CSSスタイルシート
└── script.js # JavaScriptゲームロジック
基本ファイルの作成¶
まずは、基本的なHTMLファイルを作成しましょう:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>シンプルテトリス</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="game-container">
<div class="game-info">
<h1>シンプルテトリス</h1>
<div class="score">スコア: <span id="score">0</span></div>
<div class="level">レベル: <span id="level">1</span></div>
<button id="start-button">ゲーム開始</button>
</div>
<div class="game-board" id="game-board"></div>
<div class="next-piece">
<h2>次のピース</h2>
<div id="next-piece-display"></div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
ゲームの基本構造¶
ゲーム盤の設計¶
ゲーム盤は2次元配列で表現します。グリッドの各セルは0(空)または1以上(ブロックの色を表す数値)で表します。
// ゲーム盤をHTMLに表示
function drawBoard() {
const gameBoard = document.getElementById('game-board');
gameBoard.innerHTML = '';
// 各セルを作成
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const cell = document.createElement('div');
cell.className = 'cell';
// ブロックが存在する場合は色を設定
if (board[row][col] > 0) {
cell.classList.add(`piece-${board[row][col]}`);
}
gameBoard.appendChild(cell);
}
}
}
ゲームループの実装¶
ゲームループは、ブロックの自動落下や画面の更新を制御する重要な要素です:
let gameInterval;
const GAME_SPEED = 1000; // ミリ秒
function startGame() {
createBoard();
generateNewPiece();
drawBoard();
updateNextPieceDisplay();
// ゲームループの開始
gameInterval = setInterval(() => {
moveDown();
}, GAME_SPEED);
}
function endGame() {
clearInterval(gameInterval);
alert('ゲームオーバー!');
}
ブロックの設計¶
ブロックの定義¶
テトリスには7種類の標準的なブロック(テトロミノ)があります:
const PIECES = [
// I-ピース
{
shape: [
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
color: 1
},
// L-ピース
{
shape: [
[0, 0, 2],
[2, 2, 2],
[0, 0, 0]
],
color: 2
},
// J-ピース
{
shape: [
[3, 0, 0],
[3, 3, 3],
[0, 0, 0]
],
color: 3
},
// O-ピース (正方形)
{
shape: [
[4, 4],
[4, 4]
],
color: 4
},
// Z-ピース
{
shape: [
[5, 5, 0],
[0, 5, 5],
[0, 0, 0]
],
color: 5
},
// S-ピース
{
shape: [
[0, 6, 6],
[6, 6, 0],
[0, 0, 0]
],
color: 6
},
// T-ピース
{
shape: [
[0, 7, 0],
[7, 7, 7],
[0, 0, 0]
],
color: 7
}
];
現在のピースと次のピースの管理¶
let currentPiece;
let currentPiecePosition;
let nextPiece;
function generateNewPiece() {
// 次のピースが現在のピースになる
if (nextPiece) {
currentPiece = nextPiece;
} else {
// 最初のピースをランダムに選択
const randomIndex = Math.floor(Math.random() * PIECES.length);
currentPiece = PIECES[randomIndex];
}
// 新しい次のピースを選択
const randomIndex = Math.floor(Math.random() * PIECES.length);
nextPiece = PIECES[randomIndex];
// 初期位置を設定
currentPiecePosition = {
row: 0,
col: Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2)
};
// 衝突チェック - ゲームオーバー条件
if (checkCollision()) {
endGame();
}
}
function updateNextPieceDisplay() {
const nextPieceDisplay = document.getElementById('next-piece-display');
nextPieceDisplay.innerHTML = '';
if (!nextPiece) return;
// 次のピースを表示
for (let row = 0; row < nextPiece.shape.length; row++) {
for (let col = 0; col < nextPiece.shape[row].length; col++) {
if (nextPiece.shape[row][col]) {
const cell = document.createElement('div');
cell.className = `cell piece-${nextPiece.color}`;
nextPieceDisplay.appendChild(cell);
} else {
const cell = document.createElement('div');
cell.className = 'cell';
nextPieceDisplay.appendChild(cell);
}
}
// 改行
nextPieceDisplay.appendChild(document.createElement('br'));
}
}
ゲームのロジック¶
ブロックの移動と回転¶
function moveLeft() {
currentPiecePosition.col--;
if (checkCollision()) {
currentPiecePosition.col++; // 移動をキャンセル
} else {
drawBoard();
}
}
function moveRight() {
currentPiecePosition.col++;
if (checkCollision()) {
currentPiecePosition.col--; // 移動をキャンセル
} else {
drawBoard();
}
}
function moveDown() {
currentPiecePosition.row++;
if (checkCollision()) {
currentPiecePosition.row--; // 一つ上に戻す
mergePiece(); // ボードに固定
clearLines(); // ラインクリアチェック
generateNewPiece(); // 新しいピース生成
updateNextPieceDisplay(); // 次のピース表示更新
}
drawBoard();
}
function rotate() {
// ピースの回転 (90度時計回り)
const originalShape = currentPiece.shape;
const size = originalShape.length;
// 新しい配列を作成
let newShape = Array(size).fill().map(() => Array(size).fill(0));
// 時計回りに90度回転
for (let row = 0; row < size; row++) {
for (let col = 0; col < size; col++) {
newShape[col][size - 1 - row] = originalShape[row][col];
}
}
// 一時的に形状を変更
currentPiece.shape = newShape;
// 衝突チェック
if (checkCollision()) {
// 衝突する場合は元に戻す
currentPiece.shape = originalShape;
} else {
drawBoard();
}
}
衝突判定¶
ブロックが壁や他のブロックと衝突するかどうかを判定する関数:
function checkCollision() {
for (let row = 0; row < currentPiece.shape.length; row++) {
for (let col = 0; col < currentPiece.shape[row].length; col++) {
if (currentPiece.shape[row][col]) {
const boardRow = currentPiecePosition.row + row;
const boardCol = currentPiecePosition.col + col;
// 範囲外のチェック
if (
boardRow < 0 ||
boardRow >= ROWS ||
boardCol < 0 ||
boardCol >= COLS ||
// 既存のブロックとの衝突
(boardRow >= 0 && board[boardRow][boardCol])
) {
return true; // 衝突あり
}
}
}
}
return false; // 衝突なし
}
ブロックの固定とライン消去¶
function mergePiece() {
for (let row = 0; row < currentPiece.shape.length; row++) {
for (let col = 0; col < currentPiece.shape[row].length; col++) {
if (currentPiece.shape[row][col]) {
const boardRow = currentPiecePosition.row + row;
const boardCol = currentPiecePosition.col + col;
if (boardRow >= 0) {
board[boardRow][boardCol] = currentPiece.color;
}
}
}
}
}
function clearLines() {
let linesCleared = 0;
for (let row = ROWS - 1; row >= 0; row--) {
// 行が完全に埋まっているかチェック
if (board[row].every(cell => cell > 0)) {
// 行を削除
board.splice(row, 1);
// 新しい空の行を先頭に追加
board.unshift(Array(COLS).fill(0));
linesCleared++;
// 同じ行を再チェック(複数行消去のため)
row++;
}
}
// スコア加算
if (linesCleared > 0) {
updateScore(linesCleared);
}
}
キーボード制御の実装¶
ユーザーがキーボードでゲームを操作できるようにイベントリスナーを設定します:
document.addEventListener('keydown', function(event) {
if (!gameInterval) return; // ゲームが開始していない場合は無視
switch (event.keyCode) {
case 37: // 左矢印
moveLeft();
break;
case 38: // 上矢印
rotate();
break;
case 39: // 右矢印
moveRight();
break;
case 40: // 下矢印
moveDown();
break;
case 32: // スペース
dropPiece();
break;
}
});
function dropPiece() {
// ブロックをできるだけ下に落とす
while (!checkCollision()) {
currentPiecePosition.row++;
}
// 最後の衝突位置から一つ戻す
currentPiecePosition.row--;
mergePiece();
clearLines();
generateNewPiece();
updateNextPieceDisplay();
drawBoard();
}
ゲームステータスと得点システム¶
スコアの管理¶
let score = 0;
let level = 1;
let linesTotal = 0;
function updateScore(linesCleared) {
// テトリスのスコアリングシステム
const basePoints = [0, 40, 100, 300, 1200]; // 0, 1, 2, 3, 4ライン消去に対応
// スコア計算 (レベルとライン数に基づく)
score += basePoints[linesCleared] * level;
document.getElementById('score').textContent = score;
// 総ライン数を更新
linesTotal += linesCleared;
// レベルアップチェック (10ライン消去ごとにレベルアップ)
const newLevel = Math.floor(linesTotal / 10) + 1;
if (newLevel > level) {
level = newLevel;
document.getElementById('level').textContent = level;
// ゲームスピードの更新 (レベルが上がるほど速くなる)
updateGameSpeed();
}
}
function updateGameSpeed() {
// ゲームループの更新
clearInterval(gameInterval);
// レベルに応じてスピードアップ (最低200ms)
const newSpeed = Math.max(200, GAME_SPEED - (level - 1) * 100);
gameInterval = setInterval(() => {
moveDown();
}, newSpeed);
}
ゲーム開始/一時停止¶
let isPaused = false;
document.getElementById('start-button').addEventListener('click', function() {
if (!gameInterval) {
// ゲーム開始
score = 0;
level = 1;
linesTotal = 0;
document.getElementById('score').textContent = score;
document.getElementById('level').textContent = level;
this.textContent = 'ゲーム停止';
startGame();
} else if (isPaused) {
// 一時停止から再開
isPaused = false;
this.textContent = 'ゲーム停止';
updateGameSpeed(); // ゲームループを再開
} else {
// ゲームを一時停止
isPaused = true;
this.textContent = 'ゲーム再開';
clearInterval(gameInterval);
gameInterval = null;
}
});
スタイリングとアニメーション¶
ゲームのビジュアルを向上させるためのCSSスタイリングを実装します:
/* 基本スタイル */
body {
font-family: 'Arial', sans-serif;
background-color: #f0f0f0;
display: flex;
justify-content: center;
padding: 20px;
}
.game-container {
display: flex;
flex-direction: row;
align-items: flex-start;
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* ゲーム情報エリア */
.game-info {
margin-right: 20px;
width: 150px;
}
.game-info h1 {
font-size: 1.5rem;
margin-bottom: 20px;
color: #333;
}
.score, .level {
margin-bottom: 10px;
font-size: 1rem;
}
#start-button {
padding: 8px 12px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
margin-top: 15px;
transition: background-color 0.3s;
}
#start-button:hover {
background-color: #45a049;
}
/* ゲーム盤 */
.game-board {
display: grid;
grid-template-columns: repeat(10, 25px);
grid-template-rows: repeat(20, 25px);
gap: 1px;
border: 2px solid #333;
background-color: #222;
padding: 2px;
}
.cell {
width: 25px;
height: 25px;
background-color: #111;
border-radius: 2px;
}
/* 次のピース表示 */
.next-piece {
margin-left: 20px;
width: 120px;
}
.next-piece h2 {
font-size: 1rem;
margin-bottom: 10px;
color: #333;
}
#next-piece-display {
display: inline-block;
background-color: #222;
padding: 10px;
border-radius: 4px;
}
#next-piece-display .cell {
display: inline-block;
margin: 1px;
}
#next-piece-display br {
display: block;
content: "";
}
/* ピースの色 */
.piece-1 { background-color: #00f0f0; } /* I - シアン */
.piece-2 { background-color: #f0a000; } /* L - オレンジ */
.piece-3 { background-color: #0000f0; } /* J - ブルー */
.piece-4 { background-color: #f0f000; } /* O - イエロー */
.piece-5 { background-color: #f00000; } /* Z - レッド */
.piece-6 { background-color: #00f000; } /* S - グリーン */
.piece-7 { background-color: #a000f0; } /* T - パープル */
/* アニメーション効果 */
@keyframes flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0.5; }
}
.flash {
animation: flash 0.5s;
}
/* レスポンシブ対応 */
@media (max-width: 600px) {
.game-container {
flex-direction: column;
align-items: center;
}
.game-info, .next-piece {
margin: 0 0 20px 0;
width: 100%;
text-align: center;
}
}
ライン消去エフェクト¶
ライン消去時にフラッシュアニメーションを追加します:
function clearLines() {
let linesCleared = 0;
let rowsToFlash = [];
for (let row = ROWS - 1; row >= 0; row--) {
// 行が完全に埋まっているかチェック
if (board[row].every(cell => cell > 0)) {
rowsToFlash.push(row);
linesCleared++;
}
}
if (rowsToFlash.length > 0) {
// 消去エフェクトを表示
flashRows(rowsToFlash, () => {
// エフェクト後に実際に行を消去
for (let row of rowsToFlash) {
board.splice(row, 1);
board.unshift(Array(COLS).fill(0));
}
// スコア加算
updateScore(linesCleared);
drawBoard();
});
}
}
function flashRows(rows, callback) {
const gameBoard = document.getElementById('game-board');
const cells = gameBoard.children;
// フラッシュするセルにクラスを追加
for (let row of rows) {
for (let col = 0; col < COLS; col++) {
const cellIndex = row * COLS + col;
cells[cellIndex].classList.add('flash');
}
}
// アニメーション後に行を消去
setTimeout(() => {
for (let row of rows) {
for (let col = 0; col < COLS; col++) {
const cellIndex = row * COLS + col;
cells[cellIndex].classList.remove('flash');
}
}
callback();
}, 500); // フラッシュアニメーションの時間と同期
}
拡張とカスタマイズ¶
ゲームを拡張するためのいくつかのアイデア:
ゴーストピース機能¶
現在のピースがどこに落ちるかを予測して表示する機能を追加します:
function drawBoard() {
const gameBoard = document.getElementById('game-board');
gameBoard.innerHTML = '';
// 空のボードのコピーを作成
let displayBoard = Array(ROWS).fill().map((_, r) =>
Array(COLS).fill().map((_, c) => board[r][c])
);
// ゴーストピースの位置を計算
let ghostRow = currentPiecePosition.row;
while (true) {
ghostRow++;
// 衝突チェック用に一時的に位置を変更
const originalRow = currentPiecePosition.row;
currentPiecePosition.row = ghostRow;
if (checkCollision()) {
// 衝突したら一つ上に戻す
ghostRow--;
currentPiecePosition.row = originalRow;
break;
}
currentPiecePosition.row = originalRow;
}
// ゴーストピースを表示ボードに追加
if (ghostRow > currentPiecePosition.row) {
addPieceToBoard(displayBoard, currentPiece,
{ row: ghostRow, col: currentPiecePosition.col }, 'ghost');
}
// 現在のピースを表示ボードに追加
addPieceToBoard(displayBoard, currentPiece, currentPiecePosition, 'current');
// 表示ボードを描画
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const cell = document.createElement('div');
cell.className = 'cell';
if (displayBoard[row][col] === 'ghost') {
cell.classList.add('ghost-piece');
} else if (displayBoard[row][col] === 'current') {
cell.classList.add(`piece-${currentPiece.color}`);
} else if (displayBoard[row][col] > 0) {
cell.classList.add(`piece-${displayBoard[row][col]}`);
}
gameBoard.appendChild(cell);
}
}
}
function addPieceToBoard(displayBoard, piece, position, value) {
for (let row = 0; row < piece.shape.length; row++) {
for (let col = 0; col < piece.shape[row].length; col++) {
if (piece.shape[row][col]) {
const boardRow = position.row + row;
const boardCol = position.col + col;
if (boardRow >= 0 && boardRow < ROWS &&
boardCol >= 0 && boardCol < COLS) {
displayBoard[boardRow][boardCol] = value;
}
}
}
}
}
さらにゴーストピース用のCSSスタイルを追加:
ホールド機能¶
現在のピースを保持して後で使用できる機能:
let heldPiece = null;
let canHold = true;
function holdPiece() {
if (!canHold) return;
if (heldPiece === null) {
// 初めてホールドする場合
heldPiece = {...currentPiece};
generateNewPiece();
} else {
// ホールドピースと現在のピースを交換
const temp = {...currentPiece};
currentPiece = heldPiece;
heldPiece = temp;
// 初期位置を設定
currentPiecePosition = {
row: 0,
col: Math.floor(COLS / 2) - Math.floor(currentPiece.shape[0].length / 2)
};
// 衝突チェック - ゲームオーバー条件
if (checkCollision()) {
endGame();
}
}
// 一度ホールドしたら、新しいピースが落下するまでホールドできない
canHold = false;
updateHeldPieceDisplay();
drawBoard();
}
function updateHeldPieceDisplay() {
const heldPieceDisplay = document.getElementById('held-piece-display');
heldPieceDisplay.innerHTML = '';
if (!heldPiece) return;
// ホールドピースを表示
for (let row = 0; row < heldPiece.shape.length; row++) {
for (let col = 0; col < heldPiece.shape[row].length; col++) {
if (heldPiece.shape[row][col]) {
const cell = document.createElement('div');
cell.className = `cell piece-${heldPiece.color}`;
heldPieceDisplay.appendChild(cell);
} else {
const cell = document.createElement('div');
cell.className = 'cell';
heldPieceDisplay.appendChild(cell);
}
}
// 改行
heldPieceDisplay.appendChild(document.createElement('br'));
}
}
// moveDown関数にcanHoldリセット処理を追加
function moveDown() {
currentPiecePosition.row++;
if (checkCollision()) {
currentPiecePosition.row--; // 一つ上に戻す
mergePiece(); // ボードに固定
clearLines(); // ラインクリアチェック
generateNewPiece(); // 新しいピース生成
updateNextPieceDisplay(); // 次のピース表示更新
canHold = true; // ホールド可能状態にリセット
}
drawBoard();
}
// キーボード操作にホールド機能を追加
document.addEventListener('keydown', function(event) {
if (!gameInterval) return; // ゲームが開始していない場合は無視
switch (event.keyCode) {
case 37: // 左矢印
moveLeft();
break;
case 38: // 上矢印
rotate();
break;
case 39: // 右矢印
moveRight();
break;
case 40: // 下矢印
moveDown();
break;
case 32: // スペース
dropPiece();
break;
case 67: // C キー
holdPiece();
break;
}
});
HTMLにホールドピース表示用の要素を追加:
難易度選択¶
難易度選択機能を追加してゲームの挑戦レベルを調整できるようにします:
document.getElementById('difficulty').addEventListener('change', function() {
const difficulty = this.value;
switch (difficulty) {
case 'easy':
GAME_SPEED = 1000;
break;
case 'medium':
GAME_SPEED = 600;
break;
case 'hard':
GAME_SPEED = 400;
break;
case 'expert':
GAME_SPEED = 250;
break;
}
// ゲームが実行中なら速度を更新
if (gameInterval && !isPaused) {
updateGameSpeed();
}
});
HTMLに難易度選択フォームを追加:
<div class="difficulty-selector">
<label for="difficulty">難易度:</label>
<select id="difficulty">
<option value="easy">簡単</option>
<option value="medium" selected>普通</option>
<option value="hard">難しい</option>
<option value="expert">エキスパート</option>
</select>
</div>
まとめと次のステップ¶
完全なコードと動作確認¶
すべてのコードを統合して、ブラウザで動作するテトリス風ゲームが完成しました。この実装は基本的な機能を備えていますが、さらなる改良と拡張が可能です。
学んだ概念¶
このチュートリアルを通じて以下の概念を学びました:
- ゲームループ:
setIntervalを使った定期的な更新処理 - 2次元配列: ゲーム盤の表現とデータ構造
- 衝突検出: ゲームオブジェクト間の衝突判定
- イベント処理: キーボード入力のハンドリング
- DOM操作: JavaScriptによる動的なHTML要素の生成と操作
- 状態管理: ゲームの状態を追跡し更新する方法
次のステップ:さらなる拡張¶
このゲームをさらに発展させるためのアイデアをいくつか紹介します:
- モバイル対応: タッチ操作の実装
- 音響効果: 効果音とBGMの追加
- ローカルストレージ: ハイスコアの保存
- マルチプレイヤー: WebSocketを使ったオンライン対戦
- テーマ切替: 異なる外観テーマの実装
- カスタムピース: 新しいブロック形状の追加
オンラインでのデモ確認方法¶
- すべてのコードファイルを作成
- ローカルサーバーか静的サイトホスティングサービスにアップロード
- ブラウザで開いてゲームをプレイ
リソースとさらなる学習¶
- MDN Web Docs: JavaScript、HTML5、CSSの詳細な情報
- Game Development MDN: ゲーム開発に関するリソース
- JavaScript Game Development コミュニティ: 知識と経験の共有の場
おわりに¶
このチュートリアルがあなたのJavaScriptゲーム開発の第一歩になれば幸いです。プログラミングの基本概念をゲーム開発というエキサイティングな形で学べるのは大きな魅力です。ぜひコードをカスタマイズして、自分だけのオリジナルゲームに発展させてみてください!
質問やフィードバックがあれば、コメントセクションでお待ちしています。ハッピーコーディング!