実験 #02:跳ねて避ける丸ゲーム(丸太郎と突撃猪)

■2つ目「丸太郎と突撃猪~すべての丸は僕のもの~」

 今回は、AI と一緒に作った 2 つ目のゲームの制作記録です。(^^*)
  1作品目のブロック崩しは時間がかかったので、毎日作るためにもっとシンプルなゲームを作ってみようと思い、考えてみました。

 小さい頃に遊んだ、電子ゲームのような「ピコピコ動くシンプルなゲーム」を思い出し、AI に相談してみることにしました。(=v=*)

 ただ、著作権やメモアプリの制限で完全再現は難しいらしく、AI さんが代わりに考えてくれたのが、 「クリックすると跳ねて上昇、何もしないと落下、イノシシに当たるとゲームオーバー」 というシンプルなアクションゲームでした。(>v<;)

 思っていたのとは違うけれど、動きが妙に面白い。(@o@*) 茶色い塊は犬、黄色い丸はキャンディらしく、Q太郎が元になっているので頭にアンテナも生えています。

 せっかくなので、このゲームをアレンジしてみました。 主人公は丸いので 丸太郎 にして、丸に執着する設定に。 前から向かってくる茶色い塊は イノシシ にして、ステージのボスも巨大イノシシに変更。

丸太郎が集める丸も種類を増やし、

  • 白:10点
  • 青:10点+スロー
  • 金:50点
  • 虹:無敵+点数

決まった点数を取るとボス登場!

 ボス戦ではイノシシが攻撃してくるので、丸太郎も 丸を飛ばして攻撃するシューティングモード にしました。 ボスの体力がゼロになると第2ステージへ進みます。

■ AIとのやりとりで面白かった部分

(^^*)昔の電子ゲームで、お化けのQ太郎のゲームがあったんだけど、メモアプリでコード貼り付けで動かせる再現できる?

(AI)懐かしいですね!『オバケのQ太郎 ワンワンパニック』や、学研などのLSIゲーム(電子ゲーム)のことでしょうか。

 結論から申し上げますと、「メモアプリにコードを貼り付けて、当時のゲームをそのまま完全に再現する」のは、現在の技術では少し難しいのが正直なところです。

 理由はいくつかあります:

 著作権の問題: ゲームのプログラム(ROMデータ)やキャラクター画像には著作権があるため、それらを含むコードを公開・配布することは法的に制限されています。

 メモアプリの制限: iPhoneの「メモ」やGoogle Keepなどはテキストを保存するためのもので、複雑なゲームプログラム(JavaScriptなど)を実行する機能がありません。

(^^*)懐かしくてやってみたくなったんだ。似た感じのゲームでいいよ。

(AI)その気持ち、よくわかります!あの時代の電子ゲーム特有の「限られた画面の中で必死に避ける」感覚って、妙に熱中しちゃうんですよね。
 もしよろしければ、「Q太郎のような、障害物を避けて進むシンプルなミニゲーム」のサンプルコード(HTML版)を作成しましょうか?

 〈ゲーム作りを開始。調整を進めていく中で、ボス戦のアイデアを出してみました。〉

(^^*)ボスを倒す方法を、ボス戦の時だけ丸太郎から定期的なタイミングで小さな丸を飛ばして攻撃なんてどうかな。ボスにある程度当てると撃破みたいなボスだけシューティングモードはどう?

(AI)素晴らしいアイデアですね!「左右移動を追加せず、丸太郎のこだわり(丸)を飛ばして攻撃する」というのは、キャラクター性にも合っていますし、操作感を守りつつ遊び心地をガラリと変える良い方法です。

アイデアを褒められると、やっぱりうれしい。(^~^*)

■ 実際の動作

一番最初にAIが提案してくれた「おばけの飛行訓練ゲーム」

丸太郎と突撃猪

ボス戦です。ここだけシューティングゲーム。

 AIが途中で出してくれたイメージ、
 毛が3本のおばけっぽいキャラクターと、犬と飴

■コードのポイント

 今回の記事では、AI と相談しながら作った「丸太郎と突撃猪~すべての丸は僕のもの~」の HTML コードを公開します。

 このコードは、ブラウザだけで動くシンプルなものです。
 PCの「メモ帳」を開いて、
 下のコードを 全部コピーして「marutarou.html」などの名前で保存し、
 ブラウザで開くとそのまま遊べます。
 マウス左クリックで跳ねます。イノシシの攻撃を避けて丸を集めてください。

・クリックで上昇
・何もしないと落下
・イノシシに当たるとゲームオーバー
・丸を取ると得点
・色ごとに効果が違う
・ボス戦はシューティングに変化

  今回もAI が作ってくれたコードをそのまま載せているので、
 自分で改造したり、動きを変えたり、色を変えたりして遊んでみてください。(^^*)

「※スマホでは動作しない場合があります」

 「※コードが長いので、必要な方だけコピーしてください」

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>丸太郎と突撃猪 - Ranking Complete</title>
<style>
body { text-align: center; background: #000; color: white; font-family: 'Hiragino Kaku Gothic ProN', sans-serif; overflow: hidden; margin: 0; }
canvas { border: 4px solid #fff; display: block; margin: 10px auto; background: #1a1a1a; touch-action: none; cursor: pointer; }
.info { font-size: 24px; font-weight: bold; padding: 10px; color: #0f0; }
#overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 10; }
.rank-board { background: #222; border: 2px solid #FFD700; padding: 20px; border-radius: 10px; min-width: 250px; }
.rank-item { display: flex; justify-content: space-between; font-size: 20px; margin: 10px 0; border-bottom: 1px solid #444; padding-bottom: 5px; }
.btn { background: #FFD700; color: #000; border: none; padding: 10px 20px; font-size: 18px; font-weight: bold; cursor: pointer; border-radius: 5px; margin-top: 20px; }
</style>
</head>
<body>
<div class="info">STAGE: <span id="stage">1</span> | SCORE: <span id="score">0</span></div>
<canvas id="gameCanvas" width="400" height="600"></canvas>
<div id="overlay">
<div class="rank-board">
<h2 id="resultTitle" style="color:#FFD700;">GAME OVER</h2>
<p id="currentScoreDisplay" style="font-size: 24px;">Score: 0</p>
<hr>
<h3>🏆 TOP 5 RANKING</h3>
<div id="rankingList"></div>
<button class="btn" onclick="start()">もう一度挑戦する</button>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreElement = document.getElementById('score');
const stageElement = document.getElementById('stage');
const overlay = document.getElementById('overlay');
let audioCtx = null;
let nextNoteTime = 0;
const melody = [261, 329, 392, 523, 392, 329];
const bossMelody = [196, 207, 196, 207, 185, 196];
function playSound(f, t, d, v=0.05) {
if (!audioCtx) return;
const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
o.type = t; o.frequency.setValueAtTime(f, audioCtx.currentTime);
g.gain.setValueAtTime(v, audioCtx.currentTime); g.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime+d);
o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(audioCtx.currentTime+d);
}
function updateBGM() {
if (!audioCtx || gameState !== 'playing') return;
if (audioCtx.currentTime > nextNoteTime) {
let m = boss.active ? bossMelody : melody;
let note = m[Math.floor(frame / 12) % m.length];
let speed = boss.active ? 0.15 : 0.3;
playSound(note, 'square', speed);
nextNoteTime = audioCtx.currentTime + speed;
}
}
let score = 0, stage = 1, gameState = 'title', frame = 0;
let slowTimer = 0, starTimer = 0;
const player = { x: 80, y: 300, dy: 0, gravity: 0.35, lift: -7.5 };
let obstacles = [], items = [], bullets = [], playerShots = [];
let boss = { active: false, x: 450, y: 250, dy: 2, hp: 15, maxHp: 15 };
function drawInoshishi(x, y, scale = 1, isBoss = false) {
ctx.save(); ctx.translate(x, y); ctx.scale(scale, scale);
ctx.fillStyle = isBoss ? "#5D4037" : "#795548";
ctx.beginPath(); ctx.ellipse(20, 15, 25, 18, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "white"; ctx.beginPath(); ctx.moveTo(5, 20); ctx.lineTo(-10, 15); ctx.lineTo(5, 25); ctx.fill();
ctx.fillStyle = "white"; ctx.beginPath(); ctx.arc(10, 10, 4, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "black"; ctx.beginPath(); ctx.arc(8, 10, 2, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
function update() {
if (gameState !== 'playing') return;
player.dy += player.gravity; player.y += player.dy;
if (player.y > canvas.height || player.y < 0) endGame();
updateBGM();
if (stage === 1 && score >= 300 && boss.hp > 0) {
boss.active = true;
if (boss.x > 260) boss.x -= 1;
boss.y += boss.dy;
if (boss.y < 50 || boss.y > 500) boss.dy *= -1;
if (frame % 100 === 0) bullets.push({ x: boss.x, y: boss.y + 30 });
if (frame % 20 === 0) playerShots.push({ x: player.x + 20, y: player.y });
if (starTimer === 0 && Math.hypot(player.x - (boss.x+40), player.y - (boss.y+30)) < 60) endGame();
}
playerShots.forEach((ps, i) => {
ps.x += 8;
ctx.fillStyle = "white"; ctx.beginPath(); ctx.arc(ps.x, ps.y, 5, 0, Math.PI*2); ctx.fill();
if (boss.active && ps.x > boss.x && ps.x < boss.x + 100 && ps.y > boss.y && ps.y < boss.y + 80) {
boss.hp--; playerShots.splice(i, 1);
playSound(440, 'square', 0.1);
if (boss.hp <= 0) { stage = 2; score += 500; boss.active = false; stageElement.innerText = "2"; }
}
});
playerShots = playerShots.filter(ps => ps.x < canvas.width);
let speedMult = (slowTimer > 0) ? 0.3 : 1;
if (!boss.active && frame % (stage === 1 ? 65 : 40) === 0) obstacles.push({ x: canvas.width, y: Math.random()*500+50 });
if (frame % 110 === 0) {
let r = Math.random();
let t = { c: 'white', v: 10 };
if (r > 0.9) t = { c: 'gold', v: 50 };
else if (r > 0.75) t = { c: 'cyan', v: 10, s: 180 };
else if (r > 0.6) t = { c: 'magenta', v: 100, inv: 240 };
items.push({ x: canvas.width, y: Math.random()*500+50, ...t });
}
obstacles.forEach((obs, i) => {
obs.x -= (stage === 1 ? 4.5 : 8) * speedMult;
drawInoshishi(obs.x, obs.y);
if (Math.hypot(player.x - (obs.x+20), player.y - (obs.y+15)) < (starTimer > 0 ? 50 : 30)) {
if (starTimer > 0) { obstacles.splice(i, 1); score += 10; } else endGame();
}
});
items.forEach((it, i) => {
it.x -= 3 * speedMult; ctx.fillStyle = it.c === 'magenta' ? `hsl(${frame*20}, 100%, 50%)` : it.c;
ctx.beginPath(); ctx.arc(it.x, it.y, 12, 0, Math.PI*2); ctx.fill();
if (Math.hypot(player.x - it.x, player.y - it.y) < 30) {
score += it.v; if (it.s) slowTimer = it.s; if (it.inv) starTimer = it.inv;
items.splice(i, 1); scoreElement.innerText = score; playSound(800, 'sine', 0.1);
}
});
bullets.forEach(b => { b.x -= 6 * speedMult; ctx.fillStyle = "red"; ctx.beginPath(); ctx.arc(b.x, b.y, 8, 0, Math.PI*2); ctx.fill(); if (starTimer === 0 && Math.hypot(player.x - b.x, player.y - b.y) < 20) endGame(); });
if (slowTimer > 0) slowTimer--; if (starTimer > 0) starTimer--;
frame++;
}
function loop() {
let bg = stage === 1 ? '#2E7D32' : '#000033';
if (boss.active) bg = frame % 20 < 10 ? '#7B1FA2' : '#4A148C';
ctx.fillStyle = bg; ctx.fillRect(0, 0, 400, 600);
if (gameState === 'title') {
ctx.fillStyle = "#FFD700"; ctx.textAlign = "center"; ctx.font = "bold 24px sans-serif";
ctx.fillText("丸太郎と突撃猪", 200, 240); ctx.font = "16px sans-serif";
ctx.fillText("~丸はすべて僕のもの~", 200, 280); ctx.fillStyle = "white"; ctx.fillText("CLICK TO START", 200, 380);
} else {
ctx.save(); ctx.translate(player.x, player.y);
ctx.fillStyle = starTimer > 0 ? `hsl(${frame*10}, 100%, 70%)` : "white";
ctx.beginPath(); ctx.arc(0, 0, 20, 0, Math.PI*2); ctx.fill();
ctx.fillStyle="black"; ctx.beginPath(); ctx.arc(8, -5, 3, 0, Math.PI*2); ctx.fill(); ctx.restore();
if (boss.active) {
drawInoshishi(boss.x, boss.y, 2.5, true);
ctx.fillStyle = "#333"; ctx.fillRect(250, 20, 100, 10);
ctx.fillStyle = "red"; ctx.fillRect(250, 20, (boss.hp/boss.maxHp)*100, 10);
}
update();
}
requestAnimationFrame(loop);
}
function saveAndShowRanking() {
let name = prompt(`スコア: ${score}\nランキングに載る名前を入力してね!`, "丸太郎");
if (!name) name = "ななし";
let ranks = JSON.parse(localStorage.getItem('marutaroRanks')) || [];
ranks.push({name: name, score: score});
ranks.sort((a,b) => b.score - a.score);
ranks = ranks.slice(0, 5);
localStorage.setItem('marutaroRanks', JSON.stringify(ranks));
const listDiv = document.getElementById('rankingList');
listDiv.innerHTML = ranks.map((r, i) => `<div class="rank-item"><span>${i+1}. ${r.name}</span><span>${r.score}</span></div>`).join('');
document.getElementById('currentScoreDisplay').innerText = `Your Score: ${score}`;
overlay.style.display = 'flex';
}
function endGame() {
if (gameState === 'playing') {
gameState = 'over';
playSound(100, 'square', 0.5);
saveAndShowRanking();
}
}
function start() {
if (!audioCtx) audioCtx = new AudioContext();
score = 0; stage = 1; stageElement.innerText = "1"; scoreElement.innerText = "0";
player.y = 300; player.dy = 0; obstacles = []; items = []; bullets = []; playerShots = []; boss.active = false; boss.hp = 15;
gameState = 'playing'; frame = 0; slowTimer = 0; starTimer = 0;
overlay.style.display = 'none';
}
canvas.addEventListener('mousedown', () => { if (gameState === 'title') start(); else if (gameState === 'playing') player.dy = player.lift; });
loop();
</script>
</body>
</html>

■ 今日の学び

 乗せているコードはPC向けですが、今回のゲームが面白かったので、スマホでも遊べる方法をAIに聞いて対応してみました。 ステージ2までしか作っていない短いゲームなのですが、実はまだステージ2をクリアできていません(^^;)

 ステージ2は ロケットイノシシ が登場し、すごいスピードで突進してきます。 とにかく避けてください ヽ(>v<)_ )/=3

 次回は、「奇妙なお絵描き」ゲームです。
 楽しんで作っています!次もよろしくお願いします(*^v^)ノシ

次の実験(動くお絵描き):「シミュラクラお絵描きゲーム」

コメントを残す