実験#41:拾って集めて打ってはじけて(花びら花火玉ゲーム)

■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秒の間にいっぱい部品を集めて、 夜空いっぱいに スペシャル花火 を咲かせてみてくださいね!

▼ここからコード▼(クリックで開く)
<!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: 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^*)

~このブログについて~

AIと一緒に遊び、
100の実験でゲームを育てるブログ

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


AIと作る100のゲーム実験室(*^v^*)をもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む