■65個目 「ジターリング・マスター:レジェンド・ラリー」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
以前の実験室(実験 #23)で作った「ジターリングの物理」を、
さらに進化させてみました!
ジターリングは、みーの大切な趣味のひとつです。
前回は、再現度は十分ではありませんでしたが、
リングを縦に持って回す基本技「ザ・ジター」に挑戦しました。
今回はもうひとつのシンプルな技「ローラーコースター」をゲーム化してみました。(^^*)
「ローラーコースター」は、ワイヤーを水平にして、
横回転の遠心力でビーズ(真鍮の輪っか)をシャーッと滑らせる技です。
このスピード感と爽快感をそのまま活かして、
スリリングなアクションゲームに仕立ててみました。
■ AIとのやりとりで面白かった部分
■ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。HTML形式で動かせるゲームです。
ジターリング・ローラーコースターをゲームで再現したいです。
AI: ジターリングの華である「ローラーコースター」。
横回転の遠心力でワイヤーを滑るあの感覚を、
ブラウザで遊べるレースゲームにしてみましょう。
回転力を維持しながら、
うねるコースをどれだけ速く滑り抜けられるかを競うイメージです。
回転が落ちると止まってしまうので、
タイミング良くキーを押して遠心力を加えるギミックが面白そうです。
みー:(*>_<)o” 手前画面スタートで右へ進行。
手前のワイヤーは太く、奥は細くして遠近感を出します。
右端まで行くと奥の右側から左へ、左端まで行くと手前に戻る…
というループ構造で、ローラーコースターの円運動を再現します。
真鍮の輪っかは5つ並べて、手前と奥で大きさを変えて奥行きを表現します。
テンポよく回して、何周できるかを競うゲームに進化させます。
AI: 一気に立体的なイメージになりましたね。
真鍮の5連リングがチャリンと鳴りそうな質感も含めて、
しっかり再現していきましょう。
みー:(*^_^)
アルミの輪っかの太さの切り替わりが極端なので、境目を自然にしたいです。
真鍮の輪っかはもう少し小さく、厚みのある表現にしたいです。
AI: 遠近法を自然に見せるために、
ワイヤーは細かく分割して徐々に太さと色を変えるグラデーションにしました。
真鍮の輪っかは影を少しずらして重ねることで、金属の厚みを表現しています。
みー:(^o^) アルミ輪っかを左右の手で持つ表現を入れます。
手に当たると真鍮輪っかが止まるので、
右手(Jキー)と左手(Fキー)を切り替えながら回し続けます。
スペースキーで回転維持。
手に当てずに何周できるかを競うゲームにします。
AI:
なるほど、持ち替えの要素が入ると一気に“技を出している感”が増しますね。
指先の集中力が試されるゲームになりそうです。
■ 実際の動作
こちらが、AIが最初に作ってくれたゲームの画像です。
細い針金みたいだし、まっすぐだし、細くて赤い輪っかって…
……これではジターリングではありません。(つv<`)

(画像:最初にAIが作ってくれたゲームの画像)
そしてこちらが、仕上がったスタート画面です。
奥行きがしっかり出ていて、かなり“ジターリングらしい”雰囲気になりました。

(画像:スタート画面)
左と右を交互に掴み、
手に当たらないように…
回転が止まらないように…
落とさないように…!

(画像:ゲーム画面)
あ…

(画像:手に当たった!)
うう…

(画像:失速…)
うぐぐ…

(画像:落下!ガシャーン!)
■ コードのポイント
今回の記事では、AI と相談しながら作った
「ジターリング・マスター:レジェンド・ラリー」 の HTML コードを公開します。
このコードをそのまま保存するだけで、
あなたのPCでもすぐに遊べます。
ブラウザだけで動く、とてもシンプルなゲームなので、
気軽に試してみてくださいね。
▼ 遊ぶまでの手順
①PC の「メモ帳」(※Windowsの場合)や
Mac の「テキストエディット」など、テキストエディタを開きます。
②下のコードをコピーして貼り付けます。
③「jita-ring .html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
④保存したファイルをブラウザで開けば、すぐに遊べます。
■ 遊び方・操作説明
今回は、パソコンのキーボードを両手で使って遊びます。(^_^)
▼ 使用するキー
- Fキー:左手でワイヤーを持つ
- Jキー:右手でワイヤーを持つ
- スペースキー:ポンポンと押して、リングに遠心力を与えて加速する
このゲームの最大のポイントは、
「常にどちらかの手でワイヤーを持っておく」こと!(`・_・)b
もし FキーもJキーも両方離してしまうと……
ガシャーン!と音を立ててワイヤーを落としてしまい、
即ゲームオーバーです。(><、)
「じゃあ、ずっと両手で握っていればいいの?」 と思うかもしれませんが、
それもダメなんです。
ずっと両手で握っていると、
猛スピードで走ってきた真鍮のリングが手に ガツン! と
ぶつかって止まってしまいます。
なので……
- リングが 右側 に来たら、ぶつかる前に Jキーを離す
(Fキーは押したまま) - リングが 左側 に来たら、ぶつかる前に Fキーを離す
(Jキーは押したまま)
これを、スペースキーで「シャーーーーッ!」と回転をキープしながら、
テンポよく繰り返します。
頭がこんがらがりそうになりますが、
慣れてくると指先の感覚だけで
パッパッと持ち替えられるようになりますよ。(=v=*)
“コードは長いので折りたたんでいます”
▼ここからコード▼(クリックで開く)
<html lang="ja"><head><meta charset="UTF-8"><title>ジターリング・マスター:レジェンド・ラリー</title><style> body { background-color: #111; color: #fff; font-family: 'Courier New', Courier, monospace; text-align: center; margin: 0; padding: 10px; user-select: none; overflow: hidden; } h1 { font-size: 22px; color: #d4af37; margin: 5px 0; } #gameCanvas { background-color: #000; border: 4px solid #333; display: block; margin: 0 auto; cursor: crosshair; } .ui-container { display: flex; justify-content: center; gap: 20px; margin-top: 10px; } .ranking-board { background: #222; padding: 10px; border: 2px solid #555; font-size: 14px; min-width: 150px; text-align: left; } .ranking-title { color: #d4af37; font-weight: bold; margin-bottom: 5px; text-align: center; } .key-highlight { color: #0ff; font-weight: bold; border: 1px solid #0ff; padding: 1px 5px; border-radius: 3px; }</style></head><body><h1>JITTER RING: LEGEND RALLY</h1><canvas id="gameCanvas" width="800" height="400"></canvas><div class="ui-container"> <div class="controls-info"> <span class="key-highlight">F</span>:左手 / <span class="key-highlight">J</span>:右手 / <span class="key-highlight">SPACE</span>:加速<br> ※リングが手に当たる前に離せ!両方離すと落下! </div> <div class="ranking-board"> <div class="ranking-title">HIGH SCORES</div> <div id="rankList"></div> </div></div><script> // --- 連続再生型 音響システム (Web Audio API) --- const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const bufferSize = audioCtx.sampleRate * 2; const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate); const output = noiseBuffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) output[i] = Math.random() * 2 - 1; let spinAudioInit = false; let spinNoiseGain, spinOscGain, spinFilter; let spinOscs = []; function initSpinAudio() { if (spinAudioInit) return; const noiseSrc = audioCtx.createBufferSource(); noiseSrc.buffer = noiseBuffer; noiseSrc.loop = true; spinFilter = audioCtx.createBiquadFilter(); spinFilter.type = 'bandpass'; spinFilter.Q.value = 1.0; spinNoiseGain = audioCtx.createGain(); spinNoiseGain.gain.value = 0; noiseSrc.connect(spinFilter).connect(spinNoiseGain).connect(audioCtx.destination); noiseSrc.start(); spinOscGain = audioCtx.createGain(); spinOscGain.gain.value = 0; [1800, 2400, 3200].forEach(f => { const osc = audioCtx.createOscillator(); osc.type = 'square'; osc.frequency.value = f; const filter = audioCtx.createBiquadFilter(); filter.type = 'bandpass'; filter.Q.value = 20; filter.frequency.value = f; osc.connect(filter).connect(spinOscGain); osc.start(); spinOscs.push({ osc, baseFreq: f, filter }); }); spinOscGain.connect(audioCtx.destination); spinAudioInit = true; } function playCrashSound() { const time = audioCtx.currentTime; const boomOsc = audioCtx.createOscillator(); boomOsc.type = 'triangle'; boomOsc.frequency.setValueAtTime(150, time); boomOsc.frequency.exponentialRampToValueAtTime(40, time + 0.2); const boomGain = audioCtx.createGain(); boomGain.gain.setValueAtTime(0.5, time); boomGain.gain.exponentialRampToValueAtTime(0.01, time + 0.4); boomOsc.connect(boomGain).connect(audioCtx.destination); boomOsc.start(time); boomOsc.stop(time + 0.4); const noiseSrc = audioCtx.createBufferSource(); noiseSrc.buffer = noiseBuffer; const filter = audioCtx.createBiquadFilter(); filter.type = 'highpass'; filter.frequency.value = 1000; const noiseGain = audioCtx.createGain(); noiseGain.gain.setValueAtTime(0.5, time); noiseGain.gain.exponentialRampToValueAtTime(0.01, time + 0.6); noiseSrc.connect(filter).connect(noiseGain).connect(audioCtx.destination); noiseSrc.start(time); } function playHitSound() { const time = audioCtx.currentTime; [1200, 2000, 2800].forEach((f, idx) => { const osc = audioCtx.createOscillator(); osc.type = 'triangle'; osc.frequency.setValueAtTime(f, time); osc.frequency.exponentialRampToValueAtTime(f * 0.9, time + 0.2); const gain = audioCtx.createGain(); gain.gain.setValueAtTime(0.3 / (idx + 1), time); gain.gain.exponentialRampToValueAtTime(0.01, time + 0.2); osc.connect(gain).connect(audioCtx.destination); osc.start(time); osc.stop(time + 0.2); }); } // --- ゲームロジック --- const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const cx = canvas.width / 2, cy = canvas.height / 2; const rx = 320, ry = 120; let angle = Math.PI / 2, speed = 0, maxSpeed = 0.12; let isPlaying = false, isGameOver = false, gameOverReason = ""; let spinAngle = 0, laps = 0; let keys = { f: false, j: false }; function updateRanking(newScore) { let scores = JSON.parse(localStorage.getItem('jitterScores') || '[]'); if (newScore > 0) { scores.push(newScore); scores.sort((a, b) => b - a); scores = scores.slice(0, 5); localStorage.setItem('jitterScores', JSON.stringify(scores)); } const list = document.getElementById('rankList'); list.innerHTML = scores.map((s, i) => `<div>${i+1}位: ${s} LAPS</div>`).join(''); } window.addEventListener('keydown', (e) => { if (e.code === 'KeyF') keys.f = true; if (e.code === 'KeyJ') keys.j = true; if (e.code === 'Space') { e.preventDefault(); addForce(); } }); window.addEventListener('keyup', (e) => { if (e.code === 'KeyF') keys.f = false; if (e.code === 'KeyJ') keys.j = false; }); function addForce() { if (isGameOver) { resetGame(); return; } if (audioCtx.state === 'suspended') audioCtx.resume(); initSpinAudio(); if (!isPlaying) { isPlaying = true; speed = 0.05; return; } speed += 0.015; if (speed > maxSpeed) speed = maxSpeed; } function resetGame() { angle = Math.PI / 2; speed = 0; laps = 0; isGameOver = false; isPlaying = false; } function drawWireSegment(a1, a2) { let x1 = cx + rx * Math.cos(a1), y1 = cy + ry * Math.sin(a1); let x2 = cx + rx * Math.cos(a2), y2 = cy + ry * Math.sin(a2); let depth = (Math.sin((a1 + a2) / 2) + 1) / 2; ctx.lineWidth = 10 + depth * 22; let shade = Math.floor(60 + depth * 140); ctx.strokeStyle = `rgb(${shade}, ${shade}, ${shade})`; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); } // --- 高品質なメカニカルハンド描画関数 --- function drawHighQualityHand(x, y, isLeft, isGripping, txt) { ctx.save(); ctx.translate(x, y); // 左手を基準にデザインし、右手はX軸を反転させて流用する if (!isLeft) ctx.scale(-1, 1); // 影の設定 ctx.shadowColor = 'rgba(0, 0, 0, 0.8)'; ctx.shadowBlur = 15; ctx.shadowOffsetY = 8; const baseDark = '#1a1a1a'; const baseLight = '#3a3a3a'; const armorGlow = isGripping ? '#d4af37' : '#444'; // 握るとゴールドに const neon = isGripping ? '#00ffff' : '#222'; // 握るとシアンに発光 if (isGripping) { // 【ワイヤーを握っている状態】 // 手首のアーマー let wristGrad = ctx.createLinearGradient(-50, 50, -10, 10); wristGrad.addColorStop(0, '#000'); wristGrad.addColorStop(1, baseLight); ctx.fillStyle = wristGrad; ctx.beginPath(); ctx.moveTo(-60, 60); ctx.lineTo(-20, 15); ctx.lineTo(15, 15); ctx.lineTo(-20, 60); ctx.fill(); // 親指(ワイヤーの下から手前に巻き付く) ctx.fillStyle = armorGlow; ctx.beginPath(); ctx.roundRect(-10, 5, 25, 14, 7); ctx.fill(); // 手の甲(メインユニット) let palmGrad = ctx.createLinearGradient(-35, -25, 10, 15); palmGrad.addColorStop(0, baseLight); palmGrad.addColorStop(1, baseDark); ctx.fillStyle = palmGrad; ctx.beginPath(); ctx.roundRect(-35, -30, 45, 45, 10); ctx.fill(); // ナックルの装甲プレート ctx.fillStyle = armorGlow; ctx.beginPath(); ctx.roundRect(-30, -32, 35, 15, 5); ctx.fill(); // 4本の指(ワイヤーの上から手前へ巻き付く) for (let i = 0; i < 4; i++) { let fingerGrad = ctx.createLinearGradient(0, -30, 0, 5); fingerGrad.addColorStop(0, baseDark); fingerGrad.addColorStop(0.5, armorGlow); fingerGrad.addColorStop(1, '#000'); ctx.fillStyle = fingerGrad; ctx.beginPath(); ctx.roundRect(-30 + (i * 10), -35, 8, 38, 4); ctx.fill(); // 指の関節に走るネオンライン ctx.fillStyle = neon; ctx.shadowBlur = isGripping ? 10 : 0; ctx.shadowColor = neon; ctx.fillRect(-28 + (i * 10), -20, 4, 12); ctx.shadowBlur = 15; // 影を元に戻す ctx.shadowColor = 'rgba(0,0,0,0.8)'; } } else { // 【手を開いている状態(待機)】 ctx.translate(-15, 25); // 少し下へ退避 // 手首 ctx.fillStyle = baseDark; ctx.beginPath(); ctx.moveTo(-60, 60); ctx.lineTo(-20, 15); ctx.lineTo(15, 30); ctx.lineTo(-20, 70); ctx.fill(); // 手の甲 ctx.fillStyle = baseLight; ctx.beginPath(); ctx.roundRect(-35, -5, 45, 45, 10); ctx.fill(); // 伸ばした4本の指 for (let i = 0; i < 4; i++) { ctx.fillStyle = '#444'; ctx.beginPath(); // 中指・薬指を少し長くする let length = 35 - Math.abs(i - 1.5) * 8; ctx.roundRect(-30 + (i * 10), 38, 8, length, 4); ctx.fill(); } // 伸ばした親指 ctx.beginPath(); ctx.roundRect(8, 15, 14, 25, 5); ctx.fill(); } ctx.restore(); // テキストラベル描画(反転の影響を受けないよう別で描画) ctx.save(); ctx.shadowColor = 'black'; ctx.shadowBlur = 5; ctx.fillStyle = isGripping ? neon : '#666'; ctx.font = 'bold 18px Courier New'; ctx.textAlign = 'center'; let textX = x + (isLeft ? -45 : 45); let textY = isGripping ? y + 45 : y + 65; ctx.fillText(txt, textX, textY); ctx.restore(); } function update() { if (isPlaying && !isGameOver) { speed -= 0.00035; angle += speed; spinAngle += speed * 50; if (spinAudioInit) { let s = Math.max(0, speed * 10); spinFilter.frequency.value = 2000 + s * 3000; spinNoiseGain.gain.setTargetAtTime(s * 0.15, audioCtx.currentTime, 0.05); spinOscGain.gain.setTargetAtTime(s * 0.05, audioCtx.currentTime, 0.05); spinOscs.forEach(o => { o.osc.frequency.setTargetAtTime(o.baseFreq + s * 400, audioCtx.currentTime, 0.05); o.filter.frequency.setTargetAtTime(o.baseFreq + s * 400, audioCtx.currentTime, 0.05); }); } if (angle > (Math.PI / 2) + (laps + 1) * Math.PI * 2) laps++; if (!keys.f && !keys.j) { isGameOver = true; gameOverReason = "落下!ガシャーン!"; playCrashSound(); updateRanking(laps); } const hitT = 0.28; for (let i = 0; i < 5; i++) { let a = (angle - i * 0.07) % (Math.PI * 2); if (a < 0) a += Math.PI * 2; if ((keys.j && (a < hitT || a > Math.PI*2 - hitT)) || (keys.f && Math.abs(a - Math.PI) < hitT)) { isGameOver = true; gameOverReason = "手に当たった!"; playHitSound(); updateRanking(laps); } } if (speed <= 0) { isGameOver = true; gameOverReason = "失速..."; updateRanking(laps); } } else { if (spinAudioInit) { spinNoiseGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.1); spinOscGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.1); } } ctx.clearRect(0, 0, canvas.width, canvas.height); for (let a = Math.PI; a < Math.PI * 2; a += 0.05) drawWireSegment(a, a + 0.05); let rings = []; for (let i = 0; i < 5; i++) { let a = angle - i * 0.07; rings.push({ x: cx + rx * Math.cos(a), y: cy + ry * Math.sin(a), scale: 0.4 + ((Math.sin(a) + 1)/2) * 0.8, index: i }); } rings.sort((a, b) => a.y - b.y).forEach(r => { let vW = 18 * r.scale * Math.abs(Math.cos(spinAngle - r.index * 0.5)); let vH = 32 * r.scale, tOffset = Math.sin(spinAngle - r.index * 0.5) * (12 * r.scale); ctx.lineWidth = 5 * r.scale; ctx.strokeStyle = '#6b5409'; for (let j = 4; j >= 1; j--) { ctx.beginPath(); ctx.ellipse(r.x + (tOffset * j / 4), r.y, Math.max(vW, 0.5), vH, 0, 0, Math.PI * 2); ctx.stroke(); } ctx.beginPath(); ctx.ellipse(r.x, r.y, Math.max(vW, 0.5), vH, 0, 0, Math.PI * 2); ctx.lineWidth = 4 * r.scale; ctx.strokeStyle = '#d4af37'; ctx.stroke(); }); ctx.shadowColor = 'rgba(0, 0, 0, 0.8)'; ctx.shadowBlur = 10; for (let a = 0; a < Math.PI; a += 0.05) drawWireSegment(a, a + 0.05); ctx.shadowColor = 'transparent'; // 新しくなった高品質な手を描画 drawHighQualityHand(cx + rx, cy, false, keys.j, "J"); // 右手 drawHighQualityHand(cx - rx, cy, true, keys.f, "F"); // 左手 ctx.fillStyle = '#0ff'; ctx.font = 'bold 24px Courier'; ctx.textAlign = 'left'; ctx.fillText(`LAPS: ${laps}`, 20, 40); ctx.fillStyle = '#333'; ctx.fillRect(20, 60, 200, 15); let gRatio = Math.min(speed / maxSpeed, 1); ctx.fillStyle = gRatio > 0.8 ? '#f00' : (gRatio > 0.4 ? '#0f0' : '#ff0'); ctx.fillRect(20, 60, 200 * gRatio, 15); if (isGameOver) { ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(0,0,800,400); ctx.fillStyle = '#f44'; ctx.textAlign = 'center'; ctx.font = 'bold 40px Courier'; ctx.fillText(gameOverReason, 400, 180); ctx.fillStyle = '#ff0'; ctx.font = '20px Courier'; ctx.fillText('SPACE TO RETRY', 400, 240); } else if (!isPlaying) { ctx.fillStyle = '#fff'; ctx.textAlign = 'center'; ctx.font = 'bold 30px Courier'; ctx.fillText('HOLD F & J, PRESS SPACE', 400, 200); } requestAnimationFrame(update); } updateRanking(0); update();</script></body></html>
■ 今日の学び
今回は(実験 #23)の続きとして、 「ジターリングの物理2」に挑戦し、
技そのものをゲーム化してみました。(^_^)
実はこの「ローラーコースター」という技、
本物のジターリングでは比較的やさしい部類なんです。
感覚さえ掴めば、
目をつぶっていても「シャーーーーッ!」と回し続けられるくらい、
指先が勝手に動いてくれる技だったりします。(=v=*)
…なのですが。
キーボードの「F」と「J」で再現してみると、
実際の技よりはるかに難しくなってしまいました!(><、)
頭では分かっているのに、指の持ち替えが追いつかず、
何度も落としてしまうという不思議な現象が……(^^;)
それでも、極太アルミワイヤーを滑る真鍮の重みや、
あの心地よい金属音の再現度はかなり高くて、
個人的には大満足の仕上がりになりました!Σ( ゜v ゜;)
ジターリングには、まだまだ面白い動きをする技がたくさんあります。
そのうち、また別の技も、
こんなふうにブラウザで遊べるゲームにしてみたいと思います。
■ 次回は、ちょい悪なアカメカッパがまた登場します。
盗んだ自転車で、隣の川まで 爆走ダイブ します。
道中、お地蔵さんがじっと見ていますが……
そんなことはお構いなしのアカメカッパ。
今回も、やらかします。
次回もぜひ遊びに来てくださいね。(*^v^)ノシ
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)






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