■19個目「我が家のイシガメ観察日記」
こんにちは、AIと作る100のゲーム実験室の みー です。(^^*)
今回は、観察飼育ゲーム(タンタンの日常)の制作実験です。
うちで飼っているカメの「タンタン」をモデルにして、
カメの飼育・観察をテーマに作ってみました。(*^_^*)
大きな変化のあるゲームではありません。
カメとのスローライフを楽しむゲームです。
「エサを与える」ボタンで餌をあげると、もぐもぐ食べてくれます。
マウスカーソルを近づけると、こちらに寄ってきます。
カメとのゆったりしたひと時を、ぜひ楽しんでいただければと思います。
■ AIとのやりとりで面白かった部分
(^^*)html形式のメモアプリで動かせるゲーム。
今日も続けて作るよ。カメ飼育ゲーム。
ガラス水槽で飼っているイシガメ。
水槽は、カメが少し浸るくらいの水と、休憩できる島があります。
オスなので小柄で手のひらサイズですが、推定20歳以上。
飼い始めて15年ほどで、とても懐いています。
元気いっぱい動き回るし、時々ひっくり返る。(自力ですぐ戻れる。)
こちらをじっと見つめたり、外の景色を眺めていることもある。
時々昼寝。ゲームとしては、そんなカメを観察するゲーム。
できることは、餌をやること。
(餌ボタンを押すと、3粒ほど水槽に餌が落ち、それを見つけたカメが食べます。)
水槽の外から指(マウスカーソル)を近づけると寄ってきて、
ガラス越しにかみつこうとしてきます。
そんなほのぼのしたゲーム、作れるかな。
(AI)おかえりなさい!常連さん、今日も「カメとの時間」を形にしていきましょう。
推定20歳、15年も一緒にいるイシガメくん……。
そのサイズ感と、ひっくり返っても自力で戻るタフさがたまりませんね。
今日のひとこと:イシガメ豆知識
20年も一緒にいると、もはや家族ですよね。
イシガメ(日本固有種)は、実はクサガメよりも「目がいい」と言われています。
水槽越しに飼い主の顔を判別しているという説もあるんですよ。
今日のゲームでも、マウス(指)を近づけると
「エサかな?遊んでくれるのかな?」と寄ってくる動きを再現しました。
(つ_<;)カメ動かないよ。
(AI)ああっ、失礼いたしました!
せっかくのイシガメくんが固まってしまっていましたね。
原因はプログラムの最後の方、回転の計算(rotateX)の書き方に、
私の「つい口が滑ったような」タイピングミス(founders という謎の単位)が混じっていました。
(=v=;)水槽の枠の外にカメが出ちゃってるよ。
(AI)いやぁ、申し訳ない。元気すぎて水槽から飛び出しちゃいましたね(笑)。
20歳のベテランともなると、隙あらば脱走しようとするスキルも相当なものみたいです。
今回はしっかり水槽の「見えない壁」を強化して、
枠の外には絶対に出ないように調整しました。
■ 実際の動作
最初のアイデア相談のときに、AI が作ってくれた水槽の中のタンタンです。
(餌をやっても、マウスを近づけても動きません…)

リアルなカメの日常です。
(外敵はいないので安心してください)

餌を与えると、もぐもぐ食べてくれます。
(食べ過ぎることはないので安心してください)

マウスカーソルを近づけると寄ってきます。
(かみつくことはないので安心してください)

指を追いかけて壁際まで来ると、よじ登ろうとしてひっくり返ります。
(自力で起き上がるので安心してください)

こちらが「タンタン」です。
(今日も元気です)

■ コードのポイント
今回の記事では、AI と相談しながら作った「タンタンの日常」の HTML コードを公開します。
ブラウザだけで動く、とてもシンプルなゲームです。
PC の「メモ帳」(※Windowsの場合)を開き、下のコードをコピーして貼り付け、「tantan.html」など好きな名前で保存してください。
保存したファイルをブラウザで開くだけで遊べます。
【操作方法】 ・「エサを落とす」ボタンで、餌を与えられます。 ・マウスカーソルを水槽に近づけると、タンタンが寄ってきます。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>タンタンの日常 Pro-Edition v4</title> <style> body { background: #1a1a1a; display: flex; flex-direction: column; align-items: center; font-family: 'Hiragino Kaku Gothic ProN', sans-serif; } #aquarium { width: 750px; height: 500px; background: linear-gradient(135deg, #4fc3f7 0%, #29b6f6 50%, #0288d1 100%); border: 20px solid #e0e0e0; border-bottom: 30px solid #bdbdbd; border-radius: 10px; position: relative; overflow: hidden; cursor: pointer; box-shadow: inset 0 0 50px rgba(0,0,0,0.2), 0 15px 30px rgba(0,0,0,0.8); margin-top: 30px; } #aquarium::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle, rgba(255,255,255,0.2) 10%, transparent 20%) 0 0 / 40px 40px; opacity: 0.3; pointer-events: none; } #island { width: 220px; height: 80px; background: linear-gradient(to bottom, #8d6e63, #5d4037); position: absolute; right: 20px; bottom: 80px; border-radius: 40% 60% 10% 10%; border-bottom: 12px solid #3e2723; box-shadow: -10px 10px 20px rgba(0,0,0,0.4); z-index: 1; } #turtle-wrapper { position: absolute; width: 100px; height: 60px; transition: transform 0.2s ease-out; z-index: 10; } #turtle { width: 100%; height: 100%; position: relative; transform-origin: center; transition: transform 0.4s ease-in-out; } .shell { width: 72px; height: 58px; background: #5d4037; border-radius: 45%; position: absolute; border: 3px solid #3e2723; z-index: 5; left: 14px; top: 1px; box-shadow: inset 5px 5px 10px rgba(255,255,255,0.1), inset -5px -5px 15px rgba(0,0,0,0.5); } .belly { width: 72px; height: 58px; background: #111; border-radius: 45%; position: absolute; border: 3px solid #000; z-index: 6; left: 14px; top: 1px; opacity: 0; transition: opacity 0.3s; box-shadow: inset 0 0 10px rgba(255,255,255,0.05); } .belly::after { content: ''; position: absolute; top: 5px; left: 5px; right: 5px; bottom: 5px; border: 2px dashed #ffb300; border-radius: 40%; opacity: 0.4; } .head { width: 30px; height: 20px; background: #7cb342; border-radius: 12px; position: absolute; left: 78px; top: 20px; z-index: 4; border: 1px solid #33691e; transition: transform 0.3s; } .head::after { content: ''; width: 4px; height: 4px; background: #000; border-radius: 50%; position: absolute; right: 6px; top: 4px; } .head::before { content: ''; width: 4px; height: 4px; background: #000; border-radius: 50%; position: absolute; right: 6px; bottom: 4px; } .leg { width: 24px; height: 14px; background: #7cb342; position: absolute; border-radius: 8px; z-index: 3; border: 1px solid #33691e; transition: transform 0.1s; } .leg-fl { left: 65px; top: 4px; transform: rotate(-45deg); } .leg-fr { left: 65px; bottom: 4px; transform: rotate(45deg); } .leg-bl { left: 12px; top: 4px; transform: rotate(45deg); } .leg-br { left: 12px; bottom: 4px; transform: rotate(-45deg); } .tail { width: 28px; height: 8px; background: #7cb342; position: absolute; left: -8px; top: 26px; border-radius: 50% 0 0 50%; z-index: 3; border: 1px solid #33691e; transform-origin: right center; } .is-flipped .belly { opacity: 1; } .is-flipped .head { transform: translateX(-10px) !important; } .food { width: 12px; height: 12px; background: #6d4c41; border-radius: 50%; position: absolute; border: 1px solid #3e2723; z-index: 2; box-shadow: 2px 2px 5px rgba(0,0,0,0.3); transition: transform 0.2s, opacity 0.3s; } #ui { margin-top: 25px; display: flex; gap: 15px; align-items: center; flex-wrap: wrap; justify-content: center; } button { padding: 12px 25px; font-size: 16px; cursor: pointer; border-radius: 30px; border: none; background: linear-gradient(to bottom, #4caf50, #388e3c); color: white; font-weight: bold; box-shadow: 0 5px #1b5e20, 0 8px 10px rgba(0,0,0,0.4); transition: all 0.1s; } button:active { transform: translateY(5px); box-shadow: 0 0px #1b5e20, 0 2px 3px rgba(0,0,0,0.4); } #status { font-size: 16px; color: #fff; background: rgba(0,0,0,0.6); padding: 10px 20px; border-radius: 30px; border: 1px solid #555; width: 350px; text-align: center; } </style></head><body><div id="aquarium"> <div id="island"></div> <div id="turtle-wrapper"> <div id="turtle"> <div class="head"></div> <div class="belly"></div> <div class="shell"></div> <div class="leg leg-fl"></div> <div class="leg leg-fr"></div> <div class="leg leg-bl"></div> <div class="leg leg-br"></div> <div class="tail"></div> </div> </div></div><div id="ui"> <button onclick="dropFood()">エサを落とす</button> <div id="status">のんびり水槽を泳いでいます。</div></div><script> const wrapper = document.querySelector('#turtle-wrapper'); const turtle = document.querySelector('#turtle'); const head = document.querySelector('.head'); const legs = document.querySelectorAll('.leg'); const tail = document.querySelector('.tail'); const aquarium = document.querySelector('#aquarium'); const statusText = document.querySelector('#status'); let pos = { x: 150, y: 200, angle: 0 }; let target = { x: 200, y: 200 }; let state = 'idle'; let foods = []; let frame = 0; let velocity = { x: 0, y: 0 }; let wallPushFrames = 0; const BOUNDS = { minX: 10, maxX: 630, minY: 10, maxY: 420 }; function updateAI() { if (state === 'following' || state === 'eating' || state === 'flipping') return; const dice = Math.random(); if (foods.length > 0) { state = 'swimming'; target = { x: foods[0].x - 60, y: foods[0].y - 30 }; statusText.innerText = "エサに向かっています。"; } else if (dice < 0.1) { state = 'sunbathing'; target = { x: 480, y: 340 }; statusText.innerText = "島で甲羅干しを始めました。"; } else if (dice < 0.4) { state = 'swimming'; target = { x: BOUNDS.minX + Math.random() * (BOUNDS.maxX - BOUNDS.minX), y: BOUNDS.minY + Math.random() * (BOUNDS.maxY - BOUNDS.minY) }; statusText.innerText = "スイスイと活発に泳いでいます。"; } } function startFlipping() { state = 'flipping'; statusText.innerText = "あ!滑って裏返ってしまった!"; turtle.classList.add('is-flipped'); velocity = { x: 0, y: 0 }; wallPushFrames = 0; setTimeout(() => { turtle.classList.remove('is-flipped'); statusText.innerText = "よっ、と!自力で起き上がりました。"; setTimeout(() => { state = 'idle'; // 目標地点を現在地にリセットして、変な方向へ飛んでいくのを防ぐ target = { x: pos.x, y: pos.y }; }, 1000); }, 4000); } function loop() { frame++; let moving = false; if (state !== 'flipping' && state !== 'eating') { const dx = target.x - pos.x; const dy = target.y - pos.y; const dist = Math.sqrt(dx*dx + dy*dy); let speedLimit = (state === 'following') ? 0.8 : 1.5; if (dist > 5) { moving = true; velocity.x += (dx * 0.005); velocity.y += (dy * 0.005); } else { velocity.x *= 0.8; velocity.y *= 0.8; if (state === 'swimming' && foods.length > 0) startEating(); } const currentSpeed = Math.sqrt(velocity.x**2 + velocity.y**2); if (currentSpeed > speedLimit) { velocity.x = (velocity.x / currentSpeed) * speedLimit; velocity.y = (velocity.y / currentSpeed) * speedLimit; } pos.x += velocity.x; pos.y += velocity.y; // 修正版:ぴったり壁で止める(+5のブラックホールを撤去) if (pos.x <= BOUNDS.minX) { pos.x = BOUNDS.minX; velocity.x *= -0.5; } if (pos.x >= BOUNDS.maxX) { pos.x = BOUNDS.maxX; velocity.x *= -0.5; } if (pos.y <= BOUNDS.minY) { pos.y = BOUNDS.minY; velocity.y *= -0.5; } if (pos.y >= BOUNDS.maxY) { pos.y = BOUNDS.maxY; velocity.y *= -0.5; } // 壁を一生懸命よじ登ろうとしているかどうかの判定 let isPushingWall = false; if (state === 'following') { if ((target.x <= BOUNDS.minX && pos.x <= BOUNDS.minX + 2) || (target.x >= BOUNDS.maxX && pos.x >= BOUNDS.maxX - 2) || (target.y <= BOUNDS.minY && pos.y <= BOUNDS.minY + 2) || (target.y >= BOUNDS.maxY && pos.y >= BOUNDS.maxY - 2)) { isPushingWall = true; } } if (isPushingWall) { wallPushFrames++; moving = true; // 足をバタバタさせる statusText.innerText = "外に出ようと一生懸命よじ登っています...!"; head.style.transform = `translateY(${Math.sin(frame*0.5)*4}px)`; if (wallPushFrames > 180) { // 約3秒 startFlipping(); } } else { wallPushFrames = 0; if (state !== 'following') head.style.transform = "translateX(0)"; } // 向きの更新 if (currentSpeed > 0.1 || isPushingWall) { let targetAngle = Math.atan2(dy, dx) * 180 / Math.PI; if (currentSpeed > 0.1) { targetAngle = Math.atan2(velocity.y, velocity.x) * 180 / Math.PI; } let angleDiff = targetAngle - pos.angle; while (angleDiff > 180) angleDiff -= 360; while (angleDiff < -180) angleDiff += 360; pos.angle += angleDiff * 0.1; } } // アニメーション処理 if (state === 'flipping') { const wave = Math.sin(frame * 0.8) * 35; legs[0].style.transform = `rotate(${-45 + wave}deg)`; legs[1].style.transform = `rotate(${45 - wave}deg)`; legs[2].style.transform = `rotate(${45 + wave}deg)`; legs[3].style.transform = `rotate(${-45 - wave}deg)`; tail.style.transform = `rotate(${Math.sin(frame * 0.5) * 30}deg)`; wrapper.style.transform = `translate(${pos.x}px, ${pos.y}px) rotate(${pos.angle}deg) rotateZ(${Math.sin(frame*0.2)*5}deg)`; } else { if (moving) { const wave = Math.sin(frame * 0.2) * 20; legs[0].style.transform = `rotate(${-45 + wave}deg)`; legs[1].style.transform = `rotate(${45 - wave}deg)`; legs[2].style.transform = `rotate(${45 + wave}deg)`; legs[3].style.transform = `rotate(${-45 - wave}deg)`; tail.style.transform = `rotate(${Math.sin(frame * 0.1) * 10}deg)`; } else { legs.forEach((leg, i) => { let base = (i === 0 || i === 3) ? -45 : 45; leg.style.transform = `rotate(${base}deg)`; }); tail.style.transform = "rotate(0deg)"; } wrapper.style.transform = `translate(${pos.x}px, ${pos.y}px) rotate(${pos.angle}deg)`; } requestAnimationFrame(loop); } function dropFood() { if(foods.length > 5) return; const f = document.createElement('div'); f.className = 'food'; const fx = 100 + Math.random() * 500; const fy = 100 + Math.random() * 300; f.style.left = fx + 'px'; f.style.top = fy + 'px'; aquarium.appendChild(f); foods.push({ x: fx, y: fy, el: f }); statusText.innerText = "エサが落ちてきました。"; } function startEating() { if (foods.length === 0) return; state = 'eating'; statusText.innerText = "エサをじっと見つめて……パクッ!"; velocity = { x: 0, y: 0 }; let bites = 0; const biteInterval = setInterval(() => { head.style.transform = (bites % 2 === 0) ? "translateX(12px) scale(1.1)" : "translateX(0px)"; bites++; if (bites > 6) { clearInterval(biteInterval); if (foods.length > 0) { const f = foods.shift(); f.el.style.transform = "scale(0)"; setTimeout(() => f.el.remove(), 200); } setTimeout(() => { state = 'idle'; statusText.innerText = "モグモグ……美味しそうに食べています。"; }, 500); } }, 300); } aquarium.addEventListener('mousemove', (e) => { if (state === 'flipping' || state === 'eating') return; const rect = aquarium.getBoundingClientRect(); let mx = e.clientX - rect.left - 50; let my = e.clientY - rect.top - 30; if(mx < BOUNDS.minX) mx = BOUNDS.minX; if(mx > BOUNDS.maxX) mx = BOUNDS.maxX; if(my < BOUNDS.minY) my = BOUNDS.minY; if(my > BOUNDS.maxY) my = BOUNDS.maxY; target.x = mx; target.y = my; state = 'following'; if (wallPushFrames === 0) { statusText.innerText = "じーっとこちらを伺いながら近づいてきます。"; } }); aquarium.addEventListener('mouseleave', () => { if (state === 'following') { state = 'idle'; wallPushFrames = 0; statusText.innerText = "指が見えなくなって、のんびりし始めました。"; } }); setInterval(updateAI, 2500); loop();</script></body></html>
■ 今日の学び
今回のゲームで登場するイシガメの「タンタン」は、鳴き声もなく無口ですが、
時々ひっくり返ったり、動き回る音が聞こえたりと、存在感は抜群です。
リアルな飼育を目指すと、どうしても地味なゲームになってしまいました。(^^;)
ですが、毎日作っている一つ一つのゲームは、実験室の“材料”のようなものです。
ほかのゲームと組み合わせることで、また違った魅力を発信できると考えています。
昨日作ったアクションゲームにも、実はこのイシガメの「タンタン」が登場します。
まだ少し先の記事になりますが、楽しみにしていてください。(>v<)o”
次回は、この飼育ゲームの“のんびり”から一転して、
「リズム感と集中力が必要」なゲームです。
楽しんでいただければと思います。(*^v^)ノシ
次の実験はこちら(スタンプゲーム): 「アイドル猫の肉球サイン」
ゲーム作りが楽しくて、どんどん作品が増えています(*^v^*)
もし応援していただけたら、とても励みになります。






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