実験#65:ジターリングの物理2(ジターリング・マスター:レジェンド・ラリー)

■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=*)

“コードは長いので折りたたんでいます”

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<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^*)

~このブログについて~

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

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


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

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

続きを読む