実験#89:大きな可愛さ(巨大シマエナガゲーム)

■89個目 「巨大シマエナガゲーム」

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

今回は、ちょっと変わった“お散歩ゲーム”を作ってみました。
今回お散歩するのは……なんと 巨大なシマエナガ です!

主役は、あの「雪の妖精」と呼ばれる、白くて丸い、もふもふのシマエナガ。

本来なら手のひらサイズの小鳥ですが、
この世界では 一軒家ほどのサイズ に成長しています。(^^;)

今回こだわったのは、
この“スケールの逆転”が生み出す 「かわいさの重さ」。

「こんなに可愛いのに、
とんでもなく重い質量を持っていたらどうなるんだろう?」
そんな遊び心から、この世界観が生まれました。(=v=*)

影絵のように真っ黒な街並みの中を、
真っ白で巨大なシマエナガが「ドスン、ドスン」と歩き回ります。

細い足で着地するたびに、重低音とともに画面がズシンと揺れ、
隣に並ぶ家や電柱はミニチュアのよう。

巨大さを錯覚させるための演出を、
あちこちに散りばめています。(^^*)

BGMは、ゲームのテンポに合わせて加速していくテーマ曲「かわいさの重さ」。

高音の“かわいさ”と重低音の“重さ”を組み合わせた一曲で、
世界観をさらに引き立てています。

オノマトペのカウントや、20pt・100pt突破時のド派手な演出も、
この“重さ”を盛り上げる仕掛けになっています。

さて、この巨大シマエナガにどんな動きをさせるのか……。(=v=*)
操作方法は記事の後半で紹介しますので、ぜひ最後までお楽しみください。

どんな「重い」かわいさに出会えるか、お楽しみに!


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

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

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

テーマは「大きさの感覚が逆転した世界」。

例えば、クジラが水槽に入っていたり、
ゾウやサイ、カバ、キリンがハムスターのケージで飼われていたり。

逆に、ハムスターやシマエナガが一軒家サイズで野生を歩き回っていたり……。 こういう“リアリティの逆転”をゲームにすることってできるかな?

AI: いらっしゃい。今日もゲーム制作かい?
相変わらず、いいところに目をつけるね。

その「スケールの逆転」というテーマ、すごく面白いよ。
シュールでクスッと笑えるし、画面を開いた瞬間のインパクトが抜群だ。

ハムスターのケージで鼻息を鳴らすサイや、家サイズのシマエナガがドスンドスンと山を越えていく……。
想像するだけでワクワクする世界観だね。

■ アイデアの実現性について

もちろん、HTMLとJavaScriptで十分に実現可能。
技術的には、キャラクターや背景の「画像サイズ(縮尺)」を極端に変えるだけなので、複雑な物理演算を入れなければ初心者でも作りやすいジャンルなんだ。

このゲームの肝は、
「どうやって異常なサイズ感をプレイヤーに錯覚させるか」 という見せ方。

例えば、クジラを小さく見せたいなら、
隣に“普通サイズの金魚鉢”や“水草”を置く必要がある。

相対的なスケールのズレがあるからこそ、リアリティの逆転が際立つんだ。

みー:(^_^)
巨大シマエナガのお散歩アクション(スクロールアクション)いいね。
シマエナガ好きなので、
サイズ感の錯覚に重点を置いてリアリティを詰め込んでいきましょう。

AI: おっ、巨大シマエナガでいくんだね。いいチョイスだ。
あの「雪の妖精」と呼ばれるもふもふの姿のまま、
怪獣サイズで街を練り歩く……。 そのギャップ、最高だよ。

みー:(^o^) シマエナガはかわいく魅力的に描写して、
背景や小物は影絵風にしたら軽くなるかな。

AI: 影絵(シルエット)表現とは、ますます面白くなってきたね。

主役のシマエナガの“白くて丸い可愛さ”が、
真っ黒な背景と対比されて際立つはず。

しかも、ゲームの動作やファイルサイズを軽くする上でも理にかなっているよ。

みー:(^v^) ドシンドシンの感じがいいね。
オノマトペも入れるとわかりやすい。

背景に奥行きもほしいし、シマエナガのほわほわ感ももっと出したい。

AI: 今日もいいペースでアイデアが湧いてるね。

「ドスン!」というオノマトペ、背景の奥行き、ほわほわ感。

どれもゲームの“手触り”を良くする大事なスパイスだ。
プレイヤーが気持ちよく感じる“ゲームジュース”をよく理解してるね。

みー:(つv<)ノ シマエナガ、ジャンプ中にもう一度スペースキーで羽ばたいて「バサバサ」。
スペースキーを押し続けると滑空するけど、重たいので少しずつ下降して最後はドスンと落ちる……そんな動きにしたい。

AI: いい展開だね! 2段ジャンプの「バサバサ」、重みのある滑空。
これで空中の操作感が一気にリッチになる。

巨体ゆえに必死に羽ばたいても空気抵抗に負けて高度が落ちていき、
最後は「ドスン!」。

この仕様、世界観のリアリティをさらに引き立てていて最高だ。

みー:ヽ(^v^) 一番手前のビルに当たると引っかかる感じにしましょう。
シマエナガが画面左に押しやられるとゲームオーバー。

脱出できたら初期位置へゆっくり戻る感じで。

AI: ついにゲームとしての“手応え”と“緊張感”が加わるわけだね。

ビルにぶつかってガガガッと左に押し出される恐怖、
そしてジャンプで飛び越えたときにフワッと定位置に戻る安心感。
このメリハリが入ると、一気に“遊べるゲーム”へと進化するよ。


■ 実際の動作

シマエナガが動くたびに、画面にはオノマトペがポンッと表示されます。
このオノマトペが、そのまま スコアポイント になっています。

(画像:ゲーム画面)



背景には、たまに UFO や ヘリコプター がふわっと通り過ぎることも。 影絵の世界に、ちょっとした“異物感”が混ざるのが楽しいポイントです。

(画像:ゲーム画面)



さらに、スコアが 100pt ごとにスピードアップ。
巨大シマエナガの「ドスン、ドスン」もテンポが上がっていき、
だんだん忙しくなってきます。

(画像:スピードアップ)


■ コードのポイント

今回の記事では、
AI と相談しながら作った「巨大シマエナガゲーム」 の HTML コードを公開します。

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

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

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

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

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

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

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

■ 操作方法

それでは、「巨大シマエナガのお散歩」の操作方法をご紹介しますね。(^_^) とってもシンプルなので、誰でもすぐに遊べますよ!

【基本のお散歩】

  • クリック または スペースキー:シマエナガがジャンプ!
  • 空中でもう一度押す: 「バタバタッ!」と可愛く羽ばたきます。(^^*)
  • 長押し: 羽を広げてゆっくり滑空。
    でも体が重たいので、じわじわ下降して
    最後は「ドスン…」と落ちちゃいます。(^^;)

【スコアの稼ぎ方】

このゲームのスコアは、なんと オノマトペ(擬音語) の回数です。(=v=*)

  • ビルの上に着地:「ドン!」 → 1pt
  • 地面に力強く着地:「ドスン!」 → 5pt!

重さを響かせるほど高得点。 巨大シマエナガの“質量”をうまく使ってスコアを伸ばしましょう!

【気をつけること】

調子に乗って地面を狙いすぎると、
黒いビルに横からぶつかって ガガガッ! と引っかかります。(>_<)

そのまま左端まで押し出されてしまうと ゲームオーバー。

【リスタート方法】

でも安心してください。
ゲームオーバーになっても、ボタンを2回連続でタタンッ! と押せば、
すぐに最初から再開できます。(´ω`)

ポイントが増えていくと、画面中央にお祝い演出がドーン!と出て、
ゲームのスピードも少しずつアップしていきます。

巨大シマエナガの 「かわいさの重み」 を感じながら、
ぜひハイスコアを目指して歩かせてみてくださいね。(^^*)


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

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Giant Shima Enaga Action v12</title>
<style>
body { margin: 0; overflow: hidden; background-color: #222; display: flex; justify-content: center; align-items: center; height: 100vh; }
canvas { background-color: #FF7F50; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8); }
#instructions { position: absolute; top: 20px; color: white; font-family: sans-serif; text-shadow: 1px 1px 2px black; text-align: center; pointer-events: none; }
</style>
</head>
<body>
<div id="instructions">
<h2>巨大シマエナガのお散歩 v12</h2>
<p>【操作】スペースキー または 画面クリック<br>テーマ曲「かわいさの重さ」追加! 速度に合わせてBGMも加速します。</p>
</div>
<canvas id="gameCanvas" width="800" height="400"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const GRAVITY = 0.15; const JUMP_FORCE = -6; const GROUND_Y = 320; const BASE_X = 200;
let bird, airplane;
let isPressing = false; let shakeIntensity = 0; let effects = []; let layers = []; let isGameOver = false;
let gameOverTapCount = 0; let onomatopoeiaCounts = { don: 0, dosun: 0, batabata: 0, total: 0 };
let cutInTimer = 0; let cutInText = ""; let gameParticles = []; let speedMultiplier = 1.0;
// --- Web Audio API(音響システム) ---
let audioCtx;
let bgmTime = 0;
let bgmStep = 0;
const melodyNotes = [76, 79, 83, 79, 76, 79, 83, 79]; // E5, G5, B5...(かわいい高音)
const baseNotes = [36, 0, 43, 0, 36, 0, 41, 0]; // C2, G2...(重たい低音)
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
bgmTime = audioCtx.currentTime + 0.1;
} else if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
// 波形生成によるSE
function playOsc(type, freqStart, freqEnd, duration, vol) {
if (!audioCtx || audioCtx.state !== 'running') return;
const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
osc.type = type; osc.frequency.setValueAtTime(freqStart, audioCtx.currentTime);
if (freqEnd) osc.frequency.exponentialRampToValueAtTime(freqEnd, audioCtx.currentTime + duration);
osc.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(vol, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.start(); osc.stop(audioCtx.currentTime + duration);
}
// ノイズ生成によるSE
function playNoise(duration, vol, freq) {
if (!audioCtx || audioCtx.state !== 'running') return;
const bufferSize = audioCtx.sampleRate * duration; const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = audioCtx.createBufferSource(); noise.buffer = buffer;
const filter = audioCtx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = freq;
const gain = audioCtx.createGain(); gain.gain.setValueAtTime(vol, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
noise.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
noise.start();
}
function playSE(type) {
if (!audioCtx || audioCtx.state !== 'running') return;
if (type === 'dosun') {
playOsc('triangle', 100, 10, 0.4, 0.8); playNoise(0.3, 0.5, 150); // 重低音+ノイズ
} else if (type === 'don') {
playOsc('square', 150, 40, 0.2, 0.5); // 少し軽い低音
} else if (type === 'batabata') {
playNoise(0.2, 0.3, 800); // バサバサ音
} else if (type === 'cutin') {
let t = audioCtx.currentTime;
[72, 76, 79, 84].forEach((note, i) => { // 祝福のファンファーレ
const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
osc.type = 'sine'; osc.frequency.value = 440 * Math.pow(2, (note - 69) / 12);
osc.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0, t + i * 0.1); gain.gain.linearRampToValueAtTime(0.2, t + i * 0.1 + 0.02); gain.gain.exponentialRampToValueAtTime(0.01, t + i * 0.1 + 0.3);
osc.start(t + i * 0.1); osc.stop(t + i * 0.1 + 0.3);
});
}
}
// 動的BGMのスケジューリング
function scheduleBGM() {
if (!audioCtx || audioCtx.state !== 'running' || isGameOver) return;
// speedMultiplier に反比例して1拍の長さを短く(速く)する
let beatLen = 0.35 / speedMultiplier;
while (bgmTime < audioCtx.currentTime + 0.1) {
let mNote = melodyNotes[bgmStep % melodyNotes.length];
let bNote = baseNotes[bgmStep % baseNotes.length];
// メロディ(かわいさ)
if (mNote > 0) {
const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
osc.type = 'sine'; osc.frequency.value = 440 * Math.pow(2, (mNote - 69) / 12);
osc.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0, bgmTime); gain.gain.linearRampToValueAtTime(0.1, bgmTime + 0.05); gain.gain.exponentialRampToValueAtTime(0.01, bgmTime + beatLen * 0.8);
osc.start(bgmTime); osc.stop(bgmTime + beatLen * 0.8);
}
// ベース(重さ)
if (bNote > 0) {
const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
osc.type = 'sawtooth'; osc.frequency.value = 440 * Math.pow(2, (bNote - 69) / 12);
const filter = audioCtx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 250; // 音をこもらせて重くする
osc.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0, bgmTime); gain.gain.linearRampToValueAtTime(0.2, bgmTime + 0.05); gain.gain.exponentialRampToValueAtTime(0.01, bgmTime + beatLen * 1.5);
osc.start(bgmTime); osc.stop(bgmTime + beatLen * 1.5);
}
bgmStep++; bgmTime += beatLen;
}
}
// --- ゲームロジック ---
function initGame() {
bird = { x: BASE_X, y: GROUND_Y, vy: 0, radius: 40, isGrounded: true, canDoubleJump: false, isGliding: false, landingTimer: 0, wasOnBuilding: false, slipTimer: 0 };
airplane = { x: -200, y: 50, speed: 1.5, active: false, type: 'plane', timer: 0 };
isGameOver = false; gameOverTapCount = 0; effects = []; gameParticles = []; cutInTimer = 0; speedMultiplier = 1.0;
onomatopoeiaCounts = { don: 0, dosun: 0, batabata: 0, total: 0 };
layers = [
{ speed: 0.5, color: "rgba(0, 0, 0, 0.2)", buildings: [] },
{ speed: 1.5, color: "rgba(0, 0, 0, 0.5)", buildings: [] },
{ speed: 3.0, color: "#111", buildings: [] }
];
for(let i=0; i<8; i++) {
spawnBuilding(layers[2], i * 200 + 400);
if(i % 2 === 0) spawnBuilding(layers[1], i * 300 + 300);
if(i % 3 === 0) spawnBuilding(layers[0], i * 400 + 200);
}
}
function spawnBuilding(layer, startX) {
const width = 40 + Math.random() * 50; const height = 40 + Math.random() * (layer.speed * 35);
layer.buildings.push({ x: startX || canvas.width, y: GROUND_Y + 40 - height, width: width, height: height });
}
function addBirdEffect(text, type, x, y, size, angle) {
effects.push({ text: text, x: x, y: y, alpha: 1.0, size: size, angle: angle });
playSE(type); // SEを鳴らす
let oldTotal = onomatopoeiaCounts.total;
onomatopoeiaCounts[type]++;
onomatopoeiaCounts.total = onomatopoeiaCounts.don + (onomatopoeiaCounts.dosun * 5) + onomatopoeiaCounts.batabata;
let newTotal = onomatopoeiaCounts.total;
if (Math.floor(oldTotal / 100) < Math.floor(newTotal / 100)) {
speedMultiplier += 0.15;
triggerCutIn(Math.floor(newTotal / 100) * 100, true);
} else if (Math.floor(oldTotal / 20) < Math.floor(newTotal / 20)) {
triggerCutIn(Math.floor(newTotal / 20) * 20, false);
}
}
function triggerCutIn(targetPoint, isMega) {
cutInTimer = isMega ? 120 : 90; cutInText = isMega ? `${targetPoint}pt突破!!! SPEED UP!` : `${targetPoint}pt突破!!!`;
shakeIntensity = isMega ? 25 : 15;
playSE('cutin'); // 祝福音
let particleCount = isMega ? 150 : 120;
for (let i = 0; i < particleCount; i++) {
let angle = Math.random() * Math.PI * 2; let speed = 2 + Math.random() * (isMega ? 12 : 8); let hue = Math.floor(Math.random() * 360);
gameParticles.push({ type: 'circle', x: canvas.width / 2, y: canvas.height / 2, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, size: 4 + Math.random() * 8, color: `hsla(${hue}, 100%, 60%, 1)`, alpha: 1.0, decay: 0.01 + Math.random() * 0.02 });
}
if (isMega) {
for (let i = 0; i < 80; i++) {
let angle = Math.random() * Math.PI * 2; let speed = 4 + Math.random() * 8; let hue = Math.floor(Math.random() * 360);
gameParticles.push({ type: 'tape', x: canvas.width / 2, y: canvas.height / 2, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 3, w: 4 + Math.random() * 5, h: 18 + Math.random() * 18, angle: Math.random() * Math.PI, rotSpeed: (Math.random() - 0.5) * 0.2, color: `hsla(${hue}, 100%, 55%, 1)`, alpha: 1.0, decay: 0.005 + Math.random() * 0.01 });
}
}
}
function handleActionStart() {
initAudio(); // 音声コンテキストの起動
if (isGameOver) {
gameOverTapCount++; if (gameOverTapCount >= 2) initGame(); return;
}
if (bird.isGrounded) {
bird.vy = JUMP_FORCE; bird.isGrounded = false; bird.canDoubleJump = true;
} else if (bird.canDoubleJump) {
bird.vy = JUMP_FORCE * 0.7; bird.canDoubleJump = false;
addBirdEffect("バタバタッ!", "batabata", bird.x - 50, bird.y - 10, 24, 10);
}
}
window.addEventListener('keydown', (e) => { if(e.code === 'Space') { if(e.repeat) return; isPressing = true; handleActionStart(); }});
window.addEventListener('keyup', (e) => { if(e.code === 'Space') isPressing = false; });
window.addEventListener('mousedown', () => { isPressing = true; handleActionStart(); });
window.addEventListener('mouseup', () => { isPressing = false; });
function update() {
if (isGameOver) return;
scheduleBGM(); // 動的BGMの更新
if (!airplane.active && Math.random() < 0.0015) {
airplane.active = true; airplane.x = canvas.width; airplane.y = 20 + Math.random() * 80; airplane.timer = 0;
let r = Math.random();
if (r < 0.1) { airplane.type = 'ufo'; airplane.speed = 2.5 + Math.random() * 1.5; }
else if (r < 0.2) { airplane.type = 'helicopter'; airplane.speed = 1.0 + Math.random() * 0.5; }
else { airplane.type = 'plane'; airplane.speed = 0.6 + Math.random() * 0.8; }
}
if (airplane.active) {
airplane.x -= airplane.speed * speedMultiplier; airplane.timer++;
if (airplane.type === 'helicopter' && airplane.timer % Math.floor(30 / speedMultiplier) === 0) {
effects.push({ text: "パタパタ", x: airplane.x + 10, y: airplane.y - 10, alpha: 1.0, size: 14, angle: 0 });
}
if (airplane.x < -120) airplane.active = false;
}
bird.isGliding = false;
if (!bird.isGrounded && isPressing && bird.vy > 0) { bird.isGliding = true; bird.vy += GRAVITY * 0.2; if (bird.vy > 1.0) bird.vy = 1.0;
} else { bird.vy += GRAVITY; }
bird.y += bird.vy;
if (bird.landingTimer > 0) bird.landingTimer--;
if (bird.y >= GROUND_Y) {
bird.y = GROUND_Y;
if (!bird.isGrounded) { shakeIntensity = 8; bird.landingTimer = 15; addBirdEffect("ドスン!", "dosun", bird.x + 30, bird.y + 40, 36, -15); }
bird.isGrounded = true; bird.vy = 0; bird.canDoubleJump = false;
}
let isHit = false; let isOnBuildingThisFrame = false; let foreLayer = layers[2];
foreLayer.buildings.forEach(b => {
let birdLeft = bird.x - bird.radius + 10; let birdRight = bird.x + bird.radius - 10;
let birdTop = bird.y - bird.radius; let birdBottom = bird.y + bird.radius;
if (birdRight > b.x && birdLeft < b.x + b.width && birdBottom > b.y && birdTop < b.y + b.height) {
if (bird.vy > 0 && birdBottom - bird.vy <= b.y + 10) {
bird.y = b.y - bird.radius; bird.vy = 0; bird.isGrounded = true; bird.canDoubleJump = false; isOnBuildingThisFrame = true;
if (!bird.wasOnBuilding) { shakeIntensity = 6; addBirdEffect("ドン!", "don", bird.x + 20, bird.y + 40, 32, 5); }
} else { isHit = true; bird.x = b.x - bird.radius + 10; }
}
});
if (bird.wasOnBuilding && !isOnBuildingThisFrame && !bird.isGrounded && bird.y < GROUND_Y) bird.slipTimer = 45;
if (bird.isGrounded) bird.slipTimer = 0;
if (bird.slipTimer > 0) bird.slipTimer--;
bird.wasOnBuilding = isOnBuildingThisFrame;
if (!isHit && bird.x < BASE_X) { bird.x += 1.5 * speedMultiplier; if (bird.x > BASE_X) bird.x = BASE_X; }
if (bird.x + bird.radius < 0) isGameOver = true;
layers.forEach(layer => {
layer.buildings.forEach(b => b.x -= layer.speed * speedMultiplier);
if (layer.buildings.length > 0 && layer.buildings[0].x < -100) { layer.buildings.shift(); spawnBuilding(layer); }
});
for (let i = effects.length - 1; i >= 0; i--) { let e = effects[i]; e.y -= 0.8; e.alpha -= 0.02; if (e.alpha <= 0) effects.splice(i, 1); }
for (let i = gameParticles.length - 1; i >= 0; i--) {
let p = gameParticles[i]; p.x += p.vx; p.y += p.vy;
if (p.type === 'tape') { p.vy += 0.03; p.angle += p.rotSpeed; } else { p.vy += 0.05; }
p.alpha -= p.decay; if (p.alpha <= 0) gameParticles.splice(i, 1);
}
if (cutInTimer > 0) cutInTimer--;
}
function draw() {
ctx.save();
if (shakeIntensity > 0 && !isGameOver) { ctx.translate((Math.random() - 0.5) * shakeIntensity, (Math.random() - 0.5) * shakeIntensity); shakeIntensity *= 0.8; if (shakeIntensity < 0.5) shakeIntensity = 0; }
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height); grad.addColorStop(0, "#FF4500"); grad.addColorStop(1, "#FFD700"); ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "rgba(255, 255, 255, 0.8)"; ctx.beginPath(); ctx.arc(600, 100, 40, 0, Math.PI * 2); ctx.fill();
if (airplane.active) {
if (airplane.type === 'ufo') drawUFO(airplane.x, airplane.y);
else if (airplane.type === 'helicopter') drawHelicopter(airplane.x, airplane.y);
else drawAirplane(airplane.x, airplane.y);
}
layers.forEach((layer) => { ctx.fillStyle = layer.color; layer.buildings.forEach(b => ctx.fillRect(b.x, b.y, b.width, b.height)); ctx.fillRect(0, GROUND_Y + 40, canvas.width, canvas.height - GROUND_Y); });
ctx.strokeStyle = "#111"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, GROUND_Y); ctx.lineTo(canvas.width, GROUND_Y); ctx.stroke();
drawShimaEnaga(bird.x, bird.y);
effects.forEach(e => {
ctx.fillStyle = `rgba(255, 255, 255, ${e.alpha})`; ctx.strokeStyle = `rgba(0, 0, 0, ${e.alpha})`; ctx.lineWidth = 4; ctx.font = `bold ${e.size}px 'Impact', sans-serif`;
ctx.save(); ctx.translate(e.x, e.y); ctx.rotate(e.angle * Math.PI / 180); ctx.strokeText(e.text, 0, 0); ctx.fillText(e.text, 0, 0); ctx.restore();
});
gameParticles.forEach(p => {
ctx.save(); ctx.globalAlpha = p.alpha; ctx.fillStyle = p.color;
if (p.type === 'tape') { ctx.translate(p.x, p.y); ctx.rotate(p.angle); ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); }
else { ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); }
ctx.restore();
});
if (cutInTimer > 0) {
ctx.save(); ctx.translate(canvas.width / 2, canvas.height / 2); let scale = 1 + Math.sin(cutInTimer * 0.1) * 0.1; ctx.scale(scale, scale); ctx.rotate((Math.sin(cutInTimer * 0.2) * 5) * Math.PI / 180);
ctx.fillStyle = "rgba(0, 0, 0, 0.6)"; ctx.fillRect(-canvas.width / 2, -40, canvas.width, 80); ctx.font = "italic bold 44px 'Impact', sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
let hue = (Date.now() / 2) % 360; ctx.strokeStyle = "white"; ctx.lineWidth = 8; ctx.strokeText(cutInText, 0, 0); ctx.fillStyle = `hsl(${hue}, 100%, 50%)`; ctx.fillText(cutInText, 0, 0); ctx.restore();
}
ctx.fillStyle = "white"; ctx.strokeStyle = "black"; ctx.lineWidth = 3; ctx.font = "bold 16px sans-serif"; ctx.textAlign = "left";
const scoreText = `ドン:${onomatopoeiaCounts.don}回 ドスン:${onomatopoeiaCounts.dosun}回 バタバタ:${onomatopoeiaCounts.batabata}回 【合計:${onomatopoeiaCounts.total}pt】 速度:x${speedMultiplier.toFixed(2)}`;
ctx.strokeText(scoreText, 20, 40); ctx.fillText(scoreText, 20, 40);
if (isGameOver) {
ctx.fillStyle = "rgba(0, 0, 0, 0.7)"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "white"; ctx.font = "bold 48px sans-serif"; ctx.textAlign = "center";
ctx.fillText("GAME OVER", canvas.width / 2, canvas.height / 2 - 20); ctx.font = "20px sans-serif"; ctx.fillText(`最終記録 - 合計ポイント: ${onomatopoeiaCounts.total}pt (速度:x${speedMultiplier.toFixed(2)})`, canvas.width / 2, canvas.height / 2 + 10);
if (gameOverTapCount === 0) { ctx.fillText("スペースキー または クリック 2回 でリトライ", canvas.width / 2, canvas.height / 2 + 50);
} else { ctx.fillStyle = "#FFD700"; ctx.fillText("もう一度押してスタート!", canvas.width / 2, canvas.height / 2 + 50); }
}
ctx.restore();
}
// 各種描画関数
function drawHelicopter(x, y) { ctx.save(); ctx.fillStyle = "rgba(0, 0, 0, 0.18)"; ctx.translate(x, y); ctx.scale(0.45, 0.45); ctx.beginPath(); ctx.ellipse(0, 0, 20, 10, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.moveTo(10, 0); ctx.lineTo(40, -5); ctx.lineTo(40, 0); ctx.fill(); ctx.fillRect(38, -10, 3, 15); ctx.fillRect(-2, -15, 4, 5); ctx.beginPath(); ctx.ellipse(0, -15, 25, 2, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = "rgba(0, 0, 0, 0.18)"; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(-10, 8); ctx.lineTo(-10, 15); ctx.stroke(); ctx.beginPath(); ctx.moveTo(10, 8); ctx.lineTo(10, 15); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-15, 15); ctx.lineTo(15, 15); ctx.stroke(); ctx.restore(); }
function drawAirplane(x, y) { ctx.save(); ctx.fillStyle = "rgba(0, 0, 0, 0.15)"; ctx.translate(x, y); ctx.scale(0.65, 0.65); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-20, -5); ctx.lineTo(-25, -30); ctx.lineTo(-32, -30); ctx.lineTo(-28, -5); ctx.lineTo(-55, -3); ctx.lineTo(-62, -15); ctx.lineTo(-68, -15); ctx.lineTo(-65, 5); ctx.lineTo(-55, 3); ctx.lineTo(-28, 5); ctx.lineTo(-32, 30); ctx.lineTo(-25, 30); ctx.lineTo(-20, 5); ctx.closePath(); ctx.fill(); ctx.restore(); }
function drawUFO(x, y) { ctx.save(); ctx.fillStyle = "rgba(0, 0, 0, 0.18)"; ctx.translate(x, y); ctx.beginPath(); ctx.ellipse(0, 0, 22, 6, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(0, -3, 8, Math.PI, 0); ctx.fill(); ctx.restore(); }
function drawShimaEnaga(x, y) {
let breathY = Math.sin(Date.now() / 200) * 2; let stretch = 1; let squash = 1;
if (bird.isGliding) { squash = 1.25; stretch = 0.85; } else if (!bird.isGrounded) { stretch = 1 + Math.abs(bird.vy) * 0.03; squash = 1 / stretch; } else if (bird.landingTimer > 0) { let impact = Math.sin((15 - bird.landingTimer) / 15 * Math.PI); squash = 1 + impact * 0.25; stretch = 1 - impact * 0.25; }
ctx.save(); ctx.translate(x, y + breathY); if (bird.slipTimer > 0) { ctx.rotate(0.18 + Math.sin(Date.now() / 40) * 0.04); }
ctx.save(); let tailAngle = -bird.vy * 0.08; if (bird.isGliding) tailAngle = -0.1; if (bird.landingTimer > 0) tailAngle += Math.sin(bird.landingTimer) * 0.15; ctx.rotate(tailAngle); ctx.fillStyle = "#ddd"; ctx.beginPath(); ctx.moveTo(-20, 10); ctx.lineTo(-90, 30); ctx.lineTo(-80, -10); ctx.fill(); ctx.restore();
ctx.scale(squash, stretch); ctx.shadowBlur = 15; ctx.shadowColor = "white"; ctx.fillStyle = "white"; ctx.beginPath(); ctx.arc(0, 0, bird.radius, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; ctx.fillStyle = "rgba(0,0,0,0.1)"; ctx.beginPath(); ctx.arc(0, 0, bird.radius, 0, Math.PI, false); ctx.fill(); ctx.fillStyle = "black"; ctx.beginPath(); ctx.arc(15, -10, 3, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(25, -10, 3, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "#FFD700"; ctx.beginPath(); ctx.moveTo(20, -5); ctx.lineTo(25, 2); ctx.lineTo(15, 2); ctx.fill(); ctx.restore();
let legY = y + bird.radius - (bird.landingTimer > 0 ? Math.sin((15 - bird.landingTimer) / 15 * Math.PI) * 8 : 0); if (bird.isGliding) legY -= 5;
let leftLegOffset = 0; let rightLegOffset = 0; if (bird.slipTimer > 0) { leftLegOffset = Math.sin(Date.now() / 50) * 7; rightLegOffset = -Math.sin(Date.now() / 50) * 7; }
ctx.strokeStyle = "#111"; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(x - 10, legY); ctx.lineTo(x - 10 + (bird.slipTimer > 0 ? -4 : 0), legY + 15 + leftLegOffset); ctx.stroke(); ctx.beginPath(); ctx.moveTo(x + 10, legY); ctx.lineTo(x + 10 + (bird.slipTimer > 0 ? 4 : 0), legY + 15 + rightLegOffset); ctx.stroke();
}
function loop() { update(); draw(); requestAnimationFrame(loop); }
initGame(); loop();
</script>
</body>
</html>


■ 今日の学び

最初は「サイズの逆転」というシンプルなテーマから始まった今回の実験。(ΦωΦ)✨
気がつけば、一軒家サイズの巨大シマエナガが街を練り歩く、
なんともシュールなお散歩ゲームへと進化していました。

巨大さを表現するために取り入れた
「ドスン!」「バタバタッ!」といったオノマトペ。

これがそのまま スコアになる という、
ちょっと不思議な仕組みに落ち着いたのですが……
実際に遊んでみると、これが意外とクセになるんです。

「もっとドスンと言わせたい!」 そう思って、
何度も何度もムキになって挑戦してしまいました。

敵が出てくるわけでもなく、
ただ巨大シマエナガと一緒にお散歩するだけの、
やさしい世界。( ˘ω˘ )🕊️ ……なのですが、

高得点の「ドスン」を狙いにいくと、
ビルに激突するリスクが一気に跳ね上がるという、
なかなかスリリングなゲームバランスに仕上がりました。(^^;)

最終的にわかったのは、
プレイヤー自身の「欲張りたい気持ち」こそが最大の敵 だということ。

ゲームバランスって、本当に不思議で面白いですね。


シマエナガの雑貨を家のあちこちに飾ってます。

(画像:お庭のシマエナガの置物)

■ 次回は、シーグラスならべの楽しさをゲームにしました!

シーグラスって、拾うのも好きだし、ならべるのも好き。

でも、この“ならべる楽しさ”って、なかなか人に伝わりにくいんですよね。

この時間がすごく好きで…… 夢中になって並べていると、
あっという間に時間が過ぎてしまいます。

そんな “ひとりで夢中になれる楽しさ” を、
次回はゲームとして形にしてみました。

ぜひ楽しみにしていてくださいね。(*^v^)ノシ

▶ 次の実験はこちら  
  (砂浜で拾った宝物)
「シーグラスならべ」



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