■82個目 「常闇の森のルーツ・スプライト」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今日はちょっと不思議で、とってもかわいいミクロな世界のお話を。
今回は、植物の根っこを舞台にした、
ちっちゃなちっちゃな物語を作ってみました。
私たちが普段見ている植物の根っこ。
その土の中では、実は目に見えないほど
小さな精霊たちが頑張っているのかもしれません。
このゲームの舞台は、深い深い土の中、「常闇(とこやみ)の森」です。
ここには、不思議な「太陽の種(金色の光)」を大切に抱えて眠っている
「泥の人形(ゴーレム)」たちがたくさんいます。(=v=*)
並んで眠っている姿が、なんともかわいいんです。
プレイヤーは、小さな「根の精霊(ルーツ・スプライト)」になって、
彼らの中から太陽の種を目覚めさせるお仕事をします。(^^b)
今回こだわったのは、
「ミクロな生化学の仕組み」と「ファンタジー」の融合です。(^v^)
植物が栄養を吸い取る仕組みを、ただのシミュレーションではなく、
「月のしずく(青い光)」を使って金色の種を目覚めさせる、
魔法のようなお話に描き替えました。
そして、精霊が集めた金色の種は、
「光の結界(膜)」の向こうにある鉢植えに届けられます。
金色の種が届くたびに、小さな芽が枝になり、
やがて大きな木へと育っていきます。(>v<*)
優しいBGMとピコピコした効果音に包まれながら、どこまでも大きく育ち続ける「生命の樹」の成長を見守る。
地味だけど、どこか温かい。 そんなミクロな世界の成長ストーリーを、ゆっくり楽しんでいただけたら嬉しいです。
■ AIとのやりとりで面白かった部分
● ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。html形式で動かせるゲーム。
植物の栄養吸収は根から行いますが、
栄養成分の吸収方法はとてもマクロな世界で行われています。
元素レベルでの電子の入れ替えによる成分の吸収は、
磁力・遠心力・重力とはまた違った吸着吸収で、
植物が細胞へ必要な栄養成分を取り込むためのシステムがあります。
シミュレートできるかな。
AI: 今日も素晴らしい着眼点ですね!
植物の根が栄養を吸収するミクロな世界、
特に「プロトンポンプ」と「陽イオン交換」のメカニズムは、
まさに物理的な引力や遠心力とは異なる、
細胞レベルのエレガントなシステムです。
シミュレーションのテーマ:プロトンポンプと陽イオン交換
植物は細胞膜にある「プロトンポンプ」を使って、
エネルギー(ATP)を消費しながら水素イオン(H+)を土の中に放出します。
土の粒子(マイナスに帯電している)にくっついていた
カリウム(K+)などのプラスの栄養素は、
放出されたH+と入れ替わるようにして土から離れます。
そのあと、細胞内の電気化学的な引っ張る力によって、
根の中へと吸収されていきます。
今回は、このミクロなやり取りを
「細胞膜のポンプを操作するアクションシミュレーション」として表現しました。
みー:(⁼v⁼;)
地味だけどいいかんじです。
ゲームの雰囲気をリアリティを崩さずにファンタジーよりにできるかな。
かわいいかんじで。
AI: これは面白い挑戦ですね!
「地味だけどリアリティはある」というベースを保ちつつ、
ミクロな生化学の仕組みを“魔法の植物園”のような
可愛らしい世界観に描き替えてみましょう。
変更点(ファンタジー化)
● 世界観の再定義
- 土壌 → 「豊かな常闇の森」
- 細胞内 → 「輝きの生命の源」
- 細胞膜 → 「光の結界」
● エンティティの見た目
- プロトンポンプ → 小さな根の精霊(ルーツ・スプライト)
- 土壌粒子 → 眠る泥の人形(ゴーレム)
- 水素イオン(H+) → きらめく月のしずく(ムーンデュー)
- 栄養イオン(K+) → 輝く太陽の種(サンシード)
● UIと演出
- ATP → 生命の蜜(ライフネクター)
- フォント → 柔らかいファンタジー風
- パーティクル → 光の粒が舞う演出に
みー:(^_^)/ リトライ機能と、ランキングつけましょう。
栄養を取るほど成長を感じられる機能も付けたいね。
AI: リトライ機能、ランキング、そして成長要素ですね!
ゲームとしてのループ
(遊ぶ → 結果を見る → 成長を感じてまた遊ぶ)が
しっかり出来上がってきました。
みー:(^v^) 途中で成長が止まったので、
ポイントに合わせてもっと成長の上限を上げてみましょう。
このゲームに効果音とBGMをお願い。テーマは「グロウ」。
AI: さらにゲームを面白く、
そして成長の喜びを感じられるようにアップデートしましょう!
■ 実際の動作
まずは、AIが最初に出してくれたゲーム画面です。
陽イオン交換の仕組みをシミュレートしてくれていて、
左側が土の中、右側が根毛の細胞になっています。
シューティングのようでいて、
実は“キャッチするゲーム”という不思議な感覚でした。

(画像:AIが最初に出してくれたゲーム画面)
こちらが、ファンタジー要素を取り入れて仕上がったスタート画面です。
科学的な陽イオン交換の仕組みを、
かわいらしい世界観へと変換してもらいました。

(画像:仕上がりのスタート画面)
基本的には、何かに攻撃されるゲームではありません。
根の精霊(プレイヤー)が、自分の陽イオンを飛ばすことで、
土粒子が抱えている栄養分を
“引き寄せてキャッチする”仕組みになっています。
うまくキャッチできると、
右側に表示されている植物がどんどん成長していきます。

(画像:ゲーム画面)
制限時間内に、どれだけ植物を大きく育てられるかが、このゲームの目的です。

(画像:ゲーム画面)
意外と「打つ」と「キャッチする」を同時に行うのが難しく、
高得点を狙うにはちょっとしたコツが必要でした。

(画像:スコアランキング)
■ コードのポイント
今回の記事では、AI と相談しながら作った 「常闇の森のルーツ・スプライト」 の HTML コードを公開します。
このコードをそのまま保存するだけで、 あなたのPCでもすぐに遊べます。
ブラウザだけで動く、とてもシンプルなゲームなので、 「コードは初めて…」という方でも気軽に試してみてくださいね。
▼ 遊ぶまでの手順
①PC の「メモ帳」(※Windowsの場合)や
Mac の「テキストエディット」など、テキストエディタを開きます。
②下のコードをコピーして貼り付けます。
③「nekko.html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
④保存したファイルをブラウザで開けば、すぐに遊べます。
■ 操作方法
これだけ覚えればOK!
精霊さんの操作は、とってもカンタンです。(^^b)
● 基本の操作
[↑][↓]キー: 根の精霊(ルーツ・スプライト)さんが、
光の結界沿いを上下に移動します。[Space]キー: 「月のしずく(青い光)」を放ちます。
遊び方の3ステップ
ゲームの流れは、この3つだけ!(^v^)
- 月のしずくを放つ
泥の人形(ゴーレム)さんを狙って、[Space]キーで青い光をポチッ! - 太陽の種をキャッチ
目覚めた「太陽の種(金色の光)」が結界に向かって飛んできます。
精霊さんを移動させて、
タイミングよくキャッチしてくださいね。(^^b) - 生命の樹が育つ
集めた種の数(✨ Seeds)のぶんだけ、
鉢植えの中の「生命の樹」がどんどん大きく育ちます!
時間切れになっても、[R] キーで何度でも挑戦できますよ。(^_^)
さあ、ちっちゃな精霊さんになって、
あなただけの大きな大きな「生命の樹」を育ててみてくださいね。(^^*)
“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>常闇の森のルーツ・スプライト - Grow Up Edition</title> <style> body { background-color: #1a120b; color: #ffedcc; text-align: center; font-family: 'Comic Sans MS', 'Helvetica Neue', sans-serif; margin: 0; padding: 20px; user-select: none; } #gameWrapper { position: relative; display: inline-block; } canvas { background-color: #0d0714; border: 4px solid #6b4e31; border-radius: 15px; box-shadow: 0 0 35px rgba(212, 175, 55, 0.4); } #instructions { margin-top: 15px; font-size: 15px; color: #d2c1aa; max-width: 600px; margin-left: auto; margin-right: auto; border: 2px solid #5c4033; border-radius: 10px; padding: 10px; background-color: rgba(0,0,0,0.5); } strong { color: #ffe066; } </style></head><body> <h2>常闇の森のルーツ・スプライト <span style="color:#ffe066;">- Grow Up -</span></h2> <div id="gameWrapper"> <canvas id="gameCanvas" width="600" height="400"></canvas> </div> <div id="instructions"> <p><strong>[↑] [↓] キー</strong> : 移動 / <strong>[スペース] キー</strong> : 月のしずくを放つ</p> <p>※キーを押すとBGM「グロウアップ」と効果音が流れ始めます!<br> 上限なく育ち続ける「生命の樹」をどこまで大きくできるか挑戦しましょう! (リトライ: <strong>[R]キー</strong>)</p> </div><script> const canvas = document.getElementById("gameCanvas"); const ctx = canvas.getContext("2d"); const MEMBRANE_X = 380; // 成長エリアをさらに広く let score = 0; let nector = 100; let time = 60; let frameCount = 0; let gameState = 'PLAYING'; let rankings = []; // ローカルストレージ try { const savedRanking = localStorage.getItem('rootsRankingUnlimited'); if (savedRanking) rankings = JSON.parse(savedRanking); } catch (e) {} const keys = { ArrowUp: false, ArrowDown: false, Space: false, KeyR: false }; // --- Web Audio API サウンドシステム --- let audioCtx = null; let bgmNode = null; function initAudio() { if (audioCtx) return; audioCtx = new (window.AudioContext || window.webkitAudioContext)(); startBGM(); } // 効果音生成関数 function playSound(type) { if (!audioCtx) return; const now = audioCtx.currentTime; if (type === 'shoot') { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(400, now); osc.frequency.exponentialRampToValueAtTime(800, now + 0.1); gain.gain.setValueAtTime(0.15, now); gain.gain.linearRampToValueAtTime(0, now + 0.1); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(now); osc.stop(now + 0.1); } else if (type === 'hit') { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'square'; osc.frequency.setValueAtTime(150, now); osc.frequency.setValueAtTime(300, now + 0.05); gain.gain.setValueAtTime(0.1, now); gain.gain.linearRampToValueAtTime(0, now + 0.15); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(now); osc.stop(now + 0.15); } else if (type === 'absorb') { // キラリーン!というアルペジオ和音 const freqs = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6 freqs.forEach((f, i) => { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(f, now + i * 0.04); gain.gain.setValueAtTime(0.1, now + i * 0.04); gain.gain.linearRampToValueAtTime(0, now + i * 0.04 + 0.2); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(now + i * 0.04); osc.stop(now + i * 0.04 + 0.2); }); } else if (type === 'gameover') { const freqs = [392.00, 349.23, 311.13, 246.94]; freqs.forEach((f, i) => { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(f, now + i * 0.08); gain.gain.setValueAtTime(0.15, now + i * 0.08); gain.gain.linearRampToValueAtTime(0, now + i * 0.08 + 0.3); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(now + i * 0.08); osc.stop(now + i * 0.08 + 0.3); }); } } // BGM「グロウアップ」の生成とループ再生 function startBGM() { const tempo = 120; const sixteenth = 60 / tempo / 4; // ほのぼのとした温かいコード進行 (C - G - Am - Em - F - C - F - G) const melody = [ 60, 64, 67, 72, 67, 64, 67, 72, // C 59, 62, 67, 71, 67, 62, 67, 71, // G 57, 60, 64, 69, 64, 60, 64, 69, // Am 55, 59, 62, 67, 62, 59, 62, 67, // Em 53, 57, 60, 65, 60, 57, 60, 65, // F 52, 55, 60, 64, 60, 55, 60, 64, // C 53, 57, 60, 65, 60, 57, 60, 65, // F 55, 59, 62, 67, 71, 74, 79, 83 // G (グロウアップ上昇) ]; let noteIndex = 0; function scheduleNextNote() { if (gameState === 'GAMEOVER') return; const now = audioCtx.currentTime; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'triangle'; // 柔らかいピコピコ音 const midiNote = melody[noteIndex]; const freq = 440 * Math.pow(2, (midiNote - 69) / 12); osc.frequency.setValueAtTime(freq, now); // 音の繋ぎを滑らかに gain.gain.setValueAtTime(0.06, now); gain.gain.exponentialRampToValueAtTime(0.001, now + sixteenth * 1.8); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(now); osc.stop(now + sixteenth * 2); noteIndex = (noteIndex + 1) % melody.length; setTimeout(scheduleNextNote, sixteenth * 1000 * 2); } scheduleNextNote(); } // キー入力 window.addEventListener("keydown", (e) => { initAudio(); // 最初の入力でオーディオを有効化 if (e.code === "ArrowUp") keys.ArrowUp = true; if (e.code === "ArrowDown") keys.ArrowDown = true; if (e.code === "Space") keys.Space = true; if (e.code === "KeyR") { keys.KeyR = true; if (gameState === 'GAMEOVER') resetGame(); } if (["ArrowUp", "ArrowDown", "Space", "KeyR"].includes(e.code)) e.preventDefault(); }); window.addEventListener("keyup", (e) => { if (e.code === "ArrowUp") keys.ArrowUp = false; if (e.code === "ArrowDown") keys.ArrowDown = false; if (e.code === "Space") keys.Space = false; if (e.code === "KeyR") keys.KeyR = false; }); // パーティクルシステム class Particle { constructor(x, y, color, speedX, speedY, radius, life) { this.x = x; this.y = y; this.color = color; this.speedX = speedX; this.speedY = speedY; this.radius = radius; this.life = life; this.initialLife = life; } update() { this.x += this.speedX; this.y += this.speedY; this.life--; this.radius *= 0.96; } draw() { ctx.globalAlpha = this.life / this.initialLife; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = this.color; ctx.fill(); ctx.globalAlpha = 1.0; } } let particles = []; const sprite = { y: 200, height: 40, width: 25, speed: 4.5, cooldown: 0, draw(ctx, x) { const cx = x; const cy = this.y + this.height/2; ctx.beginPath(); ctx.arc(cx, cy, 15, 0, Math.PI * 2); ctx.fillStyle = "#8fed9a"; ctx.fill(); ctx.fillStyle = "#111"; ctx.beginPath(); ctx.arc(cx - 5, cy - 2, 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(cx + 5, cy - 2, 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "#4ed95d"; ctx.beginPath(); ctx.ellipse(cx, cy - 15, 8, 4, -Math.PI/4, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = "rgba(100, 255, 100, 0.4)"; ctx.fillRect(x - this.width/2, this.y, this.width, this.height); } }; let moonDews = []; let sunSeeds = []; let mudGolems = []; function initGolems() { mudGolems = []; for (let i = 0; i < 15; i++) { mudGolems.push({ x: Math.random() * 260 + 30, y: Math.random() * 360 + 20, radius: Math.random() * 5 + 10, hasSeed: true, regenTimer: 0, draw(ctx) { const isRegen = !this.hasSeed && this.regenTimer > 0; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = isRegen ? "#4a3521" : "#5c4033"; ctx.fill(); ctx.fillStyle = isRegen ? "rgba(0,0,0,0.2)" : "rgba(255,255,255,0.1)"; ctx.beginPath(); ctx.arc(this.x + 3, this.y - 3, 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = isRegen ? "rgba(0,0,0,0.5)" : "#222"; ctx.beginPath(); ctx.moveTo(this.x - 5, this.y - 1); ctx.lineTo(this.x - 1, this.y - 1); ctx.stroke(); ctx.beginPath(); ctx.moveTo(this.x + 1, this.y - 1); ctx.lineTo(this.x + 5, this.y - 1); ctx.stroke(); if (this.hasSeed) { ctx.beginPath(); ctx.arc(this.x + 4, this.y - 4, 4, 0, Math.PI * 2); ctx.fillStyle = "#ffd700"; ctx.shadowBlur = 10; ctx.shadowColor = "#ffd700"; ctx.fill(); ctx.shadowBlur = 0; } } }); } } initGolems(); function resetGame() { score = 0; nector = 100; time = 60; frameCount = 0; moonDews = []; sunSeeds = []; particles = []; initGolems(); gameState = 'PLAYING'; if (audioCtx) startBGM(); } function saveScore() { rankings.push(score); rankings.sort((a, b) => b - a); rankings = rankings.slice(0, 5); try { localStorage.setItem('rootsRankingUnlimited', JSON.stringify(rankings)); } catch (e) {} } // --- 改良版:無限成長する生命の樹の描画ロジック --- function drawTree(ctx, score) { const baseX = 490; const baseY = 340; // スコアに基づいた無段階のスケール let level = score / 20; // 20ポイントごとに1段階相当の成長率 let trunkThickness = Math.min(4 + level * 1.5, 25); // 太くなる幹 let treeHeight = Math.min(30 + level * 12, 160); // 高くなる幹 // 鉢(ベース) ctx.fillStyle = "#8b5a2b"; ctx.beginPath(); ctx.moveTo(baseX - 35, baseY); ctx.lineTo(baseX + 35, baseY); ctx.lineTo(baseX + 25, baseY + 15); ctx.lineTo(baseX - 25, baseY + 15); ctx.fill(); if (score === 0) { // 最初は小さな双葉 ctx.fillStyle = "#4ed95d"; ctx.beginPath(); ctx.ellipse(baseX - 5, baseY - 5, 6, 3, -Math.PI/4, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(baseX + 5, baseY - 5, 6, 3, Math.PI/4, 0, Math.PI*2); ctx.fill(); return; } // 幹の描画 ctx.lineWidth = trunkThickness; ctx.strokeStyle = "#5a4225"; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.beginPath(); ctx.moveTo(baseX, baseY); ctx.quadraticCurveTo(baseX - 15, baseY - treeHeight/2, baseX - 5, baseY - treeHeight); ctx.stroke(); // 枝分かれロジック (スコアに応じて動的に枝が増える) let branches = Math.min(Math.floor(score / 30), 6); ctx.lineWidth = Math.max(2, trunkThickness * 0.6); for (let i = 1; i <= branches; i++) { let branchRatio = i / (branches + 1); let bY = baseY - treeHeight * branchRatio; let bX = baseX - 10; let direction = i % 2 === 0 ? 1 : -1; let length = Math.min(15 + level * 4, 50) * (1 - branchRatio * 0.3); ctx.beginPath(); ctx.moveTo(bX, bY); ctx.quadraticCurveTo(bX + (length/2)*direction, bY - 10, bX + length * direction, bY - 15); ctx.stroke(); // 各枝の葉っぱ ctx.fillStyle = i % 2 === 0 ? "#2e8b57" : "#3cb371"; ctx.beginPath(); ctx.arc(bX + length * direction, bY - 15, Math.min(8 + level, 24), 0, Math.PI * 2); ctx.fill(); // 豊穣の花と果実(さらに高得点) if (score > 100 && i <= score / 50) { ctx.fillStyle = i % 2 === 0 ? "#ff69b4" : "#ff4500"; // ピンクの花かオレンジの果実 ctx.beginPath(); ctx.arc(bX + length * direction + (5 * direction), bY - 10, 4, 0, Math.PI * 2); ctx.fill(); } } // 主幹のてっぺんの大きな葉 ctx.fillStyle = "#4ed95d"; ctx.beginPath(); ctx.arc(baseX - 5, baseY - treeHeight, Math.min(12 + level * 1.2, 30), 0, Math.PI * 2); ctx.fill(); // 圧倒的成長(ハイスコア)時のオーラ演出 if (score >= 150) { ctx.strokeStyle = "rgba(255, 224, 102, 0.2)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(baseX, baseY - treeHeight/2, treeHeight * 0.7, 0, Math.PI * 2); ctx.stroke(); if (frameCount % 10 === 0) { particles.push(new Particle(baseX + (Math.random()-0.5)*80, baseY - Math.random()*treeHeight, "#ffe066", 0, -Math.random()*1, 2, 40)); } } } function update() { if (gameState === 'GAMEOVER') return; frameCount++; if (frameCount % 60 === 0 && time > 0) time--; if (time <= 0) { gameState = 'GAMEOVER'; playSound('gameover'); saveScore(); } if (nector < 100 && frameCount % 6 === 0) nector += 0.4; if (keys.ArrowUp && sprite.y > 0) sprite.y -= sprite.speed; if (keys.ArrowDown && sprite.y < canvas.height - sprite.height) sprite.y += sprite.speed; if (sprite.cooldown > 0) sprite.cooldown--; if (keys.Space && sprite.cooldown === 0 && nector >= 5) { nector -= 5; moonDews.push({ x: MEMBRANE_X, y: sprite.y + sprite.height / 2, radius: 4, pulse: 0 }); sprite.cooldown = 18; playSound('shoot'); for(let i=0; i<3; i++) particles.push(new Particle(MEMBRANE_X, sprite.y+sprite.height/2, "#66ffff", -Math.random()*2, (Math.random()-0.5)*1.5, 2, 20)); } for (let i = moonDews.length - 1; i >= 0; i--) { let d = moonDews[i]; d.x -= 3.5; d.pulse += 0.2; let hit = false; for (let g of mudGolems) { let dist = Math.hypot(d.x - g.x, d.y - g.y); if (dist < g.radius + d.radius && g.hasSeed) { g.hasSeed = false; g.regenTimer = 350; sunSeeds.push({ x: g.x, y: g.y, radius: 5, pulse: 0, life: 300 }); hit = true; playSound('hit'); for(let i=0; i<5; i++) particles.push(new Particle(g.x, g.y, "#ffd700", Math.random()*2, (Math.random()-0.5)*2, 2, 25)); for(let i=0; i<3; i++) particles.push(new Particle(g.x, g.y, "#66ffff", -Math.random()*1, (Math.random()-0.5)*1, 1.5, 15)); break; } } if (hit || d.x < 0) moonDews.splice(i, 1); } for (let i = sunSeeds.length - 1; i >= 0; i--) { let s = sunSeeds[i]; s.x += 1.8; s.pulse += 0.2; s.life--; if (s.x > MEMBRANE_X) { if (s.y > sprite.y && s.y < sprite.y + sprite.height) { score += 15; playSound('absorb'); for(let i=0; i<10; i++) particles.push(new Particle(MEMBRANE_X+10, s.y, "#ffd700", Math.random()*2, (Math.random()-0.5)*3, 3, 30)); } sunSeeds.splice(i, 1); } else if (s.life < 0) { sunSeeds.splice(i, 1); } } for (let g of mudGolems) { if (!g.hasSeed) { g.regenTimer--; if (g.regenTimer <= 0) { g.hasSeed = true; for(let i=0; i<2; i++) particles.push(new Particle(g.x+4, g.y-4, "#ccaa00", (Math.random()-0.5)*0.5, (Math.random()-0.5)*0.5, 1, 15)); } } } for (let i = particles.length - 1; i >= 0; i--) { particles[i].update(); if (particles[i].life <= 0 || particles[i].radius < 0.2) particles.splice(i, 1); } } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "#1d140f"; ctx.fillRect(0, 0, MEMBRANE_X, canvas.height); ctx.fillStyle = "#0c0612"; ctx.fillRect(MEMBRANE_X, 0, canvas.width - MEMBRANE_X, canvas.height); if (frameCount % 8 === 0 && gameState === 'PLAYING') { particles.push(new Particle(Math.random()*canvas.width, Math.random()*canvas.height, Math.random()>0.5?"#ffd700":"#66ffff", 0, (Math.random()-0.5)*0.2, Math.random()*1.5, Math.random()*30+20)); } // 生命の樹の描画(無制限成長) drawTree(ctx, score); ctx.fillStyle = "rgba(100, 200, 100, 0.4)"; ctx.fillRect(MEMBRANE_X - 10, 0, 20, canvas.height); ctx.fillStyle = "rgba(200, 255, 200, 0.7)"; ctx.fillRect(MEMBRANE_X - 2, 0, 1, canvas.height); ctx.fillStyle = "rgba(200, 255, 200, 0.7)"; ctx.fillRect(MEMBRANE_X + 2, 0, 1, canvas.height); for (let g of mudGolems) g.draw(ctx); for (let d of moonDews) { ctx.beginPath(); ctx.arc(d.x, d.y, d.radius + Math.sin(d.pulse)*0.5, 0, Math.PI * 2); ctx.fillStyle = "#66ffff"; ctx.fill(); ctx.fillStyle = "#ffffff"; ctx.beginPath(); ctx.arc(d.x+1, d.y-1, d.radius*0.5, 0, Math.PI*2); ctx.fill(); } for (let s of sunSeeds) { ctx.beginPath(); ctx.arc(s.x, s.y, s.radius + Math.sin(s.pulse)*1.0, 0, Math.PI * 2); ctx.fillStyle = "#ffd700"; ctx.shadowBlur = 10 + Math.sin(s.pulse)*5; ctx.shadowColor = "#ffd700"; ctx.fill(); ctx.shadowBlur = 0; ctx.fillStyle = "#ffffff"; ctx.beginPath(); ctx.arc(s.x+1, s.y-1, s.radius*0.6, 0, Math.PI*2); ctx.fill(); } for (let p of particles) p.draw(); sprite.draw(ctx, MEMBRANE_X); ctx.fillStyle = "#ffe066"; ctx.font = "16px 'Comic Sans MS', sans-serif"; ctx.textAlign = "left"; ctx.fillText("✨ Seeds: " + score, 10, 25); ctx.fillStyle = "#ffedcc"; ctx.fillText("⏳ Time: " + time, 10, 50); ctx.fillStyle = "#ffedcc"; ctx.fillText("💧 Nector:", 10, 75); ctx.fillStyle = "#333"; ctx.fillRect(80, 63, 100, 12); const gNector = ctx.createLinearGradient(80, 0, 180, 0); gNector.addColorStop(0, "#ff4500"); gNector.addColorStop(1, "#ffe066"); ctx.fillStyle = nector > 20 ? gNector : "#8b0000"; ctx.fillRect(80, 63, nector, 12); ctx.strokeStyle = "#4ed95d"; ctx.lineWidth = 2; ctx.strokeRect(80, 63, 100, 12); if (gameState === 'GAMEOVER') { ctx.fillStyle = "rgba(0, 0, 0, 0.85)"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "#ffe066"; ctx.font = "30px 'Comic Sans MS', sans-serif"; ctx.textAlign = "center"; ctx.fillText("JOURNEY COMPLETE", canvas.width / 2, 70); ctx.font = "20px 'Comic Sans MS', sans-serif"; ctx.fillStyle = "#ffedcc"; ctx.fillText("Final Score: " + score, canvas.width / 2, 110); ctx.fillText("--- High Scores ---", canvas.width / 2, 150); for (let i = 0; i < rankings.length; i++) { let crown = i === 0 ? "👑 " : ""; ctx.fillText(`${crown}${i + 1}位 : ${rankings[i]}`, canvas.width / 2, 180 + i * 25); } ctx.fillStyle = "#4ed95d"; ctx.fillText("[R] キーで次の成長へ!", canvas.width / 2, 350); } } function loop() { update(); draw(); requestAnimationFrame(loop); } loop();</script></body></html>
■ 今日の学び
今回は、植物の根っこのミクロな仕組みを、
かわいいファンタジーゲームにするという、
ちょっと不思議な挑戦をしてみました。(^v^)
このゲーム作りを通じて、
私自身もたくさんの新しい発見と、
あたたかい学びがありました。
🌿 リアルとファンタジーの優しい置き換え、
そして AI の力
まず今回も、リアルな科学の仕組みを、
AI がとってもあたたかいファンタジーの世界観へと置き換えてくれたことに、
本当に感心してしまいました。
プロトンポンプやイオン交換といった、少し難しいミクロの世界が、
AI の手によって「月のしずく」や「太陽の種」といった、
魔法のような物語へと生まれ変わったんです。
さらに、途中で成長が止まってしまった生命の樹を
「もっと大きく育てたい!」という私の急なリクエストにも、
AI はすぐに応えてくれました。
スコアに合わせて、どこまでも太く、大きく、
そしてオーラまでまとって育ち続ける立派な「生命の樹」。
そのシステムを、
効果音と BGM 付きで一気に作り上げてくれたんです。(>v<*)
こちらの意図を汲み取りながら、
急な変化にも柔軟に対応してくれる AI の力には、
今回も驚かされるばかりでした。
📖 牧野富太郎博士の言葉と、植物の存在感
私はこれまでにも植物をテーマにしたゲームをいくつか作ってきましたが、
今回は特に、植物を単なる背景や素材ではなく、
「一つの生き物」としての存在感を出したいと強く感じていました。
私の大好きな言葉に、
植物学者・牧野富太郎博士の 「雑草という名の草はない」 という言葉があります。🌱
土の上にただあるのではなく、その一つ一つに名前があって、
特徴や機能もそれぞれ違っていて。
そして、育った場所や環境に合わせて、
同じ植物でも姿かたちが変わっていく。
ファンタジーの世界では、そんな植物たちの個性や存在感が、
もっと色鮮やかに、
状況に合わせた一つ一つの物語として描かれるんだろうな……と、
思いを馳せていました。(´ω`)
そう考えると、以前作ったゲーム(#77:癒しの緑の集合体)で、
画面いっぱいの緑の色味を、
あえてすべて微妙に変えて表現したのも、
まさにその思いからだったんだなと、
改めて気づかされたひとときでした。(✿´꒳`)
■ 次回は、世界観がマクロから宇宙規模に拡大します。
今回のゲームの、妖精たちが暮らすミクロな世界から、
次は一気にスケールアップして――宇宙へ!(^v^*)
UFO や隕石が飛び交う SF の世界観 に舞台が変わり、
さまざまな種類の「波」を駆使して、襲いくる脅威から地球を守ります。
次回もぜひ遊びに来てくださいね。(*^v^)ノシ
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)






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