■29個目「シーグラスチェイン」Sea Glass Chain
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
大好きなシーグラスを、またまたゲームにしてみました。
いつもは「こんなゲームが作りたい」とAIへ提案しているのですが、
今回は逆に、AIからゲームの基礎アイデアをもらってスタートしました。
「自分が夢中で遊べるゲームを作りたい!」とAIと相談しながら形にした作品です。
操作はクリック一回だけの、とっても簡単なゲーム。
気づけば「次もう一回!」と止まらなくなる中毒性があります。
ぜひ一度は体験してほしい、楽しいゲームになりました。ヽ(*^o^*)ノ
■ AIとのやりとりで面白かった部分
(^^*)今日もゲーム作ります。html形式で動かせるゲーム。
中毒性のあるゲーム作りたいね。
(AI)いらっしゃいませ!今日も新作の開発ですね。
「中毒性」という言葉、クリエイターの心をくすぐる最高のスパイスです。
シンプルだけど、ついつい「もう一回!」と指が動いてしまう……
そんな、メモ帳アプリでもサクサク動くHTMLゲームのアイデアを練ってみました。
●「増える」の視覚的快感:連鎖爆発パズル
中毒ポイント:1タップで画面全体が埋め尽くされる爽快感。
内容:粒子をタップして爆発 → 触れた粒子が次々誘爆していくパズル。
(^_^) 連鎖爆発パズルいいですね。粒子をシーグラスで再現できるかな。
(AI)シーグラスをモチーフにするのは視覚的にもすごく美しいですね!
透け感とパステルカラーが連鎖して弾ける様子は、まさに中毒性のある心地よさです。
●今日の豆知識
シーグラスが「海の宝石」と呼ばれるようになるまで、
20〜30年も波に揺られているそうです。
このゲームも、1回のタップでその「時間の結晶」が
一気に弾けるような儚さを表現しましょう。
(^^*)いい感じです。爆発した時にガラスの破片を散らしたいです。
あと、神秘的なBGMと効果音を入れたいね。
(AI)深海では音は空気中より約4.5倍速く伝わるんですよ。
連鎖がどんどん加速していく感じも、水の性質をイメージして調整してみました。
ブラウザの仕様上、最初のクリックで音が有効になります。
シーグラスが「チリン、チリン」と連鎖していく感覚、ぜひ楽しんでください。
■ 実際の動作
まずは、AIとの相談で作ってもらったゲームの初期状態です
シーグラスがふわふわと漂っていて、好きな場所をクリックすると
そこから連鎖がスタートします。

(画像:AIが最初に作った状態)
こちらが今回の仕上がり状態です。
レアな赤色やラベンダー色のシーグラスも追加されました。
背景は波をイメージしていて、シーグラスはランダムに漂っています。

(画像:スタートの状態)
ここが一番気持ちいい瞬間です。(*>v<)”
クリックすると、そこから連鎖が広がっていきます。
円の届く範囲にシーグラスがあると誘爆していきます。
レアな色は、爆発の円が通常より 1.2 倍大きくなります。

(画像:連鎖の状態)
クリア条件のシーグラスを消すと次のステージへ進みます。
ステージが進むごとに、クリアに必要な数が少しずつ増えていきます。

(画像:クリアした状態)
最後に、実際に拾ったシーグラスや貝殻の写真です。
この“海の宝物”たちが、今回のゲームのモチーフになっています。

(画像:おはじきのシーグラス)

(画像:拾った貝殻)
この色がゲームに使われています

(画像:シーグラスの写真)
■ コードのポイント
今回の記事では、AI と相談しながら作った
「シーグラスチェイン」の HTML コードを公開します。
ブラウザだけで動く、とてもシンプルなゲームです。
コードといっても難しい部分はありません。
貼り付けて保存するだけで動くので、気軽に試してみてください。
PC の「メモ帳」(※Windows の場合)を開き、 下のコードをコピーして貼り付け、
「chein.html」など 好きな名前で保存してください。
保存したファイルをブラウザで開くだけで遊べます。
【操作方法】
1. 画面を1回だけタップ(クリック)します。
2. タップした場所に「波紋(爆発)」が発生します。
3. その波紋に触れたシーグラスが、キラキラと輝きながら自分も波紋を発します。
4. 次々と連鎖して、画面上の何割を消せたかでスコアが決まります。
▼ PC版コードはこちら
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>Sea Glass Chain - Wavy & Level Up (Lavender Added)</title> <style> body { margin: 0; overflow: hidden; background: #001a33; font-family: sans-serif; user-select: none; } canvas { display: block; cursor: crosshair; } #ui { position: absolute; top: 10px; left: 10px; color: white; pointer-events: none; text-shadow: 1px 1px 2px black; } .btn { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); padding: 12px 24px; font-size: 20px; background: rgba(0, 150, 255, 0.3); color: white; border: 2px solid white; border-radius: 8px; cursor: pointer; display: none; pointer-events: auto; transition: 0.2s; } .btn:hover { background: rgba(0, 150, 255, 0.6); box-shadow: 0 0 10px white; } </style></head><body> <div id="ui"> <h2>Sea Glass Chain - Level <span id="levelNum">1</span></h2> <p>画面をタップして連鎖スタート!(レアな赤・ピンク・ラベンダーは波紋が広範囲)</p> <p id="targetScore" style="color: #ffcc00; font-weight: bold;">クリア目標: 50 / 150</p> <p id="score">現在の連鎖: 0</p> </div> <button id="nextBtn" class="btn" onclick="nextLevel()">クリア!次のレベルへ</button> <button id="retryBtn" class="btn" onclick="retryLevel()" style="background: rgba(255, 50, 50, 0.3);">失敗…やり直す</button> <canvas id="gameCanvas"></canvas> <script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const levelEl = document.getElementById('levelNum'); const targetEl = document.getElementById('targetScore'); const scoreEl = document.getElementById('score'); const nextBtn = document.getElementById('nextBtn'); const retryBtn = document.getElementById('retryBtn'); let width, height; let particles = [], ripples = [], shards = []; let gameStarted = false, gameEnded = false; let chainCount = 0; // レベル管理 let level = 1; const MAX_PARTICLES = 150; let targetCount = 50; // 音声設定 let audioCtx = null; function initAudio() { if (audioCtx) { if(audioCtx.state === 'suspended') audioCtx.resume(); return; } audioCtx = new (window.AudioContext || window.webkitAudioContext)(); startBGM(); } function startBGM() { const notes = [440, 523.25, 587.33, 659.25, 783.99]; setInterval(() => { if (!gameStarted || gameEnded) return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(notes[Math.floor(Math.random() * notes.length)], audioCtx.currentTime); gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.04, audioCtx.currentTime + 1); gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 2.5); osc.connect(gain).connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 2.5); }, 3000); } function playChainSound(count, isRare) { if (!audioCtx) return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); const baseFreq = isRare ? 1200 : 880; // レアは少し高い音 osc.type = 'triangle'; osc.frequency.setValueAtTime(baseFreq + (count * 15), audioCtx.currentTime); gain.gain.setValueAtTime(0.1, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3); osc.connect(gain).connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.3); } // --- ゲームクラス --- const NORMAL_COLORS = ['#ADFF2F', '#87CEEB', '#FFD700', '#F0F8FF', '#40E0D0']; class Particle { constructor() { this.reset(); } reset() { this.x = Math.random() * width; this.y = Math.random() * height; this.vx = (Math.random() - 0.5) * (0.8 + level * 0.1); this.vy = (Math.random() - 0.5) * (0.8 + level * 0.1); this.size = Math.random() * 8 + 6; this.active = true; this.borderRadius = [Math.random()*10, Math.random()*10, Math.random()*10, Math.random()*10]; // --- 追加部分:レア度の確率計算 --- const typeRand = Math.random(); if (typeRand < 0.10) { // 全体の10%がレア this.isRare = true; if (typeRand < 0.01) { this.color = '#FF4500'; // 1%の確率で「赤」 } else if (typeRand < 0.025) { this.color = '#FF69B4'; // 1.5%の確率で「ピンク」 } else { this.color = '#E6E6FA'; // 7.5%の確率で「ラベンダー」 } } else { // 残り90%はノーマル this.isRare = false; this.color = NORMAL_COLORS[Math.floor(Math.random() * NORMAL_COLORS.length)]; } } update() { if (!this.active) return; this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > width) this.vx *= -1; if (this.y < 0 || this.y > height) this.vy *= -1; } draw() { if (!this.active) return; ctx.fillStyle = this.color + "CC"; if(this.isRare) { ctx.shadowColor = this.color; ctx.shadowBlur = this.color === '#FF4500' ? 15 : 10; // 赤は強めに光る } else { ctx.shadowBlur = 0; } ctx.beginPath(); ctx.roundRect(this.x - this.size/2, this.y - this.size/2, this.size, this.size, this.borderRadius); ctx.fill(); ctx.shadowBlur = 0; // リセット } explode() { this.active = false; playChainSound(chainCount, this.isRare); // 赤色は破片を少し多めに散らす let sc = 6; if (this.isRare) sc = this.color === '#FF4500' ? 12 : 10; for (let i = 0; i < sc; i++) { shards.push(new Shard(this.x, this.y, this.color)); } } } class Ripple { // 色を受け取れるように変更 constructor(x, y, isRare = false, color = '#FFFFFF') { this.x = x; this.y = y; this.radius = 0; this.isRare = isRare; this.color = color; // レアなら寿命を1.2倍にして、より遠くまで広がるようにする this.life = isRare ? 120 : 100; this.active = true; } update() { this.radius += 2.0; this.life -= 1.5; if (this.life <= 0) this.active = false; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); const alpha = Math.max(0, this.life / (this.isRare ? 120 : 100)); // 色に応じて波紋の色を変化 if (this.isRare) { if (this.color === '#FF4500') { ctx.strokeStyle = `rgba(255, 100, 100, ${alpha})`; } else if (this.color === '#FF69B4') { ctx.strokeStyle = `rgba(255, 150, 200, ${alpha})`; } else if (this.color === '#E6E6FA') { ctx.strokeStyle = `rgba(200, 180, 255, ${alpha})`; // ラベンダー色の波紋 } else { ctx.strokeStyle = `rgba(255, 100, 150, ${alpha})`; } ctx.lineWidth = 3; } else { ctx.strokeStyle = `rgba(200, 240, 255, ${alpha})`; ctx.lineWidth = 1.5; } ctx.stroke(); } } class Shard { constructor(x, y, color) { this.x = x; this.y = y; this.color = color; this.size = Math.random() * 3 + 1; const angle = Math.random() * Math.PI * 2; const speed = Math.random() * 4 + 1; this.vx = Math.cos(angle) * speed; this.vy = Math.sin(angle) * speed; this.gravity = 0.05; this.life = 100; this.active = true; } update() { this.x += this.vx; this.y += this.vy; this.vy += this.gravity; this.life -= 2.0; if (this.life <= 0) this.active = false; } draw() { ctx.fillStyle = this.color; ctx.globalAlpha = Math.max(0, this.life / 100); ctx.fillRect(this.x, this.y, this.size, this.size); ctx.globalAlpha = 1; } } function initLevel() { width = canvas.width = window.innerWidth; height = canvas.height = window.innerHeight; // レベルに応じた目標値の計算(最大90%) const ratio = Math.min(0.3 + (level * 0.05), 0.9); targetCount = Math.floor(MAX_PARTICLES * ratio); levelEl.innerText = level; targetEl.innerText = `クリア目標: ${targetCount} / ${MAX_PARTICLES}`; scoreEl.innerText = `現在の連鎖: 0`; particles = Array.from({ length: MAX_PARTICLES }, () => new Particle()); ripples = []; shards = []; chainCount = 0; gameStarted = false; gameEnded = false; nextBtn.style.display = 'none'; retryBtn.style.display = 'none'; } // 波打つ背景の描画 function drawWaves() { const time = Date.now() * 0.001; // 奥の波 ctx.fillStyle = 'rgba(0, 40, 80, 0.4)'; ctx.beginPath(); ctx.moveTo(0, height); for(let x = 0; x <= width; x += 30) { let y = height * 0.4 + Math.sin(x * 0.003 + time) * 60 + Math.sin(x * 0.01 - time * 0.5) * 20; ctx.lineTo(x, y); } ctx.lineTo(width, height); ctx.fill(); // 手前の波 ctx.fillStyle = 'rgba(0, 60, 100, 0.3)'; ctx.beginPath(); ctx.moveTo(0, height); for(let x = 0; x <= width; x += 30) { let y = height * 0.6 + Math.sin(x * 0.004 - time * 0.8) * 40; ctx.lineTo(x, y); } ctx.lineTo(width, height); ctx.fill(); } function animate() { // 背景クリア ctx.fillStyle = '#001a33'; ctx.fillRect(0, 0, width, height); // 背景の波を描画 drawWaves(); particles.forEach(p => { p.update(); p.draw(); ripples.forEach(r => { const dist = Math.hypot(p.x - r.x, p.y - r.y); if (p.active && dist < r.radius + p.size && dist > r.radius - 20) { p.explode(); // 爆発した粒子の色を波紋に引き継がせる ripples.push(new Ripple(p.x, p.y, p.isRare, p.color)); chainCount++; scoreEl.innerText = `現在の連鎖: ${chainCount}`; } }); }); ctx.globalCompositeOperation = 'lighter'; shards = shards.filter(s => s.active); shards.forEach(s => { s.update(); s.draw(); }); ctx.globalCompositeOperation = 'source-over'; ripples = ripples.filter(r => r.active); ripples.forEach(r => { r.update(); r.draw(); }); // ゲーム終了判定 if (gameStarted && !gameEnded && ripples.length === 0 && shards.length === 0) { gameEnded = true; if (chainCount >= targetCount) { nextBtn.style.display = 'block'; } else { retryBtn.style.display = 'block'; } } requestAnimationFrame(animate); } window.addEventListener('mousedown', (e) => { if (e.target.tagName === 'BUTTON') return; initAudio(); if (!gameStarted) { // 最初のタップ波紋は白色 ripples.push(new Ripple(e.clientX, e.clientY, false, '#FFFFFF')); gameStarted = true; } }); function nextLevel() { level++; initLevel(); } function retryLevel() { initLevel(); } window.addEventListener('resize', initLevel); initLevel(); animate(); </script></body></html>
■ スマホ版コード(タッチ操作対応)
この「シーグラスチェイン」は、AIとの相談で
スマホでも遊べるようにしたタッチ操作バージョンも作っていました。(^^*)
画面を指でタップすると、その場所から波紋(爆発)が広がります。
PC版と同じように、シーグラスが連鎖していく気持ちよさを楽しめます。
【スマホでの操作方法】
・画面を1回タップ → 波紋(爆発)が発生
・波紋に触れたシーグラスが誘爆していきます
・クリア条件の数を消すと次のステージへ進みます
▼ スマホ版コードはこちら
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>Sea Glass Chain - Perfect Control</title> <style> body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #001a33; font-family: sans-serif; user-select: none; -webkit-user-select: none; touch-action: none; } canvas { display: block; width: 100%; height: 100%; cursor: crosshair; } #ui { position: absolute; top: 10px; left: 10px; color: white; pointer-events: none; text-shadow: 1px 1px 3px rgba(0,0,0,0.8); z-index: 10; } h2 { margin: 0 0 5px 0; font-size: 20px; } p { margin: 2px 0; font-size: 14px; } #targetScore { font-size: 16px; } /* 点滅するスタートメッセージ */ .blink { animation: blinker 1.5s linear infinite; color: #87CEEB; font-weight: bold; font-size: 16px; margin-top: 5px; } @keyframes blinker { 50% { opacity: 0.3; } } .btn { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); padding: 15px 30px; font-size: 20px; font-weight: bold; background: rgba(0, 150, 255, 0.4); color: white; border: 2px solid rgba(255,255,255,0.8); border-radius: 10px; cursor: pointer; display: none; pointer-events: auto; z-index: 20; box-shadow: 0 4px 10px rgba(0,0,0,0.5); } #portrait-warning { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: #001a33; color: white; z-index: 9999; text-align: center; flex-direction: column; justify-content: center; align-items: center; } #portrait-warning svg { width: 50px; height: 50px; margin-bottom: 20px; animation: rotatePhone 2s infinite ease-in-out; } @keyframes rotatePhone { 0% { transform: rotate(0deg); } 50% { transform: rotate(-90deg); } 100% { transform: rotate(-90deg); } } @media screen and (orientation: portrait) { #portrait-warning { display: flex; } } </style></head><body> <div id="portrait-warning"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect><line x1="12" y1="18" x2="12.01" y2="18"></line></svg> <h3>スマホを横向きにしてね</h3> <p>画面いっぱいの海を楽しむための設定です</p> </div> <div id="ui"> <h2>Sea Glass Chain - Level <span id="levelNum">1</span></h2> <p id="startMsg" class="blink">画面をタップして連鎖スタート!</p> <p id="targetScore" style="color: #ffcc00; font-weight: bold;">クリア目標: 0 / 0</p> <p id="score">現在の連鎖: 0</p> </div> <button id="nextBtn" class="btn">次のレベルへ</button> <button id="retryBtn" class="btn" style="background: rgba(255, 50, 50, 0.4);">やり直す</button> <canvas id="gameCanvas"></canvas> <script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const levelEl = document.getElementById('levelNum'); const targetEl = document.getElementById('targetScore'); const scoreEl = document.getElementById('score'); const startMsg = document.getElementById('startMsg'); const nextBtn = document.getElementById('nextBtn'); const retryBtn = document.getElementById('retryBtn'); let width, height; let particles = [], ripples = [], shards = []; let gameStarted = false, gameEnded = false; let chainCount = 0; let level = 1; let currentMaxParticles = 150; let targetCount = 50; let SCALE = 1; let audioCtx = null; function initAudio() { try { if (audioCtx) { if(audioCtx.state === 'suspended') audioCtx.resume(); return; } const AC = window.AudioContext || window.webkitAudioContext; if (AC) { audioCtx = new AC(); startBGM(); } } catch (e) {} } function startBGM() { const notes = [440, 523.25, 587.33, 659.25, 783.99]; setInterval(() => { if (!gameStarted || gameEnded || !audioCtx) return; try { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(notes[Math.floor(Math.random() * notes.length)], audioCtx.currentTime); gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.04, audioCtx.currentTime + 1); gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 2.5); osc.connect(gain).connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 2.5); } catch(e) {} }, 3000); } function playChainSound(count, isRare) { if (!audioCtx) return; try { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); const baseFreq = isRare ? 1200 : 880; osc.type = 'triangle'; osc.frequency.setValueAtTime(Math.min(baseFreq + (count * 15), 3000), audioCtx.currentTime); gain.gain.setValueAtTime(0.08, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3); osc.connect(gain).connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.3); } catch(e) {} } const NORMAL_COLORS = ['#ADFF2F', '#87CEEB', '#FFD700', '#F0F8FF', '#40E0D0']; class Particle { constructor() { this.reset(); } reset() { this.x = Math.random() * width; this.y = Math.random() * height; this.vx = (Math.random() - 0.5) * (0.8 + level * 0.1) * SCALE; this.vy = (Math.random() - 0.5) * (0.8 + level * 0.1) * SCALE; this.size = (Math.random() * 8 + 6) * SCALE; this.active = true; const typeRand = Math.random(); if (typeRand < 0.10) { this.isRare = true; if (typeRand < 0.01) this.color = '#FF4500'; else if (typeRand < 0.025) this.color = '#FF69B4'; else this.color = '#E6E6FA'; } else { this.isRare = false; this.color = NORMAL_COLORS[Math.floor(Math.random() * NORMAL_COLORS.length)]; } } update() { if (!this.active) return; this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > width) this.vx *= -1; if (this.y < 0 || this.y > height) this.vy *= -1; } draw() { if (!this.active) return; ctx.save(); ctx.globalAlpha = 0.8; ctx.fillStyle = this.color; if(this.isRare) { ctx.shadowColor = this.color; ctx.shadowBlur = (this.color === '#FF4500' ? 15 : 10) * SCALE; } ctx.beginPath(); ctx.arc(this.x, this.y, this.size/2, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } explode() { this.active = false; playChainSound(chainCount, this.isRare); let sc = 6; if (this.isRare) sc = this.color === '#FF4500' ? 12 : 10; for (let i = 0; i < sc; i++) { shards.push(new Shard(this.x, this.y, this.color)); } } } class Ripple { constructor(x, y, isRare = false, color = '#FFFFFF') { this.x = x; this.y = y; this.radius = 0; this.isRare = isRare; this.color = color; this.life = isRare ? 120 : 100; this.expansionSpeed = 2.0 * SCALE; this.active = true; } update() { this.radius += this.expansionSpeed; this.life -= 1.5; if (this.life <= 0) this.active = false; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); const alpha = Math.max(0, this.life / (this.isRare ? 120 : 100)); if (this.isRare) { let r = 255, g = 100, b = 150; if (this.color === '#FF4500') { r = 255; g = 100; b = 100; } else if (this.color === '#FF69B4') { r = 255; g = 150; b = 200; } else if (this.color === '#E6E6FA') { r = 200; g = 180; b = 255; } ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`; ctx.lineWidth = 3 * SCALE; } else { ctx.strokeStyle = `rgba(200, 240, 255, ${alpha})`; ctx.lineWidth = 1.5 * SCALE; } ctx.stroke(); } } class Shard { constructor(x, y, color) { this.x = x; this.y = y; this.color = color; this.size = (Math.random() * 3 + 1) * SCALE; const angle = Math.random() * Math.PI * 2; const speed = (Math.random() * 4 + 1) * SCALE; this.vx = Math.cos(angle) * speed; this.vy = Math.sin(angle) * speed; this.gravity = 0.05 * SCALE; this.life = 100; this.active = true; } update() { this.x += this.vx; this.y += this.vy; this.vy += this.gravity; this.life -= 2.0; if (this.life <= 0) this.active = false; } draw() { ctx.save(); ctx.fillStyle = this.color; ctx.globalAlpha = Math.max(0, this.life / 100); ctx.fillRect(this.x, this.y, this.size, this.size); ctx.restore(); } } function initLevel() { const dpr = window.devicePixelRatio || 1; width = window.innerWidth; height = window.innerHeight; canvas.width = width * dpr; canvas.height = height * dpr; canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; ctx.scale(dpr, dpr); const baseDiagonal = Math.sqrt(1280 * 720); const currentDiagonal = Math.sqrt(width * height); SCALE = Math.max(0.4, Math.min(currentDiagonal / baseDiagonal, 1.2)); const baseArea = 1280 * 720; const currentArea = width * height; currentMaxParticles = Math.max(40, Math.min(200, Math.floor(150 * (currentArea / baseArea)))); const ratio = Math.min(0.3 + (level * 0.05), 0.9); targetCount = Math.floor(currentMaxParticles * ratio); levelEl.innerText = level; targetEl.innerText = `クリア目標: ${targetCount} / ${currentMaxParticles}`; scoreEl.innerText = `現在の連鎖: 0`; startMsg.style.display = 'block'; // メッセージを再表示 particles = []; for (let i = 0; i < currentMaxParticles; i++) particles.push(new Particle()); ripples = []; shards = []; chainCount = 0; gameStarted = false; gameEnded = false; nextBtn.style.display = 'none'; retryBtn.style.display = 'none'; } function drawWaves() { const time = Date.now() * 0.001; ctx.fillStyle = 'rgba(0, 40, 80, 0.4)'; ctx.beginPath(); ctx.moveTo(0, height); for(let x = 0; x <= width; x += 30 * SCALE) { let y = height * 0.4 + Math.sin(x * 0.003 / SCALE + time) * 60 * SCALE + Math.sin(x * 0.01 / SCALE - time * 0.5) * 20 * SCALE; ctx.lineTo(x, y); } ctx.lineTo(width, height); ctx.fill(); ctx.fillStyle = 'rgba(0, 60, 100, 0.3)'; ctx.beginPath(); ctx.moveTo(0, height); for(let x = 0; x <= width; x += 30 * SCALE) { let y = height * 0.6 + Math.sin(x * 0.004 / SCALE - time * 0.8) * 40 * SCALE; ctx.lineTo(x, y); } ctx.lineTo(width, height); ctx.fill(); } function animate() { ctx.fillStyle = '#001a33'; ctx.fillRect(0, 0, width, height); drawWaves(); particles.forEach(p => { p.update(); p.draw(); ripples.forEach(r => { const dx = p.x - r.x; const dy = p.y - r.y; const dist = Math.sqrt(dx * dx + dy * dy); const hitThickness = 20 * SCALE; if (p.active && dist < r.radius + p.size && dist > r.radius - hitThickness) { p.explode(); ripples.push(new Ripple(p.x, p.y, p.isRare, p.color)); chainCount++; scoreEl.innerText = `現在の連鎖: ${chainCount}`; } }); }); ctx.globalCompositeOperation = 'lighter'; shards = shards.filter(s => s.active); shards.forEach(s => { s.update(); s.draw(); }); ctx.globalCompositeOperation = 'source-over'; ripples = ripples.filter(r => r.active); ripples.forEach(r => { r.update(); r.draw(); }); if (gameStarted && !gameEnded && ripples.length === 0 && shards.length === 0) { gameEnded = true; if (chainCount >= targetCount) { nextBtn.style.display = 'block'; } else { retryBtn.style.display = 'block'; } } requestAnimationFrame(animate); } // --- 強力なボタンイベント管理 --- function handleNextLevel(e) { e.preventDefault(); e.stopPropagation(); level++; initLevel(); } function handleRetryLevel(e) { e.preventDefault(); e.stopPropagation(); initLevel(); } nextBtn.addEventListener('touchstart', handleNextLevel, { passive: false }); nextBtn.addEventListener('mousedown', handleNextLevel); retryBtn.addEventListener('touchstart', handleRetryLevel, { passive: false }); retryBtn.addEventListener('mousedown', handleRetryLevel); // --- キャンバスのタッチ管理 --- function handleInput(e) { if (e.target.tagName === 'BUTTON') return; e.preventDefault(); initAudio(); if (!gameStarted) { startMsg.style.display = 'none'; // タップしたらメッセージを消す let x = e.clientX; let y = e.clientY; if (e.touches && e.touches.length > 0) { x = e.touches[0].clientX; y = e.touches[0].clientY; } if (x !== undefined && y !== undefined) { ripples.push(new Ripple(x, y, false, '#FFFFFF')); gameStarted = true; } } } window.addEventListener('mousedown', handleInput, { passive: false }); window.addEventListener('touchstart', handleInput, { passive: false }); window.addEventListener('resize', () => { setTimeout(initLevel, 200); }); initLevel(); animate(); </script></body></html>
■ 今日の学び
…夢中になって、気づけば1時間ぐらい遊んでしまいました…
連鎖の音もとても癒されます。(* ̄o ̄*)~♪
レベル25ぐらいになるとシーグラスの動きが早くなり、クリアも簡単にはできなくなります。
ゲームを仕上げている途中、シーグラスが出なくなってしまう不具合が発生しました。
何度もAIと修正しても改善せず困っていたので、動かなくなる直前のコードをコピーして相談すると…
AI:「原因の切り分け、本当にありがとう!
動いていた確実なコード(v4.0)を教えてもらったおかげで、どこでつまずいていたのかハッキリわかったよ。
私が良かれと思って追加した“新しい透明度の計算”や“描画エンジンの軽量化処理”が、
そちらの環境(メモ帳での実行環境)と相性が悪くてフリーズを引き起こしていたみたいだ。ごめんね。」
AIも正確に原因をつかんでくれて、すぐにきちんとした仕上がりにしてくれました。
「伝え方を工夫することの大切さ」を感じた出来事でした。(>v<)b”
■ 次回は、小6ギャルの学校との対立
次回の主人公として登場する小6ギャルは、
以前 AI と一緒にサウンドノベルを作ろうとしたときに
生まれたキャラクターです。
そのときの物語は記事化していませんが、
AI が考えてくれた設定がとても面白く、
今回のゲームでも再登場してもらいました。
ゲームの化学反応を楽しみながらAIと作ったゲームです。
次回もお楽しみに。(*^v^*)ノシ
次の実験はこちら(物理と物語のシューティング):「エクレアジター弾幕シューティング」
ゲーム作りが楽しくて、どんどん作品が増えています(*^v^*)
もし応援していただけたら、とても励みになります。






🌈はじめての方へ
🌳 実験一覧
🌱 このブログについて
🍃 プライバシーポリシー
🐣未記事ゲーム展示室