プログラミング学習で行き詰まっていませんか?HTMLファイル1つで完結するブロック崩しゲームの作り方を、カスタマイズ方法と実際のデモ付きで詳しく解説します。
ゲーム開発でプログラミング学習につまづいていませんか?
「JavaScriptを勉強してるけど、どうやって実践的なものを作れば良いか分からない」「ゲーム開発に興味があるけど、複雑なツールやフレームワークは敷居が高い」「作ったプログラムを友達に見せるのに、サーバーやデプロイが必要で困っている」
こんなお悩みをお持ちのプログラミング初心者〜中級者の方は多いのではないでしょうか。
神奈川を拠点に20年以上Web制作に携わってきた私たちFivenine Designでも、多くのクライアントから「プログラミング学習の効果的な方法」について相談を受けます。特に、理論は理解できても実際に動くものが作れない、という壁に当たる方が非常に多いのが現状です。
本記事では、そんな課題を解決する実践的なアプローチとして、HTMLファイル1つで完結するブロック崩しゲームの作り方を詳しく解説します。動画チュートリアルでは説明しきれなかった細かい仕様やカスタマイズ方法まで、実際に動くデモと共にお伝えしていきます。
なぜブロック崩しゲームが学習に最適なのか
プログラミング学習における3つの壁
弊社でこれまで手がけた教育系Webサイトの開発案件では、プログラミング学習者が直面する課題が明確に見えてきました。特に以下の3つが大きな障壁となっています。
1. 環境構築の複雑さ 多くのチュートリアルでは、Node.js、Webpack、各種ライブラリのインストールなど、本来学びたい内容とは関係ない部分で挫折してしまいます。
2. 結果が見えにくい コンソールに文字を出力するだけのプログラムでは、達成感や学習のモチベーションを維持するのが困難です。
3. 共有の難しさ 作ったものを他の人に見せるのに、サーバー設定やデプロイの知識が必要になり、学習の本質から外れてしまいます。
ブロック崩しゲームで得られる学習効果
これらの課題を解決するために、私たちは「ブロック崩しゲーム」という題材を選びました。実際にあるクライアントの社内研修で導入したところ、以下のような成果が得られました。
ブロック崩しゲームを作ることで、以下の重要な概念を自然に学べます:
- イベント処理:キーボード入力、衝突判定
- アニメーション:requestAnimationFrame、座標計算
- オブジェクト指向:ボール、パドル、ブロックの概念
- 状態管理:ゲームの進行状況、スコア管理
基本的なブロック崩しゲームの作成
HTMLファイルの基本構造
まずは、すべてを含んだHTMLファイルを作成しましょう。以下がベースとなるコードです:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ブロック崩しゲーム</title>
<style>
body {
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #1a1a1a;
font-family: 'Courier New', monospace;
}
canvas {
border: 2px solid #fff;
background: #000;
}
.info {
color: white;
text-align: center;
margin-top: 10px;
}
</style>
</head>
<body>
<div>
<canvas id="gameCanvas" width="480" height="320"></canvas>
<div class="info">
<p>スコア: <span id="score">0</span></p>
<p>矢印キーでパドルを操作</p>
</div>
</div>
<script>
// ゲームの基本設定
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// ゲーム変数
let score = 0;
let rightPressed = false;
let leftPressed = false;
// ボールの設定
const ball = {
x: canvas.width / 2,
y: canvas.height - 30,
dx: 2,
dy: -2,
radius: 10
};
// パドルの設定
const paddle = {
width: 75,
height: 10,
x: (canvas.width - 75) / 2
};
// ブロックの設定
const bricks = [];
const brickRowCount = 3;
const brickColumnCount = 5;
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 60;
const brickOffsetLeft = 30;
// ブロック配列の初期化
for (let c = 0; c < brickColumnCount; c++) {
bricks[c] = [];
for (let r = 0; r < brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0, status: 1 };
}
}
// イベントリスナー
document.addEventListener('keydown', keyDownHandler);
document.addEventListener('keyup', keyUpHandler);
function keyDownHandler(e) {
if (e.key === 'Right' || e.key === 'ArrowRight') {
rightPressed = true;
} else if (e.key === 'Left' || e.key === 'ArrowLeft') {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key === 'Right' || e.key === 'ArrowRight') {
rightPressed = false;
} else if (e.key === 'Left' || e.key === 'ArrowLeft') {
leftPressed = false;
}
}
// 描画関数
function drawBall() {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = '#0095DD';
ctx.fill();
ctx.closePath();
}
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddle.x, canvas.height - paddle.height, paddle.width, paddle.height);
ctx.fillStyle = '#0095DD';
ctx.fill();
ctx.closePath();
}
function drawBricks() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
if (bricks[c][r].status === 1) {
const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = '#0095DD';
ctx.fill();
ctx.closePath();
}
}
}
}
function collisionDetection() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
const b = bricks[c][r];
if (b.status === 1) {
if (ball.x > b.x && ball.x < b.x + brickWidth &&
ball.y > b.y && ball.y < b.y + brickHeight) {
ball.dy = -ball.dy;
b.status = 0;
score++;
document.getElementById('score').textContent = score;
}
}
}
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
collisionDetection();
// ボールの移動
if (ball.x + ball.dx > canvas.width - ball.radius || ball.x + ball.dx < ball.radius) {
ball.dx = -ball.dx;
}
if (ball.y + ball.dy < ball.radius) {
ball.dy = -ball.dy;
} else if (ball.y + ball.dy > canvas.height - ball.radius) {
if (ball.x > paddle.x && ball.x < paddle.x + paddle.width) {
ball.dy = -ball.dy;
} else {
alert('ゲームオーバー');
document.location.reload();
}
}
// パドルの移動
if (rightPressed && paddle.x < canvas.width - paddle.width) {
paddle.x += 7;
} else if (leftPressed && paddle.x > 0) {
paddle.x -= 7;
}
ball.x += ball.dx;
ball.y += ball.dy;
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>
コードの解説
このコードは大きく以下の部分に分かれています:
1. HTML構造とスタイル Canvasエレメントとスコア表示のための最小限のHTML、そして見た目を整えるためのCSSです。
2. ゲーム状態の管理
const ball = { x: canvas.width / 2, y: canvas.height - 30, dx: 2, dy: -2, radius: 10 };
ボール、パドル、ブロックの位置や状態を管理するオブジェクトを定義しています。
3. 衝突判定
collisionDetection() 関数では、ボールがブロックに当たったかを判定し、当たった場合はブロックを消去してスコアを増やします。
4. 描画ループ
requestAnimationFrame() を使用して、滑らかなアニメーションを実現しています。
カスタマイズ方法の詳細解説
動画チュートリアルでは時間の都合で詳しく説明できなかった部分を、実際のクライアント案件で行ったカスタマイズ事例と共に解説します。
ボールの見た目をカスタマイズ
function drawBall() {
// グロー効果付きのボール
ctx.shadowColor = '#00ff00';
ctx.shadowBlur = 20;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
// グラデーション効果
const gradient = ctx.createRadialGradient(ball.x, ball.y, 0, ball.x, ball.y, ball.radius);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(1, '#00ff00');
ctx.fillStyle = gradient;
ctx.fill();
ctx.closePath();
// 影をリセット
ctx.shadowBlur = 0;
}
壊れないブロックの実装
// ブロック初期化時に特殊ブロックを設定
for (let c = 0; c < brickColumnCount; c++) {
bricks[c] = [];
for (let r = 0; r < brickRowCount; r++) {
// 特定の位置に壊れないブロックを配置
const isUnbreakable = (c === 2 && r === 1); // 中央のブロック
bricks[c][r] = {
x: 0,
y: 0,
status: 1,
unbreakable: isUnbreakable,
hitCount: 0
};
}
}
// 描画関数も修正
function drawBricks() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
if (bricks[c][r].status === 1) {
const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
if (bricks[c][r].unbreakable) {
ctx.fillStyle = '#ff0000'; // 赤色で表示
} else {
ctx.fillStyle = '#0095DD';
}
ctx.fill();
ctx.closePath();
}
}
}
}
// 衝突判定も修正
function collisionDetection() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
const b = bricks[c][r];
if (b.status === 1) {
if (ball.x > b.x && ball.x < b.x + brickWidth &&
ball.y > b.y && ball.y < b.y + brickHeight) {
ball.dy = -ball.dy;
if (!b.unbreakable) {
b.status = 0;
score++;
} else {
b.hitCount++;
// 3回当たったら壊れる設定も可能
if (b.hitCount >= 3) {
b.status = 0;
score += 5; // 高得点
}
}
document.getElementById('score').textContent = score;
}
}
}
}
}
複数ボールの実装
// ボールを配列で管理
const balls = [
{ x: canvas.width / 2, y: canvas.height - 30, dx: 2, dy: -2, radius: 10 },
{ x: canvas.width / 3, y: canvas.height - 50, dx: -1.5, dy: -1.8, radius: 8 }
];
function drawBalls() {
balls.forEach(ball => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = '#0095DD';
ctx.fill();
ctx.closePath();
});
}
function updateBalls() {
balls.forEach((ball, index) => {
// 壁との衝突
if (ball.x + ball.dx > canvas.width - ball.radius || ball.x + ball.dx < ball.radius) {
ball.dx = -ball.dx;
}
if (ball.y + ball.dy < ball.radius) {
ball.dy = -ball.dy;
} else if (ball.y + ball.dy > canvas.height - ball.radius) {
// パドルとの衝突判定
if (ball.x > paddle.x && ball.x < paddle.x + paddle.width) {
ball.dy = -ball.dy;
} else {
// ボールを削除(ゲームオーバー条件は全ボールがなくなった時)
balls.splice(index, 1);
}
}
ball.x += ball.dx;
ball.y += ball.dy;
});
// すべてのボールがなくなったらゲームオーバー
if (balls.length === 0) {
alert('ゲームオーバー');
document.location.reload();
}
}
よくある失敗パターンと対処法
20年以上の開発経験の中で、ブロック崩しゲーム制作において開発者がよく陥る失敗パターンを整理しました。
1. requestAnimationFrameの誤用
よくある間違い:
// 間違った例:setIntervalを使用
setInterval(draw, 16); // 60FPSのつもり
問題点:
- ブラウザがバックグラウンドでも動き続ける
- フレームレートが不安定
- バッテリー消費が大きい
正しい方法:
function draw() {
// ゲームロジック
requestAnimationFrame(draw);
}
2. 衝突判定の精度不足
よくある問題: ボールが高速になると、ブロックを素通りしてしまう現象が発生します。
対処法:
function preciseCollisionDetection(ball, brick) {
// 前フレームと現フレームの間の軌跡をチェック
const prevX = ball.x - ball.dx;
const prevY = ball.y - ball.dy;
// 線分とボックスの交差判定(詳細な実装は省略)
return lineIntersectsRect(prevX, prevY, ball.x, ball.y, brick);
}
3. メモリリークの発生
ある企業研修で実際に起こった事例です。ゲームを繰り返し再起動していると、だんだん動作が重くなっていく問題がありました。
原因: イベントリスナーが重複して登録されていました。
// 問題のあるコード
function startGame() {
document.addEventListener('keydown', keyDownHandler); // 毎回追加される
draw();
}
解決法:
function startGame() {
// 既存のリスナーを削除
document.removeEventListener('keydown', keyDownHandler);
document.removeEventListener('keyup', keyUpHandler);
// 新しいリスナーを追加
document.addEventListener('keydown', keyDownHandler);
document.addEventListener('keyup', keyUpHandler);
}
4. レスポンシブ対応の落とし穴
問題: Canvas要素のサイズをCSSで変更すると、座標系がずれてしまいます。
正しい対応:
function resizeCanvas() {
const ratio = Math.min(
window.innerWidth / 480,
window.innerHeight / 320
);
canvas.style.width = (480 * ratio) + 'px';
canvas.style.height = (320 * ratio) + 'px';
// Canvas内部の座標系は変更しない
canvas.width = 480;
canvas.height = 320;
}
実際のデモとさらなる拡張
パフォーマンス最適化のポイント
実案件で学んだパフォーマンス改善のテクニックをご紹介します:
// オブジェクトプールパターンでメモリ使用量を削減
class ObjectPool {
constructor(createFn, resetFn, size = 50) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
for (let i = 0; i < size; i++) {
this.pool.push(this.createFn());
}
}
get() {
if (this.pool.length > 0) {
return this.pool.pop();
}
return this.createFn();
}
release(obj) {
this.resetFn(obj);
this.pool.push(obj);
}
}
// パーティクル効果用のオブジェクトプール
const particlePool = new ObjectPool(
() => ({ x: 0, y: 0, vx: 0, vy: 0, life: 0 }),
(particle) => {
particle.life = 0;
}
);
音響効果の追加
class SoundManager {
constructor() {
this.sounds = {};
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
createBeepSound(frequency, duration) {
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
oscillator.frequency.value = frequency;
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + duration);
oscillator.start(this.audioContext.currentTime);
oscillator.stop(this.audioContext.currentTime + duration);
}
playBrickHit() {
this.createBeepSound(800, 0.1);
}
playPaddleHit() {
this.createBeepSound(200, 0.1);
}
}
const soundManager = new SoundManager();
まとめと次のステップ
HTMLファイル1つで完結するブロック崩しゲームの作成を通じて、JavaScriptの実践的な活用方法を学んできました。このアプローチにより、環境構築の複雑さを回避しながら、ゲーム開発の基礎概念を習得できます。
学習効果の振り返り
今回のゲーム制作で習得できた技術要素:
- Canvas APIを使った2Dグラフィックス
- イベント駆動プログラミング
- アニメーションループの実装
- 衝突判定アルゴリズム
- オブジェクト指向の基本概念
- パフォーマンス最適化の考え方
次に取り組むべきステップ
レベル1:基本機能の改善
- ライフシステムの追加
- レベル制の導入
- ハイスコア保存機能
レベル2:技術的な拡張
- WebGLを使った3D表現
- Web Audio APIによる音響効果
- localStorage を使ったセーブ機能
レベル3:本格的なゲーム開発
- Phaser.jsなどのゲームフレームワークの学習
- マルチプレイヤー機能の実装
- モバイル対応の最適化
実践的な活用方法
弊社のクライアントでは、このようなゲーム開発アプローチを以下の用途で活用されています:
- 社内研修:プログラミング学習の導入教材として
- 採用試験:技術力評価のための課題として
- 顧客エンゲージメント:Webサイトのインタラクティブコンテンツとして
プログラミング学習は、理論だけでなく「作る楽しさ」を体験することが継続の鍵です。今回のブロック崩しゲーム開発が、皆さんのプログラミング学習journey の新しいスタートとなることを願っています。
より高度なWeb開発やゲーム開発にチャレンジしたい、または企業での技術研修プログラムをお考えの場合は、20年以上の実績を持つ私たちFivenine Designまでお気軽にご相談ください。皆さんの技術的成長をサポートいたします。