クリックゲームの進化 ~続編:新機能&改善点を解説~


前回の記事では、シンプルなクリックゲームの基本設計や、スコアアップの仕組みについてご紹介しました。今回はその続編として、より本格的なゲーム体験を実現するために実装した以下の改善点や新機能について解説します。
今回更新したゲームはこちら。
前回までのゲームはこちら。
時間制限とゲーム開始ボタンの追加
前編では
・時間制限もなく、シンプルなクリックでスコアを積み上げるものでした。
今回の改善点
・Canvas上にスタートボタンを直接描画しました。
・ゲームが開始していない状態では、画面中央に青い「START」ボタンが表示され、ユーザーはそのボタンをクリックするだけでゲームがスタートします。
・ゲーム中はボタン表示を消し、操作が混乱しないように制御しています。
・一定の秒数(例えば10秒)でゲームを制限し、スタートボタンを押すとカウントダウンが開始する仕組みを追加
今日のハイスコアと今までのハイスコア
前編では
・シンプルにハイスコアを保存する仕組みを実装していました。
今回の改善点
・時間制限ができたことで、1ゲーム中でのハイスコアを記録できるようになりました。
・「今日のハイスコア」と「全体の最高記録」を別々に管理するようになりました。
・ローカルストレージを利用し、日付ごとに記録することで、毎日新たな挑戦ができる仕組みを実装。
ゲーム性の向上:難易度(レベル)と敵の体力
前編では
・単純なスコアアップが中心で、ゲームに難易度の概念はありませんでした。
今回の改善点
・一定のクリック数をクリアするごとにレベルアップし、難易度が段階的に上昇する仕組みを導入。
・加えて、敵の体力を表すプログレスバーを追加。
- クリックするごとにプログレスバーが減少し、残りの体力に応じて色(緑→黄色→赤)が変化するようにしました。
- 「Enemy Health」といったテキストも表示し、視覚的に体力の残りを分かりやすくしています。
ターゲットのビジュアル改善
前編では
・クリック対象はシンプルな赤い円で表現していました。
今回の改善点
・ターゲットを画像表示に変更。
・複数の画像URLを用意し、レベルクリアのたびに画像が変更されるため、ゲームのビジュアルが豊かになりました。
・また、画像の読み込みタイミングを考慮し、確実に描画されるよう工夫を加えています。
その他の改善点
・二重起動防止機能
- ゲーム開始ボタンをCanvas上に統合した際、連打によるタイマーの重複起動を防ぐ処理を追加しました。
・ユーザー体験の細部改善
- 各機能の追加に伴い、デバッグ用ログやリセット処理の改善も行い、快適な操作性を追求しています。
コード
document.addEventListener("DOMContentLoaded", function() {
const canvas = document.getElementById("click-game-canvas");
const progressBar = document.getElementById("click-game-progress");
if (!canvas || !progressBar) return;
const ctx = canvas.getContext("2d");
canvas.width = 300;
canvas.height = 300;
let level = 1;
let defaultClicks = 2; // 初期のクリック回数
let targetClicks = defaultClicks;
let addClicks = 2; // 加算するクリック数
let ClickGameTimeLeft = 10;
let gameActive = false;
let timer;
let remainingClicks;
let targetX, targetY;
const targetRadius = 20;
const today = new Date().toISOString().split('T')[0]; // 今日の日付(YYYY-MM-DD)
const savedData = JSON.parse(localStorage.getItem("clickGameScores")) || {};
// ハイスコアをローカルストレージから取得(なければ 0)
let todayHighScore = savedData[today] || 0;
let highScore = savedData["highScore"] || 0;
const targetImages = clickGameData.imageUrls; // PHP から渡された画像URL
let targetImage = new Image(); // グローバル変数として定義
function loadRandomTargetImage() {
targetImage.src = targetImages[Math.min(level - 1, targetImages.length - 1)];
targetImage.style.left = `${Math.random() * (canvas.width - 48)}px`;
targetImage.style.top = `${Math.random() * (canvas.height - 48)}px`;
}
function drawGame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.font = "18px Arial";
// 現在のレベルを表示
ctx.fillText("Level: " + level, 200, 40);
// 残り時間を表示
ctx.fillText("Time: " + ClickGameTimeLeft, 200, 20);
// ハイスコアを表示
ctx.fillText("High Score: " + highScore, 10, 20);
ctx.fillText("Today High Score: " + todayHighScore, 10, 40);
if (gameActive) {
ctx.drawImage(targetImage, targetX - targetRadius, targetY - targetRadius, targetRadius * 2, targetRadius * 2);
} else {
// ゲーム開始ボタンを描画
ctx.fillStyle = "blue";
ctx.fillRect(90, 120, 120, 40);
ctx.fillStyle = "white";
ctx.font = "20px Arial";
ctx.fillText("START", 120, 147);
}
}
function updateProgressBar() {
const percentage = Math.max((remainingClicks / targetClicks) * 100, 0);
progressBar.style.width = percentage + "%";
// 色を変更
if (percentage > 66) {
progressBar.style.backgroundColor = "green"; // 2/3以上 → 緑
} else if (percentage > 33) {
progressBar.style.backgroundColor = "yellow"; // 1/3以上 → 黄
} else {
progressBar.style.backgroundColor = "red"; // 1/3以下 → 赤
}
}
function moveTarget() {
targetX = Math.random() * (canvas.width - 2 * targetRadius) + targetRadius;
targetY = Math.random() * (canvas.height - 2 * targetRadius) + targetRadius;
loadRandomTargetImage();
}
function checkClick(event) {
if (!gameActive) return;
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const distance = Math.sqrt((x - targetX) ** 2 + (y - targetY) ** 2);
if (distance < targetRadius) {
remainingClicks--;
updateProgressBar();
if (remainingClicks <= 0) { levelUp(); return; } moveTarget(); } drawGame(); } function levelUp() { clearInterval(timer); gameActive = false; ctx.fillStyle = "green"; ctx.font = "24px Arial"; ctx.fillText("Level Up!", 100, 150); setTimeout(() => {
level++;
targetClicks += addClicks;
startGame();
}, 2000);
}
function updateHighScore() {
if (level > todayHighScore) {
todayHighScore = level;
savedData[today] = todayHighScore;
}
if (level > highScore) {
highScore = level;
savedData["highScore"] = highScore;
}
localStorage.setItem("clickGameScores", JSON.stringify(savedData));
}
function gameOver() {
clearInterval(timer);
gameActive = false;
// ハイスコアを更新
updateHighScore();
ctx.fillStyle = "black";
ctx.font = "24px Arial";
ctx.fillText("Game Over!", 80, 150);
ctx.fillText("High Score: " + highScore, 80, 180);
ctx.fillText("Today High Score: " + todayHighScore, 80, 210);
setTimeout(() => {
level = 1; // レベルリセット
targetClicks = defaultClicks;
ClickGameTimeLeft = 10;
drawGame();
}, 3000);
}
function startGame() {
if (gameActive) return; // ゲーム中は開始しない
clearInterval(timer);
ClickGameTimeLeft = 10;
gameActive = true;
remainingClicks = targetClicks;
moveTarget();
updateProgressBar();
drawGame();
timer = setInterval(() => {
if (ClickGameTimeLeft > 0) {
ClickGameTimeLeft--;
drawGame();
} else {
gameOver();
}
}, 1000);
}
canvas.addEventListener("click", checkClick);
// startButton.addEventListener("click", startGame);
canvas.addEventListener("click", function(event) {
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
if (!gameActive && x >= 90 && x <= 210 && y >= 120 && y <= 160) {
startGame();
}
});
drawGame();
});
おわりに
今回のアップデートで、もともとのシンプルなクリックゲームが、戦略性やビジュアル、操作性の面で大幅に進化しました。前編でご紹介した基礎をさらに発展させることで、ユーザーに新たな挑戦と楽しさを提供できるようになっています。
これからも、細かな改善や新機能の追加を通じて、より魅力的なゲーム体験を追求していきます。ぜひ、最新バージョンをダウンロードして、進化したクリックゲームを体験してみてください!