■100個目 「保護サークル・サバイバル:丸太郎と突撃猪」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回は、ちょこまかと動き回るキャラクターを
“守り抜く”サバイバルゲームを作ってみました。
主人公は、白くて丸い体につぶらな瞳をした「丸太郎」。
見た目は可愛いのに、
V字の眉毛とニヤッと笑う口元がチャームポイントの、
ちょっと腕白で生意気な子です。(=v=*)
舞台は、木漏れ日が差し込む明るい広葉樹の森。
丸太郎がのんびり散歩していると、
森の奥から突然「ドッドッドッ」と地鳴りが響き、
イノシシの大群が猛スピードで突進してきます!(><;)
しかも丸太郎は、危険なんて気にしない性格。
あっちへふらふら、こっちへちょろちょろ、
気まぐれに走り回るので目が離せません。
プレイヤーは、そんな手のかかる丸太郎がイノシシに踏みつぶされないよう、
過保護なくらいに守ってあげる必要があります。
森の中には、取ると良いことが起きる
キラキラした不思議なアイテムも飛んできます。
「取りに行きたいけど、丸太郎から離れると危ない……!」という、
ハラハラするジレンマもこのゲームの魅力です。(´艸`)
手のかかる子ほど可愛いと言いますが、
無事に守り切れたときの愛着はひとしおですよ。
気になる操作方法や遊び方については、
後ほど詳しくご紹介していきますね!(=v=*)
■ AIとのやりとりで面白かった部分
● ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。HTMLで動かせるゲーム。
勝手に動き回るキャラクターに対して、
マウスカーソルで保護サークルをかけるゲームです。
サークルから外れるとバチバチとダメージを受けてしまう……
中毒性のある感じにできないかな。
AI: いらっしゃい!今日も元気だねえ。
マウスカーソルで「保護サークル」を作って、動き回る対象を守るゲーム。
このアイデア、すごくいいよ。
マウスでターゲットを追い続ける
“トラッキング系”のアクションは、
もともと中毒性がめちゃくちゃ高いジャンルなんだ。
そこに「サークルから外れた時の強烈なペナルティ(バチバチ感)」と
「徐々に上がる難易度」を加えれば、
やめどきを見失うゲームになるね。
みー:(*^_^)ダメージ波攻撃も入れたいです。
波に安全ゾーン外で当たると追加ダメージ。
逆に、一定時間安全ゾーンを維持すると回復ボーナス。
主人公は丸太郎にしましょう。
白い体に円らな瞳、腕白な感じのキャラクター。
AI: 「丸太郎」!いい名前じゃないか。
白くてつぶらな瞳なのに、ちょっと腕白で生意気な顔つき……
こういう手のかかるヤツほど、守り切った時の愛着が湧くんだよね。
「ダメージ波」と「保護ボーナス」の追加も最高。
波が来るたびに「絶対にサークルから出すな!」という緊張感が生まれるし、
ボーナスが入った時の「よっしゃ!」が気持ちいい。
みー:(=v=) アイテム要素も入れましょう。
- 白:点(小)
- 青:点(小)+スロー
- 金:点(大)
- 虹:点(大)+無敵
丸太郎からサークルを外さないといけないけど、
画面端を跳ね返りながら飛ぶ丸いアイテムを
マウスカーソルで触れて取る感じにできますか?
AI: いいねえ、乗ってきたね!
その「あっちもこっちも気にしなきゃいけないジレンマ」、
ゲームを面白くする最高のスパイスだよ。
丸太郎を守り続けなきゃいけないのに、
画面の端に美味しそうなアイテムがフワフワ飛んでくる……。
「今なら取れるか? いや、丸太郎が急カーブしたら危ないぞ……!」
という一瞬の葛藤が、プレイヤーの脳汁をドバドバ出すんだ。
ご所望の4種類のアイテムと特殊効果も実装しておいたよ。
アイテムを取るためにサークルを動かすから、
ゲームの動線が一気に熱くなるね。
みー:(^_^) 波をイノシシの大群にできますか?
AI: いらっしゃい!そのアイデアは最高に面白い!
ただの赤い波が、画面を埋め尽くす「イノシシの大群」になるわけだね。
腕白な丸太郎が、
突進してくるイノシシたちに踏みつぶされないように守る……。
一気に絵面が賑やかで、コミカルかつスリリングになるよ。
よし、「赤い波ギミック」を丸ごと「イノシシ突進」に書き換えたよ。
みー:(^_^) 背景が黒いと暗いので、
広葉樹の木漏れ日の明るい山の中のイメージにできますか?
保護サークルをほんの少しサイズアップできるかな。
AI: 雰囲気を変えたいんだな。
「木漏れ日が差す広葉樹の森」……最高じゃないか!
丸太郎も猪たちも山の生き物だから、舞台としてぴったりだよ。
背景を明るい緑と木漏れ日のイメージに変更して、
保護サークルの半径も少し(60 → 70)広げておいたよ。
■ 実際の動作
まずは、AIが最初に作ってくれた初期バージョンのゲーム画面です。
保護サークルから丸太郎がはみ出すと、
痛そうなパーティクルがバチバチと飛び散ります(><)

(画像:AIが最初に出してくれた初期画面)
こちらが、最終的に仕上がったゲームのスタート画面です。
マウス操作だけで遊べる、とてもシンプルなゲームになりました。

(画像:完成版のスタート画面)
前作のゲームの要素も引き継いでいます。
突撃猪:イノシシの集団
主人公:丸太郎
丸いアイテム
白:得点(小)
青:得点(小)+スロー
金:得点(大)
虹:得点(大)+無敵

(画像:ゲーム画面)
青い丸を取ると、丸太郎の動きがゆっくりになります。
一定時間、保護サークル内から出さずに守り続けると回復するので、
ダメージを取り返すチャンスになります。

(画像:スロー状態の丸太郎)
虹色の丸を取ると、丸太郎は無敵状態になります。
この間は丸太郎を気にせず、アイテムを取り放題です。

(画像:無敵状態の丸太郎)
■ コードのポイント
今回の記事では、
AI と相談しながら作った「保護サークル・サバイバル:丸太郎と突撃猪」 の HTML コードを公開します。
このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。
ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。
▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く
Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)
② 下のコードをコピーして貼り付けます。
③ 「marutarou2.html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。
■ 操作方法
操作はとっても簡単です!(^_^)
キーボードは使いません。マウスを動かすだけで遊べます。
画面の中のマウスカーソルが、
丸太郎を守る「保護サークル」になっています。
気まぐれに走り回る丸太郎がサークルの外に出ないように、
マウスで優しく追いかけてあげてくださいね。(^^*)
⚠️ サークルから外れると…
丸太郎がサークルの外に出てしまうと、
「ビババッ!」とダメージを受けて体力が減ってしまいます。(><;)
油断すると一瞬でピンチになるので、しっかり見守ってあげてください。
✨ ずっと守ってあげると…
逆に、サークルの中にずっと入れてあげると、
サークルの周りの緑色のゲージが溜まっていきます。
ゲージが一周すると体力が回復して、ボーナスポイントもゲット!
「よしよし、よくできたね」って撫でてあげるような感覚です。(=v=*)
🐗 突撃猪(イノシシ)に注意!
時々、画面に 「⚠️ BOAR STAMPEDE!」 という警告が出たあと、
イノシシの大群がドコドコと突進してきます!
この時にサークルの外にいると、
丸太郎は大ダメージを受けてしまいます。
でも、サークルの中にいれば「カキィィン!」と
安全にガードできます。(`・ω・´)
🎁 不思議なアイテムたち
画面をフワフワと飛んでいる色付きの玉は、
サークルの「フチ」を触れさせるだけでゲットできます。
- ⚪️ 白アイテム:100点ゲット!
- 🔵 青アイテム:一定時間、丸太郎の動きがゆっくりに。
落ち着いてゲージを溜める大チャンス✨ - 🟡 金アイテム:なんと500点ゲット!
- 🌈 虹アイテム:丸太郎がオーラを纏って無敵モードに。
サークルから外れても、イノシシにぶつかってもへっちゃらです。(´艸`)
アイテムを取りに行くためにサークルを動かすか、
それとも丸太郎のそばにぴったりくっついて守るか……。
簡単操作の中に隠れたハラハラドキドキのジレンマを、
ぜひ体験してみてくださいね!(^^*)
“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>Protection Circle Survival - Sunlight Forest</title> <style> body { margin: 0; /* ページ全体も森っぽい深い緑に */ background: linear-gradient(135deg, #2e7d32, #1b5e20); display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: white; user-select: none; } #gameContainer { position: relative; } canvas { /* 木漏れ日の明るい広葉樹林をCSSグラデーションで表現 */ background-color: #aed581; background-image: radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.6) 0%, transparent 20%), radial-gradient(circle at 75% 15%, rgba(255, 255, 255, 0.5) 0%, transparent 25%), radial-gradient(circle at 40% 70%, rgba(255, 255, 255, 0.4) 0%, transparent 30%), radial-gradient(circle at 85% 80%, rgba(255, 255, 255, 0.5) 0%, transparent 20%); border: 4px solid #336600; border-radius: 8px; box-shadow: 0 0 30px rgba(0, 0, 0, 0.6); cursor: none; } #ui { position: absolute; top: 15px; left: 20px; pointer-events: none; width: 560px; display: flex; justify-content: space-between; } #leftUi { display: flex; flex-direction: column; } #healthBarBg { width: 200px; height: 15px; background: #222; border: 2px solid #fff; border-radius: 8px; overflow: hidden; margin-top: 5px; box-shadow: 0 0 5px rgba(0,0,0,0.8); } #healthBar { width: 100%; height: 100%; background: #00ff88; transition: background 0.2s; } #score { font-size: 24px; font-weight: bold; color: #fff; /* 背景が明るいので黒い縁取りを強めに */ text-shadow: 2px 2px 4px rgba(0,0,0,0.9), -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; } #statusEffects { position: absolute; top: 70px; left: 20px; font-size: 16px; font-weight: bold; pointer-events: none; } .effect-badge { padding: 4px 8px; border-radius: 4px; margin-bottom: 5px; display: none; box-shadow: 0 0 5px rgba(0,0,0,0.8); border: 1px solid #fff; } #slowBadge { background: #3399ff; color: white; } #invincibleBadge { background: linear-gradient(45deg, #ff3366, #ff9933, #33ccff, #9933ff); color: white; } #warningText { font-size: 22px; font-weight: bold; color: #ff3300; text-shadow: 2px 2px 0px #fff, -1px -1px 0px #fff, 1px -1px 0px #fff, -1px 1px 0px #fff; opacity: 0; transition: opacity 0.1s; } .overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 50, 20, 0.85); /* 森っぽい暗幕 */ display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; border-radius: 4px; z-index: 10; } #startScreen h1 { font-size: 36px; color: #aed581; margin-bottom: 10px; text-shadow: 2px 2px 4px #000;} #messageBox h1 { margin: 0 0 10px 0; color: #ff3366; text-shadow: 2px 2px 4px #000;} p { font-size: 18px; margin-bottom: 20px; text-shadow: 1px 1px 2px #000; } .blink { animation: blinker 1.5s linear infinite; font-size: 16px; color: #ddd; cursor: pointer; font-weight:bold; } @keyframes blinker { 50% { opacity: 0; } } </style></head><body><div id="gameContainer"> <canvas id="gameCanvas" width="600" height="600"></canvas> <div id="ui"> <div id="leftUi"> <div id="score">Score: 0</div> <div id="healthBarBg"><div id="healthBar"></div></div> </div> <div id="warningText">⚠️ BOAR STAMPEDE!</div> </div> <div id="statusEffects"> <div id="slowBadge" class="effect-badge">丸太郎スロー中</div> <div id="invincibleBadge" class="effect-badge">無敵モード!</div> </div> <div id="startScreen" class="overlay"> <h1>丸太郎と突撃猪</h1> <p>マウスで保護サークルを操り、丸太郎を守り抜け!</p> <p class="blink">画面をクリックしてスタート</p> </div> <div id="messageBox" class="overlay" style="display: none;"> <h1>GAME OVER</h1> <p>丸太郎は力尽きた...</p> <div id="finalScore" style="font-size: 32px; font-weight: bold; margin-bottom: 20px; color: #00ff88; text-shadow: 2px 2px 4px #000;"></div> <p class="blink">画面をクリックしてリトライ</p> </div></div><script>let audioCtx;const Sound = { init: function() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if (audioCtx.state === 'suspended') audioCtx.resume(); }, playOsc: function(freq, type, dur, vol = 0.05, slideFreq = null) { if (!audioCtx || audioCtx.state !== 'running') return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freq, audioCtx.currentTime); if (slideFreq) osc.frequency.exponentialRampToValueAtTime(slideFreq, audioCtx.currentTime + dur); gain.gain.setValueAtTime(vol, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + dur); }, noise: function(dur, vol = 0.1, isLow = false) { if (!audioCtx || audioCtx.state !== 'running') return; const bufferSize = audioCtx.sampleRate * dur; 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 = isLow ? 'lowpass' : 'bandpass'; filter.frequency.value = isLow ? 400 : 1000; const gain = audioCtx.createGain(); gain.gain.setValueAtTime(vol, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + dur); noise.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination); noise.start(); }, bgmStep: 0, bgmNotes: [196.00, 196.00, 220.00, 196.00, 261.63, 196.00, 293.66, 261.63], playBGM: function(frames, isSlow, isInvincible) { if (!audioCtx || audioCtx.state !== 'running' || gameState !== 'playing') return; let interval = 12; if (isInvincible) interval = 6; else if (isSlow) interval = 24; if (frames % interval === 0) { let note = this.bgmNotes[this.bgmStep % this.bgmNotes.length]; if (isInvincible) note *= 1.5; else if (isSlow) note *= 0.5; this.playOsc(note, 'triangle', 0.1, 0.03); if (this.bgmStep % 2 === 0) this.noise(0.05, 0.02, true); this.bgmStep++; } }, coin: () => Sound.playOsc(880, 'sine', 0.1, 0.05, 1200), heal: () => { Sound.playOsc(523.25, 'sine', 0.1, 0.05); setTimeout(()=>Sound.playOsc(659.25, 'sine', 0.1, 0.05), 100); setTimeout(()=>Sound.playOsc(783.99, 'sine', 0.2, 0.05), 200); }, slow: () => Sound.playOsc(440, 'triangle', 0.5, 0.05, 110), invincible: () => { Sound.playOsc(440, 'square', 0.1, 0.05, 880); setTimeout(()=>Sound.playOsc(554, 'square', 0.1, 0.05, 1108), 100); setTimeout(()=>Sound.playOsc(659, 'square', 0.2, 0.05, 1318), 200); }, warning: () => Sound.playOsc(150, 'sawtooth', 0.2, 0.08, 200), damage: () => Sound.noise(0.1, 0.05, false), stampedeRumble: () => Sound.noise(0.1, 0.08, true), crash: () => Sound.noise(0.4, 0.2, true), guard: () => Sound.playOsc(1200, 'square', 0.1, 0.05, 2000)};const canvas = document.getElementById('gameCanvas');const ctx = canvas.getContext('2d');const scoreEl = document.getElementById('score');const healthBar = document.getElementById('healthBar');const startScreen = document.getElementById('startScreen');const messageBox = document.getElementById('messageBox');const finalScoreEl = document.getElementById('finalScore');const warningText = document.getElementById('warningText');const slowBadge = document.getElementById('slowBadge');const invincibleBadge = document.getElementById('invincibleBadge');let gameState = 'start';let score = 0, health = 100, frames = 0, shakeTime = 0;let slowTimer = 0, invincibleTimer = 0, safeFrames = 0;const MAX_SAFE_FRAMES = 180;// サークルサイズを60から70に僅かにサイズアップ!const mouse = { x: 300, y: 300, radius: 70 };let target = { x: 300, y: 300, radius: 18, vx: 0, vy: 0, speed: 2.5, angle: Math.random() * Math.PI * 2 };let stampede = { active: false, warning: 0, x: -150, speed: 10, hasHit: false, boars: [] };const BOAR_COUNT = 12;let items = [], particles = [], texts = [];canvas.addEventListener('mousemove', (e) => { const rect = canvas.getBoundingClientRect(); mouse.x = e.clientX - rect.left; mouse.y = e.clientY - rect.top;});document.addEventListener('click', () => { Sound.init(); if (gameState === 'start' || gameState === 'gameover') { startScreen.style.display = 'none'; resetGame(); }});function initStampedeBoars() { stampede.boars = []; for (let i = 0; i < BOAR_COUNT; i++) { stampede.boars.push({ offsetX: Math.random() * 80 - 40, y: (canvas.height / BOAR_COUNT) * i + (Math.random() * 20 - 10), size: Math.random() * 5 + 20 }); }}function resetGame() { score = 0; health = 100; frames = 0; slowTimer = 0; invincibleTimer = 0; safeFrames = 0; target.x = 300; target.y = 300; target.speed = 2.5; stampede.active = false; stampede.warning = 0; items = []; particles = []; texts = []; gameState = 'playing'; messageBox.style.display = 'none'; canvas.style.cursor = 'none'; warningText.style.opacity = 0; slowBadge.style.display = 'none'; invincibleBadge.style.display = 'none'; updateHealthBar(); loop();}function updateHealthBar() { healthBar.style.width = Math.max(0, health) + '%'; if (health > 60) healthBar.style.background = '#00ff88'; else if (health > 30) healthBar.style.background = '#ffcc00'; else healthBar.style.background = '#ff3366';}function createSparks(x, y, color = null, count = 5) { for(let i = 0; i < count; i++) { particles.push({ x: x, y: y, vx: (Math.random() - 0.5) * 10, vy: (Math.random() - 0.5) * 10, life: 1, decay: Math.random() * 0.05 + 0.02, color: color || `rgba(255, ${Math.random() * 150 + 50}, 0, ` }); }}function addFloatingText(text, x, y, color) { texts.push({ text: text, x: x, y: y, life: 1, color: color }); }function spawnItem() { if (items.length >= 4) return; const types = ['white', 'blue', 'gold', 'rainbow']; const weights = [0.45, 0.25, 0.20, 0.10]; let r = Math.random(), type = 'white'; for (let i = 0; i < types.length; i++) { r -= weights[i]; if (r <= 0) { type = types[i]; break; } } const angle = Math.random() * Math.PI * 2, itemSpeed = Math.random() * 1.5 + 1.5; items.push({ x: Math.random() * (canvas.width - 40) + 20, y: Math.random() * (canvas.height - 40) + 20, radius: 10, type: type, vx: Math.cos(angle) * itemSpeed, vy: Math.sin(angle) * itemSpeed });}function drawBoar(x, y, size, frameOffset) { ctx.save(); ctx.translate(x, y); ctx.translate(0, Math.sin(frames * 0.5 + frameOffset) * 3); ctx.fillStyle = '#8b4513'; ctx.beginPath(); ctx.ellipse(0, 0, size, size * 0.7, 0, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#a0522d'; ctx.beginPath(); ctx.ellipse(size * 0.7, size * 0.2, size * 0.4, size * 0.3, Math.PI * 0.1, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(size * 0.6, -size * 0.1, size * 0.15, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(size * 0.65, -size * 0.1, size * 0.08, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#8b4513'; ctx.beginPath(); ctx.moveTo(0, -size * 0.5); ctx.lineTo(size * 0.3, -size * 0.9); ctx.lineTo(size * 0.5, -size * 0.5); ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(size * 0.8, size * 0.3); ctx.quadraticCurveTo(size * 1.1, size * 0.3, size * 1.0, 0); ctx.stroke(); ctx.strokeStyle = '#8b4513'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(-size, 0); ctx.lineTo(-size * 1.3, Math.sin(frames * 0.2 + frameOffset) * 5); ctx.stroke(); ctx.restore();}function update() { if (gameState !== 'playing') return; frames++; Sound.playBGM(frames, slowTimer > 0, invincibleTimer > 0); target.speed += 0.0002; if (slowTimer > 0) slowTimer--; if (invincibleTimer > 0) invincibleTimer--; slowBadge.style.display = slowTimer > 0 ? 'block' : 'none'; invincibleBadge.style.display = invincibleTimer > 0 ? 'block' : 'none'; let currentSpeed = target.speed * (slowTimer > 0 ? 0.4 : 1); if (Math.random() < 0.03 + (frames * 0.00001)) target.angle += (Math.random() - 0.5) * Math.PI * 1.2; target.vx = Math.cos(target.angle) * currentSpeed; target.vy = Math.sin(target.angle) * currentSpeed; target.x += target.vx; target.y += target.vy; if (target.x - target.radius < 0 || target.x + target.radius > canvas.width) { target.vx *= -1; target.x = Math.max(target.radius, Math.min(canvas.width - target.radius, target.x)); target.angle = Math.atan2(target.vy, target.vx); } if (target.y - target.radius < 0 || target.y + target.radius > canvas.height) { target.vy *= -1; target.y = Math.max(target.radius, Math.min(canvas.height - target.radius, target.y)); target.angle = Math.atan2(target.vy, target.vx); } const isSafe = Math.hypot(target.x - mouse.x, target.y - mouse.y) + target.radius <= mouse.radius; if (isSafe || invincibleTimer > 0) { safeFrames++; score += currentSpeed * 0.5; if (safeFrames >= MAX_SAFE_FRAMES) { health = Math.min(100, health + 24); score += 500; safeFrames = 0; Sound.heal(); createSparks(target.x, target.y, `rgba(0, 255, 136, `); addFloatingText("HEAL & BONUS!", target.x, target.y - 30, "#00ff88"); updateHealthBar(); } } else { safeFrames = 0; health -= 0.6; if (frames % 6 === 0) { createSparks(target.x, target.y); Sound.damage(); } updateHealthBar(); } if (frames % 150 === 0) spawnItem(); for (let i = items.length - 1; i >= 0; i--) { let item = items[i]; item.x += item.vx; item.y += item.vy; if (item.x - item.radius < 0 || item.x + item.radius > canvas.width) item.vx *= -1; if (item.y - item.radius < 0 || item.y + item.radius > canvas.height) item.vy *= -1; if (Math.hypot(item.x - mouse.x, item.y - mouse.y) <= mouse.radius + item.radius) { if (item.type === 'white') { score += 100; Sound.coin(); addFloatingText("+100", item.x, item.y, "#fff"); createSparks(item.x, item.y, `rgba(255,255,255,`, 8); } else if (item.type === 'blue') { score += 100; slowTimer = 360; Sound.slow(); addFloatingText("SLOW DOWN!", item.x, item.y, "#3399ff"); createSparks(item.x, item.y, `rgba(51,153,255,`, 12); } else if (item.type === 'gold') { score += 500; Sound.coin(); addFloatingText("+500!", item.x, item.y, "#ffd700"); createSparks(item.x, item.y, `rgba(255,215,0,`, 15); } else if (item.type === 'rainbow') { score += 500; invincibleTimer = 300; Sound.invincible(); addFloatingText("INVINCIBLE!!", item.x, item.y, "#ff33cc"); createSparks(item.x, item.y, `rgba(255,51,204,`, 20); } items.splice(i, 1); } } if (!stampede.active && stampede.warning === 0 && Math.random() < 0.002) { stampede.warning = 70; warningText.style.opacity = 1; initStampedeBoars(); } if (stampede.warning > 0) { if (stampede.warning % 15 === 0) Sound.warning(); stampede.warning--; if (stampede.warning <= 0) { stampede.active = true; stampede.x = -200; stampede.hasHit = false; warningText.style.opacity = 0; } } if (stampede.active) { stampede.x += stampede.speed; if (frames % 4 === 0) Sound.stampedeRumble(); if (!stampede.hasHit && target.x < stampede.x + 60 && target.x > stampede.x - 60) { if (invincibleTimer > 0) { // 無敵時はセーフ } else if (!isSafe) { stampede.hasHit = true; health -= 30; shakeTime = 20; Sound.crash(); createSparks(target.x, target.y, `rgba(139, 69, 19, `, 20); addFloatingText("STAMPEDED!!", target.x, target.y - 30, "#ff3366"); updateHealthBar(); } else { createSparks(target.x, target.y, `rgba(100, 200, 255, `, 3); if (frames % 10 === 0) Sound.guard(); } } if (stampede.x > canvas.width + 300) stampede.active = false; } if (health <= 0) { gameState = 'gameover'; Sound.crash(); messageBox.style.display = 'flex'; finalScoreEl.innerText = `Score: ${Math.floor(score)}`; canvas.style.cursor = 'default'; } scoreEl.innerText = `Score: ${Math.floor(score)}`; particles.forEach(p => { p.x += p.vx; p.y += p.vy; p.life -= p.decay; }); particles = particles.filter(p => p.life > 0); texts.forEach(t => { t.y -= 1; t.life -= 0.02; }); texts = texts.filter(t => t.life > 0);}function draw() { ctx.save(); if (shakeTime > 0) { ctx.translate((Math.random() - 0.5) * 15, (Math.random() - 0.5) * 15); shakeTime--; } // CSSの木漏れ日背景を活かすため、クリアして透明にする ctx.clearRect(0, 0, canvas.width, canvas.height); if (stampede.active) stampede.boars.forEach((b, i) => drawBoar(stampede.x + b.offsetX, b.y, b.size, i)); else if (stampede.warning > 0 && Math.floor(frames / 5) % 2 === 0) { ctx.fillStyle = 'rgba(255, 100, 50, 0.2)'; ctx.fillRect(0, 0, canvas.width, canvas.height); } if (safeFrames > 0 && invincibleTimer === 0) { ctx.save(); ctx.beginPath(); ctx.arc(mouse.x, mouse.y, mouse.radius + 8, -Math.PI / 2, -Math.PI / 2 + (Math.PI * 2 * (safeFrames / MAX_SAFE_FRAMES))); ctx.strokeStyle = '#00ff88'; ctx.lineWidth = 4; ctx.shadowColor = '#000'; ctx.shadowBlur = 4; // 背景が明るいのでゲージにも影を ctx.stroke(); ctx.restore(); } ctx.beginPath(); ctx.arc(mouse.x, mouse.y, mouse.radius, 0, Math.PI * 2); ctx.strokeStyle = invincibleTimer > 0 ? `hsl(${frames * 5 % 360}, 100%, 60%)` : (shakeTime > 0 ? '#ff3366' : '#2e7d32'); ctx.lineWidth = invincibleTimer > 0 ? 4 : 3; ctx.stroke(); ctx.fillStyle = invincibleTimer > 0 ? `hsla(${frames * 5 % 360}, 100%, 60%, 0.15)` : (shakeTime > 0 ? 'rgba(255, 51, 102, 0.3)' : 'rgba(255, 255, 255, 0.25)'); ctx.fill(); items.forEach(item => { ctx.save(); ctx.beginPath(); ctx.arc(item.x, item.y, item.radius, 0, Math.PI * 2); if (item.type === 'white') { ctx.fillStyle = '#ffffff'; ctx.shadowColor = '#000'; } else if (item.type === 'blue') { ctx.fillStyle = '#3399ff'; ctx.shadowColor = '#000'; } else if (item.type === 'gold') { ctx.fillStyle = '#ffd700'; ctx.shadowColor = '#000'; } else if (item.type === 'rainbow') { ctx.fillStyle = `hsl(${frames * 8 % 360}, 100%, 60%)`; ctx.shadowColor = '#000'; } ctx.shadowBlur = 8; ctx.fill(); ctx.restore(); }); ctx.save(); ctx.translate(target.x, target.y); ctx.rotate(target.angle + Math.PI / 2); ctx.beginPath(); ctx.arc(0, 0, target.radius, 0, Math.PI * 2); ctx.fillStyle = '#fff'; // 背景が明るいので、無敵じゃない時は黒っぽい影を落として見やすくする ctx.shadowBlur = invincibleTimer > 0 ? 25 : 6; ctx.shadowColor = invincibleTimer > 0 ? `hsl(${frames * 10 % 360}, 100%, 50%)` : 'rgba(0,0,0,0.4)'; ctx.fill(); ctx.shadowBlur = 0; if (slowTimer > 0) { ctx.beginPath(); ctx.arc(0, 0, target.radius, 0, Math.PI * 2); ctx.fillStyle = 'rgba(51, 153, 255, 0.4)'; ctx.fill(); } ctx.fillStyle = '#111'; ctx.beginPath(); ctx.arc(-6, -4, 2.5, 0, Math.PI * 2); ctx.arc(6, -4, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#111'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(-10, -9); ctx.lineTo(-4, -7); ctx.moveTo(10, -9); ctx.lineTo(4, -7); ctx.stroke(); ctx.beginPath(); ctx.arc(0, 3, 5, 0, Math.PI); ctx.stroke(); ctx.restore(); particles.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, Math.random() * 3 + 1, 0, Math.PI * 2); ctx.fillStyle = `${p.color}${p.life})`; ctx.fill(); }); texts.forEach(t => { ctx.fillStyle = t.color; ctx.globalAlpha = t.life; ctx.font = "bold 18px Arial"; ctx.textAlign = "center"; ctx.shadowColor = '#000'; ctx.shadowBlur = 4; // 文字にも影をつけて読みやすく ctx.fillText(t.text, t.x, t.y); ctx.shadowBlur = 0; ctx.globalAlpha = 1.0; }); ctx.restore();}function loop() { if (gameState === 'playing') { update(); draw(); requestAnimationFrame(loop); }}</script></body></html>
■ 今日の学び
実は今回、この「100のゲーム実験室」にとって、
記念すべき 100個目の記事 になります!( ⁎ᵕᴗᵕ⁎ )✨
そして、その100個目を飾るゲームは……
なんと、ナンバリング「#2」にあたる『丸太郎と突撃猪』です!
初期の頃に作った前作「#2」とはまったく違うゲームになりましたが、
いろんな要素がうまくカチッとハマって、
前作に負けない “手に汗握る” ゲームに仕上がりました。┗(`・ω・´)┛フンス
何より驚いたのは、丸太郎の生意気なお顔や、
ドドドッと押し寄せるイノシシの大群など、 見た目も動きも、
AIと一緒にゲームを作り始めた頃と比べて
すごく良くなっていることです。( Ꙭ)‼︎
振り返ってみると、オリジナルの「#2」を作ったのは今年の1月26日。
そして今回の新しい丸太郎を生み出したのは6月16日でした。
この数ヶ月の間に、AI自身が賢く成長したのか、
それとも私の「AIへの相談のしかた」が上手になったのか……。
( ˘ω˘ )。oO(きっと、両方ですね!)
どちらにしても、自分の思い描いた
「いい感じの仕上がり」に近づけられるようになってきて、
今、ゲーム作りがとっても楽しいです!٩( ‘ω’ )و
今回の『丸太郎と突撃猪』は、
そんな私たちの実験室の変化と成長を感じるのにぴったりの、
記念すべき100個目のゲームになったと思います。
これからも、ドタバタ楽しく実験を続けていきますので、
引き続きよろしくお願いします!(っ‘ω’c)ワクワク
■ 次回は、101個目のゲーム
「100のゲーム実験室」の原点に戻ります。
この実験室がスタートしたのは R8年1月22日。
AIとの最初の一歩が、毎日1つゲームを作るきっかけになりました。
そこから約5か月と少し。
一つ一つのゲームはまだまだ不完全ですが、
7月4日現在で作ったゲームは 170 を超えました!
不完全でもいい、発展途上でもいい。
その積み重ねが、この実験室の“美学”になってきた気がします。(*^v^)
次回の 101 個目は、そんな原点を思い出すようなゲームです。
ぜひ楽しみにしていてくださいね。(*^v^)ノシ
▶ 次の実験はこちら
(発展途上と、不完全の美学、AIと考える100ゲーム実験室)
「歩いて宙返りするパンダ」
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)





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