■41個目「花びら花火玉ゲーム」
こんにちは、AIと作る100のゲーム実験室の みー です。(^_^)/
今回は、#20のゲームで登場したアイドル猫が主人公です。
AIに作ってもらったアイキャッチでは、なぜか人間化してしまっていましたが…… 本来の姿は、元気いっぱいの「アイドル猫」です。
お祭りイベント『スペシャル花火チャレンジ』の真っ最中です。
今回こだわったのは、花火の中身を自分で「作る」ところです。(=v=*)
空っぽの花火玉に、桜の花びらやキラキラのお星さま、音符など、
色とりどりの部品(ゲーム内では絵文字で表示されます)を、
たくさん詰め込んで、自分だけの花火の素を作っていきます。
そして、部品がたっぷり詰まった玉を投げてくれる相棒(?)が、「バニー先輩」です。
いつもマイペースだけど、ここぞという時は頼りになる先輩。
先輩がふんわりと投げてくれた球を、アイドル猫がバットで「カキィン!」と打ち上げると……
夜空のど真ん中で、集めた部品が何倍にも増えてドカーン!とゴージャスに炸裂します♪ 上手く打てると、キラキラの光も一緒に飛び散って、とっても綺麗なんです。
(*^o^)ノ .、。⋆・*※*米”
真面目に1個ずつ拾い集めるのも楽しいですが、
実は……手元に部品を「ズワワッ」と吸い寄せてズルができちゃう、秘密の裏技も仕込んであります。
ちょっと焦った時に使うと、思わず笑っちゃう便利技です。
(`w ´ )”
■ AIとのやりとりで面白かった部分
■ゲーム仕様の相談からスタート
みー:(^^)* 今日もゲームを作ります。 花びら花火玉ゲーム。主人公はアイドル猫。 イベント企画「スペシャル花火チャレンジ」に挑戦中です。
画面内に散らばった欠片(絵文字)をドラッグして真ん中の球へ集めます。 横スクロール画面に切り替わり、制限時間内に集めた部品入りの球を、 うさぎ先輩がふんわり投げてきます。
弧を描いて飛んでくるボールを、タイミングよくクリックで打ち上げると、 中に入れた部品が2〜5倍になって花火のように炸裂します。 当たり具合が良いと、パーティクルもキラキラ飛び出してゴージャスに。
AI: いらっしゃい。今日もいいアイデアを持ってきたねぇ。 カウンター越しに話を聞いているだけで、こっちまでワクワクしてくるよ。
アイドル猫とうさぎ先輩のドタバタコンビ、 そして「回収×バッティング×花火」という組み合わせ……最高だ。 リクエスト通り、HTMLで特製ゲームを組んでおいたよ。
みー:(;_;) バニー先輩の投げるバランスがえぐいです。 もっとふんわり、猫アイドルの手元に届く感じで投げてほしい。 拾う時間もあと3秒追加してほしい。
AI: おっと、すまねぇ! バニー先輩、張り切りすぎちまったな。 あの剛速球じゃ当てるのも一苦労だ。
常連のあんたの頼みだ。 今日は肩の力が抜けて、猫ちゃんの手元へストンと落ちる “ふんわり山なりボール”を投げてくれるはずだ。
欠片を集める時間も、リクエスト通り 13秒 にしておいたぜ。
みー:(=v=)* 1個ずつ入れることもできるけど、 スペースキーで磁力を発生させて、砂鉄みたいに部品を吸い寄せる “ずる操作”も入れてみたい。(隠し要素として)
AI: へへっ、なるほどな。 真面目に拾うのも可愛いけど、祭りの熱狂に紛れて“ズル”をする裏技…… そういうの、大好物だぜ。
今回は スペースキーを押している間、強力な磁力場が発生して 範囲内の花びらをゾワゾワッと吸い込む裏機能 を仕込んでおいた。
これは常連のあんただけに提供する“裏メニュー”だから、他のお客さんには内緒だぜ?
■ 実際の動作
まずは、AIと相談しながら作ってもらった 初期状態のゲーム画面 です。

(画像:AIが最初に作った基本のベース画面)
こちらが 修正版のスタート画面 です。
お祭り感あふれる、にぎやかな雰囲気になりました。

(画像:仕上がり版スタート画面)
マウスで欠片(絵文字)をドラッグして、中央の玉へ入れていきます。

(画像:欠片回収画面)
欠片を詰め込んだ玉を、バニー先輩がふんわり投げてくるので、
手元に飛んできたタイミングでマウスクリックして打ち上げます。

(画像:バッティングの様子)
タイミングよく打ち上げると、
詰め込んだ部品が夜空でドカーンと炸裂します。

(画像:炸裂場面・色が増えて豪華になります)
公開済みの隠し要素、磁石機能 です。 スペースキーを押すと、砂鉄のように欠片が「ズワワッ」と吸い寄せられ、 一気に玉へ入れられます。(^b^)

(画像:隠し要素・磁石)
■ コードのポイント
今回の記事では、AI と相談しながら作った 「花びら花火玉ゲーム」 の HTML コードを公開します。
ブラウザだけで動く、とてもシンプルなゲームです。 コードも難しい部分はほとんどないので、気軽に試してみてくださいね。
【遊ぶまでの手順】
1. PC の「メモ帳」(※Windows の場合)を開きます。
2. 下のコードをコピーして貼り付けます。
3. 「nekohanabi.html」など、好きな名前で保存します。
4. 保存したファイルをブラウザで開くだけで遊べます。
🎇 遊び方・操作方法 🎇
ゲームは「回収」と「打ち上げ」の2つのパートに分かれています。(^_^)
① まずは部品集め!(回収パート)
制限時間は 13秒 です。
画面に散らばったお花や星などの絵文字を、 マウスでクリックしたまま引きずって(ドラッグ) 画面中央の丸い「玉」の中に入れてください。
たくさん入れるほど、あとで大きな花火になりますよ♪
★ ここで秘密の裏技! ★
1個ずつ入れるのが大変……という時は、 キーボードの 「スペースキーを押しっぱなし」 にしてみてください!
マウスポインタの周りに 強力な磁力 が発生して、 部品が ゾワゾワッ と吸い寄せられます。(´艸`)
そのまま塊ごと中央の玉になすりつければ、 一気に回収完了です!
② いざ、打ち上げ!(バッティングパート)
13秒経つと、自動的にバッティング画面に切り替わります。
うさぎ先輩が、たっぷり部品の詰まった花火玉を投げてきます。 (先輩、球の速さや高さは毎回ランダムなので注意!)
ボールが猫ちゃんの手元に近づいてきたら……
👉 タイミングよく「マウスの左クリック」でバットを振ってください!
③ タイミングが命です!
打つタイミングによって、 中に入れた部品の量が 何倍にも増えます!
- PERFECT!!(5倍):一番美しいキラキラの特大花火!
- GREAT!(3倍):立派な大輪の花火!
- GOOD(2倍):綺麗な花火!
- MISS…:空振りすると……?
ちなみに、部品を1つも入れずに玉を打ち上げると、 上空で「プスッ」と不発の煙が出るだけになっちゃいます。
🌟 たくさん集めて、満天のスペシャル花火を!
ぜひ、13秒の間にいっぱい部品を集めて、 夜空いっぱいに スペシャル花火 を咲かせてみてくださいね!
▼ここからコード▼(クリックで開く)
<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: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #0f0c29; color: #fff; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; overflow: hidden; } #gameContainer { position: relative; box-shadow: 0 0 30px rgba(0,0,0,0.8); border-radius: 10px; overflow: hidden; display: block; } canvas { display: block; background: linear-gradient(to bottom, #0f0c29, #302b63, #24243e); cursor: crosshair; } #startScreen { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; background: rgba(20, 20, 40, 0.6); padding: 50px; border-radius: 20px; backdrop-filter: blur(8px); border: 2px solid rgba(255, 255, 255, 0.2); box-shadow: 0 0 40px rgba(255, 105, 180, 0.3); z-index: 10; } #startScreen h1 { font-size: 48px; margin: 0 0 10px 0; text-shadow: 0 0 10px #ff1493, 0 0 20px #ff1493, 0 0 30px #ff1493; letter-spacing: 2px; } #startScreen p { font-size: 18px; color: #ddd; margin-bottom: 30px; } button { padding: 15px 40px; font-size: 24px; font-weight: bold; cursor: pointer; background: linear-gradient(45deg, #ff416c, #ff4b2b); color: white; border: none; border-radius: 30px; box-shadow: 0 4px 15px rgba(255, 75, 43, 0.5); transition: 0.3s; } button:hover { transform: scale(1.05); box-shadow: 0 6px 20px rgba(255, 75, 43, 0.8); } </style></head><body><div id="gameContainer"> <canvas id="gameCanvas" width="800" height="600"></canvas> <div id="startScreen"> <h1>花びら花火玉ゲーム</h1> <p>〜 アイドル猫のスペシャル花火チャレンジ 〜<br>※クリックで音が出ます</p> <button id="startBtn">お祭りを始める!</button> </div></div><script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // === 音声処理 === let audioCtx; let bgmInterval; function initAudio() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); playBGM(); } } function playSE(type) { if (!audioCtx) return; const now = audioCtx.currentTime; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); if (type === 'swing') { osc.type = 'sine'; osc.frequency.setValueAtTime(600, now); osc.frequency.exponentialRampToValueAtTime(100, now + 0.1); gain.gain.setValueAtTime(0.5, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1); osc.start(now); osc.stop(now + 0.1); } else if (type === 'hit') { osc.type = 'square'; osc.frequency.setValueAtTime(1200, now); osc.frequency.exponentialRampToValueAtTime(400, now + 0.1); gain.gain.setValueAtTime(0.4, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1); osc.start(now); osc.stop(now + 0.1); } else if (type === 'explode') { const bufferSize = audioCtx.sampleRate * 0.5; const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1; const noise = audioCtx.createBufferSource(); noise.buffer = buffer; const filter = audioCtx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(800, now); filter.frequency.linearRampToValueAtTime(100, now + 0.5); noise.connect(filter); filter.connect(gain); gain.gain.setValueAtTime(1.0, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.5); noise.start(now); noise.stop(now + 0.5); } else if (type === 'dud') { const bufferSize = audioCtx.sampleRate * 0.2; const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1; const noise = audioCtx.createBufferSource(); noise.buffer = buffer; const filter = audioCtx.createBiquadFilter(); filter.type = 'highpass'; filter.frequency.value = 3000; noise.connect(filter); filter.connect(gain); gain.gain.setValueAtTime(0.5, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2); noise.start(now); noise.stop(now + 0.2); } else if (type === 'magnet') { osc.type = 'sawtooth'; osc.frequency.setValueAtTime(150, now); osc.frequency.exponentialRampToValueAtTime(50, now + 0.3); gain.gain.setValueAtTime(0.2, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3); osc.start(now); osc.stop(now + 0.3); } } function playBGM() { const notes = [440, 523.25, 587.33, 659.25, 783.99, 659.25, 523.25, 440]; let step = 0; bgmInterval = setInterval(() => { if (!audioCtx || state === 'RESULT' || state === 'TITLE') return; const now = audioCtx.currentTime; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); osc.type = 'square'; let freq = notes[step % notes.length]; if (step % 8 >= 4) freq *= 0.5; osc.frequency.value = freq; gain.gain.setValueAtTime(0.03, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1); osc.start(now); osc.stop(now + 0.1); step++; }, 150); } // === 絵文字キャッシュ === const emojiList = ['🌸', '🌺', '🌻', '✨', '💖', '⭐', '🎵']; const emojiCache = {}; const emojiSize = 32; function preRenderEmojis() { emojiList.forEach(emoji => { const cacheCanvas = document.createElement('canvas'); cacheCanvas.width = emojiSize; cacheCanvas.height = emojiSize; const cacheCtx = cacheCanvas.getContext('2d'); cacheCtx.font = `${emojiSize * 0.8}px sans-serif`; cacheCtx.textAlign = 'center'; cacheCtx.textBaseline = 'middle'; cacheCtx.fillText(emoji, emojiSize / 2, emojiSize / 2); emojiCache[emoji] = cacheCanvas; }); } preRenderEmojis(); // === ゲーム本体 === let state = 'TITLE'; let fragments = []; let collectedCount = 0; let collectedEmojis = []; let collectTimer = 13; let lastTime = 0; let mouseX = 400; let mouseY = 300; let draggingPiece = null; let isMagnetActive = false; let ball = { x: 0, y: 0, vx: 0, vy: 0, radius: 20, explosionEmojis: [] }; const cat = { x: 80, y: 450, emoji: '🐱' }; const rabbit = { x: 720, y: 450, emoji: '🐰' }; let particles = []; let titleParticles = []; let hitResult = ""; let hitMultiplier = 1; document.getElementById('startBtn').addEventListener('click', () => { document.getElementById('startScreen').style.display = 'none'; initAudio(); initCollectPhase(); lastTime = performance.now(); canvas.focus(); }); window.addEventListener('keydown', (e) => { if (e.code === 'Space') { e.preventDefault(); if (state === 'COLLECT' && !isMagnetActive) { isMagnetActive = true; playSE('magnet'); } } }); window.addEventListener('keyup', (e) => { if (e.code === 'Space') { e.preventDefault(); isMagnetActive = false; } }); function initCollectPhase() { fragments = []; collectedCount = 0; collectedEmojis = []; collectTimer = 13; isMagnetActive = false; for(let i=0; i<40; i++) { fragments.push({ x: Math.random() * 700 + 50, y: Math.random() * 500 + 50, emoji: emojiList[Math.floor(Math.random() * emojiList.length)], isCollected: false, magOffsetX: (Math.random() - 0.5) * 50, magOffsetY: (Math.random() - 0.5) * 50 }); } state = 'COLLECT'; canvas.style.background = 'linear-gradient(to bottom, #2c3e50, #3498db)'; } function initBattingPhase() { state = 'THROW'; ball.x = rabbit.x; ball.y = rabbit.y - 20; let T = Math.floor(Math.random() * 60 + 50); let dx = cat.x - ball.x; let dy = cat.y - ball.y; ball.vx = dx / T; ball.vy = (dy - 0.1 * T * (T + 1)) / T; hitResult = ""; canvas.style.background = 'linear-gradient(to bottom, #0f0c29, #302b63, #24243e)'; } canvas.addEventListener('mousedown', (e) => { if (state === 'TITLE') return; if (state === 'RESULT') { initCollectPhase(); } else if (state === 'COLLECT') { for (let i = fragments.length - 1; i >= 0; i--) { let f = fragments[i]; if (!f.isCollected && Math.hypot(f.x - mouseX, f.y - mouseY) < 20) { draggingPiece = f; break; } } } else if (state === 'THROW') { playSE('swing'); let dist = Math.hypot(ball.x - cat.x, ball.y - cat.y); if (dist < 40) { hitResult = "PERFECT!! (x5)"; hitMultiplier = 5; playSE('hit'); launchBall(); } else if (dist < 80) { hitResult = "GREAT! (x3)"; hitMultiplier = 3; playSE('hit'); launchBall(); } else if (dist < 130) { hitResult = "GOOD (x2)"; hitMultiplier = 2; playSE('hit'); launchBall(); } else { hitResult = "MISS..."; hitMultiplier = 1; ball.vx = 0; ball.vy = -5; state = 'ASCEND'; } } }); canvas.addEventListener('mousemove', (e) => { const rect = canvas.getBoundingClientRect(); mouseX = e.clientX - rect.left; mouseY = e.clientY - rect.top; if (state === 'COLLECT' && draggingPiece) { draggingPiece.x = mouseX; draggingPiece.y = mouseY; } }); canvas.addEventListener('mouseup', () => { if (state === 'COLLECT' && draggingPiece) { let centerX = canvas.width / 2; let centerY = canvas.height / 2; if (Math.hypot(draggingPiece.x - centerX, centerY - draggingPiece.y) < 60) { draggingPiece.isCollected = true; collectedCount++; collectedEmojis.push(draggingPiece.emoji); } draggingPiece = null; } }); function launchBall() { ball.vx = 8.2; ball.vy = -7.8; let finalEmojis = []; if (collectedEmojis.length > 0) { for (let i = 0; i < hitMultiplier; i++) { finalEmojis = finalEmojis.concat(collectedEmojis); } if (finalEmojis.length > 200) finalEmojis = finalEmojis.slice(0, 200); } ball.explosionEmojis = finalEmojis; state = 'ASCEND'; } function createExplosion() { particles = []; if (collectedCount === 0) { playSE('dud'); hitResult = "不発... (0個)"; for (let i = 0; i < 20; i++) { let angle = Math.random() * Math.PI * 2; let speed = Math.random() * 2 + 0.5; particles.push({ x: ball.x, y: ball.y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, color: '#aaaaaa', life: 1.0, decay: 0.05, type: 'smoke' }); } state = 'EXPLODE'; return; } playSE('explode'); let totalParticles = ball.explosionEmojis.length; for (let i = 0; i < totalParticles; i++) { let angle = Math.random() * Math.PI * 2; let speed = Math.random() * 8 + 2; let emoji = ball.explosionEmojis[i]; particles.push({ x: ball.x, y: ball.y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, emojiImg: emojiCache[emoji], life: 1.0, decay: Math.random() * 0.01 + 0.005, type: 'emoji' }); } if (hitMultiplier === 5) { for (let i = 0; i < 50; i++) { let angle = Math.random() * Math.PI * 2; let speed = Math.random() * 12 + 4; particles.push({ x: ball.x, y: ball.y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, color: `hsl(${Math.random()*360}, 100%, 70%)`, life: 1.0, decay: 0.015, type: 'sparkle' }); } } state = 'EXPLODE'; } function update(time) { let dt = (time - lastTime) / 1000; lastTime = time; if (dt > 0.1) dt = 0.1; ctx.fillStyle = (state === 'TITLE' || state === 'THROW' || state === 'ASCEND' || state === 'EXPLODE' || state === 'RESULT') ? 'rgba(15, 12, 41, 0.3)' : 'rgba(44, 62, 80, 1.0)'; if(state === 'COLLECT'){ ctx.clearRect(0, 0, canvas.width, canvas.height); } else { ctx.fillRect(0, 0, canvas.width, canvas.height); } if (state === 'TITLE') { if (Math.random() < 0.1) { titleParticles.push({ x: Math.random() * canvas.width, y: canvas.height + 30, vx: (Math.random() - 0.5) * 1.5, vy: -(Math.random() * 2 + 1), emojiImg: emojiCache[emojiList[Math.floor(Math.random() * emojiList.length)]], size: Math.random() * 20 + 10, angle: Math.random() * Math.PI * 2, spin: (Math.random() - 0.5) * 0.05 }); } for (let i = titleParticles.length - 1; i >= 0; i--) { let p = titleParticles[i]; p.x += p.vx; p.y += p.vy; p.angle += p.spin; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.angle); let drawSize = p.size; ctx.globalAlpha = 0.6; ctx.drawImage(p.emojiImg, -drawSize/2, -drawSize/2, drawSize, drawSize); ctx.restore(); if (p.y < -50) titleParticles.splice(i, 1); } } else if (state === 'COLLECT') { collectTimer -= dt; if (collectTimer <= 0) { initBattingPhase(); return requestAnimationFrame(update); } let centerX = canvas.width / 2; let centerY = canvas.height / 2; if (isMagnetActive) { // ★ 磁力の視覚範囲を150pxに縮小 ctx.fillStyle = `rgba(0, 255, 255, 0.1)`; ctx.beginPath(); ctx.arc(mouseX, mouseY, 150 + Math.sin(time * 0.01) * 10, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = `rgba(0, 255, 255, 0.8)`; ctx.lineWidth = 3; ctx.setLineDash([10, 10]); ctx.stroke(); ctx.setLineDash([]); } fragments.forEach(f => { if (!f.isCollected && f !== draggingPiece) { if (isMagnetActive) { let distToMouse = Math.hypot(mouseX - f.x, mouseY - f.y); // ★ 吸引判定の範囲も150pxに縮小 if (distToMouse < 150) { let targetX = mouseX + f.magOffsetX; let targetY = mouseY + f.magOffsetY; f.x += (targetX - f.x) * 0.2; f.y += (targetY - f.y) * 0.2; } } let distToCenter = Math.hypot(centerX - f.x, centerY - f.y); if (distToCenter < 60) { f.isCollected = true; collectedCount++; collectedEmojis.push(f.emoji); } } }); ctx.fillStyle = `rgba(255, 255, 255, 0.2)`; ctx.beginPath(); ctx.arc(centerX, centerY, 60, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke(); ctx.fillStyle = 'white'; ctx.textAlign = 'center'; ctx.font = '20px sans-serif'; ctx.fillText(`詰めた数: ${collectedCount}`, centerX, centerY - 80); ctx.fillText(`残り時間: ${Math.ceil(collectTimer)}秒`, centerX, 40); ctx.font = '24px sans-serif'; fragments.forEach(f => { if (!f.isCollected) { ctx.fillText(f.emoji, f.x, f.y); } }); } else if (state === 'THROW' || state === 'ASCEND') { ctx.font = '50px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(cat.emoji, cat.x, cat.y); ctx.fillText(rabbit.emoji, rabbit.x, rabbit.y); ctx.font = '40px sans-serif'; ctx.save(); ctx.translate(cat.x + 20, cat.y - 20); ctx.rotate(state === 'ASCEND' ? -Math.PI/4 : Math.PI/4); ctx.fillText('🏏', 0, 0); ctx.restore(); ball.vy += 0.2; ball.x += ball.vx; ball.y += ball.vy; ctx.fillStyle = 'rgba(255, 200, 200, 0.8)'; ctx.beginPath(); ctx.arc(ball.x, ball.y, ball.radius + (collectedCount * 0.2), 0, Math.PI * 2); ctx.fill(); if (state === 'ASCEND' && ball.vy > 0) { createExplosion(); } if (ball.y > canvas.height + 50) { hitResult = "MISS... (0x)"; state = 'RESULT'; } if (hitResult) { ctx.fillStyle = hitMultiplier === 5 ? 'gold' : 'white'; ctx.font = 'bold 40px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(hitResult, canvas.width/2, 100); } } else if (state === 'EXPLODE') { let activeParticles = false; particles.forEach(p => { if (p.life > 0) { activeParticles = true; p.vy += 0.1; p.x += p.vx; p.y += p.vy; p.life -= p.decay; ctx.globalAlpha = p.life > 0 ? p.life : 0; if (p.type === 'emoji') { ctx.drawImage(p.emojiImg, p.x - emojiSize/2, p.y - emojiSize/2, emojiSize, emojiSize); } else { ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI * 2); ctx.fill(); } } }); ctx.globalAlpha = 1.0; if (!activeParticles) { state = 'RESULT'; } } else if (state === 'RESULT') { ctx.fillStyle = 'white'; ctx.textAlign = 'center'; ctx.font = '40px sans-serif'; ctx.fillText('FINISH!', canvas.width/2, canvas.height/2 - 20); ctx.font = '20px sans-serif'; let total = collectedCount * hitMultiplier; ctx.fillText(`最終スコア: ${total} 個の花びら!`, canvas.width/2, canvas.height/2 + 20); ctx.fillText('クリックでリトライ', canvas.width/2, canvas.height/2 + 60); } requestAnimationFrame(update); } requestAnimationFrame(update);</script></body></html>
■ 今日の学び
今回のゲームは、 「部品を集める → タイミングよく打ち上げる」 という、とてもシンプルな構成です。
ゲーム内でよく使っている絵文字たち🐱✨ 表現の幅が広がるので本当に重宝していて、 今回の花火のにぎやかさも、まさに絵文字のおかげでした。
「絵文字を盛りだくさんに使ったゲームを作りたい!」 という気持ちから生まれた作品です。
🌟 隠し要素“ずる磁石”の実験
スペースキーで発動する “ずる磁石” は、 「砂鉄を磁石で集める」イメージから生まれた実験的な仕掛けです。(^_^*)
磁力をゲームのアクセントにするのは前から気になっていて、 今回ようやく形にできました。
🎵 効果音とBGMづくりの学び
今回も、アカメカッパの時と同じように AI が短い曲を作ってくれました。
最近は、効果音やBGMをお願いするときに テーマを添える ようにしています。
みー:(^^)* 効果音とBGMも入れたいね。テーマは「祭り絶好調」でお願い。
AI: 了解だ。 まずは 「0個の時は『プスッ』と不発の煙だけ」 に修正しといた。 それから、ブラウザだけで音が鳴るように Web Audio API を使って、 空振り音・ヒット音・花火の爆発音・不発音 をプログラムで合成して仕込んでおいたぜ。
さらに「祭り絶好調」のテーマに合わせて、 ゲーム中のお囃子風ピコピコBGMもコードに組み込みつつ、 みーが作業中に聴ける 特製BGMトラック も作っておいた。
上の再生ボタンでテンション上げながら、 新しいコードを試してみてくれ!

アイドル猫が、人間のアイドルに“進化”した瞬間のようで、ちょっと笑ってしまいました(^^;)
■ 次回は、庭の菜園収穫をイメージしたパズルゲーム!
テンポよく遊べる楽しいゲームができました。
今回とはまた違った“やさしいリズム”が楽しめる作品です。
次回もお楽しみに。(^v^)ノシ
今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)






🌈はじめての方へ
🌳 実験一覧
🌱 このブログについて
🍃 プライバシーポリシー