実験#33:探す側も隠れる側も全力(UFOかくれんぼ)

■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秒間」使えなくなります。使いどころを見極めてくださいね。

迫りくる光のプレッシャーに負けず、
ぜひ地球の夜空をスイスイと飛び回ってみてください!

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<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^*)
 もし応援していただけたら、とても励みになります。

~このブログについて~

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

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


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

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

続きを読む