Next.jsを使用したブロック崩しゲーム
背景
業務でJsライブラリ・フレームワークを使用し、アプリケーションを作成する機会がありました。
その時、あまり使用したことがなかったNext.jsを使用し、ブロック崩しゲームを作成してみました。
作成し始めは苦戦しましたが、周りの方々の手助けや先輩エンジニアのアドバイスもあり、なんとかそれなりのものができました。
作成したアプリのリンクを貼り付けたので、暇な時間などで遊んでいただけると幸いです。
ブロック崩しゲーム
https://breaking-blocks-blush.vercel.app/
(※PCのみ対応)
コードの解説
ざっくりとしたコードの解説をいたします。
"use client";
import React, { useState, useEffect, useRef } from 'react';
const BlockMain = () => {
const [elapsedTime, setElapsedTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef(null);
const canvasRef = useRef(null);
const gameRef = useRef({
ball: {
x: 0,
y: 0,
radius: 10,
dx: 2,
dy: -2,
},
paddle: {
height: 10,
width: 75,
x: 0,
},
blocks: [], // ブロックを管理する配列
});
const startTimeRef = useRef(null);
const gameStartedRef = useRef(false); // ゲームの開始フラグ
// ボールを描画
const drawBall = (ctx, ball) => {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
};
// パドルを描画
const drawPaddle = (ctx, paddle, canvas) => {
ctx.beginPath();
ctx.rect(paddle.x, canvas.height - paddle.height, paddle.width, paddle.height);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
};
// ブロックを描画
const drawBlocks = (ctx, blocks) => {
blocks.forEach((block) => {
if (!block.isDestroyed) {
ctx.beginPath();
ctx.rect(block.x, block.y, block.width, block.height);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
});
};
// ゲームのアニメーション(ボールの動き、衝突判定など)
const draw = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const { ball, paddle, blocks } = gameRef.current;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBall(ctx, ball);
drawPaddle(ctx, paddle, canvas);
drawBlocks(ctx, blocks);
// ボールが左右の壁に当たった場合の反射角度を変更
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
) {
// パドルの中央に対するボールの相対的な位置を計算
const relativePosition = (ball.x - (paddle.x + paddle.width / 2)) / (paddle.width / 2);
// 反射角度を計算
const reflectionAngle = relativePosition * Math.PI / 4;
// ボールの速度を増加
const speedMultiplier = 1;
// ボールの速度を変更し、速度の向きを維持
const speed = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy) * speedMultiplier;
ball.dx = Math.sin(reflectionAngle) * speed;
ball.dy = -Math.cos(reflectionAngle) * speed;
} else {
// ゲームオーバーの処理
alert('GAME OVER');
document.location.reload();
return;
}
}
// ブロックとの当たり判定
let allBlocksDestroyed = true; // すべてのブロックが破壊されたかどうかのフラグ
blocks.forEach((block) => {
if (!block.isDestroyed) {
allBlocksDestroyed = false; // まだ破壊されていないブロックがある
if (
ball.x > block.x &&
ball.x < block.x + block.width &&
ball.y > block.y &&
ball.y < block.y + block.height
) {
block.isDestroyed = true;
ball.dy = -ball.dy;
}
}
});
if (allBlocksDestroyed) {
// すべてのブロックが破壊された場合、ゲームクリアの処理
const endTime = new Date();
const timeDiff = endTime - startTimeRef.current;
const seconds = Math.floor(timeDiff / 1000);
alert(`ゲームクリア! おめでとうございます😁\nクリアにかかった時間:${seconds}秒`);
document.location.reload();
return;
}
ball.x += ball.dx;
ball.y += ball.dy;
requestAnimationFrame(draw);
};
// マウスの位置に応じてパドルの位置を更新
const mouseMoveHandler = (e) => {
const canvas = canvasRef.current;
const { paddle } = gameRef.current;
const relativeX = e.clientX - canvas.offsetLeft;
if (relativeX > 0 && relativeX < canvas.width) {
paddle.x = relativeX - paddle.width / 2;
}
};
// useEffect 内の ball.x, ball.y の初期化部分
useEffect(() => {
const canvas = canvasRef.current;
const { ball, paddle } = gameRef.current;
// ボールの初期位置をランダムに設定
ball.x = Math.random() * (canvas.width - 3 * ball.radius) + ball.radius;
ball.y = Math.random() * (canvas.height - 3 * ball.radius) + ball.radius;
ball.dx = 4; // X方向の速度
ball.dy = -4; // y方向の速度
paddle.x = (canvas.width - paddle.width) / 2;
// ブロックの初期化
const blockRowCount = 3;
const blockColumnCount = 5;
for (let c = 0; c < blockColumnCount; c++) {
for (let r = 0; r < blockRowCount; r++) {
gameRef.current.blocks.push({
x: c * (75 + 10),
y: r * (20 + 10),
width: 75,
height: 20,
isDestroyed: false,
});
}
}
canvas.addEventListener("mousemove", mouseMoveHandler);
return () => {
canvas.removeEventListener("mousemove", mouseMoveHandler);
};
}, []);
const startGame = () => {
if (!gameStartedRef.current) {
gameStartedRef.current = true;
startTimeRef.current = new Date();
setIsRunning(true);
draw();
}
};
// 経過時間の測定
useEffect(() => {
if (isRunning) {
intervalRef.current = setInterval(() => {
setElapsedTime((prevTime) => prevTime + 1);
}, 1000);
} else {
clearInterval(intervalRef.current);
}
return () => {
clearInterval(intervalRef.current);
};
}, [isRunning]);
return (
<div className='main-content'>
<p>経過時間: {elapsedTime}秒</p>
<canvas ref={canvasRef} width={410} height={250} style={{ border: '1px solid #000' }} />
<div className='start-button'>
<button onClick={startGame}>ゲームスタート</button>
</div>
</div>
);
};
export default BlockMain;
①コンポーネントの初期化
canvasRef
と gameRef
は、それぞれ canvas 要素とゲームの状態を参照するための React の useRef フックで初期化されています。
②描画関数
drawBall
関数はボールを描画する関数です。
drawPaddle
関数はパドルを描画する関数です。
drawBlocks
関数はブロックを描画する関数です。
③アニメーション関数
draw
関数は、ゲームのメインアニメーション関数で、ボールの動きや衝突判定、ブロックの破壊などが行われます。
requestAnimationFrame
を使用して繰り返し描画が行われます。
④当たり判定
ボールが左右と上の壁に当たると反射します。
ボールがパドルに当たると、ボールの反射角度が変更され、速度も増加します。
ボールがブロックに当たると、ブロックが破壊され、ボールが反射します。
⑤ゲームオーバーとゲームクリア
ボールが画面下端に到達するとゲームオーバーとなり、ゲームオーバーを表すアラートメッセージが表示され、ページがリロードされます。
すべてのブロックが破壊されるとゲームクリアとなり、ゲームクリアを表すアラートメッセージが表示され、ページがリロードされます。
⑥マウスの位置に応じたパドルの移動
マウスが動いたときに、その位置に応じてパドルの位置が更新されます。
⑦useEffect
コンポーネントがマウントされたとき、ボールやパドル、ブロックの初期状態を設定し、マウスの移動イベントのリスナーを追加しています。
以上がコードの解説になります。
まとめ
今回、Next.jsをほとんど初めて使用しました。
自分としてはブロック崩しゲームを作成することができた点に関しては大変満足しています。
ただ、Next.js 特有の機能を生かしてアプリを作成することができなかったので、次回はそのようなことを踏まえてアプリを作成していきたいです。
Discussion