■92個目 「香りの導きと、想いの短歌」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回は、見えない香りをテーマにしたゲームを作ってみました。
タイトルは、
『香りの導きと、想いの短歌~尋ね香、見えぬ君を追ひて~』 です!
舞台は、真っ暗な夜の庭。 プレイヤーは小さな夜の蝶となり、
遠く離れた恋人のもとへ向かいます。(=v=*)
今回こだわったのは、「見えない力」の表現です。
植物たちが静かに放つ香りの攻防──虫たちを呼んだり遠ざけたりする、
あの不思議なやり取りを体感できるゲームにしたいと思いました。
真っ暗な画面の中で頼りになるのは、
風に乗って流れてくる微かな香りの粒子と、
近づくほど高くなるピコピコ音。
あえて視覚情報を極限まで削ぎ落とし、
手探りで進む感覚を大切にしています。
そしてゴールで恋人の花に触れると……
想いの短歌が詠まれます。
逢瀬までの刻(タイム)によって、
どんな一句が生まれるのか、お楽しみに。
柔らかな光をまとったドット絵の蝶と、
見えない香りに導かれる雅な恋の駆け引き。
ぜひ、夜の世界を彷徨ってみてくださいね。(^_^)
■ AIとのやりとりで面白かった部分
● ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。HTMLで動かせるゲームです。
今回は「見えない力」や「見えない存在」をテーマにしたいと思っています。
その中でも、庭のハーブや植物たちが持つ
“香り”に注目したゲームにしたいです。
昆虫にとって香りは、忌避にも誘因にもなる不思議な力。
これをゲームとして表現する良い案があれば、いくつか出してみて。
AI: 「見えない香り」を主軸にするなんて、とても粋な発想だね。
植物たちが虫を呼んだり遠ざけたりする香りの攻防は、
まさに“目に見えないエネルギーのやり取り”そのものだ。
(中略:植物の香りの豆知識)
さて、その「香りという見えない力」をHTMLでどう表現するか。
いくつかゲーム案を作ってみたよ。
みー:(*^_^)” 提案してくれたこのゲームをベースにしてみましょう。
夜の蝶のナビゲーション(嗅覚探索アクション)でいきたいです。
(中略:ゲーム仕様の相談)
みー:(^~^*)蝶と花をもっと幻想的にしたいです。
障害物に当たると一瞬引っかかるようにして、
花までの時間をスコアにしましょう。
AI: タイムアタックの話が出たから、
「時間」と「見えない存在」にまつわる植物の逸話を紹介しよう。
(中略:ダーウィンと蘭の話)
みー:( ̄v ̄*)遠く離れた恋人同士みたいなイメージで、
もう少し距離を広げたいです。
ゴールした時に百人一首風の恋の短歌が出るのも素敵ですね。
AI: 「香りを頼りに恋人に会いに行く」というのは、
平安時代の恋の作法そのものなんだ。
(中略:薫物の話)
キャンバスを横長にして距離を広げ、
スコアに応じて百人一首風の短歌が表示されるように調整したよ。
■ 実際の動作
仕上がったスタート画面がこちらです。
画像でも分かりにくいほど、ほんのりと香りのパーティクルが漂っています。

(画像:スタート画面)
障害物にぶつかると、蝶の動きが一瞬だけ止まります。
危険な香りがうっすらと漂っているので、
感覚を頼りに避けながら進みましょう。

(画像:ゲーム画面)
ゴールにたどり着くと、
クリアタイムに応じて「香りにまつわる短歌」が表示されます。
夜の闇を越えて出会う恋人たちの想いを、そっと詠んだ一首です。

(画像:香りにまつわる短歌)
クリアタイムが短いほど、情熱的な恋の歌に。
ゆっくり進むと、静かに想いを重ねるような一首が現れます。
■ コードのポイント
今回の記事では、
AI と相談しながら作った「香りの導きと、想いの短歌」 の HTML コードを公開します。
このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。
ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。
▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く
Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)
② 下のコードをコピーして貼り付けます。
③ 「kaori.html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。
■ 操作方法
ここからは、具体的なゲームの操作方法と、
真っ暗な夜を進むためのヒントをお伝えしますね。(^^*)🌙
● ゲームの始め方
画面上のどこでもクリックすると、
ピコピコと音が鳴り、小さなドット絵の蝶が夜の空を舞い始めます。🦋
● 蝶の動かし方
キーボード操作で蝶を動かします。
真っ暗な画面ですが、小さな羽ばたきを頼りに進んでいきましょう。(^^)
● ゲームの目的
右側のどこかにある「恋人の花(ゴール)」を見つけることです。🌸
はるか遠く、見えぬ君を追いかけて──手探りの逢瀬へ向かいます。(=v=*)
● 真っ暗闇を進むヒント
こだわりの「見えない力の表現」が、あなたを導いてくれます。✨
① 香りの導き
微かな光の粒(四角いピクセル)が流れてきたら、それが香りのサイン。
- ピンクの粒 … 甘い香り。ゴールの方向へ
- 緑の粒 … 忌避の香り。障害物の気配
パーティクルは左へ流れているので、
その流れに逆らって進むイメージです。🌬️
② 音のヒント
ピコピコ音が一定間隔で鳴ります。
ゴールに近づくほど、音が高く鋭い音質に変化します。🔔
音を頼りに、進む方向を探ってみてくださいね。(^_^)
● 見えない障害物のペナルティ
ハーブの忌避効果(見えない壁)に当たると、
「バチッ」という音とともに蝶が赤くなり、
一瞬だけ動きが止まります。(><)
当たった瞬間だけ、そのハーブの姿がふっと見えるので、
次は気をつけて避けてくださいね。🪴
タイムロスを抑えながら進むのがコツです。
● クリアとリトライ
恋人の花にたどり着くと、幻想的な花とともに、
逢瀬までの刻(タイム)に応じた「想いの短歌」が詠まれます。📜🌸
どんな短歌に出逢えるか、お楽しみに。(=v=*)
クリア後は「もう一度夜へ飛ぶ」ボタンで、何度でも挑戦できます。🦋✨
香りに導かれ、想いの短歌に出逢ってくださいね。(^^*)🌙
“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head><meta charset="UTF-8"><title>尋ね香、見えぬ君を追ひて</title><style> body { background: #050508; color: #eee; text-align: center; font-family: 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 20px; } canvas { background: #000; border: 1px solid #222; border-radius: 4px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.8); image-rendering: pixelated; } p { font-size: 14px; color: #888; margin-bottom: 20px; } #retryBtn { display: none; margin: 15px auto; padding: 12px 24px; background: transparent; color: #f9a; border: 1px solid #f9a; border-radius: 20px; cursor: pointer; font-family: 'Yu Mincho', 'MS Mincho', serif; font-size: 18px; transition: all 0.3s; text-shadow: 0 0 5px #f9a; box-shadow: 0 0 10px rgba(255, 150, 200, 0.2); } #retryBtn:hover { background: rgba(255, 150, 200, 0.1); box-shadow: 0 0 15px rgba(255, 150, 200, 0.5); }</style></head><body><h2>尋ね香、見えぬ君を追ひて</h2><p>画面をクリックしてスタート。<br>十字キーで移動。微かな香りを頼りに、闇夜を進んでください。</p><canvas id="gameCanvas" width="960" height="540"></canvas><br><button id="retryBtn">もう一度夜へ飛ぶ</button><script>const canvas = document.getElementById('gameCanvas');const ctx = canvas.getContext('2d');const retryBtn = document.getElementById('retryBtn');let audioCtx;let isPlaying = false;let isGameOver = false;let player, goal, obstacles, particles, trails;let startTime, endTime;let keys = {}; function initGame() { player = { x: 60, y: 270, speed: 3.5, snagTimer: 0 }; goal = { x: 880, y: Math.random() * 440 + 50, radius: 30, revealAlpha: 0, pulse: 0 }; obstacles = []; for(let i=0; i<8; i++) { obstacles.push({ x: 200 + Math.random() * 600, y: 50 + Math.random() * 440, radius: 35 + Math.random() * 30, revealAlpha: 0 }); } particles = []; trails = []; isGameOver = false; startTime = Date.now(); endTime = null; retryBtn.style.display = 'none'; keys = {};}initGame();function spawnParticles() { if (Math.random() < 0.2) { particles.push({ x: goal.x + (Math.random()-0.5) * 40, y: goal.y + (Math.random()-0.5) * 40, vx: -1.0 - Math.random() * 1.5, vy: (Math.random() - 0.5) * 0.8, life: 400, type: 'sweet', size: Math.floor(Math.random() * 2) + 2 }); } obstacles.forEach(obs => { if (Math.random() < 0.08) { particles.push({ x: obs.x + (Math.random()-0.5) * obs.radius, y: obs.y + (Math.random()-0.5) * obs.radius, vx: -0.8 - Math.random(), vy: (Math.random() - 0.5) * 1.2, life: 150, type: 'repellent', size: Math.floor(Math.random() * 2) + 2 }); } });}function playSound(freq, type, duration = 0.15) { if (!audioCtx) return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freq, audioCtx.currentTime); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); gain.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + duration); osc.stop(audioCtx.currentTime + duration);}function drawDotButterfly(x, y, isSnagged) { ctx.save(); ctx.translate(x, y); const wingColor = isSnagged ? '#f44' : '#0ee'; const subColor = isSnagged ? '#a00' : '#088'; const bodyColor = '#fff'; const dot = 2; const isFlapping = Math.floor(Date.now() / 100) % 2 === 0; if (isFlapping) { ctx.fillStyle = wingColor; ctx.fillRect(-5*dot, -5*dot, 3*dot, 4*dot); ctx.fillRect(2*dot, -5*dot, 3*dot, 4*dot); ctx.fillStyle = subColor; ctx.fillRect(-6*dot, -1*dot, 4*dot, 3*dot); ctx.fillRect(2*dot, -1*dot, 4*dot, 3*dot); } else { ctx.fillStyle = wingColor; ctx.fillRect(-6*dot, -2*dot, 4*dot, 3*dot); ctx.fillRect(2*dot, -2*dot, 4*dot, 3*dot); ctx.fillStyle = subColor; ctx.fillRect(-5*dot, 1*dot, 3*dot, 4*dot); ctx.fillRect(2*dot, 1*dot, 3*dot, 4*dot); } ctx.fillStyle = bodyColor; ctx.fillRect(-1*dot, -3*dot, 2*dot, 6*dot); ctx.fillStyle = subColor; ctx.fillRect(-2*dot, -5*dot, 1*dot, 2*dot); ctx.fillRect(1*dot, -5*dot, 1*dot, 2*dot); ctx.restore();}function drawGlowingFlower(x, y, alpha, pulse) { ctx.save(); ctx.globalAlpha = alpha; ctx.shadowBlur = 30 + Math.sin(pulse) * 10; ctx.shadowColor = '#f0f'; ctx.fillStyle = '#f9a'; for(let i=0; i<8; i++) { let angle = (i * Math.PI) / 4 + pulse * 0.3; ctx.beginPath(); ctx.ellipse(x + Math.cos(angle)*12, y + Math.sin(angle)*12, 18, 5, angle, 0, Math.PI*2); ctx.fill(); } ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI*2); ctx.fill(); ctx.restore();}function drawGlowingObstacle(x, y, radius, alpha) { ctx.save(); ctx.globalAlpha = alpha; ctx.shadowBlur = 20; ctx.shadowColor = '#0f0'; ctx.strokeStyle = '#5f5'; ctx.lineWidth = 2; ctx.beginPath(); for(let i=0; i<12; i++) { let angle = (i * Math.PI) / 6; let r = radius + (i % 2 === 0 ? 10 : -10); let px = x + Math.cos(angle) * r; let py = y + Math.sin(angle) * r; if(i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.closePath(); ctx.stroke(); ctx.fillStyle = 'rgba(0, 50, 0, 0.5)'; ctx.fill(); ctx.restore();}function getRomanticPoemLines(timeInSeconds) { let options = []; if (timeInSeconds <= 18) { options = [ "闇夜にも 迷はぬほどに 濃き匂ひ / 導く先は 君の腕の中", "風に乗る 微かな香を 捕らへては / 焦がれる胸が 君へと急ぐ", "幾千の 闇を越へても 違へじな / わが魂を 呼ぶその香りは", "忌避の棘 恐るるに足らず 今宵こそ / 匂ひほころぶ 君に逢ふなり", "月見えず 星も隠るる 宵なれど / 君の残り香 闇を切り裂く" ]; } else if (timeInSeconds <= 35) { options = [ "探しても 見えぬ姿に 惑ふ夜 / されど確かな 君の残り香", "見えねども 確かに其処に ある気配 / 香りをたより 君を尋ねむ", "宵闇に 紛れて探す 恋の道 / 香りが繋ぐ 二人の絆", "惑ひつつ 幾重の壁を 越えゆかむ / 甘き調べに 身を委ねつつ", "ためらひの 羽根を休める 間もあらず / 君の匂ひに ただ惹かれゆく" ]; } else { options = [ "待ちわびて 宵から夜更け 彷徨へど / 香り絶えねば いつか逢へるを", "長き夜を 幾度迷ひて 過ぎぬれど / 逢瀬の香りは 色褪せざりけり", "遠き君 隔てる闇の 深くとも / 想ひは香りの ごとく漂ふ", "傷ついた 羽根を慰む 残り香よ / 長き迷ひも 報はれにけり", "逢ひたくて 幾つの壁に 阻まれて / 辿り着きけり 君の咲く場所" ]; } const selected = options[Math.floor(Math.random() * options.length)]; return selected.split(' / ');}window.addEventListener('keydown', e => keys[e.code] = true);window.addEventListener('keyup', e => keys[e.code] = false);canvas.addEventListener('mousedown', () => { if (!isPlaying) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); isPlaying = true; startTime = Date.now(); setInterval(sonarPulse, 400); loop(); }});retryBtn.addEventListener('click', () => { initGame(); if (!isPlaying) { isPlaying = true; loop(); }});function sonarPulse() { if (!isPlaying || isGameOver) return; const distGoal = Math.hypot(goal.x - player.x, goal.y - player.y); let minDistObs = Infinity; obstacles.forEach(obs => { const dist = Math.hypot(obs.x - player.x, obs.y - player.y); if (dist < minDistObs) minDistObs = dist; }); if (distGoal < 450) { playSound(900 - distGoal * 1.2, 'sine'); } if (minDistObs < 150) { setTimeout(() => playSound(150 + minDistObs, 'triangle'), 100); }}let finalPoemLines = null;function loop() { if (!isPlaying) return; ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; ctx.fillRect(0, 0, canvas.width, canvas.height); let currentTime = Date.now(); let displayTime = isGameOver ? (endTime - startTime) : (currentTime - startTime); let secondsNum = displayTime / 1000; let secondsStr = secondsNum.toFixed(2); if (!isGameOver) { goal.pulse += 0.05; let currentSpeed = player.speed; let isSnagged = false; if (player.snagTimer > 0) { player.snagTimer--; currentSpeed = player.speed * 0.1; isSnagged = true; } if (keys['ArrowUp']) player.y -= currentSpeed; if (keys['ArrowDown']) player.y += currentSpeed; if (keys['ArrowLeft']) player.x -= currentSpeed; if (keys['ArrowRight']) player.x += currentSpeed; player.x = Math.max(0, Math.min(canvas.width, player.x)); player.y = Math.max(0, Math.min(canvas.height, player.y)); if (!isSnagged && (keys['ArrowUp'] || keys['ArrowDown'] || keys['ArrowLeft'] || keys['ArrowRight'])) { trails.push({ x: player.x, y: player.y, alpha: 1.0 }); } obstacles.forEach(obs => { const dx = player.x - obs.x; const dy = player.y - obs.y; const dist = Math.hypot(dx, dy); if (dist < obs.radius) { if (player.snagTimer === 0) { player.snagTimer = 40; playSound(100, 'sawtooth', 0.3); } player.x += (dx / dist) * currentSpeed * 2; player.y += (dy / dist) * currentSpeed * 2; obs.revealAlpha = 1.0; } }); const distToGoal = Math.hypot(player.x - goal.x, player.y - goal.y); if (distToGoal < goal.radius) { goal.revealAlpha = 1.0; isGameOver = true; endTime = currentTime; finalPoemLines = getRomanticPoemLines(secondsNum); playSound(800, 'sine', 0.5); setTimeout(() => playSound(1200, 'sine', 1.0), 200); setTimeout(() => playSound(1600, 'sine', 2.0), 400); retryBtn.style.display = 'block'; } spawnParticles(); } for (let i = trails.length - 1; i >= 0; i--) { let t = trails[i]; t.alpha -= 0.04; ctx.fillStyle = `rgba(0, 255, 255, ${t.alpha * 0.5})`; ctx.fillRect(t.x - 2, t.y - 2, 4, 4); if (t.alpha <= 0) trails.splice(i, 1); } ctx.globalCompositeOperation = 'lighter'; for (let i = particles.length - 1; i >= 0; i--) { let p = particles[i]; p.x += p.vx; p.y += p.vy; p.life--; const alpha = Math.max(0, p.life / 400) * 0.35; ctx.fillStyle = p.type === 'sweet' ? `rgba(255, 100, 200, ${alpha})` : `rgba(100, 255, 100, ${alpha})`; ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size); if (p.life <= 0) particles.splice(i, 1); } ctx.globalCompositeOperation = 'source-over'; obstacles.forEach(obs => { if (obs.revealAlpha > 0) { drawGlowingObstacle(obs.x, obs.y, obs.radius, obs.revealAlpha); obs.revealAlpha -= 0.015; } }); if (goal.revealAlpha > 0) { drawGlowingFlower(goal.x, goal.y, goal.revealAlpha, goal.pulse); if (!isGameOver) goal.revealAlpha -= 0.015; } drawDotButterfly(player.x, player.y, player.snagTimer > 0); if (!isGameOver) { ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx.font = '14px monospace'; ctx.textAlign = 'right'; ctx.fillText(`${secondsStr}s`, canvas.width - 20, 30); ctx.textAlign = 'left'; } if (isGameOver && finalPoemLines) { ctx.fillStyle = `rgba(0, 0, 0, ${Math.min(0.7, goal.revealAlpha)})`; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = `rgba(255, 200, 220, ${Math.min(1, goal.revealAlpha)})`; ctx.textAlign = 'center'; ctx.font = '26px "Yu Mincho", "MS Mincho", serif'; ctx.shadowBlur = 15; ctx.shadowColor = '#f9a'; ctx.fillText(finalPoemLines[0], canvas.width/2, canvas.height/2 - 20); ctx.fillText(finalPoemLines[1], canvas.width/2, canvas.height/2 + 20); ctx.font = '14px "Yu Mincho", "MS Mincho", serif'; ctx.fillStyle = `rgba(255, 255, 255, ${Math.min(0.5, goal.revealAlpha)})`; ctx.shadowBlur = 0; ctx.fillText(`逢瀬までの刻:${secondsStr} 秒`, canvas.width/2, canvas.height/2 + 80); ctx.textAlign = 'left'; } requestAnimationFrame(loop);}ctx.fillStyle = '#fff';ctx.font = '20px sans-serif';ctx.textAlign = 'center';ctx.fillText('クリックして夜の世界へ', canvas.width/2, canvas.height/2);ctx.textAlign = 'left';</script></body></html>
■ 今日の学び
「雰囲気ゲーム」というジャンルがあるのかどうか、
以前はよく分からなかったのですが……
実は「雰囲気ゲー」という言葉は、
インディーゲーム界隈では
しっかりとしたジャンルとして認識されているそうです。( ˘꒳˘ )✨
今回は、そんな“見えないもの”をゲームで表現できないかなと、
AIへ相談したところから制作が始まりました。
注目したのは「香り」。
見えないけれど、風や重力、あるいは人の感情のように、
確かにそこにあって影響を与える存在です。
その香りを、匂いのない画面越しの世界でどう伝えるか──
AIと一緒に、ああでもないこうでもないと試行錯誤しました。
最終的には、微かな光の粒とピコピコという電子音を重ねて表現する、
静かでどこか古風なゲームに落ち着きました。
(見えないものを手探りで探すって、ちょっとロマンチックですよね)
いつものように、その日のうちに作り上げるHTMLゲームなので、
どこか不十分で荒削りな部分もあります。
でも今回も、その“未完成さ”を含めて、
心から楽しみながら仕上げることができました。(^^*)
【おまけ:これからの実験室について】
毎日コツコツ作っているうちに、
ブログに載せていないゲームがかなり溜まってきました。
実は「100のゲーム」という当初の目標をとうに超えて、
いまでは 160個近く の不思議なゲームたちが、
手元のメモ帳の中で出番を待っています。( ¯•ω•¯ )💭
せっかくなので、
ブログ内に「未記事ゲームの一覧ページ」となる
固定ページを作ってみようかなと考え中です。
そしてこれからは、皆さんから
「このゲームの記事が読みたい!」
「こんなゲームを見てみたい!」 といったリクエストをいただいて、
それをもとに記事化していくのも面白いかなと思っています。
160の実験を超えてさらに広がる、AIとのゲーム実験室を、
これからもよろしくお願いします! ヽ(>v<*)=3
■ 次回も、大切な人に会いに行くゲームです。
しかし、待ち合わせのたびに降り出すのは、決まって雨。
しかも土砂降りの大荒れ──。
それでもめげずに向かいます。
大切な人に会うために。
次回も、そんな“想いで進むゲーム”をお届けします。(*^v^)ノシ
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)






🌈はじめての方へ
🌳 実験一覧
🌱 このブログについて
🍃 プライバシーポリシー
🥚未記事ゲーム展示室①
🐣未記事ゲーム展示室②