実験#85:ツバメたちの訓練遊び(朝空のフェザー・ドリフト)

■85個目 「朝空のフェザー・ドリフト」

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

今回は、さわやかな朝の空を舞台にしたアクションゲームを作ってみました。

主役は……
気持ちよさそうに大空を飛ぶ “ツバメ” たちです!

昨日、ふわふわと舞う羽毛を、
ツバメたちが飛びながらくわえては離し、

まるでパス回しをして遊んでいるような光景を見かけました。

その楽しそうな姿が忘れられなくて、
「このままゲームにしたい!」と思ったのが今回のきっかけです。(=v=*)

今回こだわったのは、仲間ツバメたちとの息の合った連携プレイ。
画面には自分と、2羽の仲間ツバメが飛んでいます。

仲間たちは、下に落ちてきた羽毛をサッと拾い上げると、
「ほらっ、次よろしく!」と言わんばかりに、
ふんわり優しくパスを出してくれます。(´▽`)

そして、自分が羽毛を受け取ったときだけ、
一番高い空までグーンと運んでいけるんです。

ツバメ特有の「ビューン」と風に乗る滑空の気持ちよさと、
空中に舞う5つの羽毛を地面に落とさないように繋ぐハラハラ感が合わさって、
ちょっと忙しいけれど癒やされるゲームになりました。

小鳥のさえずりのようなBGMも流れるので、
ぜひ音を出して遊んでみてくださいね。

ツバメたちと一緒に、さわやかな朝の空の散歩をお楽しみください!

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

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

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

昨日、ツバメたちがふわふわ舞う羽毛をくわえては離し、
まるでパス遊びをしているような姿を見ました。

とても楽しそうだったので、これをゲームにしてみたいです。

AI: ツバメたちの羽毛パス遊び……
想像するだけで微笑ましいですね!

あの「自然の中の無邪気な一瞬」をゲームに落とし込むのは、
とても面白いテーマです。

みー:(=v=;)
仲間のツバメたちが上空でパスし続けてしまって、
羽毛が落ちてこないです。

AI: ああ、なるほど! 仲間が優秀すぎて、
ずっと上空で羽毛を回し続けてしまうんですね。

これだとプレイヤー(緑のツバメ)が下で待つだけになってしまいます。

原因は、羽毛を離した直後に
全員がまた全力で追いかけてしまう ことです。

そこで、

  • 羽毛をトスしたツバメはしばらく「見守りモード」で休憩
  • トスはもっと高く・横へ大きく という調整を入れてみましょう。

    これで羽毛が画面全体をふわふわ落ちてくるようになります!

みー:(* ̄v ̄)
羽毛をくわえた後、上に投げてる感じになってるけど、
「くわえて上昇する」感じがいいな。

AI: なるほど!
「羽毛をくわえたツバメ自身が羽ばたいて上昇する」動きですね。
前回は“物理的に弾く”動きだったので、
今回はツバメが自分でぐんぐん高度を上げるように修正します。

そして上空で「ここまで来れば十分」と羽毛を離すと、
またふわふわ落ちてくる……

そんな自然なサイクルにします。

みー:(^v^) いいね。場面は早朝の晴れたさわやかな朝だよ。

ツバメの見た目や背景ももっとリアルにしたい。

AI: 「早朝の晴れたさわやかな朝」
……最高のロケーションです!

透明感のある空気、朝露の光、ツバメの滑らかな飛行。

一気に世界観が深まりました。

みー:(^v^) 自分のツバメももっと自由に飛び回りたい。

AI: 確かに!
せっかくの朝の空を、プレイヤー自身も自由に楽しめる方が絶対に良いです。
そこで、
マウスの動きに合わせて画面のどこへでも飛べるフリーフライトにしました。

✨ 今回のバージョンアップのポイント
  1. 完全フリーフライト  
    マウスカーソルの位置へ自由に飛べます。  
    仲間と一緒に上空で遊んだり、急降下したり、空を満喫できます。
  2. 自分で上空へ運ぶ楽しさ  
    羽毛をキャッチしたら、自動ではなく「自分の操作」で上空へ運べます。  
    上まで行くと、気持ちよくパッと離してくれます。
  3. 運べる時間は3秒に延長  
    羽毛をくわえていられる時間を少し長めにして、  
    ゆっくり上空へ運べるようにしました。

みーさん自身も、仲間ツバメたちと一緒に
朝の空を自由に飛び回れる最新版になりました。


■ 実際の動作

まずは、AIが最初に出してくれた画像です。

朝というより夕空のようで、
どこかお魚がエサを取り合っているような雰囲気でした。

下の方で待機している緑のツバメがプレイヤーで、
落ちてきた羽毛を上へ運んでいきます。

(画像:AIが最初に生成したツバメ案)

こちらが、仕上がりのゲーム画面です。
プレイヤーのツバメは、マウスの動きに合わせて
朝の空を自由に飛び回れるようになりました。

(画像:仕上がりのゲーム画面)


羽毛をくわえて、左クリックで「トス!」できます。
仲間ツバメたちも、それぞれのタイミングでふんわりトスしてくれます。

(画像:トス!の様子)



羽毛が画面下に ひとつでも落ちたらゲーム終了 です。
朝の空気の中で、どれだけ長く羽毛をつなげるか挑戦してみてください。

(画像:ゲームオーバー画面)


■ コードのポイント

今回の記事では、
AI と相談しながら作った 「朝空のフェザー・ドリフト」 の HTML コードを公開します。

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

ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。

▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く

Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)

② 下のコードをコピーして貼り付けます。
③ 「tubame.html」など、好きな名前で保存します。

(※拡張子は .html にしてください)

④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。

■ 操作方法 

ここからは、遊び方と操作方法のご紹介です!(^_^)

● 基本操作

■ 移動(マウス)

マウスを動かすと、 自分のツバメ(「▼ じぶん」のマーク付き)がスーッとついてきます。 画面のどこへでも自由に飛び回れますよ~!

■ キャッチ(羽毛をくわえる)

落ちてくる羽毛に触れると、 ツバメがパッとくわえてくれます。

■ トス(クリック)

羽毛をくわえた状態で クリック すると、 上に向かって ポワン! とパスを出します。(=v=*)

飛びながらクリックすると、 その勢いで遠くまで投げられますよ!

● ルール

羽毛は 全部で5つ。 仲間ツバメたちと協力して、 1つでも地面に落としたらゲームオーバー です。

仲間たちはとっても優秀で、 一番下まで落ちてきてピンチな羽毛を優先して拾い、 「みーさん、お願い!」と言わんばかりに こちらへふんわりパスしてくれます。(^^*)

パスを受け取ったら、 好きなタイミングでクリックして、また空へ返してあげましょう。

● 空中ジャグリングの楽しさ

5つの羽毛が飛び交う、 ちょっと忙しいけれど楽しい 空中ジャグリング

果たして、何回パスを繋げるか……!? ぜひツバメになりきって、大空を自由に飛び回ってみてくださいね♪(^_^)


“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>朝空のフェザー・ドリフト - Swallow Morning Drift</title>
<style>
body {
background-color: #e0f2f1;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: 'Helvetica Neue', Arial, sans-serif;
overflow: hidden;
user-select: none;
}
canvas {
background: linear-gradient(to top, #fff3e0, #e0f7fa, #80deea);
box-shadow: 0 15px 35px rgba(0,0,0,0.15);
border-radius: 12px;
cursor: none;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="600" height="500"></canvas>
<script>
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
// --- ゲーム状態 ---
let gameState = "start"; // start, playing, gameover
let rallyCount = 0;
let highScore = 0;
let mouseX = canvas.width / 2;
let mouseY = canvas.height - 100;
let particles = [];
let clouds = [
{x: 0, y: 350, speed: 0.1, size: 150},
{x: 300, y: 380, speed: 0.08, size: 200}
];
// --- サウンドマネージャー ---
const Sound = {
ctx: null,
bgmTimer: null,
init() {
if (this.ctx) return;
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.ctx = new AudioContext();
this.startBGM();
},
playCatch() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(880, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1760, this.ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.05, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.1);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.1);
},
playToss() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(1200, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(600, this.ctx.currentTime + 0.15);
gain.gain.setValueAtTime(0.05, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.15);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.15);
},
playDrop() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(50, this.ctx.currentTime + 0.5);
gain.gain.setValueAtTime(0.08, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.5);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.5);
},
startBGM() {
if (this.bgmTimer) clearInterval(this.bgmTimer);
this.bgmTimer = setInterval(() => {
if (!this.ctx || gameState !== "playing") return;
if (Math.random() > 0.4) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
const baseFreq = 2000 + Math.random() * 1500;
osc.frequency.setValueAtTime(baseFreq, this.ctx.currentTime);
osc.frequency.linearRampToValueAtTime(baseFreq + 600, this.ctx.currentTime + 0.05);
osc.frequency.linearRampToValueAtTime(baseFreq - 200, this.ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.015, this.ctx.currentTime + 0.02);
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.1);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 0.1);
}, 1200);
}
};
class Feather {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 2.5;
this.vy = 0.5;
this.angle = 0;
this.wobbleSpeed = 0.03 + Math.random() * 0.02;
this.wobbleCount = Math.random() * 10;
this.holder = null;
this.holdTimer = 0;
}
}
let feathers = [];
class Swallow {
constructor(x, y, color, isPlayer, name) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 4;
this.vy = (Math.random() - 0.5) * 2;
this.color = color;
this.isPlayer = isPlayer;
this.name = name;
this.wingAngle = Math.random() * 10;
this.angle = 0;
this.aiTargetX = x;
this.aiTargetY = y;
this.aiTimer = 0;
this.satietyTimer = 0;
this.isGliding = false;
}
update() {
if (this.satietyTimer > 0) this.satietyTimer--;
let myFeather = feathers.find(f => f.holder === this);
if (this.isPlayer) {
let targetX = mouseX;
let targetY = mouseY;
if (myFeather) {
this.wingAngle += 0.15;
if (Math.random() < 0.3) {
particles.push(new Particle(this.x, this.y + 10, '#fff', 2.5));
}
}
this.vx = (targetX - this.x) * 0.15;
this.vy = (targetY - this.y) * 0.15;
this.x += this.vx;
this.y += this.vy;
this.angle = this.vx * 0.04;
if (Math.abs(this.vx) > 0.5 || Math.abs(this.vy) > 0.5 || myFeather) {
this.wingAngle += 0.25;
this.isGliding = false;
} else {
this.isGliding = true;
}
} else {
this.aiTimer--;
if (myFeather) {
this.aiTargetX = this.x + this.vx;
this.aiTargetY = this.y - 50;
this.isGliding = false;
this.wingAngle += 0.15;
} else {
let availableFeathers = feathers.filter(f => f.holder === null && f.y > 100);
if (availableFeathers.length > 0 && this.satietyTimer <= 0) {
let targetFeather = availableFeathers.sort((a, b) => b.y - a.y)[0];
this.aiTargetX = targetFeather.x + targetFeather.vx * 4;
this.aiTargetY = targetFeather.y + 5;
this.isGliding = false;
} else {
let distToTarget = Math.sqrt((this.aiTargetX - this.x)**2 + (this.aiTargetY - this.y)**2);
if (this.aiTimer <= 0 || distToTarget < 50) {
this.aiTargetX = Math.random() * (canvas.width - 120) + 60;
this.aiTargetY = 100 + Math.random() * 200;
this.aiTimer = 100 + Math.random() * 100;
this.isGliding = Math.random() < 0.5;
}
}
}
let dx = this.aiTargetX - this.x;
let dy = this.aiTargetY - this.y;
let dist = Math.sqrt(dx*dx + dy*dy);
let speedLimit = myFeather ? 5.0 : (this.isGliding ? 3.0 : 5.5);
let accel = myFeather ? 0.2 : (this.isGliding ? 0.05 : 0.2);
if (dist > 10) {
this.vx += (dx / dist) * accel;
this.vy += (dy / dist) * accel;
}
let currentSpeed = Math.sqrt(this.vx*this.vx + this.vy*this.vy);
if (currentSpeed > speedLimit) {
this.vx = (this.vx / currentSpeed) * speedLimit;
this.vy = (this.vy / currentSpeed) * speedLimit;
} else if (currentSpeed < 1.5) {
this.vx = (this.vx / (currentSpeed || 1)) * 1.5;
this.vy = (this.vy / (currentSpeed || 1)) * 1.5;
}
this.x += this.vx;
this.y += this.vy;
this.angle = this.vx * 0.04;
if (this.isGliding) {
this.wingAngle = 0;
} else {
this.wingAngle += 0.22;
}
}
let margin = 30;
if (this.x < margin) { this.x = margin; this.vx *= -1; this.aiTimer = 0; }
if (this.x > canvas.width - margin) { this.x = canvas.width - margin; this.vx *= -1; this.aiTimer = 0; }
if (this.y < 40) { this.y = 40; this.vy = Math.abs(this.vy); this.aiTimer = 0; }
if (this.y > canvas.height - 80) { this.y = canvas.height - 80; this.vy = -Math.abs(this.vy); this.aiTimer = 0; }
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.moveTo(0, -10);
ctx.quadraticCurveTo(7, -3, 3, 12);
ctx.lineTo(8, 32);
ctx.lineTo(2, 22);
ctx.lineTo(0, 26);
ctx.lineTo(-2, 22);
ctx.lineTo(-8, 32);
ctx.lineTo(-3, 12);
ctx.quadraticCurveTo(-7, -3, 0, -10);
ctx.closePath();
ctx.fill();
let wSin = Math.sin(this.wingAngle);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(24, -6 + wSin * 14, 38, -12 + wSin * 24);
ctx.quadraticCurveTo(15, 6 + wSin * 8, 0, 5);
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(-24, -6 + wSin * 14, -38, -12 + wSin * 24);
ctx.quadraticCurveTo(-15, 6 + wSin * 8, 0, 5);
ctx.fill();
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
ctx.beginPath();
ctx.ellipse(0, 3, 3.5, 9, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
class Particle {
constructor(x, y, color, size=1.8) {
this.x = x + (Math.random() - 0.5) * 8;
this.y = y;
this.vx = (Math.random() - 0.5) * 0.6;
this.vy = 0.2 + Math.random() * 0.4;
this.alpha = 1;
this.color = color;
this.size = size;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.alpha -= 0.02;
}
draw() {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
const flock = [
new Swallow(canvas.width / 2, canvas.height - 100, "#0d1b2a", true, "You"),
new Swallow(120, 250, "#1b263b", false, "Swallow A"),
new Swallow(480, 280, "#415a77", false, "Swallow B")
];
window.addEventListener("mousemove", (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
});
window.addEventListener("mousedown", (e) => {
if (gameState === "start") {
Sound.init();
resetGame();
gameState = "playing";
return;
}
if (gameState === "gameover") {
resetGame();
gameState = "playing";
return;
}
if (gameState === "playing") {
let pSwallow = flock.find(s => s.isPlayer);
let heldFeather = feathers.find(f => f.holder === pSwallow);
if (heldFeather) {
heldFeather.holder = null;
Sound.playToss();
heldFeather.vx = pSwallow.vx * 1.0 + (Math.random() - 0.5) * 1.5;
heldFeather.vy = pSwallow.vy - 3.0;
pSwallow.satietyTimer = 10;
for(let i=0; i<6; i++) {
particles.push(new Particle(heldFeather.x, heldFeather.y, '#fff', 2.0));
}
}
}
});
function resetGame() {
rallyCount = 0;
feathers = [
new Feather(canvas.width * 0.15, 60),
new Feather(canvas.width * 0.35, 120),
new Feather(canvas.width * 0.50, 40),
new Feather(canvas.width * 0.65, 100),
new Feather(canvas.width * 0.85, 80)
];
flock.forEach(s => {
s.satietyTimer = 0;
s.x = Math.random() * canvas.width;
s.y = 200 + Math.random() * 150;
s.vx = (Math.random() - 0.5) * 3;
s.vy = (Math.random() - 0.5) * 2;
});
particles = [];
}
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
function update() {
if (gameState !== "playing") return;
clouds.forEach(c => {
c.x += c.speed;
if (c.x > canvas.width) c.x = -c.size;
});
flock.forEach(swallow => swallow.update());
particles.forEach((p, idx) => {
p.update();
if (p.alpha <= 0) particles.splice(idx, 1);
});
feathers.forEach(f => {
if (f.holder !== null) {
let holder = f.holder;
let rad = holder.angle + Math.PI / 2;
f.x = holder.x + Math.cos(rad) * 4;
f.y = holder.y + Math.sin(rad) * 4;
f.vx = holder.vx;
f.vy = holder.vy;
f.angle = holder.angle;
f.holdTimer--;
if (!holder.isPlayer && f.holdTimer <= 0) {
let pSwallow = flock.find(s => s.isPlayer);
f.holder = null;
Sound.playToss();
let dxToPlayer = pSwallow.x - f.x;
f.vx = holder.vx * 0.5 + (dxToPlayer * 0.012);
f.vy = -2.5;
// ★仲間はパスを出したあと、たっぷり3秒間(180フレーム)見守ります
holder.satietyTimer = 180;
holder.isGliding = true;
for(let i=0; i<4; i++) {
particles.push(new Particle(f.x, f.y, '#fff', 2.0));
}
}
} else {
f.wobbleCount += f.wobbleSpeed;
f.vx += Math.sin(f.wobbleCount) * 0.035;
f.vy += 0.012;
if (f.vy > 0.85) f.vy = 0.85;
f.vx *= 0.96;
f.x += f.vx;
f.y += f.vy;
f.angle = Math.sin(f.wobbleCount * 0.6) * 0.4;
let fMargin = 12;
if (f.x < fMargin) { f.x = fMargin; f.vx = Math.abs(f.vx) * 0.6; }
if (f.x > canvas.width - fMargin) { f.x = canvas.width - fMargin; f.vx = -Math.abs(f.vx) * 0.6; }
if (f.y < fMargin) { f.y = fMargin; f.vy = Math.abs(f.vy) * 0.6; }
for (let swallow of flock) {
if (swallow.satietyTimer > 0) continue;
if (feathers.some(other => other.holder === swallow)) continue;
let dx = f.x - swallow.x;
let dy = f.y - swallow.y;
let dist = Math.sqrt(dx*dx + dy*dy);
if (!swallow.isPlayer && f.y < 120) continue;
if (dist < 24) {
f.holder = swallow;
Sound.playCatch();
if (!swallow.isPlayer) {
f.holdTimer = 10;
}
rallyCount++;
if (rallyCount > highScore) highScore = rallyCount;
break;
}
}
if (f.y > canvas.height) {
if (gameState === "playing") {
Sound.playDrop();
}
gameState = "gameover";
}
}
});
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "rgba(255, 255, 255, 0.22)";
clouds.forEach(c => {
ctx.beginPath();
ctx.arc(c.x, c.y, c.size * 0.6, 0, Math.PI * 2);
ctx.arc(c.x + c.size * 0.4, c.y - 20, c.size * 0.5, 0, Math.PI * 2);
ctx.fill();
});
if (gameState === "playing" || gameState === "gameover") {
particles.forEach(p => p.draw());
flock.forEach(swallow => swallow.draw());
feathers.forEach(f => {
ctx.save();
ctx.translate(f.x, f.y);
ctx.rotate(f.angle);
ctx.strokeStyle = "rgba(255, 255, 255, 0.9)";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(0, -7);
ctx.quadraticCurveTo(1.5, 0, 0, 9);
ctx.stroke();
ctx.fillStyle = "rgba(255, 255, 255, 0.65)";
ctx.beginPath();
ctx.moveTo(0, -7);
ctx.quadraticCurveTo(4.5, -1.5, 2, 3);
ctx.quadraticCurveTo(3.5, 7, 0, 9);
ctx.quadraticCurveTo(-3.5, 7, -2, 3);
ctx.quadraticCurveTo(-4.5, -1.5, 0, -7);
ctx.fill();
ctx.restore();
});
ctx.fillStyle = "#37474f";
ctx.font = "bold 14px 'Helvetica Neue', Arial, sans-serif";
ctx.textAlign = "left";
ctx.fillText(`✨ SCORE: ${rallyCount}`, 20, 35);
ctx.fillStyle = "#78909c";
ctx.fillText(`🏆 BEST: ${highScore}`, 20, 55);
flock.forEach(s => {
if (s.isPlayer) {
ctx.textAlign = "center";
ctx.font = "bold 11px sans-serif";
if (feathers.some(f => f.holder === s)) {
ctx.fillStyle = "rgba(255, 87, 34, 0.9)";
ctx.fillText("▼ クリックでトス!", s.x, s.y - 25);
} else {
ctx.fillStyle = "rgba(13, 27, 42, 0.4)";
ctx.fillText("▼ じぶん", s.x, s.y - 25);
}
}
});
}
if (gameState === "start") {
ctx.fillStyle = "rgba(255, 255, 255, 0.4)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#263238";
ctx.font = "bold 32px sans-serif";
ctx.textAlign = "center";
ctx.fillText("朝空のフェザー・ドリフト", canvas.width / 2, canvas.height / 2 - 20);
ctx.fillStyle = "#00acc1";
ctx.font = "bold 16px sans-serif";
ctx.fillText("画面をクリックしてスタート", canvas.width / 2, canvas.height / 2 + 30);
}
if (gameState === "gameover") {
ctx.fillStyle = "rgba(255, 255, 255, 0.75)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#263238";
ctx.font = "bold 26px sans-serif";
ctx.textAlign = "center";
ctx.fillText("Drop...", canvas.width / 2, canvas.height / 2 - 20);
ctx.font = "16px sans-serif";
ctx.fillStyle = "#37474f";
ctx.fillText(`繋いだパス回数: ${rallyCount} 回`, canvas.width / 2, canvas.height / 2 + 15);
ctx.font = "bold 14px sans-serif";
ctx.fillStyle = "#00acc1";
ctx.fillText("画面をクリックして、もう一度空へ", canvas.width / 2, canvas.height / 2 + 55);
}
}
gameLoop();
</script>
</body>
</html>


■ 今日の学び

今回の「朝空のフェザー・ドリフト」は、
私の日常のほんの一コマから生まれました。

今年、初めて我が家にツバメが巣を作ってくれたんです。
家の周りを元気に飛び交うツバメたちを眺めるのが、
毎朝のささやかな楽しみになっていました。

でも、自然は時に厳しくて……。
ある風の強い日、せっせと作っていた途中の巣が落ちて壊れてしまったんです。

「ああっ……」と胸がぎゅっとなったのですが、
ツバメたちは決してめげませんでした。

すぐにまた土を運び、
前よりもっとしっかりとした立派な巣を作り上げたんです。

その小さな体のどこにそんなパワーがあるんだろうって、
本当に驚かされました。ε(๑•ө•๑)з ブワッ!!

今では毎朝、元気な鳴き声が家に響き渡っています。
ツバメが家に巣を作ると縁起が良いと言われますが、

晴れた朝の光の中で楽しそうに遊ぶ姿を見ていると、
まさに「幸せを運んできてくれる存在」そのものだと感じます。

ゲームの中で、絶対に立ち止まらずに
大空をビューンと駆け抜けるツバメたち。

それは、何度巣が壊れても前を向く、
あの力強い生命力そのものなのかもしれません。

忙しい日々の中でも、 このゲームのツバメたちと一緒に空を飛んで、
ほんの少しでも「朝のあたたかな幸せ」を感じていただけたら嬉しいです。

今年初めて作られたツバメの巣

(画像:ツバメの巣)

■ 次回は、虹リボンの鮮やかなゲームです

都会の暗闇にふわりと浮かび上がる、一本の虹のリボン。

思い通りに描けるパーティクルの軌跡が、
夜の空気の中で光をまとって広がっていきます。

“ひかりを追いかける”ような、
ちょっとアートで、ちょっと不思議な感覚のゲームです。

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

次の実験はこちら  
  (虹とパーティクル)「虹リボン」



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

~このブログについて~

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

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


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

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

続きを読む