実験#24:水とドライアイス・昇華と演歌(ドライアイス演歌)

■24個目「ドライアイス演歌

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

 今日は「ドライアイス演歌」という、前回のジターリングに続く
 ちょっと実験要素の強い“雰囲気ゲーム”のその2を作りました。

 まず、ドライアイスを水に入れたときに出る、
 あのポコポコした泡と、白いモワモワの煙のような現象を
 ブラウザ上で再現できないかな?というところから始まりました。

 再現はなかなか大変でしたが、
 せっかくなのでゲーム要素も入れてみたくて、
 最終的に「演歌」と組み合わせた不思議な実験になりました。

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

(^^*)今日も HTML 形式のメモアプリで動かせるゲームを作ろうと思っています。

 ドライアイスを水に入れたときの、あのポコポコと泡が出てはじけて、
 白いモワモワが広がる現象を再現できるかな。

(AI)
ドライアイスの「ポコポコ」と「白いモワモワ(霧)」の質感、
 HTML/Canvas で再現すると絶対に癒やされますね!

(^_^)/ 二酸化炭素は空気より比重が重いので、
 水面に沿ってモワモワがたまっていく感じも欲しいです。
 器の端から外へ流れ落ちる動きがあると、実際の様子に近づきそう。

(AI)その通りです!
 霧のパーティクルに「重力」「器の壁との衝突判定」
「器の端から外へ落ちる動き」を追加してみました。

(^^;)あれ、水面に出た泡が水中に落ちていってるよ。
 水面にたまるようにお願い。器の下は床でお願いします。
 落ちたモワモワは床を伝って広がるイメージで。

〈修正してもモワモワは水中へ落ちてしまい、横にも広がりません。〉

(つ_<*)
やっぱり水中に落ちちゃう。
 気体になった二酸化炭素は水より軽いので浮きます。
 でも空気よりは重いので、水面を這うように広がります。

(AI)
なるほど、私の物理計算が「重さ」に寄りすぎていましたね!
 「水より軽いけれど空気より重い」という絶妙なバランスが大事でした。

〈この後もモワモワの広がり方やスピード、消え方などの調整が難しかったです〉
(>v<*)

(^o^;)水槽の水の色が黒すぎてわかりにくいです。
 少し明るめにしましょう。
 気泡が水面にたまってしまって、床まで流れていくのがほとんどありません。

(AI)「酢思想(水槽ですね!)」の色を明るくし、
 霧がもっとダイナミックに溢れ出して床まで届くように調整しました!

〈誤字もきちんと察してくれる優秀なAIさんです〉


■ 実際の動作

 一番最初に、AIとの相談で作ってもらったゲームの初期状態です。
 ここから「モワモワの流れ方」や「演歌との組み合わせ」などを
 少しずつ提案して改良していきました。
(画像:AIが最初に作った状態)

 水槽が中央にあるので、マウスでクリックすると、ドライアイスが入ります。

(画像:クリックでドライアイス投入)

 ポコポコとドライアイスが昇華され、
 白いモワモワとなって低い場所へ広がっていきます。

(画像:モワモワが広がる様子)

 足元にドライアイスのモワモワが広がってくると、
 雪が降りはじめて、演歌っぽい歌詞が流れ出します。

(画像:演歌が流れ始めるシーン)

■ コードのポイント

今回の記事では、AI と相談しながら作った
「ドライアイス演歌」の HTML コードを公開します。

 ブラウザだけで動く、とてもシンプルなゲームです。

 PC の「メモ帳」(※Windowsの場合)を開き、
下のコードをコピーして貼り付け、「doraiaisu.html」など
好きな名前で保存してください。

 保存したファイルをブラウザで開くだけで遊べます。

【操作方法】
・水槽の中をクリックして、ドライアイスを入れます。

 コードといっても難しい部分はありません。
 貼り付けて保存するだけで動きます。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Enka Final Stage - Fixed Spotlight</title>
<style>
body { margin: 0; background: #000; overflow: hidden; color: white; font-family: 'Hiragino Mincho ProN', 'MS Mincho', serif; text-rendering: optimizeLegibility; }
canvas { display: block; }
.ui { position: absolute; top: 10px; right: 10px; pointer-events: none; opacity: 0.7; font-size: 14px; text-align: right; text-shadow: 1px 1px 2px black; }
</style>
</head>
<body>
<div class="ui">水槽をクリック:超スローな霧が溜まると<br>雪、歌詞、照明が変わります。<br>※音声ファイル不要。JavaScriptで生成します。</div>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const floorY = canvas.height - 50;
const bowl = { x: canvas.width * 0.55, y: canvas.height * 0.45, width: 380, height: 120 };
const waterLevel = bowl.y + 10;
const particles = [];
const chunks = [];
const snowflakes = [];
// --- Web Audio API 設定(癒しのポコポコ音) ---
let audioContext;
let audioInitialized = false;
function playPopSound() {
if (!audioInitialized || !audioContext) return;
const now = audioContext.currentTime;
const osc1 = audioContext.createOscillator();
const gain1 = audioContext.createGain();
osc1.type = 'sine';
osc1.frequency.setValueAtTime(250 + Math.random() * 100, now);
const osc2 = audioContext.createOscillator();
const gain2 = audioContext.createGain();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(500 + Math.random() * 200, now);
const filter = audioContext.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(800, now);
filter.Q.setValueAtTime(3, now);
const attack = 0.005;
const release = 0.15;
const peakGain = 0.4;
gain1.gain.setValueAtTime(0, now);
gain1.gain.linearRampToValueAtTime(peakGain * 0.7, now + attack);
gain1.gain.exponentialRampToValueAtTime(0.001, now + attack + release);
gain2.gain.setValueAtTime(0, now);
gain2.gain.linearRampToValueAtTime(peakGain * 0.3, now + attack);
gain2.gain.exponentialRampToValueAtTime(0.001, now + attack + release * 1.2);
osc1.connect(gain1);
osc2.connect(gain2);
gain1.connect(filter);
gain2.connect(filter);
filter.connect(audioContext.destination);
osc1.start(now);
osc2.start(now);
osc1.stop(now + attack + release);
osc2.stop(now + attack + release * 1.2);
}
function initializeAudio() {
if (!audioInitialized) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioInitialized = true;
}
}
// --- 演歌の歌詞設定 ---
const lyrics = [
"北の漁場はよぉ…", "波が牙を剥く", "おんな一人の…", "戻り川",
"津軽の海は…", "雪に埋もれる", "情けひとつを…", "道連れに"
];
let currentLyricIdx = 0;
let lyricTimer = 0;
let showEnkaEffect = false;
class Chunk {
constructor(x, y) {
this.x = x; this.y = y;
this.timer = 400;
}
update() {
this.timer--;
if (this.timer > 0 && this.timer % 10 === 0) {
particles.push(new Particle(this.x + (Math.random()-0.5)*15, this.y, 'bubble'));
playPopSound(); // 泡が出るたびにポコポコ!
}
}
draw() {
if (this.timer <= 0) return;
ctx.fillStyle = "#fff"; ctx.fillRect(this.x - 4, this.y - 4, 8, 8);
}
}
class Particle {
constructor(x, y, type) {
this.x = x; this.y = y;
this.type = type;
this.vx = (Math.random() - 0.5) * 0.2;
this.vy = type === 'bubble' ? -Math.random() * 1.0 - 0.5 : 0;
this.size = type === 'bubble' ? 3 : Math.random() * 45 + 35;
if (type === 'snowflake') {
this.vx = (Math.random() - 0.5) * 1.0; this.vy = Math.random() * 1.0 + 0.5; this.size = Math.random() * 3 + 1;
}
this.life = 1.0; this.friction = 0.96; this.floorDir = Math.random() > 0.5 ? 1 : -1;
}
update() {
if (this.type === 'bubble') {
this.y += this.vy;
if (this.y <= waterLevel) {
this.life = 0;
for(let i=0; i<2; i++) {
let f = new Particle(this.x, waterLevel - 5, 'fog'); f.vx = (Math.random() - 0.5) * 1.0; particles.push(f);
}
}
} else if (this.type === 'fog') {
this.x += this.vx; this.y += this.vy; this.vx *= this.friction;
if (this.y >= waterLevel - 30 && this.y <= bowl.y + bowl.height && this.x > bowl.x && this.x < bowl.x + bowl.width) {
this.vy = (waterLevel - 10 - this.y) * 0.03; this.vx += (this.x < bowl.x + bowl.width/2) ? -0.05 : 0.05;
}
if (this.x < bowl.x - 5 || this.x > bowl.x + bowl.width + 5) {
if (this.y < floorY - 10) this.vy += 0.08;
}
if (this.y > floorY - this.size * 0.4) {
this.y = floorY - this.size * 0.4; this.vy = 0; this.vx += this.floorDir * 0.08;
}
this.life -= 0.0003; this.size += 0.04;
} else {
this.x += this.vx; this.y += this.vy;
if (this.y > floorY - 100) this.vx += (this.x < canvas.width / 2 ? 0.02 : -0.02);
if (this.y > floorY - 10) this.life = 0;
}
}
draw() {
if (this.life <= 0) return;
ctx.beginPath();
if (this.type === 'bubble') {
ctx.strokeStyle = `rgba(255, 255, 255, ${this.life * 0.3})`; ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.stroke();
} else if (this.type === 'fog') {
let grad = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size);
grad.addColorStop(0, `rgba(255, 255, 255, ${this.life * 0.22})`); grad.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.fillStyle = grad; ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill();
} else {
ctx.fillStyle = `rgba(255, 255, 255, ${this.life * 0.8})`; ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill();
}
}
}
function drawStage() {
const bgGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
bgGrad.addColorStop(0, '#020205'); bgGrad.addColorStop(1, '#050510');
ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#0a0a0a'; ctx.fillRect(0, floorY, canvas.width, 50);
if (showEnkaEffect) {
ctx.save();
ctx.font = "38px 'Hiragino Mincho ProN', serif";
ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
ctx.shadowBlur = 20; ctx.shadowColor = "rgba(255,255,255,0.5)";
ctx.textAlign = "center";
ctx.fillText(lyrics[currentLyricIdx], canvas.width / 2, canvas.height * 0.35);
ctx.restore();
}
// --- スポットライト(修正版:発光して明るく!) ---
ctx.save();
ctx.globalCompositeOperation = 'screen'; // 光の合成モードで明るくする
let spotlightColor1, spotlightColor2;
if (showEnkaEffect) {
const time = Date.now() * 0.001;
// RGBのベース値を上げて、はっきりとした光にする
const r = Math.floor(Math.sin(time) * 100 + 155); // 55〜255
const g = 50;
const b = Math.floor(Math.sin(time + Math.PI/2) * 100 + 155); // 55〜255
spotlightColor1 = `rgba(${r}, ${g}, ${b}, 0.35)`; // アルファ値もアップ
spotlightColor2 = `rgba(${r*0.5}, 0, ${b*0.8}, 0)`;
} else {
spotlightColor1 = 'rgba(200, 220, 255, 0.25)';
spotlightColor2 = 'rgba(0,0,0,0)';
}
const lightGrad = ctx.createRadialGradient(250, 150, 0, 250, 500, 600);
lightGrad.addColorStop(0, spotlightColor1); lightGrad.addColorStop(1, spotlightColor2);
ctx.fillStyle = lightGrad; ctx.beginPath(); ctx.moveTo(250, 0); ctx.lineTo(80, floorY); ctx.lineTo(420, floorY); ctx.fill();
ctx.restore();
// --- 演歌歌手のシルエット ---
ctx.save();
ctx.translate(250, floorY - 160);
const hairGrad = ctx.createLinearGradient(0, -30, 0, 30);
hairGrad.addColorStop(0, '#555'); hairGrad.addColorStop(1, '#111');
ctx.fillStyle = hairGrad;
ctx.beginPath(); ctx.arc(0, 0, 28, Math.PI, 0); ctx.bezierCurveTo(22, -35, 35, -12, 28, 12); ctx.bezierCurveTo(22, 35, -22, 35, -28, 12); ctx.bezierCurveTo(-35, -12, -22, -35, 0, -28); ctx.fill();
const kimonoGrad = ctx.createLinearGradient(0, 25, 0, 160);
kimonoGrad.addColorStop(0, '#aaa'); kimonoGrad.addColorStop(1, '#eee');
ctx.fillStyle = kimonoGrad; ctx.shadowBlur = 10; ctx.shadowColor = 'rgba(255,255,255,0.3)';
ctx.beginPath(); ctx.moveTo(-18, 28); ctx.bezierCurveTo(-28, 70, -30, 80, -28, 90); ctx.bezierCurveTo(-45, 110, -35, 130, -25, 135); ctx.lineTo(-25, 150); ctx.bezierCurveTo(-45, 160, -35, 170, 0, 170); ctx.bezierCurveTo(35, 170, 45, 160, 25, 150); ctx.lineTo(25, 135); ctx.bezierCurveTo(35, 130, 45, 110, 28, 90); ctx.lineTo(28, 28); ctx.closePath(); ctx.fill();
const obiGrad = ctx.createLinearGradient(-20, 75, 20, 95);
obiGrad.addColorStop(0, '#444'); obiGrad.addColorStop(1, '#888');
ctx.fillStyle = obiGrad; ctx.fillRect(-20, 75, 40, 22);
ctx.strokeStyle = "#fff"; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(15, 60); ctx.lineTo(15, 100); ctx.stroke();
ctx.fillStyle = '#eee'; ctx.beginPath(); ctx.arc(15, 60, 4, 0, Math.PI * 2); ctx.fill();
ctx.restore();
// 水槽
ctx.fillStyle = 'rgba(70, 130, 180, 0.2)'; ctx.fillRect(bowl.x, waterLevel, bowl.width, (bowl.y + bowl.height) - waterLevel);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; ctx.lineWidth = 1; ctx.strokeRect(bowl.x, bowl.y, bowl.width, bowl.height);
}
function animate() {
drawStage();
const fogOnFloor = particles.filter(p => p.type === 'fog' && p.y > floorY - 100).length;
showEnkaEffect = fogOnFloor > 100;
if (showEnkaEffect) {
if (Math.random() > 0.8) snowflakes.push(new Particle(Math.random() * canvas.width, 0, 'snowflake'));
lyricTimer++;
if (lyricTimer > 300) { currentLyricIdx = (currentLyricIdx + 1) % lyrics.length; lyricTimer = 0; }
}
chunks.forEach((c, i) => { c.update(); c.draw(); if (c.timer <= 0) chunks.splice(i, 1); });
for (let i = particles.length - 1; i >= 0; i--) { particles[i].update(); particles[i].draw(); if (particles[i].life <= 0) particles.splice(i, 1); }
for (let i = snowflakes.length - 1; i >= 0; i--) { snowflakes[i].update(); snowflakes[i].draw(); if (snowflakes[i].life <= 0) snowflakes.splice(i, 1); }
requestAnimationFrame(animate);
}
canvas.addEventListener('mousedown', (e) => {
initializeAudio();
if (e.clientX > bowl.x && e.clientX < bowl.x + bowl.width && e.clientY > waterLevel) {
chunks.push(new Chunk(e.clientX, e.clientY));
}
});
animate();
</script>
</body>
</html>


■ 今日の学び

 今回は、“雰囲気ゲーム”というものができました。
 これも新しいジャンルになるのかな。(^^;)

 今までも、雰囲気を感じられるゲームづくりをしてきましたが、
 AIとはいつも、感覚やニュアンスを言葉にしながら相談しています。

 情景がふわっと浮かぶような、
 なんとなく演歌が聞こえてきそうな…そんなゲームになりました。

 アイキャッチは最後に作ったのですが、
 水槽が「酢思想」になっていて、
 AIにちょっといじられている感じになりました。(>o<;)

 ぜひ楽しんでみてください。(*>v<*)

■ 次回は、ゲーム実験室その3です。

「できるかな」をもとに作ってみた、小さな実験ゲームたちを紹介します。
 今回も、ちょっとクセのある“実験室らしいゲーム”になりそうです。

 次の記事も、どんな動きをするのか、どんな仕組みなのか……
 作っている本人もワクワクしながら試した実験です。

 お楽しみに。(*^v^)ノシ

次の実験はこちら(磁力 × 渦 × 加温による撹拌):
「ホットスターラーシミュレーター」



 ゲーム作りが楽しくて、どんどん作品が増えています(*^v^*)
 もし応援していただけたら、とても励みになります。

~このブログについて~

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

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


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

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

続きを読む