■33個目「UFOかくれんぼ」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回は、静かな夜空でひっそりと繰り広げられる、宇宙規模のかくれんぼのお話です。
地球の文化をこっそり観察しにやってきた、おっちょこちょいなUFOパイロット。
順調に空から調査を進めていたのですが、あろうことか普段なら“姿を完全に消せる”はずの…「完全透明化装置」が故障してしまいました。
夜空に浮かぶ怪しい影に、地上の人々も黙ってはいません。
「なんだあの影は!?」「UFOじゃないか?」
勘のいい人たちが、強力なサーチライトを持ち出して空を照らし始めました。
じりじりと迫りくる巨大な光や、直線的に夜空を切り裂く警戒ライト。
もしあの光に照らされて姿をハッキリ見られてしまったら、地上は大パニック間違いなしです。
頼みの綱は、夜風に流れてくる分厚い雲と、UFOに搭載された少しクセのある緊急回避機能だけ。
地上からの追跡をかわして、無事に地球の夜空から逃げ切れるでしょうか。
☁️☁️☁️🛸💦 🔦(¬_¬ ) むむ?気のせいか…
■ AIとのやりとりで面白かった部分
(^^*)今日もHTML形式で動かせるゲームを作ります。「UFOかくれんぼゲーム」です。
プレイヤーはUFOを操縦して、ライトに見つからないように逃げ切るゲームです。
完全透明化装置が壊れてしまっているので、夜空に異変を感じた人たちが照らしてくる
円形のスポットライトをうまくかわす必要があります。
ライトには2種類あります。
・じりじりとUFOに近づいてくる“追跡ライト”
・直線的に動き、画面端で反射する“警戒ライト”
UFOにはピンチを乗り切るための3つの機能があります。
① 3秒間だけ姿がぶれて見える「ブレール機能」
② 進行方向へ一気に加速する「ブースト機能」
③ ランダム位置にテレポートする「ブットビ機能」
※いずれも使用後15秒のクールタイムあり。
画面内には雲が流れてきて、その後ろに隠れるとライトから見つかりません。
操作はマウスドラッグで移動。①Fキー ②Jキー ③スペースキーで発動します。
(AI)かしこまりました。夜空の逃走劇「UFOかくれんぼ」ですね。
少しミステリアスな雰囲気のUIに仕上げています。
(^_^)雲の後ろに隠れる感じを出したいです。
ブーストは押した瞬間にUFO5個分くらいギュンと移動するようにしましょう。
(AI)了解! 雲をUFOの手前に描画するようにして立体感を出しました。
ブーストも約250pxの瞬間移動に変更済みです。
(*^^)リトライは「最初から」と「続きから」を選べるように。
追跡ライトは数が増えるのではなく、サイズアップで難易度を上げたいです。
(AI)常連さんへ贈る、今夜のUFOトリビア。
第二次世界大戦中のパイロットたちが見た謎の光「フー・ファイター」って知ってますか?
今回の追跡ライトは、その雰囲気を出すために少し赤みがかった色にしてあります。
(*^^)ブーストとブットビが自爆スイッチみたいなので、
発動中はブレール状態も同時に発生するようにしましょう。
ブーストはランダム方向へ1秒高速移動、画面端で跳ね返る動きにできますか?
(AI)任せな! ランダム方向へ1秒猛ダッシュし、端で反射する仕様に変更済みです。
(^o^)ブーストのスピードを倍にして、流れ星みたいに光のかたまりとして飛ぶ感じに。
ステージクリア時間は40秒に短縮しましょう。
(AI)了解! ブースト中はUFOを光の玉として描き、尾を引くようにしました。
勘のいい人たちも「今の流れ星か?」と思うはずです。
ステージクリア時間も40秒に調整済みです。
■ 実際の動作
まずは、AIとの相談で作ってもらったゲームの初期状態です。

(画像:AIが最初に作った状態)
今回の仕上がりです。スタート画面では、ほとんど最初の仕上がりと同じです。

(画像:仕上がりの画面)
3秒間だけぶれて見えて、気のせいかなと思わせている間に移動して逃げる
「ブレール機能」。その場で発動し、見つからなくなります。

(画像:ブレール機能発動)
進行方向へ2秒間瞬間加速して危機を脱出する「ブースト機能」。
発動した瞬間、高速で流れ星のように移動し、見つからない状態になります。

(画像:ブースト機能発動)
ランダム位置にテレポートする「ブットビ機能」。
発動した瞬間、離れた場所へ瞬間移動して見つからなくなります。

(画像:ブットビ機能発動)
■ コードのポイント
今回の記事では、AI と相談しながら作った
「UFOかくれんぼ」の HTML コードを公開します。
ブラウザだけで動く、とてもシンプルなゲームです。
コードといっても難しい部分はありません。
貼り付けて保存するだけで動くので、気軽に試してみてください。
【遊ぶまでの手順】
1. PC の「メモ帳」(※Windows の場合)を開きます。
2. 下のコードをコピーして貼り付けます。
3. 「ufo.html」など、好きな名前で保存します。
4. 保存したファイルをブラウザで開くだけで遊べます。
【操作方法】
基本操作はマウスだけ!
UFOをマウスでドラッグしている間、その方向へ移動します。
地上から照らされるサーチライトの円にUFOが触れてしまうとゲームオーバー。
巧みなマウスさばきで光を避けながら、40秒間逃げ切ればステージクリアです。
夜空を漂う「雲」の後ろに隠れている間は、ライトの光をやり過ごすことができます。
■ ピンチを救う3つの緊急回避機能
どうしても逃げきれない!そんな時はキーボードでUFOの特殊機能を使いましょう。
・Fキー【ブレール】
3秒間だけUFOが残像状態になり、ライトをすり抜けることができます。
・Jキー【ブースト】
シューーーン!と1秒間「流れ星」のように画面を縦横無尽に超高速移動し、
強引に危機を脱出します。発動後3秒間はライトに見つかりません。
・スペースキー【ブットビ】
画面内のどこかにランダムで瞬間移動(テレポート)します。
こちらも発動後3秒間は安全です。
※どれも強力な機能ですが、一度使うとエネルギーチャージのために
「15秒間」使えなくなります。使いどころを見極めてくださいね。
迫りくる光のプレッシャーに負けず、
ぜひ地球の夜空をスイスイと飛び回ってみてください!
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>UFOかくれんぼ - Midnight Escape</title> <style> body { margin: 0; background: #050510; color: #0f0; font-family: 'Courier New', Courier, monospace; overflow: hidden; user-select: none; } canvas { display: block; cursor: crosshair; } #ui { position: absolute; top: 10px; left: 10px; pointer-events: none; text-shadow: 2px 2px 2px #000; z-index: 10; } .skill { margin-bottom: 5px; padding: 2px 10px; border: 1px solid #0f0; background: rgba(0, 50, 0, 0.5); } .cooldown { color: #f00; border-color: #f00; opacity: 0.5; } #overlay { position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.7); display: none; flex-direction: column; justify-content: center; align-items: center; z-index: 20; } #msg { font-size: 48px; text-align: center; margin-bottom: 30px; text-shadow: 2px 2px 0 #000; } #btn-box { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; } .btn { padding: 15px 30px; font-size: 24px; color: #0f0; background: #000; border: 2px solid #0f0; cursor: pointer; font-family: inherit; transition: all 0.2s; } .btn:hover { background: #0f0; color: #000; } #replay-indicator { position: absolute; top: 20px; right: 20px; color: #f00; font-size: 24px; font-weight: bold; display: none; z-index: 10; animation: blink 1s infinite; } @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } } </style></head><body><div id="ui"> <div id="timer">TIME: 40.0</div> <div id="stage">STAGE: 1</div> <div id="skill1" class="skill">F: ブレール (無敵 3秒)</div> <div id="skill2" class="skill">J: ブースト (流れ星回避 1秒 + 無敵 3秒)</div> <div id="skill3" class="skill">SPACE: ブットビ (テレポート + 無敵 3秒)</div></div><div id="replay-indicator">● REPLAY PLAYING</div><div id="overlay"> <div id="msg"></div> <div id="btn-box"></div></div><canvas id="gameCanvas"></canvas><script>const canvas = document.getElementById('gameCanvas');const ctx = canvas.getContext('2d');const overlay = document.getElementById('overlay');const msgDisplay = document.getElementById('msg');const btnBox = document.getElementById('btn-box');const replayIndicator = document.getElementById('replay-indicator');let stage = 1;// ステージクリア時間を40秒に変更const CLEAR_TIME = 40.0;let timeLeft = CLEAR_TIME;let gameState = 'playing';const RECHARGE_TIME = 15000;let ufo = {};let lights = [];let clouds = [];let floatingTexts = [];let recordedFrames = [];let replayCursor = 0;function initLevel() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; ufo = { x: canvas.width / 2, y: canvas.height / 2, r: 20, targetX: canvas.width / 2, targetY: canvas.height / 2, isDragging: false, invincible: false, boost: false, boostVx: 0, boostVy: 0, boostTimer: 0, lastF: 0, lastJ: 0, lastSpace: 0 }; floatingTexts = []; recordedFrames = []; timeLeft = CLEAR_TIME; // ここも修正 gameState = 'playing'; overlay.style.display = 'none'; replayIndicator.style.display = 'none'; spawnObjects(); requestAnimationFrame(gameLoop);}function spawnObjects() { lights = []; lights.push({ type: 'chaser', x: Math.random()*canvas.width, y: 0, r: 80 + (stage * 15), vx: 0, vy: 0, speed: 1.5 + (stage * 0.1) }); for(let i=0; i < Math.floor(stage/2) + 1; i++) { lights.push({ type: 'bouncer', x: Math.random()*canvas.width, y: Math.random()*canvas.height, r: 70, vx: 3 + stage, vy: 3 + stage }); } clouds = []; for(let i=0; i<6; i++) { clouds.push({ x: Math.random()*canvas.width, y: Math.random()*canvas.height, w: 180, h: 100, speed: 0.5 + Math.random() }); }}function spawnText(text, color) { floatingTexts.push({ text: text, x: ufo.x, y: ufo.y - 30, life: 60, maxLife: 60, color: color });}window.addEventListener('mousedown', () => ufo.isDragging = true);window.addEventListener('mouseup', () => ufo.isDragging = false);window.addEventListener('mousemove', (e) => { if (gameState === 'playing') { ufo.targetX = e.clientX; ufo.targetY = e.clientY; }});window.addEventListener('keydown', (e) => { if (gameState !== 'playing') return; const now = Date.now(); // F: ブレール if (e.code === 'KeyF' && now - ufo.lastF > RECHARGE_TIME) { ufo.invincible = true; ufo.lastF = now; spawnText("ブレール!", "#0ff"); setTimeout(() => ufo.invincible = false, 3000); } // J: ブースト (流れ星回避 - スピード倍増) if (e.code === 'KeyJ' && now - ufo.lastJ > RECHARGE_TIME) { let angle = Math.random() * Math.PI * 2; // スピードを以前(18)の倍の「36」に設定 let speed = 36; ufo.boostVx = Math.cos(angle) * speed; ufo.boostVy = Math.sin(angle) * speed; ufo.boost = true; ufo.boostTimer = 60; // 60フレーム (約1秒) ufo.invincible = true; ufo.lastJ = now; // セリフを変更 spawnText("シューーーン!", "#fff"); setTimeout(() => ufo.invincible = false, 3000); } // Space: ブットビ if (e.code === 'Space' && now - ufo.lastSpace > RECHARGE_TIME) { ufo.x = Math.random() * canvas.width; ufo.y = Math.random() * canvas.height; ufo.targetX = ufo.x; ufo.targetY = ufo.y; ufo.invincible = true; ufo.lastSpace = now; spawnText("ブットビ!", "#f0f"); setTimeout(() => ufo.invincible = false, 3000); }});function startReplay() { gameState = 'replaying'; replayCursor = 0; overlay.style.display = 'none'; replayIndicator.style.display = 'block'; requestAnimationFrame(replayLoop);}function recordState() { recordedFrames.push({ // リプレイでも流れ星の尾を描画するためにブースト速度も記録 ufo: { x: ufo.x, y: ufo.y, invincible: ufo.invincible, boost: ufo.boost, boostVx: ufo.boostVx, boostVy: ufo.boostVy }, lights: lights.map(l => ({ x: l.x, y: l.y, r: l.r, type: l.type })), clouds: clouds.map(c => ({ x: c.x, y: c.y, w: c.w, h: c.h })), texts: floatingTexts.map(t => ({ text: t.text, x: t.x, y: t.y, life: t.life, maxLife: t.maxLife, color: t.color })) });}function gameLoop() { if (gameState !== 'playing') return; // UFO移動処理 if (ufo.boost) { // ブースト中の流れ星移動と壁反射 ufo.x += ufo.boostVx; ufo.y += ufo.boostVy; if (ufo.x < ufo.r) { ufo.x = ufo.r; ufo.boostVx *= -1; } if (ufo.x > canvas.width - ufo.r) { ufo.x = canvas.width - ufo.r; ufo.boostVx *= -1; } if (ufo.y < ufo.r) { ufo.y = ufo.r; ufo.boostVy *= -1; } if (ufo.y > canvas.height - ufo.r) { ufo.y = canvas.height - ufo.r; ufo.boostVy *= -1; } ufo.boostTimer--; if (ufo.boostTimer <= 0) ufo.boost = false; } else if (ufo.isDragging) { // 通常移動 let dx = ufo.targetX - ufo.x; let dy = ufo.targetY - ufo.y; ufo.x += dx * 0.1; ufo.y += dy * 0.1; } // エフェクトテキスト floatingTexts.forEach(t => { t.y -= 1; t.life--; }); floatingTexts = floatingTexts.filter(t => t.life > 0); // 雲 clouds.forEach(c => { c.x += c.speed; if (c.x > canvas.width) c.x = -c.w; }); let isHidden = clouds.some(c => ufo.x > c.x && ufo.x < c.x + c.w && ufo.y > c.y && ufo.y < c.y + c.h); // ライト lights.forEach(l => { if (l.type === 'chaser') { let dx = ufo.x - l.x, dy = ufo.y - l.y, dist = Math.sqrt(dx*dx + dy*dy); if(dist > 0) { l.x += (dx/dist) * l.speed; l.y += (dy/dist) * l.speed; } } else { l.x += l.vx; l.y += l.vy; if (l.x < 0 || l.x > canvas.width) l.vx *= -1; if (l.y < 0 || l.y > canvas.height) l.vy *= -1; } let distUfo = Math.sqrt((ufo.x - l.x)**2 + (ufo.y - l.y)**2); if (distUfo < l.r + ufo.r && !ufo.invincible && !isHidden) endGame('gameover'); }); timeLeft -= 1/60; if (timeLeft <= 0 && gameState === 'playing') endGame('clear'); recordState(); drawScene(ufo, lights, clouds, floatingTexts); updateUI(); if (gameState === 'playing') requestAnimationFrame(gameLoop);}function replayLoop() { if (gameState !== 'replaying') return; if (replayCursor < recordedFrames.length) { const frame = recordedFrames[replayCursor]; drawScene(frame.ufo, frame.lights, frame.clouds, frame.texts); replayCursor++; requestAnimationFrame(replayLoop); } else { replayIndicator.style.display = 'none'; // クリア時間に合わせてフレーム判定を修正 endGame(recordedFrames.length >= (CLEAR_TIME * 60) - 10 ? 'clear' : 'gameover'); }}function drawScene(d_ufo, d_lights, d_clouds, d_texts) { // 1. 背景 ctx.fillStyle = '#050510'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 2. ライト d_lights.forEach(l => { let grad = ctx.createRadialGradient(l.x, l.y, 0, l.x, l.y, l.r); if(l.type === 'chaser') { grad.addColorStop(0, 'rgba(255, 100, 100, 0.6)'); grad.addColorStop(1, 'rgba(255, 100, 100, 0)'); } else { grad.addColorStop(0, 'rgba(255, 255, 200, 0.6)'); grad.addColorStop(1, 'rgba(255, 255, 200, 0)'); } ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(l.x, l.y, l.r, 0, Math.PI*2); ctx.fill(); }); // 3. UFO (雲の後ろ、ブースト中は流れ星として描画) if (d_ufo.boost) { // 流れ星の尾を描画 // 進行方向の逆へ半透明の円をずらして描画 let vDist = Math.sqrt(d_ufo.boostVx**2 + d_ufo.boostVy**2); let dx = -d_ufo.boostVx / vDist; let dy = -d_ufo.boostVy / vDist; for(let i=1; i<=3; i++) { ctx.fillStyle = `rgba(255, 255, 255, ${0.8 - i*0.2})`; ctx.beginPath(); // 本体に近いものほど大きく、不透明に ctx.arc(d_ufo.x + dx*i*10, d_ufo.y + dy*i*10, d_ufo.r*(1 - i*0.2), 0, Math.PI*2); ctx.fill(); } // UFO本体を「光のかたまり(白)」として描画 ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.ellipse(d_ufo.x, d_ufo.y, d_ufo.r, d_ufo.r, 0, 0, Math.PI*2); ctx.fill(); } else { // 通常のUFO描画 ctx.save(); if (d_ufo.invincible) { ctx.globalAlpha = 0.4; ctx.filter = 'blur(4px)'; ctx.drawImage(canvas, d_ufo.x - 30, d_ufo.y - 10, 60, 20); // ブレール残像 } ctx.translate(d_ufo.x, d_ufo.y); ctx.fillStyle = '#888'; ctx.beginPath(); ctx.ellipse(0, 0, 25, 10, 0, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#333'; ctx.beginPath(); ctx.arc(0, -5, 8, 0, Math.PI, true); ctx.fill(); ctx.fillStyle = '#0f0'; for(let i=-1; i<=1; i++) { ctx.beginPath(); ctx.arc(i*10, 2, 2, 0, Math.PI*2); ctx.fill(); } ctx.restore(); } // 4. 雲 (UFOの上に被せる) ctx.fillStyle = 'rgba(40, 40, 60, 0.95)'; d_clouds.forEach(c => { ctx.beginPath(); ctx.ellipse(c.x + c.w/2, c.y + c.h/2, c.w/2, c.h/2, 0, 0, Math.PI*2); ctx.fill(); }); // 5. テキスト演出 (一番手前) ctx.font = "bold 24px sans-serif"; ctx.textAlign = "center"; d_texts.forEach(t => { ctx.fillStyle = t.color; ctx.globalAlpha = t.life / t.maxLife; ctx.fillText(t.text, t.x, t.y); ctx.globalAlpha = 1.0; });}function updateUI() { const now = Date.now(); document.getElementById('timer').innerText = `TIME: ${Math.max(0, timeLeft).toFixed(1)}`; document.getElementById('stage').innerText = `STAGE: ${stage}`; const checkCool = (id, last, baseText) => { const el = document.getElementById(id); if (now - last < RECHARGE_TIME) { el.classList.add('cooldown'); el.innerText = baseText + ` (${Math.ceil((RECHARGE_TIME - (now - last))/1000)}s)`; } else { el.classList.remove('cooldown'); el.innerText = baseText; } }; checkCool('skill1', ufo.lastF, "F: ブレール (無敵 3秒)"); // スキル説明を変更 checkCool('skill2', ufo.lastJ, "J: ブースト (流れ星回避 1秒 + 無敵 3秒)"); checkCool('skill3', ufo.lastSpace, "SPACE: ブットビ (テレポート + 無敵 3秒)");}function endGame(result) { gameState = result; btnBox.innerHTML = ''; overlay.style.display = 'flex'; if (result === 'gameover') { msgDisplay.innerText = "見つかってしまった!"; msgDisplay.style.color = "#f00"; btnBox.innerHTML = ` <button id="btn-continue" class="btn">続きから (STAGE ${stage})</button> <button id="btn-restart" class="btn">最初から (STAGE 1)</button> <button id="btn-replay" class="btn">REPLAY</button> `; document.getElementById('btn-continue').onclick = () => initLevel(); document.getElementById('btn-restart').onclick = () => { stage = 1; initLevel(); }; document.getElementById('btn-replay').onclick = startReplay; } else { msgDisplay.innerText = `STAGE ${stage} CLEAR!`; msgDisplay.style.color = "#0f0"; btnBox.innerHTML = ` <button id="btn-next" class="btn">NEXT STAGE</button> <button id="btn-replay" class="btn">REPLAY</button> `; document.getElementById('btn-next').onclick = () => { stage++; initLevel(); }; document.getElementById('btn-replay').onclick = startReplay; }}window.onresize = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; };initLevel();</script></body></html>
■ 今日の学び
毎日1個ずつゲームを作ってきて、最初の頃と比べると、
AIが分かってくれて形にしてくれそうな説明や相談の仕方が
少しずつ上達してきたように思います。(^^*)
最初のゲーム案のところは、できるだけ分かりやすく、
少し長めに説明するようにしています。
具体的な動きやイメージを伝えると、AIがより正確に形にしてくれます。
AIから返ってきた最初のコードを確認しながら、
補足や世界観の調整を加えて、その日の仕上がりに近づけていきます。
限られた時間の中では、満足いくところまで到達できない日もありますが、
AIと一緒にゲームを作っている時間は、とても楽しいです。(>v<*)ノ
■ 次回は、夢がテーマです。
夢のリアリティや、健康との深い関係性を再現したゲームを作ります。
“眠りの世界”で起こる不思議な体験を、どこまでゲームとして表現できるのか——。
次回もお楽しみに。(*^v^*)ノシ
次の実験はこちら(想像と健康×夢と目覚):「夢メーカー」
ゲーム作りが楽しくて、どんどん作品が増えています(*^v^*)
もし応援していただけたら、とても励みになります。






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