実験#61:白い3匹のメダカたち(天使のしずく )

■61個目 白い3匹のメダカたち(天使のしずく )

こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/

今回は、とっても癒やされる、 光のしずくがきらめく幻想的なアクアリウムゲームを作ってみました。 主役になるのは……白くて美しいメダカ、“天使のしずく”たちです!

「天使のしずく」というのは、最近お迎えした本物のメダカの種類なんです。 お店で見かけたその名前の可愛さと、白く透き通るような姿にひとめぼれしてしまって……。

<@)二ニ=< ~~

「この子たちの魅力を、そのままゲームの世界で泳がせたい!」 そんな気持ちから、この作品が生まれました。

ゲームの中では、深い青色に包まれた静かな水槽を、
大・中・小の3匹のメダカが、光り輝く「しずく」を集めて泳ぎ回ります。

今回こだわったのは、その世界観と音です。  
「もしも水槽の中に、本物の天使のしずくが漂っていたら?」という優しい想像から生まれた、心温まるアクアリウムになっています。

水槽の中には、メダカたちのほかに、
少し不思議な装飾も浮かべてみました。(・ω・*)

① 石造りの小さな橋(その下をくぐれます!)  
② 大きな巻貝(水底でひと休み)  
③ 透明な浮き球につながれた、ガラス製の大きなカクレクマノミ
 (メダカが当たるとポコッと動きます!)

メダカたちが光のしずくを集めるたびに、
このカクレクマノミの色が魔法のようにどんどん変化していきます。(^∀^)  そして背景には、透き通るようなシンセサイザーの音色。

まさに「天使のしずく」をテーマにした、音と光の癒やしの空間です。
ちなみに、
Fキーを押すとメダカたちが気ままに自由に泳ぎだす秘密のモードも……?

操作方法は後ほど詳しくご紹介しますが、
まずはこの美しい水槽の世界を覗いてみてくださいね。  
どんな色のカクレクマノミに出会えるか、お楽しみに!

■ AIとのやりとりで面白かった部分

■ゲーム仕様の相談からスタート

みー:(^v^) 今日もゲームを作ります。HTML形式で動かせるゲームです。

最近メダカを買ったんだけど、
「天使のしずく」という白い大中小の大きさの3匹のメダカ。
そのメダカのゲームつくります。

AI:メダカの「天使のしずく」、名前からして透明感があって素敵ですね!
白い体が水槽の中でキラキラ光る様子は、
確かにゲームのインスピレーションになりそうです。

みー:(=v=) なんかいいね。メダカっぽい形に近づけられるかな。
愛嬌のある目と、パクパクする口、スタイリッシュな尾びれ、きらめく体。
水槽の中を泳ぎまわる姿みたいね。

AI: では、前回の「丸い形」から一歩踏み込んで、
「愛嬌のある目」「パクパクする口」
「スタイリッシュな尾びれ」「きらめく体」を持つ、
よりメダカらしい、愛着の湧くキャラクターを
HTMLのCanvas(描画機能)だけで表現してみましょう。

また、今回は3匹が独立して、
あなた(親メダカ/プレイヤー)の後ろを可愛らしくついてくる(群れをなす)ようにします。

みー:(^_^)ノスペースキーで先頭のメダカを切り替えられるようにしましょう。
水中の演出で、結構リアルな感じの水草を漂わせることできるかな。

AI: 「天使のしずく」たちの冒険、さらに進化させましょう!

スペースキーによる「主役(先導役)の交代」と、
水槽のリアリティを格段に上げる「揺れる水草」の実装ですね。

特に水草は、一本一本が独立して水の流れに身を任せているように、
ベジェ曲線と三角関数を使って「しなり」を表現します。

みー:(^o^)水槽の底に砂利石、橋のような飾りも底に置いて、
大き目の貝殻とかも置きましょう。

AI: 素晴らしいこだわりですね!砂利、橋、
そして大きな貝殻が加わることで、水槽の世界観がグッと深まります。

「天使のしずく」たちが、白い砂利の上を通り抜け、橋をくぐり、
貝殻の影でひと休みするような、より「アクアリウム(情景)」としての完成度を高めたコードを作成しました。

■ 実際の動作

こちらが、AIが最初に作ってくれたメダカです。
(画像:最初にAIが作ってくれたゲームの画像)


最初はシンプルな形でしたが、
ここから少しずつ“メダカらしさ”が加わっていきます。

(画像:初期のメダカ)



目や尾びれがついて、愛嬌のある姿に進化しました。
水槽の雰囲気も整えていきます。

(画像:改良版のメダカ)


砂利や橋、巻貝などを置いて、 アクアリウムらしい世界観ができてきました。

そしてこちらが仕上がりです。
振ってくる光のしずくを取ると、クマノミの色が魔法のように変わります。

(画像:完成版の水槽)

■ コードのポイント

今回の記事では、AI と相談しながら作った
「白い3匹のメダカたち(天使のしずく )」 の HTML コードを公開します。

このコードをそのまま保存するだけで、
あなたのPCでもすぐに遊べます。

ブラウザだけで動く、とてもシンプルなゲームなので、
気軽に試してみてくださいね。

▼ 遊ぶまでの手順

①PC の「メモ帳」(※Windowsの場合)や  
 Mac の「テキストエディット」など、テキストエディタを開きます。

②下のコードをコピーして貼り付けます。

③「medaka.html」など、好きな名前で保存します。
 (※拡張子は .html にしてください)

④保存したファイルをブラウザで開けば、すぐに遊べます。

■ 遊び方・操作方法

この小さな水槽の遊び方(操作方法)をご紹介しますね。(^^*)  
とってもシンプルなので、ゲームが苦手な方でもゆったり楽しめますよ。

■ ゲームのはじめかた  
画面(水槽)のどこかをクリックすると、ゲームスタートです!  

同時に「天使のしずく」をイメージした癒やしの音楽が流れ始めます。
ぜひ音を出して遊んでみてくださいね♪

■ マウス移動:メダカたちの誘導  
水槽の中でマウスをゆっくり動かしてみてください。
 
先頭のメダカがマウスカーソルを追いかけて、
後ろの2匹も仲良く一列に並んでついてきます

水面に浮かんでいるボールや、
ガラスのカクレクマノミにコツンとぶつかると、
ポコッと音を立ててリアルに揺れるので、
ぜひ色々なところへ誘導して遊んでみてくださいね。

■ スペースキー:リーダーの交代  
キーボードの [Space] キーで、
先頭を泳ぐメダカが「大 → 中 → 小」の順番で交代します。

上から降ってくる「光のしずく」を集められるのは先頭のメダカだけ。
しずくの位置に合わせて、上手くバトンタッチさせてあげてくださいね。

■ Fキー:気ままにお散歩モード(フリーモード)  
これが私の一番のお気に入り機能です!(=v=*)  

キーボードの [F] キーを押すと、3匹がピタッと列を離れて、
それぞれ水槽の中をバラバラに、自由に泳ぎ始めます。  

操作を忘れて、まるで本物のアクアリウムのように、
ただただ眺めて癒やされるモードです。

お散歩モード中にマウスをピクッと動かすと……  
「ハッ!」としたように、3匹が慌てて元の列に戻ってくるんです。

その健気な姿がとっても可愛いので、ぜひ試してみてくださいね!

お仕事や勉強の合間に、この「天使のしずく」たちの水槽で、
少しでもほっと一息ついてもらえたら嬉しいです。(^_^

“コードは長いので折りたたんでいます”

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>天使のしずく - Musical Aquarium</title>
<style>
body { background: #02050a; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; color: white; font-family: 'Hiragino Mincho ProN', serif; overflow: hidden; user-select: none; }
canvas { background: linear-gradient(180deg, #2a5298 0%, #051020 70%, #1a1a1a 100%); border: 6px solid #444; border-radius: 8px; box-shadow: 0 0 50px rgba(100, 200, 255, 0.2); cursor: none; }
#ui { position: absolute; top: 25px; left: 25px; pointer-events: none; text-shadow: 0 0 10px rgba(0,0,0,0.8); }
.controls { position: absolute; bottom: 20px; font-size: 13px; color: rgba(255,255,255,0.5); letter-spacing: 1px; }
#startScreen {
position: absolute; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(2, 5, 10, 0.9); z-index: 100;
display: flex; flex-direction: column; align-items: center; justify-content: center;
cursor: pointer; transition: opacity 1s;
}
#startScreen h1 { font-weight: normal; letter-spacing: 5px; color: #d0e0ff; text-shadow: 0 0 20px #fff; }
#startScreen p { color: #88aadd; font-size: 14px; }
.mode-badge {
color: #fff; background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 4px; display: inline-block; margin-top: 5px; transition: background 0.3s;
}
.mode-free { background: rgba(100, 255, 150, 0.2); color: #afffaf; }
</style>
</head>
<body>
<div id="startScreen" onclick="startGame()">
<h1>天使のしずく</h1>
<p>〜 クリックして水槽を覗く(♪ Sound ON) 〜</p>
</div>
<div id="ui">
<div style="font-size: 12px; opacity: 0.8; color: #aaf;">SHIMMER GAINED</div>
<div style="font-size: 36px; font-weight: bold; font-variant-numeric: tabular-nums;" id="score">0</div>
<div class="mode-badge" id="currentMedaka">Leader: 大メダカ</div>
</div>
<canvas id="aquariumCanvas" width="900" height="550"></canvas>
<div class="controls">SPACE: Switch Leader / F: Free Mode / MOUSE: Guide</div>
<script>
const canvas = document.getElementById('aquariumCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const medakaNameEl = document.getElementById('currentMedaka');
let mouse = { x: 450, y: 275 };
let score = 0;
let leaderIndex = 0;
let isGameRunning = false;
let isFreeMode = false; // フリーモードのフラグ
let audioCtx;
function initAudio() {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
playAmbientBGM();
}
function playAmbientBGM() {
const scale = [523.25, 587.33, 659.25, 783.99, 880.00, 1046.50];
setInterval(() => {
if(!audioCtx || document.hidden) return;
const time = audioCtx.currentTime;
const osc = audioCtx.createOscillator(), gain = audioCtx.createGain();
osc.type = 'sine'; osc.frequency.value = scale[Math.floor(Math.random() * scale.length)];
gain.gain.setValueAtTime(0, time); gain.gain.linearRampToValueAtTime(0.04, time + 2); gain.gain.linearRampToValueAtTime(0, time + 6);
osc.connect(gain); gain.connect(audioCtx.destination);
osc.start(time); osc.stop(time + 6);
}, 800);
}
function playDropSE() {
if(!audioCtx) return;
const time = audioCtx.currentTime, osc = audioCtx.createOscillator(), gain = audioCtx.createGain();
osc.type = 'sine'; osc.frequency.setValueAtTime(1200, time); osc.frequency.linearRampToValueAtTime(2000, time + 0.1);
gain.gain.setValueAtTime(0.15, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.4);
osc.connect(gain); gain.connect(audioCtx.destination); osc.start(time); osc.stop(time + 0.4);
}
function playBumpSE(power) {
if(!audioCtx) return;
const time = audioCtx.currentTime, osc = audioCtx.createOscillator(), gain = audioCtx.createGain();
osc.type = 'sine'; osc.frequency.setValueAtTime(400 + power*20, time); osc.frequency.exponentialRampToValueAtTime(100, time + 0.1);
gain.gain.setValueAtTime(0.05 + Math.min(power*0.02, 0.1), time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.1);
osc.connect(gain); gain.connect(audioCtx.destination); osc.start(time); osc.stop(time + 0.1);
}
// --- 入力制御 ---
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const newX = e.clientX - rect.left;
const newY = e.clientY - rect.top;
// マウスが動いたらフリーモード解除
if (isFreeMode && (Math.abs(newX - mouse.x) > 2 || Math.abs(newY - mouse.y) > 2)) {
isFreeMode = false;
updateModeUI();
}
mouse.x = newX;
mouse.y = newY;
});
window.addEventListener('keydown', (e) => {
if (!isGameRunning) return;
// スペースキー: リーダー切り替え
if (e.code === 'Space') {
isFreeMode = false; // 切り替え時もフリーモード解除
leaderIndex = (leaderIndex + 1) % 3;
updateModeUI();
}
// Fキー: フリーモード切り替え
if (e.code === 'KeyF') {
isFreeMode = true;
updateModeUI();
}
});
function updateModeUI() {
if (isFreeMode) {
medakaNameEl.innerText = `Mode: 気ままにお散歩中`;
medakaNameEl.classList.add('mode-free');
} else {
const names = ["大メダカ", "中メダカ", "小メダカ"];
medakaNameEl.innerText = `Leader: ${names[leaderIndex]}`;
medakaNameEl.classList.remove('mode-free');
}
}
// --- 各種クラス ---
class ClownfishFloat {
constructor(x, y, r, lineLength, fishSize) {
this.x = x; this.y = y; this.baseY = y; this.r = r; this.vx = 0; this.vy = 0; this.friction = 0.96; this.t = Math.random() * 10;
this.lineLength = lineLength; this.fishSize = fishSize; this.hue = 30;
}
evolveColor() { this.hue = (this.hue + 45) % 360; }
update() {
this.x += this.vx; this.y += this.vy; this.vx *= this.friction; this.vy *= this.friction; this.t += 0.05;
if(Math.abs(this.vx) < 0.1 && Math.abs(this.vy) < 0.1) this.y = this.baseY + Math.sin(this.t) * 5; else this.baseY = this.y - Math.sin(this.t) * 5;
if(this.x - this.r < 0) { this.x = this.r; this.vx *= -0.5; } if(this.x + this.r > canvas.width) { this.x = canvas.width - this.r; this.vx *= -0.5; }
if(this.y - this.r < 0) { this.y = this.r; this.vy *= -0.5; } if(this.y + this.r > 150) { this.y = 150 - this.r; this.vy *= -0.5; this.baseY = this.y; }
}
draw() {
ctx.save(); ctx.translate(this.x, this.y);
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(0, 0, this.r, 0, Math.PI*2); ctx.stroke();
const gradBall = ctx.createRadialGradient(-this.r/3, -this.r/3, 0, 0, 0, this.r);
gradBall.addColorStop(0, 'rgba(255, 255, 255, 0.4)'); gradBall.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradBall; ctx.beginPath(); ctx.arc(0, 0, this.r, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = "rgba(255, 255, 255, 0.15)"; ctx.lineWidth = 0.5; ctx.beginPath(); ctx.moveTo(0, this.r); ctx.lineTo(0, this.r + this.lineLength); ctx.stroke();
ctx.translate(0, this.r + this.lineLength); ctx.rotate(Math.sin(this.t * 1.5) * 0.1);
const gradBody = ctx.createLinearGradient(-this.fishSize/2, 0, this.fishSize/2, 0);
gradBody.addColorStop(0, `hsla(${this.hue}, 100%, 55%, 0.6)`); gradBody.addColorStop(1, `hsla(${this.hue + 20}, 100%, 65%, 0.4)`);
ctx.fillStyle = gradBody; ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; ctx.lineWidth = this.fishSize / 10;
ctx.beginPath(); ctx.moveTo(this.fishSize/2, 0); ctx.bezierCurveTo(this.fishSize/4, -this.fishSize/3, -this.fishSize/4, -this.fishSize/4, -this.fishSize/2, 0); ctx.bezierCurveTo(-this.fishSize/4, this.fishSize/4, this.fishSize/4, this.fishSize/3, this.fishSize/2, 0); ctx.fill();
ctx.beginPath(); ctx.moveTo(this.fishSize/4, -this.fishSize/3.5); ctx.bezierCurveTo(this.fishSize/5, -this.fishSize/4, this.fishSize/5, this.fishSize/4, this.fishSize/4, this.fishSize/3.5); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, -this.fishSize/3); ctx.bezierCurveTo(-this.fishSize/10, -this.fishSize/4, -this.fishSize/10, this.fishSize/4, 0, this.fishSize/3); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-this.fishSize/4, -this.fishSize/4); ctx.bezierCurveTo(-this.fishSize/3.5, -this.fishSize/5, -this.fishSize/3.5, this.fishSize/5, -this.fishSize/4, this.fishSize/4); ctx.stroke();
const gradGlow = ctx.createRadialGradient(0, 0, 0, 0, 0, this.fishSize/2);
gradGlow.addColorStop(0, 'rgba(255, 255, 255, 0.5)'); gradGlow.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = gradGlow; ctx.shadowBlur = 15; ctx.shadowColor = `hsla(${this.hue}, 100%, 70%, 1)`; ctx.beginPath(); ctx.arc(0, 0, this.fishSize/2, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(17, 17, 17, 0.8)"; ctx.beginPath(); ctx.arc(this.fishSize/2.5, -this.fishSize/8, this.fishSize/10, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
checkCollision(medaka) {
const headX = medaka.x + Math.cos(medaka.angle) * medaka.length/2, headY = medaka.y + Math.sin(medaka.angle) * medaka.length/2;
const dx = headX - this.x, dy = headY - this.y, dist = Math.sqrt(dx*dx + dy*dy), minDist = this.r + medaka.baseSize + 5;
if (dist < minDist) {
const power = (minDist - dist) * 0.1 + (leaderIndex === medaka.id ? 2 : 1);
this.vx -= Math.cos(Math.atan2(dy, dx)) * power; this.vy -= Math.sin(Math.atan2(dy, dx)) * power; playBumpSE(power);
}
}
}
class FloatingBall { constructor(x, y, r) { this.x = x; this.y = y; this.baseY = y; this.r = r; this.vx = 0; this.vy = 0; this.friction = 0.96; this.t = Math.random() * 10; } update() { this.x += this.vx; this.y += this.vy; this.vx *= this.friction; this.vy *= this.friction; this.t += 0.05; if(Math.abs(this.vx) < 0.1 && Math.abs(this.vy) < 0.1) this.y = this.baseY + Math.sin(this.t) * 5; else this.baseY = this.y - Math.sin(this.t) * 5; if(this.x - this.r < 0) { this.x = this.r; this.vx *= -0.5; } if(this.x + this.r > canvas.width) { this.x = canvas.width - this.r; this.vx *= -0.5; } if(this.y - this.r < 0) { this.y = this.r; this.vy *= -0.5; } if(this.y + this.r > 150) { this.y = 150 - this.r; this.vy *= -0.5; this.baseY = this.y; } } draw() { ctx.save(); ctx.translate(this.x, this.y); const grad = ctx.createRadialGradient(-this.r/3, -this.r/3, 0, 0, 0, this.r); grad.addColorStop(0, '#aaffaa'); grad.addColorStop(0.5, '#44aa44'); grad.addColorStop(1, '#1a5a1a'); ctx.fillStyle = grad; ctx.shadowBlur = 10; ctx.shadowColor = "rgba(0,0,0,0.5)"; ctx.beginPath(); ctx.arc(0, 0, this.r, 0, Math.PI*2); ctx.fill(); ctx.restore(); } checkCollision(medaka) { const headX = medaka.x + Math.cos(medaka.angle) * medaka.length/2, headY = medaka.y + Math.sin(medaka.angle) * medaka.length/2, dx = headX - this.x, dy = headY - this.y, dist = Math.sqrt(dx*dx + dy*dy), minDist = this.r + medaka.baseSize + 5; if (dist < minDist) { const power = (minDist - dist) * 0.1 + (leaderIndex === medaka.id ? 2 : 1); this.vx -= Math.cos(Math.atan2(dy, dx)) * power; this.vy -= Math.sin(Math.atan2(dy, dx)) * power; playBumpSE(power); } } }
class Seaweed { constructor(x, depth) { this.x = x; this.y = canvas.height - 10; this.height = Math.random() * 100 + 80; this.offset = Math.random() * 10; this.depth = depth; } draw(t) { ctx.beginPath(); ctx.lineWidth = this.depth ? 4 : 7; ctx.strokeStyle = this.depth ? "rgba(40,100,60,0.8)" : "rgba(20,60,30,0.6)"; ctx.moveTo(this.x, this.y); for(let i=1; i<=6; i++) { const sway = Math.sin(t + i*0.4 + this.offset) * (i*3); ctx.lineTo(this.x + sway, this.y - (this.height/6)*i); } ctx.stroke(); } }
// --- 進化したメダカクラス ---
class Medaka {
constructor(size, id) {
this.id = id; this.x = Math.random()*canvas.width; this.y = Math.random()*canvas.height;
this.baseSize = size; this.length = size*3; this.angle = 0; this.t = Math.random()*100;
// フリーモード用の目標地点
this.freeTarget = { x: Math.random() * canvas.width, y: Math.random() * canvas.height };
}
update(target) {
this.t += 0.1;
let tx, ty, speedFactor;
// フリーモードと追従モードの挙動分岐
if (isFreeMode) {
tx = this.freeTarget.x;
ty = this.freeTarget.y;
speedFactor = 0.015; // フリー時はのんびり泳ぐ
// 目的地に近づいたら、次の目的地をランダムに設定
if (Math.sqrt((tx-this.x)**2 + (ty-this.y)**2) < 40) {
this.freeTarget.x = Math.random() * canvas.width;
this.freeTarget.y = 50 + Math.random() * (canvas.height - 100);
}
} else {
tx = target.x;
ty = target.y;
speedFactor = (leaderIndex === this.id) ? 0.08 : 0.04;
}
const dx = tx - this.x, dy = ty - this.y;
const dist = Math.sqrt(dx*dx + dy*dy);
// 追従モードの時だけ、前の個体との車間距離(minDist)をとる
const minDist = (!isFreeMode && leaderIndex === this.id) ? 5 : (isFreeMode ? 0 : 50);
if (dist > minDist) {
this.x += dx * speedFactor;
this.y += dy * speedFactor;
}
if (dist > 5) {
let targetAngle = Math.atan2(dy, dx), diff = targetAngle - this.angle;
while(diff < -Math.PI) diff += Math.PI*2;
while(diff > Math.PI) diff -= Math.PI*2;
// フリーモード時は旋回もゆったり
this.angle += diff * (isFreeMode ? 0.03 : 0.1);
}
}
draw() {
ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle);
const swim = Math.sin(this.t*0.8);
// フリーモード中は誰にも強い光は当てない
ctx.shadowBlur = (!isFreeMode && leaderIndex === this.id) ? 20 : 5;
ctx.shadowColor = "white";
const g = ctx.createLinearGradient(-this.length/2, 0, this.length/2, 0);
g.addColorStop(0, '#fff'); g.addColorStop(1, 'rgba(200,230,255,0.4)'); ctx.fillStyle = g;
ctx.beginPath(); ctx.moveTo(this.length/2, 0); ctx.bezierCurveTo(this.length/4, -this.baseSize+swim, -this.length/4, -this.baseSize/2, -this.length/2, 0); ctx.bezierCurveTo(-this.length/4, this.baseSize/2, this.length/4, this.baseSize-swim, this.length/2, 0); ctx.fill();
ctx.beginPath(); ctx.moveTo(-this.length/2, 0); ctx.lineTo(-this.length/2-this.baseSize, -this.baseSize+swim*8); ctx.lineTo(-this.length/2-this.baseSize*0.6, swim*4); ctx.lineTo(-this.length/2-this.baseSize, this.baseSize+swim*8); ctx.fill();
ctx.fillStyle = "#111"; ctx.beginPath(); ctx.arc(this.length/3.5, -this.baseSize/3, this.baseSize/4, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
}
class Drop { constructor() { this.reset(); this.y = Math.random()*canvas.height; } reset() { this.x = Math.random()*canvas.width; this.y = -20; this.speed = Math.random()*0.5+0.5; } update() { this.y += this.speed; if(this.y > canvas.height) this.reset(); } draw() { ctx.fillStyle = "white"; ctx.shadowBlur = 10; ctx.shadowColor = "white"; ctx.beginPath(); ctx.arc(this.x, this.y, 3, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; } }
const pebbles = []; for(let i=0; i<150; i++) pebbles.push({ x: Math.random() * canvas.width, y: canvas.height - Math.random() * 25, r: Math.random() * 4 + 2, color: `rgb(${150+Math.random()*50}, ${150+Math.random()*50}, ${160+Math.random()*40})` });
function drawBridge() { ctx.save(); ctx.translate(450, 525); ctx.fillStyle = "rgba(0,0,0,0.4)"; ctx.beginPath(); ctx.ellipse(0, 5, 100, 20, 0, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = "#555"; ctx.beginPath(); ctx.arc(0, 0, 80, Math.PI, 0); ctx.arc(0, 0, 60, 0, Math.PI, true); ctx.fill(); ctx.strokeStyle = "#444"; for(let i=0; i<10; i++){ let ang = Math.PI + (Math.PI/10)*i; ctx.beginPath(); ctx.moveTo(Math.cos(ang)*60, Math.sin(ang)*60); ctx.lineTo(Math.cos(ang)*80, Math.sin(ang)*80); ctx.stroke(); } ctx.restore(); }
function drawShell(x, y, size) { ctx.save(); ctx.translate(x, y); ctx.rotate(-0.3); ctx.fillStyle = "#e0d0c0"; ctx.shadowBlur = 10; ctx.shadowColor = "rgba(0,0,0,0.5)"; for(let i=0; i<6; i++) { ctx.beginPath(); ctx.ellipse(i*8, 0, size-(i*3), size-(i*2), i*0.2, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = "#c0b0a0"; ctx.stroke(); } ctx.restore(); }
const medakas = [new Medaka(22, 0), new Medaka(16, 1), new Medaka(10, 2)];
const seaweeds = []; for(let i=0; i<12; i++) seaweeds.push(new Seaweed(Math.random()*canvas.width, i%2));
const drops = Array.from({length: 5}, () => new Drop());
const mainClownfish = new ClownfishFloat(canvas.width / 2, 60, 15, 80, 36);
const aquariumDecorations = [
new FloatingBall(canvas.width / 4, 50, 25),
new FloatingBall(canvas.width * 3/4, 70, 20),
mainClownfish
];
function loop(time) {
if (!isGameRunning) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const t = time * 0.002;
pebbles.forEach(p => { ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2); ctx.fill(); });
drawBridge(); drawShell(150, 520, 20);
seaweeds.filter(s => s.depth === 0).forEach(s => s.draw(t));
drops.forEach(d => {
d.update(); d.draw();
// しずくの回収判定(フリーモード中は先導役がいないので誰でも回収できるようにしても面白いですが、今回はリーダーが回収する元の仕様を残しています)
if (!isFreeMode) {
const l = medakas[leaderIndex];
if(Math.sqrt((l.x-d.x)**2+(l.y-d.y)**2) < 25) {
d.reset(); score++; scoreEl.innerText = score; playDropSE(); mainClownfish.evolveColor();
}
}
});
medakas.forEach((m, i) => {
let target;
if (isFreeMode) {
target = null; // フリーモード中は個別のランダム目標を参照させる
} else {
target = (i === leaderIndex) ? mouse : (i === 0 ? medakas[2] : medakas[i-1]);
if(i !== leaderIndex && (i === 0 ? 2 : i-1) === leaderIndex) target = medakas[leaderIndex];
}
m.update(target);
aquariumDecorations.forEach(deco => deco.checkCollision(m));
m.draw();
});
seaweeds.filter(s => s.depth === 1).forEach(s => s.draw(t));
aquariumDecorations.forEach(deco => { deco.update(); deco.draw(); });
requestAnimationFrame(loop);
}
function startGame() {
const screen = document.getElementById('startScreen');
screen.style.opacity = '0';
setTimeout(() => screen.style.display = 'none', 1000);
initAudio();
isGameRunning = true;
updateModeUI();
requestAnimationFrame(loop);
}
</script>
</body>
</html>


■ 今日の学び

この「天使のしずく」ゲームを作ってみて気づいた、
ちょっとした「学び(というより発見?)」をお話ししますね。(^^*)

それは…… 「メダカの撮影は、本物もゲームもすごく難しい!」 ということです。

ブログ用にスクリーンショットを撮ろうとすると、
マウスを動かせばスイスイついてきちゃうし、
Fキーでお散歩させると、3匹が好きな方向へ散ってしまうし……。
なかなか綺麗に並んだ“ベストショット”が撮れなくて、
何度も取り直してしまいました。(;^_^A

でもこれ、本物の「天使のしずく」たちを撮ろうとした時と、
まったく同じ苦労だったんです。
ちょこまか動く元気な姿を見ながら、
「ああ、本物のメダカそっくりの躍動感を作れたのかも!」と、
撮影に苦戦しつつも少し嬉しくなりました。(=v=*)

ちなみに庭には、「天使のしずく」のほかにも、
名前は分からないのですがオレンジ色のメダカたちも暮らしています。

これからも、日常の「好き!」や「癒やし」を、
こんなふうに楽しくゲームにしていきたいと思います。
ぜひブラウザの水槽で、メダカたちと遊んでみてくださいね。

こちらが、本物の「天使のしずく」です。
ゲームのモデルになった、白くてやさしい光をまとった子たちです。

(画像:天使のしずく)

■ 次回は、太陽と虫眼鏡と●●●の  
ちょっと危ない組み合わせをテーマにしたシミュレーションゲームです。

安全に配慮したゲームに仕上げていますが、
現実では絶対にまねしないでくださいね。

次回もぜひ遊びに来てくださいね。(*^v^)ノシ

▶ 次の実験はこちら(光と熱の収束点)「ソーラー イグニッション」



 ✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)


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

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

続きを読む