■9つ目「フォール・フィット」
「とにかく落ちていく爽快感を味わえるゲームを作りたい!」という思いから生まれた、落下アクションゲームです!
▼ゲームの基本ルール
・プレイヤーはひたすら下へ落下していきます。
・地面にはいくつもの“層”があり、下の層へ行くほどスコアがアップ。
・落下スピードもどんどん速くなり、スリルが増していきます。
・落ちながら宝石を取ると追加ポイントが入ります。
↓
|||
ヽ( ゜。゜)ノ
( )
/ /
↓
▼ダメージとゲームオーバー
・層にぶつかるとダメージ。
・3回ぶつかるとゲームオーバーになります。
▼操作とヒント
・層には穴が空いていて、そこを通り抜けると次の層へ進めます。
・穴の位置は、左右の矢印で「どのあたりにあるか」を教えてくれる仕組みです。
ゲームのイメージを伝えたくて、つい長文でAIへ伝えてしまいます。(^^;)↓
■ AIとのやりとりで面白かった部分
(^^*) ゲームスタートすると自由落下が始まります。背景が流れて落下感を再現。落下地点の穴を狙って落ちていくゲームです。大きめの矢印が穴の位置をナビしてくれて、落下地点までの距離は数字で表示されます。穴を抜けるとそのまま第2ステージへ進み、さらに次の落下地点を目指します。
こんな感じで、いくつもの層をどんどん突破していくゲームです。層を抜けるたびにボーナス点が入り、落下中にはお宝も一緒に落ちてくるので、それを取ると追加得点になります。お宝はプレイヤーより少し遅いスピードで落ちていくので、追い越しながら取るのが気持ちいいポイント。
スコアはランキング形式で記録され、上位を目指して挑戦できます。深い層に行くほど高得点のお宝が期待できるので、つい「もう一回!」となるタイプのゲームです。お宝感のある絵があると雰囲気が出そう。プレイヤーは図形を組み合わせた人型で、頭から落ちていくイメージ。穴をすり抜けられなかったらゲームオーバーです。
(AI) 「フォールフィット」の具体的なゲームデザイン、とてもワクワクしますね! “無限に深く潜っていく爽快感”と“穴を狙い澄ます緊張感”が同居した、中毒性の高いゲームになりそうです。
(^^*) 下層になってきたときの難易度を高くしたいね。
(AI) もちろんです!下層に行くほど手に汗握る仕掛けを追加してみましょう。
- 動く層(Moving Gates) — ステージ10以降、層が左右にゆっくり動き始めます。深くなるほど動きが速くなり、正確な狙いが必要に。
- 特殊エフェクト(Flash & Particle) — お宝を取った瞬間に画面がキラッと光り、ドットの破片が飛び散る演出で爽快感アップ。
- 表情システム — 通常時は「・・」、お宝ゲットで「^‿^」、ダメージで「><」と、プレイヤーの顔が状況に応じて変化。
- 環境変化(Deep Fog) — 深いゾーンに進むほど背景に濃い霧が増え、視覚的なプレッシャーを演出。
(^^*)表情システムナイスです!
■ 実際の動作
宝石を取ると一瞬笑顔になります(^v^)

下の穴の位置は”◁”と”▷”で教えてくれます。

左上のハートがなくなるとゲームオーバー

下層に行くほどに落下スピードが早くなり、お宝はいいものになって、難易度や背景色など変化してきます。

■ コードのポイント
今回の記事では、AI と相談しながら作った「フォール・フィット」の HTML コードを丸ごと公開します。 このゲームはブラウザだけで動く、とてもシンプルな仕組みです。
▼遊ぶための準備
- PC の「メモ帳」を開く
- 下のコードをすべてコピーして貼り付ける
- 「otiru.html」など好きな名前で保存
- 保存したファイルをブラウザで開く
これだけで、すぐにゲームが遊べます。
▼操作方法
- マウスをクリックするとゲームスタート
- あとは マウスを左右に動かして穴を狙い、層に当たらないように落ちていくだけ
- シンプルだけど、スピードが上がると意外と手に汗にぎる操作感です(^^*)
▼コードについて
今回も AI が作ってくれたコードをそのまま載せています。 気になる部分を自分で改造したり、色や動きをアレンジして遊んでみてください。
「※コードが長いので、必要な方だけコピーしてください」
「※スマホでは動作しない場合があります」
▼ここからコード▼(クリックで開く)
<!DOCTYPE html><html lang="ja"><head> <meta charset="UTF-8"> <title>FALL FIT - Deep Survival Edition</title> <style> body { margin: 0; overflow: hidden; background: #0f380f; color: #fff; font-family: 'Courier New', Courier, monospace; transition: background 1.5s; } canvas { display: block; image-rendering: pixelated; } #ui { position: absolute; top: 10px; left: 10px; pointer-events: none; z-index: 10; } #life-ui { font-size: 2em; color: #ff4444; margin-bottom: 5px; } #dist-wrap { position: absolute; top: 100px; width: 100%; text-align: center; pointer-events: none; } #dist { font-size: 80px; font-weight: bold; color: #fff; text-shadow: 4px 4px #000; } .nav-arrow { position: absolute; top: 50%; transform: translateY(-50%); font-size: 140px; font-weight: bold; color: #fff; text-shadow: 5px 5px #000, -3px -3px #000, 3px -3px #000, -3px 3px #000; opacity: 0; transition: opacity 0.1s; pointer-events: none; } #arrowL { left: 40px; } #arrowR { right: 40px; } #msg { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; background: rgba(0,0,0,0.9); border: 4px solid #fff; padding: 30px; display: none; z-index: 20; } </style></head><body> <div id="ui"> <div id="life-ui">❤️❤️❤️</div> <div id="info" style="font-size: 1.5em; font-weight: bold;">STAGE: 1 | SCORE: 0</div> <div>HI-SCORE: <span id="hiScore">0</span></div> </div> <div id="dist-wrap"><div id="dist">NEXT: ---</div></div> <div id="arrowL" class="nav-arrow">◀</div> <div id="arrowR" class="nav-arrow">▶</div> <div id="msg"> <h1 id="status">FALL FIT</h1> <p>SCORE: <span id="finalScore">0</span></p> <p>PRESS SPACE TO START</p> </div> <canvas id="gameCanvas"></canvas><script>const canvas = document.getElementById('gameCanvas');const ctx = canvas.getContext('2d');canvas.width = window.innerWidth;canvas.height = window.innerHeight;const audioCtx = new (window.AudioContext || window.webkitAudioContext)();function beep(freq, duration, type = 'square', vol = 0.1) { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freq, audioCtx.currentTime); gain.gain.setValueAtTime(vol, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + duration);}function playBGM() { const notes = [130, 146, 164, 174]; let i = 0; setInterval(() => { if (gameState === 'playing') beep(notes[i++ % notes.length], 0.2, 'triangle', 0.04); }, 450);}let gameState = 'title';let score = 0, stage = 1, lives = 3, invincibility = 0;let faceType = "normal"; // normal, happy, hurtlet hiScore = localStorage.getItem('fallFitHi') || 0;document.getElementById('hiScore').innerText = hiScore;const zoneColors = ["#306230", "#304262", "#623055", "#624030", "#403062"];let particles = [], treasures = [], gates = [], fx = [];const player = { x: canvas.width/2, y: 150, targetX: canvas.width/2 };window.addEventListener('mousemove', (e) => player.targetX = e.clientX);window.addEventListener('keydown', (e) => { if (e.code === 'Space') { if (gameState !== 'playing') resetGame(); audioCtx.resume(); } });window.addEventListener('mousedown', () => { if (gameState !== 'playing') resetGame(); audioCtx.resume(); });function resetGame() { score = 0; stage = 1; lives = 3; invincibility = 0; treasures = []; gates = []; fx = []; gameState = 'playing'; document.getElementById('msg').style.display = 'none'; updateLifeUI(); spawnGate(); beep(440, 0.1); setTimeout(() => beep(880, 0.2), 100);}function updateLifeUI() { document.getElementById('life-ui').innerText = '❤️'.repeat(lives) + '🖤'.repeat(3 - lives);}function spawnGate() { const holeW = Math.max(100, 280 - (stage * 5)); const holeX = Math.random() * (canvas.width - holeW); const zoneLevel = Math.floor(stage / 5); const distMultiplier = Math.pow(1.3, zoneLevel); const spawnY = canvas.height + (1500 * distMultiplier); // ステージ10から層が動き出す const moveSpeed = stage >= 10 ? (Math.random() - 0.5) * (stage / 2) : 0; const decorations = []; for(let i=0; i<15; i++) { decorations.push({ x: Math.random() * canvas.width, type: Math.floor(Math.random() * 4), offsetY: Math.random() * 40 }); } gates.push({ y: spawnY, x: holeX, w: holeW, passed: false, decos: decorations, vx: moveSpeed });}function createFX(x, y, color) { for(let i=0; i<10; i++) { fx.push({ x, y, vx: (Math.random()-0.5)*10, vy: (Math.random()-0.5)*10, life: 20, color }); }}function update() { if (gameState !== 'playing') return; player.x += (player.targetX - player.x) * 0.15; const currentSpeed = 16 + (Math.floor(stage / 5) * 3); const colorIndex = Math.floor(stage / 5) % zoneColors.length; document.body.style.background = zoneColors[colorIndex]; if (invincibility > 0) invincibility--; else faceType = "normal"; // 粒子(深くなるほど増える) if (Math.random() < 0.4 + (stage * 0.01)) particles.push({ x: Math.random()*canvas.width, y: canvas.height, s: Math.random()*20+5 }); particles.forEach((p, i) => { p.y -= currentSpeed + 8; if (p.y < -50) particles.splice(i, 1); }); // FXの更新 fx.forEach((f, i) => { f.x += f.vx; f.y += f.vy; f.life--; if(f.life <= 0) fx.splice(i, 1); }); treasures.forEach((t, i) => { t.y -= currentSpeed * 0.75; const hitBox = 40 + (t.zone * 5); if (Math.abs(t.x - player.x) < hitBox && Math.abs(t.y - player.y) < hitBox) { score += 100 * (t.zone + 1); createFX(t.x, t.y, "#fff"); faceType = "happy"; treasures.splice(i, 1); beep(1500 + (t.zone * 200), 0.05, 'square', 0.05); } if (t.y < -50) treasures.splice(i, 1); }); const thickness = 100; gates.forEach((g, i) => { g.y -= currentSpeed; if (g.vx !== 0) { g.x += g.vx; if (g.x < 0 || g.x + g.w > canvas.width) g.vx *= -1; // 壁跳ね返り } const holeCenter = g.x + g.w / 2; const diff = holeCenter - player.x; if (Math.abs(diff) > g.w / 2.5) { if (diff > 0) { document.getElementById('arrowR').style.opacity = "1"; document.getElementById('arrowL').style.opacity = "0"; } else { document.getElementById('arrowL').style.opacity = "1"; document.getElementById('arrowR').style.opacity = "0"; } } else { document.getElementById('arrowL').style.opacity = "0"; document.getElementById('arrowR').style.opacity = "0"; } if (!g.passed) { if (g.y < player.y + 40 && g.y + thickness > player.y - 10) { if (player.x > g.x && player.x < g.x + g.w) { // 通過中 } else if (invincibility <= 0) { lives--; updateLifeUI(); faceType = "hurt"; if (lives <= 0) { gameOver(); } else { invincibility = 60; beep(100, 0.3, 'sawtooth', 0.2); } } } if (g.y + thickness < player.y - 10) { g.passed = true; score += 1000; stage++; if (stage % 5 === 0) beep(500, 0.5, 'square', 0.1); // ゾーン突入音 spawnGate(); } } if (g.y < -500) gates.splice(i, 1); }); if (Math.random() < 0.05) treasures.push({ x: Math.random()*canvas.width, y: canvas.height, zone: Math.floor(stage/5) }); document.getElementById('info').innerText = `STAGE: ${stage} | SCORE: ${score}`; const dist = gates[0] ? Math.max(0, Math.floor((gates[0].y - player.y)/10)) : 0; document.getElementById('dist').innerText = `NEXT: ${dist}m`;}function gameOver() { gameState = 'over'; beep(50, 1.0, 'sawtooth'); document.getElementById('arrowL').style.opacity = "0"; document.getElementById('arrowR').style.opacity = "0"; if (score > hiScore) { hiScore = score; localStorage.setItem('fallFitHi', hiScore); document.getElementById('hiScore').innerText = hiScore; } document.getElementById('finalScore').innerText = score; document.getElementById('msg').style.display = 'block';}function drawPlayer(frame) { ctx.save(); ctx.translate(player.x, player.y); if (invincibility > 0 && Math.floor(frame / 2) % 2 === 0) ctx.globalAlpha = 0.3; const armSwing = Math.sin(frame * 0.3) * 8; ctx.fillStyle = '#8bac0f'; ctx.fillRect(-12, -10, 24, 32); // 胴 ctx.fillRect(-8, -24, 16, 16); // 頭 ctx.fillRect(-20, -35 + armSwing, 6, 30); // 腕 ctx.fillRect(14, -35 - armSwing, 6, 30); // 顔の描画 ctx.fillStyle = '#000'; if (faceType === "normal") { ctx.fillRect(-4, -18, 2, 2); ctx.fillRect(2, -18, 2, 2); // 目 ctx.fillRect(-2, -12, 4, 1); // 口 } else if (faceType === "happy") { ctx.font = "bold 12px Arial"; ctx.fillText("^ ^", -8, -14); ctx.fillText("‿", -4, -10); } else if (faceType === "hurt") { ctx.font = "bold 12px Arial"; ctx.fillText("> <", -8, -14); } ctx.fillStyle = '#000000'; ctx.fillRect(-12, 22, 10, 25); ctx.fillRect(2, 22, 10, 25); ctx.restore();}function drawTreasure(t) { ctx.save(); ctx.translate(t.x, t.y); const colors = ["#fff", "#fbd400", "#ff4444", "#44ffff", "#ffffff"]; const baseSize = 20 + (t.zone * 4); ctx.fillStyle = "#000"; const type = t.zone % 4; ctx.beginPath(); if (type === 0) { ctx.moveTo(0, -baseSize); ctx.lineTo(baseSize, 0); ctx.lineTo(0, baseSize); ctx.lineTo(-baseSize, 0); } else if (type === 1) { for(let i=0; i<8; i++) { ctx.rect(-2, -baseSize, 4, baseSize*2); ctx.rotate(Math.PI/4); } } else if (type === 2) { for(let i=0; i<6; i++) { ctx.lineTo(Math.cos(i*Math.PI/3)*baseSize, Math.sin(i*Math.PI/3)*baseSize); } } else { for(let i=0; i<10; i++){ let r = i % 2 === 0 ? baseSize : baseSize/2; ctx.lineTo(Math.cos(i*Math.PI/5)*r, Math.sin(i*Math.PI/5)*r); } } ctx.fill(); ctx.fillStyle = colors[t.zone % colors.length]; ctx.fillRect(-baseSize/4, -baseSize/4, baseSize/2, baseSize/2); ctx.restore();}function drawDeco(ctx, x, y, type) { ctx.fillStyle = "#222"; if (type === 0) { ctx.beginPath(); ctx.moveTo(x-20, y); ctx.lineTo(x, y-25); ctx.lineTo(x+20, y); ctx.fill(); } else if (type === 1) { ctx.fillRect(x-10, y-5, 20, 5); ctx.fillStyle = "#ff4400"; ctx.fillRect(x-5, y-15, 10, 10); } else if (type === 2) { ctx.fillRect(x-10, y-20, 20, 20); ctx.fillRect(x-12, y-15, 2, 10); ctx.fillRect(x+10, y-15, 2, 10); } else { ctx.fillRect(x-5, y-15, 10, 15); ctx.fillStyle = "#fbd400"; ctx.fillRect(x-3, y-12, 6, 6); }}function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); const colorIndex = Math.floor(stage / 5) % zoneColors.length; ctx.fillStyle = zoneColors[colorIndex]; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'rgba(255,255,255,0.2)'; particles.forEach(p => ctx.fillRect(p.x, p.y, 3, p.s)); treasures.forEach(t => drawTreasure(t)); fx.forEach(f => { ctx.fillStyle = f.color; ctx.fillRect(f.x, f.y, 4, 4); }); gates.forEach(g => { ctx.fillStyle = '#000000'; ctx.fillRect(0, g.y, g.x, 100); ctx.fillRect(g.x + g.w, g.y, canvas.width, 100); g.decos.forEach(d => { if (d.x < g.x - 20 || d.x > g.x + g.w + 20) drawDeco(ctx, d.x, g.y + d.offsetY + 30, d.type); }); }); drawPlayer(Date.now() / 50); update(); requestAnimationFrame(draw);}document.getElementById('msg').style.display = 'block';playBGM();draw();</script></body></html>
■ 今日の学び
今回のゲームは、あえて ゲームボーイ風の雰囲気 を出したくて、AI に色味や演出を相談しながら作りました。
初代ゲームボーイのあの独特の色合いと、ピコピコした音の感じが大好きなんです。ヽ(*^o^*)ノ
最終的には難易度がかなり高めのゲームになりましたが、操作はシンプルで手軽に遊べるので、ぜひ体験してみてください。
次回は、「頭の中で繰り返す2角取り」ゲームです。
どうぞお楽しみに(*^v^)ノシ
次の実験(2角取り):「オノマトペ2角取り」

コメントを残す