実験#52:綿帽子とシャボンと火(綿帽子吹き飛ばしシミュレーター)

■52個目「綿帽子吹き飛ばしシミュレーター

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

今回は、パソコンやスマホに向かって思わず「ふーっ」と息を吹きかけたくなる、ちょっと変わったシミュレーターを作ってみました。

画面の中で楽しめるのは、3つの世界です。

たんぽぽの綿毛:ふわっと空へ舞い上がる、春の風景。
シャボン玉のわっか:そっと風を当てると、まあるい泡が生まれる瞬間。
焚き火の枝火:風に揺られて、消えそうで消えない小さな炎。

今回こだわったのは、「風を操る心地よさ」です。 
 *⁂⁑⋆~~ヽ(^。^*)

風の強さや角度を変えるだけで、綿毛の飛び方が変わったり、
わっかからシャボン玉がふわっと生まれたり、
炎がゆらいで消えそうになったり、また元気に戻ったり……。

小さな風の変化が、画面の中の世界をやさしく動かしてくれます。

なんだか、画面の中の空気と自分の息が繋がっているような、不思議で優しい気持ちになれるゲームになっています。

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

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

みー:(^v^) 今日もゲームを作ります。HTML形式で動かせるゲーム。
たんぽぽの綿帽子吹き飛ばしシミュレーターを考えてます。
エアの吹く位置と向き・強さをマウスで調整する感じで、
綿毛の飛んでいく描写は立体的でリアルにしたいです。

AI: 今日も制作おつかれさまです。ゆっくり進めていきましょう。
たんぽぽと聞いて、ひとつ面白い話を思い出しました。
(中略:セイヨウタンポポの小話)
風に乗ってどこまでも飛んでいく力強さ、
今回のシミュレーターにも活かせそうですね。

みー:(^_^)b” リトライできるようにしましょう。

AI: 了解です。何度も挑戦して、一番きれいな飛び方を探せるのは楽しいですよね。 (中略:綿毛の渦の小話) 今回はその“空気の渦”を感じられるよう、3D描写を強化しています。

(綿毛の動きを調整しているとき)

みー:(^^;) たんぽぽの綿毛が飛ばずに残るのがあるね。
吹き続ければ全部飛びきるようにしましょう。

AI: 最後までしぶとく残る子がいるんですね。
ゲームとしては、全部飛ばせたほうが気持ちいいですよね。
(中略:根っこの小話)

(別の世界の案を追加するとき)

みー:(^_^)* 切り替えボタンでシャボン玉シミュレーターと 火のついた焚火吹きシミュレーターを追加できるかな。

AI: いいアイデアです!風の当て方でまったく違う反応が返ってくるので、 世界が一気に広がりますね。

みー:(^_^)* シャボン玉は、わっかに向かって吹くと泡が生まれる感じ。 焚火は、強く吹くと火が飛んで、風が収まると残り火から復活する感じにしたいです。

AI: どちらも“自分の風で世界が動く”感覚があって素敵です。 シャボン玉は一定以上の風でポンッと生まれ、 焚火は風で揺らぎながらも、そっと息を吹き返すように再点火する。 そんな体験を目指していきましょう。

■ 実際の動作

こちらが完成したシミュレーターの画面です。
たんぽぽの綿毛は、マウスのポインタ位置と、
ドラッグして引っぱる強さ・向きで風を送ることができます。
(画像:スタート画面)

弱いそよ風から、思いきり吹いたときの強風までシミュレートできます。
飛ばしている途中で別方向から風を当てたり、いろいろ試すことができます。
綿毛は「リセット」ボタンでいつでも最初の状態に戻せます。

(画像:たんぽぽの綿毛を吹き飛ばす様子)


こちらはシャボン玉バージョンです。
強く吹きすぎると割れてしまうので、加減を調整しながら飛ばします。

(画像:シャボン玉バージョン)


枝火バージョンでは、風の強さによって火が揺れたり、吹き消えたりします。
風が弱まると、枝先の残り火からふわっと火が戻ってきます。

(画像:枝火バージョン)

風の影響は、火や煙の動きにも現れます。
小さな風の変化が、画面の中の世界をやさしく揺らしてくれます。

(画像:風の影響の様子)

■ コードのポイント

今回の記事では、AI と相談しながら作った「綿帽子吹き飛ばしシミュレーター」の HTML コードを公開します。

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

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

▼ 遊ぶまでの手順

1. PC の「メモ帳」(※Windowsの場合)や、  
 Mac の「テキストエディット」など、テキストエディタを開きます。
2. 下のコードをコピーして貼り付けます。
3. 「fukitobasi.html」など、好きな名前で保存します。
 (※拡張子は .html にしてください)
4. 保存したファイルをブラウザで開けば、すぐに遊べます。

■ 遊び方・操作説明

操作はとっても簡単です!(^_^)  
パソコンなら「マウスでクリックしたまま引っ張る(ドラッグ)」、スマホなら「画面を指でタッチしたままスッと滑らせる(スワイプ)」だけ。

引っ張った方向に向かって、シューッと風が吹き抜けます。(^^*)  指を少しだけ動かすと「優しいそよ風」、長く引っ張ると「勢いのあるつよい風」になりますよ。

それぞれの世界での楽しみ方はこんな感じです。(=v=*)

① たんぽぽ  そよ風でじわじわと綿毛を剥がすのもよし、つよい風で一気に吹き飛ばすのもよし。しぶとく残った最後の一本まで、ふーっと見届けてあげてくださいね。

② シャボン玉  少し強めに「ふーっ!」と風を送ると、緑色のわっかから大小いろんなシャボン玉がフワァッと生まれます。強すぎる突風を当てると割れちゃうので、優しくたくさん作ってみてください!

③ 枝火  つよい風を当てると、炎がちぎれて火の粉が舞い、やがてスッと消えてしまいます。でも大丈夫。風を止めて静かに待っていると、枝先の赤い残り火から、ジワァ……っとまたあたたかい炎が育ってきます。

画面の上にあるボタンを押せば、いつでも好きな世界に切り替えたり、最初からやり直したり(リセット)できます。

息を吹きかけるたびに変わる、不思議な画面の中の空気。  あなただけの心地よい「ふーっ」を見つけてみてくださいね!(^^*)

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>統合吹込シミュレーター - わっか&枝火エディション</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #1a1a2e;
color: #fff;
font-family: sans-serif;
user-select: none;
}
#ui-layer {
position: absolute;
top: 20px;
left: 20px;
pointer-events: none;
z-index: 10;
}
.btn-group {
pointer-events: auto;
margin-top: 15px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 10px 15px;
background-color: #333;
color: white;
border: 2px solid #555;
border-radius: 5px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
button:hover { background-color: #555; }
button.active {
background-color: #4a7c59;
border-color: #7acc90;
}
#retry-btn {
background-color: #8b4513;
border-color: #cd853f;
}
#retry-btn:hover { background-color: #a0522d; }
canvas {
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
</style>
</head>
<body>
<div id="ui-layer">
<h2>統合吹込シミュレーター</h2>
<p>画面をドラッグしたままにして、「ふーっ」と風を送り続けよう!</p>
<div class="btn-group">
<button id="btn-dandelion" class="active">たんぽぽ</button>
<button id="btn-bubble">シャボン玉</button>
<button id="btn-campfire">枝火(焚火)</button>
</div>
<div class="btn-group">
<button id="retry-btn">リセット</button>
</div>
<p id="status-text"></p>
</div>
<canvas id="gameCanvas"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const statusText = document.getElementById('status-text');
let width, height, centerX, centerY;
const focalLength = 400;
let currentMode = 'dandelion';
let isBlowing = false;
let startMouse = { x: 0, y: 0 };
let currentMouse = { x: 0, y: 0 };
let fluffs = [];
let bubbles = [];
let fireParticles = [];
let smokeParticles = [];
let windParticles = [];
// 枝火用のステータス (1.0で最大、0.0で鎮火寸前)
let flameHealth = 1.0;
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
centerX = width / 2;
centerY = height / 2 + 100;
}
window.addEventListener('resize', resize);
resize();
// ==========================================
// たんぽぽクラス
// ==========================================
class Fluff {
constructor() {
const theta = Math.random() * 2 * Math.PI;
const phi = Math.acos((Math.random() * 2) - 1);
const radius = 60 + Math.random() * 10;
this.baseX = radius * Math.sin(phi) * Math.cos(theta);
this.baseY = radius * Math.cos(phi);
this.baseZ = radius * Math.sin(phi) * Math.sin(theta);
this.x = this.baseX;
this.y = this.baseY - 150;
this.z = this.baseZ;
this.vx = 0; this.vy = 0; this.vz = 0;
this.attached = true;
this.size = 2 + Math.random() * 1.5;
this.gripStrength = Math.random() * 2 + 0.5;
}
applyForce(fx, fy, fz, power) {
if (this.attached) {
this.x += fx * 2; this.y += fy * 2; this.z += fz * 2;
this.gripStrength -= power * 0.015;
const disp = Math.sqrt((this.x - this.baseX)**2 + (this.y - (this.baseY - 150))**2 + (this.z - this.baseZ)**2);
if (disp > this.gripStrength * 15 || this.gripStrength <= 0) {
this.attached = false;
this.vx = fx * 15 + (Math.random() - 0.5) * 2;
this.vy = fy * 15 + (Math.random() - 0.5) * 2;
this.vz = fz * 15 + (Math.random() - 0.5) * 2;
}
} else {
this.vx += fx; this.vy += fy; this.vz += fz;
}
}
update() {
if (this.attached) {
this.x += (this.baseX - this.x) * 0.15;
this.y += (this.baseY - 150 - this.y) * 0.15;
this.z += (this.baseZ - this.z) * 0.15;
} else {
this.x += this.vx; this.y += this.vy; this.z += this.vz;
this.vx *= 0.92; this.vy *= 0.92; this.vz *= 0.92;
this.vy += 0.05;
this.vx += (Math.random() - 0.5) * 0.3; this.vz += (Math.random() - 0.5) * 0.3;
}
}
draw() {
const scale = focalLength / (focalLength + this.z);
if (this.z < -focalLength) return;
const screenX = centerX + this.x * scale;
const screenY = centerY + this.y * scale;
const alpha = Math.max(0.1, Math.min(1, scale));
const drawSize = this.size * scale;
ctx.save();
ctx.translate(screenX, screenY);
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.7})`;
ctx.lineWidth = 0.5 * scale;
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (Math.PI * 2 / 5) * i;
ctx.moveTo(0, 0);
ctx.lineTo(Math.cos(angle) * drawSize * 3, Math.sin(angle) * drawSize * 3);
}
ctx.stroke();
ctx.fillStyle = `rgba(200, 180, 150, ${alpha})`;
ctx.beginPath();
ctx.arc(0, drawSize * 3, drawSize * 0.8, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
// ==========================================
// シャボン玉クラス
// ==========================================
class Bubble {
constructor(startX, startY, initVx, initVy) {
this.x = startX + (Math.random() - 0.5) * 10;
this.y = startY + (Math.random() - 0.5) * 10;
// 風の力を初期速度として受け取る
this.vx = initVx * 5 + (Math.random() - 0.5) * 2;
this.vy = initVy * 5 + (Math.random() - 0.5) * 2 - 1;
this.radius = 5 + Math.random() * 25; // 枠から出るので少しサイズにばらつきを
this.life = 1;
this.wobbleOffset = Math.random() * Math.PI * 2;
}
applyForce(fx, fy, power) {
this.vx += fx * 0.5;
this.vy += fy * 0.5;
if (power > 4.5 && Math.random() < 0.05) this.life = 0; // 強すぎる風で割れる
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vx *= 0.95; // 空気抵抗
this.vy *= 0.95;
this.vy -= 0.05; // 浮力
this.x += Math.sin(Date.now() * 0.003 + this.wobbleOffset) * 0.5;
if (this.y < -this.radius * 2 || this.x < -this.radius * 2 || this.x > width + this.radius * 2) {
this.life = 0;
}
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
const grad = ctx.createRadialGradient(-this.radius*0.3, -this.radius*0.3, this.radius*0.1, 0, 0, this.radius);
grad.addColorStop(0, 'rgba(255, 255, 255, 0.8)');
grad.addColorStop(0.2, 'rgba(200, 255, 255, 0.4)');
grad.addColorStop(0.8, 'rgba(255, 200, 255, 0.2)');
grad.addColorStop(1, 'rgba(200, 200, 255, 0.5)');
ctx.fillStyle = grad;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.restore();
}
}
// ==========================================
// 枝火パーティクルクラス
// ==========================================
class Fire {
constructor(isSmoke, startX, startY, baseVx, baseVy, isSpark = false) {
this.isSmoke = isSmoke;
this.isSpark = isSpark;
this.x = startX + (Math.random() - 0.5) * 15;
this.y = startY + (Math.random() - 0.5) * 10;
// 吹き飛ぶ火花か、通常の炎かで初期速度を変える
if (isSpark) {
this.vx = baseVx * 5 + (Math.random() - 0.5) * 4;
this.vy = baseVy * 5 + (Math.random() - 0.5) * 4 - 2;
this.size = 2 + Math.random() * 4;
this.decay = 0.04 + Math.random() * 0.02;
} else {
this.vx = baseVx + (Math.random() - 0.5) * 1;
this.vy = isSmoke ? baseVy - 2 - Math.random() * 2 : baseVy - 3 - Math.random() * 3;
this.size = isSmoke ? 20 + Math.random() * 20 : 10 + Math.random() * 15;
this.decay = isSmoke ? 0.01 + Math.random() * 0.01 : 0.03 + Math.random() * 0.03;
}
this.life = 1.0;
}
applyForce(fx, fy, power) {
if (this.isSpark) return; // 火花は初速だけで飛んでいく
this.vx += fx * (this.isSmoke ? 0.8 : 0.4);
this.vy += fy * (this.isSmoke ? 0.8 : 0.4);
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life -= this.decay;
if (!this.isSmoke && !this.isSpark) this.size *= 0.92;
else if (this.isSmoke) this.size += 0.3;
}
draw() {
ctx.save();
ctx.globalCompositeOperation = this.isSmoke ? 'source-over' : 'lighter';
let r, g, b, a;
if (this.isSmoke) {
r = 150; g = 150; b = 150; a = this.life * 0.3;
} else {
if (this.life > 0.7) { r = 255; g = 255; b = 200; }
else if (this.life > 0.4) { r = 255; g = 150; b = 0; }
else { r = 255; g = 50; b = 0; }
a = this.life;
}
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`;
ctx.beginPath();
ctx.arc(this.x, this.y, Math.max(0.1, this.size), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
// ==========================================
// 風エフェクトクラス
// ==========================================
class WindParticle {
constructor(startX, startY, angle, power) {
this.x = startX + (Math.random() - 0.5) * 60;
this.y = startY + (Math.random() - 0.5) * 60;
this.vx = Math.cos(angle) * power * 3;
this.vy = Math.sin(angle) * power * 3;
this.life = 1.0;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life -= 0.03;
}
draw() {
if (this.life <= 0) return;
ctx.strokeStyle = `rgba(200, 230, 255, ${this.life * 0.5})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x - this.vx, this.y - this.vy);
ctx.stroke();
}
}
function initMode(mode) {
currentMode = mode;
fluffs = []; bubbles = []; fireParticles = []; smokeParticles = []; windParticles = [];
isBlowing = false;
flameHealth = 1.0;
document.querySelectorAll('.btn-group button').forEach(btn => btn.classList.remove('active'));
if(mode === 'dandelion') document.getElementById('btn-dandelion').classList.add('active');
if(mode === 'bubble') document.getElementById('btn-bubble').classList.add('active');
if(mode === 'campfire') document.getElementById('btn-campfire').classList.add('active');
if (mode === 'dandelion') {
for (let i = 0; i < 400; i++) fluffs.push(new Fluff());
document.body.style.backgroundColor = '#1a1a2e';
} else if (mode === 'bubble') {
document.body.style.backgroundColor = '#87CEEB';
} else if (mode === 'campfire') {
document.body.style.backgroundColor = '#0a0a0a';
}
}
document.getElementById('btn-dandelion').addEventListener('click', () => initMode('dandelion'));
document.getElementById('btn-bubble').addEventListener('click', () => initMode('bubble'));
document.getElementById('btn-campfire').addEventListener('click', () => initMode('campfire'));
document.getElementById('retry-btn').addEventListener('click', () => initMode(currentMode));
canvas.addEventListener('mousedown', (e) => { isBlowing = true; startMouse = { x: e.clientX, y: e.clientY }; currentMouse = { x: e.clientX, y: e.clientY }; });
canvas.addEventListener('mousemove', (e) => { if (isBlowing) currentMouse = { x: e.clientX, y: e.clientY }; });
window.addEventListener('mouseup', () => isBlowing = false);
canvas.addEventListener('touchstart', (e) => { isBlowing = true; startMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY }; currentMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY }; }, {passive: false});
canvas.addEventListener('touchmove', (e) => { if (isBlowing) currentMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY }; e.preventDefault(); }, {passive: false});
window.addEventListener('touchend', () => isBlowing = false);
// --- 描画補助 ---
function drawDandelionStem() {
ctx.strokeStyle = '#4a7c59'; ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(centerX, centerY - 150);
ctx.quadraticCurveTo(centerX + 20, centerY + 100, centerX, height); ctx.stroke();
ctx.fillStyle = '#4a7c59'; ctx.beginPath();
ctx.arc(centerX, centerY - 145, 15, 0, Math.PI, false); ctx.fill();
}
function drawBubbleWand() {
// 持ち手
ctx.strokeStyle = '#2c8a4c'; ctx.lineWidth = 6;
ctx.beginPath(); ctx.moveTo(centerX, height); ctx.lineTo(centerX, centerY - 50); ctx.stroke();
// わっか
ctx.strokeStyle = '#4ade80'; ctx.lineWidth = 4;
ctx.beginPath(); ctx.arc(centerX, centerY - 80, 30, 0, Math.PI * 2); ctx.stroke();
// わっかの中のシャボン液(膜)
ctx.fillStyle = 'rgba(200, 255, 255, 0.2)';
ctx.beginPath(); ctx.arc(centerX, centerY - 80, 28, 0, Math.PI * 2); ctx.fill();
}
function drawBranch() {
// 斜めに刺さった枝
ctx.strokeStyle = '#3d2314'; ctx.lineWidth = 14;
ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(centerX - 80, height); ctx.lineTo(centerX, centerY); ctx.stroke();
// 枝の先端の残り火(flameHealthが低くても光る)
const emberGlow = Math.max(0.2, flameHealth);
ctx.fillStyle = `rgba(255, 60, 0, ${emberGlow})`;
ctx.beginPath(); ctx.arc(centerX, centerY, 8, 0, Math.PI * 2); ctx.fill();
}
// メインループ
function animate() {
ctx.clearRect(0, 0, width, height);
let fx = 0, fy = 0, fz = 0, power = 0;
if (isBlowing) {
const dx = currentMouse.x - startMouse.x;
const dy = currentMouse.y - startMouse.y;
const distance = Math.sqrt(dx*dx + dy*dy);
if (distance > 10) {
power = Math.min(distance / 50, 5);
const angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * power * 0.2;
fy = Math.sin(angle) * power * 0.2;
fz = (Math.random() - 0.5) * power * 0.1;
if (Math.random() > 0.3) windParticles.push(new WindParticle(startMouse.x, startMouse.y, angle, power));
}
}
if (currentMode === 'dandelion') {
drawDandelionStem();
let attachedCount = 0;
if (isBlowing) fluffs.forEach(f => f.applyForce(fx, fy, fz, power));
fluffs.sort((a, b) => b.z - a.z);
fluffs.forEach(f => {
f.update(); f.draw();
if (f.attached) attachedCount++;
});
statusText.innerText = `残りの綿毛: ${attachedCount}`;
}
else if (currentMode === 'bubble') {
drawBubbleWand();
// 風を吹いた時だけ、わっかからシャボン玉が生まれる
if (isBlowing && power > 1.5) {
// 風の強さに応じて生成確率が上がる
if (Math.random() < power * 0.08) {
bubbles.push(new Bubble(centerX, centerY - 80, fx, fy));
}
}
if (isBlowing) bubbles.forEach(b => b.applyForce(fx, fy, power));
for (let i = bubbles.length - 1; i >= 0; i--) {
bubbles[i].update(); bubbles[i].draw();
if (bubbles[i].life <= 0) bubbles.splice(i, 1);
}
statusText.innerText = `漂うシャボン玉: ${bubbles.length}`;
}
else if (currentMode === 'campfire') {
drawBranch();
// 炎の健康度(flameHealth)の計算
if (isBlowing) {
if (power > 2.5) {
flameHealth -= 0.05; // 強い風で鎮火していく
} else if (power > 0) {
flameHealth += 0.005; // 弱い風は酸素を供給して少し復活を助ける
}
} else {
flameHealth += 0.015; // 風が止むと自然に復活する
}
flameHealth = Math.max(0, Math.min(1, flameHealth)); // 0〜1の間に収める
// 炎の生成(健康度が高いほどたくさん出る)
let spawnCount = Math.floor(flameHealth * 4);
if (flameHealth > 0.1 && Math.random() < flameHealth) spawnCount++;
for(let i=0; i<spawnCount; i++) {
fireParticles.push(new Fire(false, centerX, centerY, fx, fy));
}
// 強い風の時、火の粉が吹き飛ぶ演出
if (isBlowing && power > 2.5 && flameHealth > 0) {
if (Math.random() < 0.4) fireParticles.push(new Fire(false, centerX, centerY, fx, fy, true));
}
// 煙の生成(鎮火しかけている時ほど煙が出やすい)
if (Math.random() < (1.0 - flameHealth) * 0.5 + 0.1) {
smokeParticles.push(new Fire(true, centerX, centerY, fx, fy));
}
if (isBlowing) {
fireParticles.forEach(p => p.applyForce(fx, fy, power));
smokeParticles.forEach(p => p.applyForce(fx, fy, power));
}
for (let i = smokeParticles.length - 1; i >= 0; i--) {
smokeParticles[i].update(); smokeParticles[i].draw();
if (smokeParticles[i].life <= 0) smokeParticles.splice(i, 1);
}
for (let i = fireParticles.length - 1; i >= 0; i--) {
fireParticles[i].update(); fireParticles[i].draw();
if (fireParticles[i].life <= 0) fireParticles.splice(i, 1);
}
let stateMsg = flameHealth > 0.8 ? "勢いよく燃焼中" : (flameHealth > 0.3 ? "風で揺らいでいる" : "鎮火寸前…!");
statusText.innerText = `炎の状態: ${stateMsg}`;
}
for (let i = windParticles.length - 1; i >= 0; i--) {
windParticles[i].update(); windParticles[i].draw();
if (windParticles[i].life <= 0) windParticles.splice(i, 1);
}
requestAnimationFrame(animate);
}
initMode('dandelion');
animate();
</script>
</body>
</html>


■ 今日の学び

今回は、見えない「風」を画面の中で表現する実験でした。

「ふーっと吹きかける風を作りたい!」と思っても、
難しい物理の計算やコードは、まだまだ分からないことだらけです……。

これまでにいくつもゲームを作ってきて、
ほんの少しずつ仕組みが分かるようになってきましたが、
まだ初心者レベルを抜け出せていないなぁ、と感じます。(つv<;)

なので今回も、頼れる AI にしっかり助けてもらいました!( ̄v ̄*)

「たんぽぽはしぶとく耐えてからポロリと剥がれてほしい」
「シャボン玉は強風で割れてほしい」 そんなイメージを伝えると、
AI がそれを見事に計算式(物理演算)にしてくれました。

AI が作ってくれた土台の上で、
「もうちょっと風を弱くしよう」と数字を少し調整しただけなのに、
綿毛や炎の動きが本当に“生きているみたい”に変わっていくのが
感動的でした。(=v=*)

みなさんも、ぜひいろんな強さの風を当てて、 動きの違いを観察してみてください。 きっと、自分だけの「風の実験」が見つかると思います!

■ 次回は、実験器具シミュレーターゲームです。

色ごとに“比重”を持たせて混ぜ合わせた液体を、
遠心分離器にかけるとどうなるのか…

そんな実験をゲームにしました。

回転の強さで色が分かれたり、思わぬ模様が生まれたり。
ちょっとした“予測ゲーム”の要素も入れて仕上げています。

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

▶ 次の実験はこちら(混ざる色と予測値)
  「遠心分離シミュレーター」



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

~このブログについて~

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

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


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

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

続きを読む