実験#66:自転車泥棒と難所の地蔵峠(アカメカッパの自転車暴走ゲーム)

■66個目 「アカメカッパの自転車暴走ゲーム」

こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/

今回も、ちょっと(いや、かなり?)やんちゃなカッパが主役の
爆走アクションゲームを紹介します。

主人公は、赤い目が特徴の「アカメカッパ」。
ある日、川の土手に置いてあった「ママチャリ」を偶然見つけてしまいます。

「よし、これに乗って隣の川まで爆走だ!」

そんな無謀な思いつきから、アカメカッパの暴走劇が幕を開けます。

今回こだわったのは、アカメカッパの“爆走スタイル”。
ただ速いだけではありません。 舌を出したり、腕を大きく広げたり、
挑発的なポーズを決めながら 超高速で土手道を駆け抜けていきます。

見ているこちらがハラハラしてしまうような、
シュールで笑える光景が魅力です。

舞台は、人里近くの田舎道。 道端には木が生え、
お地蔵さんが優しく(?)見守ってくれています。

「ガッシャン!」

もし道から外れてしまったら……大変なことに。
隣の川まで無事にたどり着けるのか、ハラハラドキドキの爆走劇です。

そしてゴールに到着すると、アカメカッパは勢いよく川へダイブ!(^^*)
無事にたどり着けたら、次のステージへ進めるかもしれません。

 

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

■ゲーム仕様の相談からスタート

みー:(^v^) 今日もゲームを作ります。html形式で動かせるゲーム。
アカメカッパの自転車暴走ゲームです。
いたずら好きのアカメカッパが、
川のそばに置いてあったママチャリに乗り、隣の川まで爆走します。

ゲームとしては、左から右へ向かって
ランダム生成された曲がりくねった1本道のコースに沿って、
制限時間内にマウスで線を引きます。

その線の上を、自転車に乗ったアカメカッパが
挑発的なポーズをとりながら高速で走ります。

線がコースから外れていると「ガッシャン!」でゲームオーバー。
川までたどり着くと次のステージへ。難易度は徐々に上がっていきます。

AI: いらっしゃい、みーさん。今日も元気そうだね。

例のアカメカッパがママチャリで爆走するゲーム、作ってみたよ。
「なぞった線を爆走する」「外れたらガッシャン」「挑発ポーズ」っていう組み合わせ、なかなかシュールで笑える仕上がりになった。
テストプレイ中に思わず笑っちゃったよ。

みー:(^^;) このゲームの主人公なので、
アカメカッパと自転車は絵文字じゃなくて表現したいです。

前に作ったアカメカッパのコードからキャラクター作れますか?
貼り付けてみますね。 〈アカメカッパのコード〉

AI: お、前のコードを持ってきたんだね。
なるほど、Canvas APIで描いていたアカメカッパを、
今回の自転車ゲーム用に「画像ファイル(PNG)」として使いたいってことか。

過去の資産を再利用して新しいゲームに組み込むなんて、さすがみーさん。
そのまま使えるように、各ポーズをキャンバスに描画して、
クリック一つで保存できる『画像生成ツール』を作ってみたよ。

みー:( ̄v ̄*)
コードだけで完結するゲームにしたいです。
移動の様子がもう少し見たいので、アカメカッパのスピードを少し落とします。

背景が殺風景なので、木を生やしたり、
各ステージにランダムでお地蔵さんを置いたりしましょう。

AI: 了解。カッパのスピードは半分くらいにして、
ポーズや動きがしっかり見えるように調整したよ。

背景にはランダムに木を生やして、コース脇のどこかに毎回1体だけ
「お地蔵さん」をそっと配置するようにした。

そういえば、お地蔵さんって赤いよだれかけや帽子をつけてることが多いよね。

あれは昔から「赤」が魔除けの色とされていて、
子どもの無事を願う気持ちが込められているんだって。

今回のお地蔵さんにも、ちゃんと赤いよだれかけを描いておいたよ。
……まあ、ママチャリで爆走するカッパの安全まで
守ってくれるかは分からないけど(笑)。

■ 実際の動作

こちらが、AIが最初に作ってくれたゲームの画像です。
(画像:最初にAIが作ってくれたゲームの画像)


そしてこちらが、調整を重ねて仕上がったスタート画面です。
(画像:スタート画面)


右側の川まで、時間内にコースから外れないように線を引きます。

(画像:ゲーム画面)

アカメカッパは、引いた線の通りに動きます。

(画像:移動時の挑発ポーズ)


時間切れになったり、コースアウトするとゲームオーバー。

(画像:ゲームオーバー)

ステージ10あたりからは、まさに“難所の峠”レベルの難易度に突入します。
こちらはステージ21の様子です。

(画像:ステージ21)

■ コードのポイント

今回の記事では、AI と相談しながら作った
「アカメカッパの自転車暴走ゲーム」 の HTML コードを公開します。

このコードをそのまま保存するだけで、
あなたのPCでもすぐに遊べます。

ブラウザだけで動く、とてもシンプルなゲームなので、
気軽に試してみてくださいね。

▼ 遊ぶまでの手順

①PC の「メモ帳」(※Windowsの場合)や  
 Mac の「テキストエディット」など、テキストエディタを開きます。

②下のコードをコピーして貼り付けます。

③「bakusou .html」など、好きな名前で保存します。
 (※拡張子は .html にしてください)

④保存したファイルをブラウザで開けば、すぐに遊べます。

 

■ 遊び方・操作説明

こんにちは!後半では、このゲームの遊び方を詳しく説明しますね。(^^*) アカメカッパが隣の川へたどり着けるかどうかは、あなたの“なぞる力”にかかっています!

① 制限時間内に「道」をなぞろう!

ゲームが始まると、ランダムに曲がりくねった1本道が表示されます。 まずは、その道からはみ出さないように、左から右へマウスで線を引いてください。(^_^*)

制限時間はわずか! 焦らず、でも急いでなぞるのがポイントです。

もし線を引く前に制限時間が終わってしまったら……
アカメカッパは走り出せません。

② カッパが爆走開始!コースアウトに注意!

線を引き終わると、アカメカッパがその線の上を高速で移動し始めます。
この時、カッパは舌を出したり、ポーズをとったりと、
あなたを挑発してきます。(=v=*)

でも、それに惑わされてはいけません!

もし引いた線が少しでも元のコースから外れていたら…… 「ガッシャン!」とクラッシュしてゲームオーバー。(>_<)

③ 無事にゴールして、次の川へ!

見事、コースアウトせずに隣の川(右端)までたどり着けばステージクリア! アカメカッパは豪快に川へダイブします。(^^*)

クリアすると、すぐに次のステージが始まります。 ステージが進むにつれて、道はさらに曲がりくねり、制限時間も短くなっていきます。

あなたはいくつのステージをクリアできるでしょうか? アカメカッパの挑発に負けず、ハラハラドキドキの爆走を楽しんでくださいね!

さあ、レッツ爆走!(^^)

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

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>アカメカッパの自転車暴走ゲーム</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #2c3e50;
color: #ecf0f1;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
h1 {
margin-bottom: 10px;
font-size: 24px;
color: #f1c40f;
}
canvas {
background-color: #27ae60;
border: 4px solid #bdc3c7;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
cursor: crosshair;
}
p {
margin-top: 10px;
font-size: 14px;
}
</style>
</head>
<body>
<h1>アカメカッパの自転車暴走ゲーム</h1>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<p>左の川からスタート!制限時間内にドラッグしてコース上に線を引け!離すと爆走開始だ!</p>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// ゲームの状態管理
let state = 'START';
let stage = 1;
let gameOver = false;
// コースと線のデータ
let coursePoints = [];
let drawnPoints = [];
let isDrawing = false;
// 背景の装飾データ
let trees = [];
let jizoPos = null;
// 難易度調整用変数
let courseWidth = 80;
let maxTime = 5000;
let timeLeft = maxTime;
// アニメーション用変数
let kappaIndex = 0;
let frameCount = 0;
let currentLane = 1;
const runLanes = [1, 4, 0, 2];
// ゲーム初期化
function initStage() {
state = 'DRAWING';
drawnPoints = [];
isDrawing = false;
gameOver = false;
courseWidth = Math.max(30, 90 - stage * 8);
maxTime = Math.max(2000, 6000 - stage * 400);
timeLeft = maxTime;
coursePoints = [];
let amp1 = 30 + stage * 8;
let amp2 = 20 + stage * 5;
let freq1 = 0.004 + stage * 0.001;
let freq2 = 0.009 + stage * 0.0015;
let phase1 = Math.random() * Math.PI * 2;
let phase2 = Math.random() * Math.PI * 2;
for (let x = 0; x <= 800; x += 5) {
let y = 300 + Math.sin(x * freq1 + phase1) * amp1 + Math.sin(x * freq2 + phase2) * amp2;
y = Math.max(80, Math.min(520, y));
coursePoints.push({ x, y });
}
// 背景の木をランダムに生成
trees = [];
for (let i = 0; i < 15; i++) {
trees.push({
x: 60 + Math.random() * 680,
y: 30 + Math.random() * 540,
scale: 0.6 + Math.random() * 0.6
});
}
// お地蔵さんをコース脇のランダムな位置に1体配置
let jizoIndex = Math.floor(coursePoints.length * 0.2 + Math.random() * (coursePoints.length * 0.6));
let baseP = coursePoints[jizoIndex];
jizoPos = {
x: baseP.x,
y: Math.max(30, baseP.y - courseWidth / 2 - 25)
};
}
// マウスイベント
canvas.addEventListener('mousedown', (e) => {
if (state === 'START') {
initStage();
} else if (state === 'DRAWING') {
isDrawing = true;
drawnPoints = [{ x: e.offsetX, y: e.offsetY }];
} else if (state === 'GAMEOVER') {
stage = 1;
initStage();
}
});
canvas.addEventListener('mousemove', (e) => {
if (state === 'DRAWING' && isDrawing) {
let lastP = drawnPoints[drawnPoints.length - 1];
let dist = Math.hypot(e.offsetX - lastP.x, e.offsetY - lastP.y);
if (dist > 5) {
drawnPoints.push({ x: e.offsetX, y: e.offsetY });
}
}
});
canvas.addEventListener('mouseup', () => {
if (state === 'DRAWING' && isDrawing) startRunning();
});
canvas.addEventListener('mouseleave', () => {
if (state === 'DRAWING' && isDrawing) startRunning();
});
// 爆走開始
function startRunning() {
isDrawing = false;
if (drawnPoints.length < 2) {
state = 'GAMEOVER';
gameOver = true;
return;
}
state = 'RUNNING';
gameOver = false;
kappaIndex = 0;
frameCount = 0;
currentLane = 1;
}
function isOffCourse(px, py) {
let minDist = 9999;
for (let p of coursePoints) {
let d = Math.hypot(p.x - px, p.y - py);
if (d < minDist) minDist = d;
}
return minDist > courseWidth / 2;
}
// 背景オブジェクトの描画関数
function drawTree(x, y, scale) {
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale);
ctx.fillStyle = '#5c4033';
ctx.fillRect(-10, 0, 20, 40);
ctx.fillStyle = '#228b22';
ctx.beginPath(); ctx.arc(0, -10, 30, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(-20, -30, 25, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(20, -30, 25, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(0, -50, 25, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
function drawJizo(x, y) {
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = '#95a5a6';
ctx.beginPath(); ctx.ellipse(0, 10, 12, 16, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(0, -12, 11, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#c0392b';
ctx.beginPath(); ctx.arc(0, 0, 9, 0, Math.PI, false); ctx.fill();
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(-4, -14, 3, 0, Math.PI); ctx.stroke();
ctx.beginPath(); ctx.arc(4, -14, 3, 0, Math.PI); ctx.stroke();
ctx.restore();
}
// ママチャリ描画関数
function drawBicycle(x, y, isCrash) {
ctx.save();
ctx.translate(x, y);
if (isCrash) {
ctx.rotate(Math.PI / 3);
ctx.translate(0, 15);
}
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 6;
ctx.beginPath(); ctx.arc(-30, 0, 15, 0, Math.PI*2); ctx.stroke();
ctx.beginPath(); ctx.arc(30, 0, 15, 0, Math.PI*2); ctx.stroke();
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-45, 0); ctx.lineTo(-15, 0); ctx.moveTo(-30, -15); ctx.lineTo(-30, 15);
ctx.moveTo(15, 0); ctx.lineTo(45, 0); ctx.moveTo(30, -15); ctx.lineTo(30, 15);
ctx.stroke();
ctx.strokeStyle = '#bdc3c7';
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(-30, 0); ctx.lineTo(-10, -25); ctx.lineTo(20, -25); ctx.lineTo(30, 0);
ctx.moveTo(-10, -25); ctx.lineTo(0, 0); ctx.lineTo(20, -25);
ctx.stroke();
ctx.fillStyle = '#2c3e50';
ctx.beginPath(); ctx.ellipse(-12, -28, 10, 4, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.moveTo(20, -25); ctx.lineTo(15, -40); ctx.lineTo(25, -45); ctx.stroke();
ctx.strokeStyle = '#7f8c8d';
ctx.lineWidth = 2;
ctx.strokeRect(22, -40, 12, 10);
ctx.restore();
}
// アカメカッパ描画関数
function drawKappa(x, y, lane) {
const kColor = '#3a6332';
const beakColor = '#ffcc00';
const shellColor = '#1d3d1a';
ctx.save();
ctx.translate(x, y);
let armLeft = { angle: 0, x: -30, y: -50, width: 15 };
let armRight = { angle: 0, x: 30, y: -50, width: 15 };
let legLeft = { x: -20, y: -10, width: 10, height: 15 };
let legRight = { x: 10, y: -10, width: 10, height: 15 };
let headTilt = 0;
let headShiftY = 0;
let bodyScale = { w: 1, h: 1 };
let dishTilt = 0;
let beakTilt = 0;
let bodyAngle = 0;
let showTongue = false;
if (gameOver) {
const flicker = Date.now() % 300 < 100 ? beakColor : (Date.now() % 300 < 200 ? 'white' : 'black');
bodyScale.w *= (0.9 + Math.random() * 0.2); bodyScale.h *= (0.9 + Math.random() * 0.2);
ctx.fillStyle = flicker;
} else {
ctx.fillStyle = kColor;
}
switch(lane) {
case 0: bodyAngle = -0.1; armLeft.y = -60; armLeft.x = -15; armLeft.angle = -0.8; armRight.y = -50; armRight.x = 35; armRight.angle = 1.0; headTilt = -0.3; dishTilt = -0.3; beakTilt = -0.2; showTongue = true; break;
case 1: bodyAngle = 0.3; bodyScale.h = 0.8; armLeft.y = -40; armLeft.x = -10; armLeft.angle = 1.2; armRight.y = -40; armRight.x = 10; armRight.angle = 1.2; headShiftY = 10; headTilt = 0.2; dishTilt = 0.1; break;
case 2: bodyScale.w = 1.1; armLeft.y = -55; armLeft.x = -35; armLeft.angle = -0.3; armRight.y = -65; armRight.x = 40; armRight.angle = -1.2; break;
case 3: bodyScale.w = 0.9; bodyScale.h = 0.9; armLeft.y = -60; armLeft.x = -10; armLeft.angle = 1.2; armRight.y = -60; armRight.x = 10; armRight.angle = 1.2; headTilt = -0.1; break;
case 4: bodyScale.w = 0.8; armLeft.y = -40; armLeft.x = -25; armLeft.angle = 0.5; armRight.y = -40; armRight.x = 25; armRight.angle = 0.5; headTilt = 0.2; dishTilt = 0.2; break;
}
if (!gameOver) ctx.fillStyle = kColor;
ctx.beginPath(); ctx.ellipse(legLeft.x, legLeft.y, legLeft.width, legLeft.height, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(legRight.x, legRight.y, legRight.width, legRight.height, 0, 0, Math.PI*2); ctx.fill();
const bodyW = 60 * bodyScale.w;
const bodyH = 60 * bodyScale.h;
ctx.save();
ctx.rotate(bodyAngle);
ctx.fillRect(-bodyW / 2, -65 + headShiftY, bodyW, bodyH);
if (!gameOver) ctx.fillStyle = shellColor;
if (gameOver) ctx.globalAlpha = 0.5;
const shellW = 44 * bodyScale.w;
const shellH = 50 * bodyScale.h;
ctx.fillRect(-shellW / 2, -60 + headShiftY, shellW, shellH);
ctx.restore();
if (gameOver) ctx.globalAlpha = 1;
if (!gameOver) ctx.strokeStyle = kColor;
if (gameOver) ctx.strokeStyle = ctx.fillStyle;
ctx.lineWidth = armLeft.width; ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(armLeft.x, armLeft.y + headShiftY);
ctx.lineTo(armLeft.x - 15, armLeft.y + 10 + headShiftY);
ctx.lineTo(armLeft.x - 5 + Math.sin(armLeft.angle)*15, armLeft.y + 25 + Math.cos(armLeft.angle)*15 + headShiftY); ctx.stroke();
ctx.lineWidth = armRight.width;
ctx.beginPath(); ctx.moveTo(armRight.x, armRight.y + headShiftY);
ctx.lineTo(armRight.x + 15, armRight.y + 10 + headShiftY);
ctx.lineTo(armRight.x + 5 + Math.sin(armRight.angle)*15, armRight.y + 25 + Math.cos(armRight.angle)*15 + headShiftY); ctx.stroke();
ctx.translate(0, -75 + headShiftY);
ctx.rotate(headTilt);
if (!gameOver) ctx.fillStyle = kColor;
if (gameOver) ctx.fillStyle = ctx.fillStyle;
ctx.beginPath(); ctx.arc(0, 0, 25, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = 'red';
ctx.beginPath(); ctx.arc(-10, -5, 7, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(10, -5, 7, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = beakColor;
ctx.save();
ctx.rotate(beakTilt);
ctx.beginPath();
ctx.moveTo(0, 5); ctx.lineTo(-10, 15); ctx.lineTo(10, 15); ctx.closePath(); ctx.fill();
ctx.fillStyle = '#bfa100';
ctx.beginPath(); ctx.ellipse(0, 15, 10, 3, 0, 0, Math.PI*2); ctx.fill();
if (showTongue && !gameOver) {
ctx.fillStyle = '#ff6688';
ctx.beginPath(); ctx.ellipse(5, 18, 4, 8, -0.2, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
ctx.fillStyle = '#b3ffb3';
ctx.beginPath(); ctx.ellipse(0, -22, 18, 5, dishTilt, 0, Math.PI*2); ctx.fill();
if (gameOver) {
ctx.strokeStyle = beakColor; ctx.lineWidth = 3; ctx.lineCap = 'round';
for (let i = 0; i < 12; i++) {
const angle = (Math.PI * 2 / 12) * i + Math.random() * 0.3;
const startX = Math.sin(angle) * 35;
const startY = Math.cos(angle) * 35;
const sparkLen = 15 + Math.random() * 25;
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(startX + Math.sin(angle) * sparkLen * 0.5, startY + Math.cos(angle) * sparkLen * 0.5 + Math.random() * 5);
ctx.lineTo(startX + Math.sin(angle + 0.2) * sparkLen * 0.8, startY + Math.cos(angle + 0.2) * sparkLen * 0.8 + Math.random() * 8);
ctx.lineTo(startX + Math.sin(angle - 0.1) * sparkLen, startY + Math.cos(angle - 0.1) * sparkLen + Math.random() * 10);
ctx.stroke();
}
}
ctx.restore();
}
// メインループ
function update() {
// 背景(草地)
ctx.fillStyle = '#27ae60';
ctx.fillRect(0, 0, 800, 600);
// 背景の木を描画
if (state !== 'START') {
for (let t of trees) {
drawTree(t.x, t.y, t.scale);
}
}
// 川
ctx.fillStyle = '#3498db';
ctx.fillRect(0, 0, 50, 600);
ctx.fillRect(750, 0, 50, 600);
if (state !== 'START') {
// コース描画
ctx.beginPath();
ctx.moveTo(coursePoints[0].x, coursePoints[0].y);
for (let p of coursePoints) {
ctx.lineTo(p.x, p.y);
}
ctx.lineWidth = courseWidth;
ctx.strokeStyle = '#d35400';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
// お地蔵さんを描画
if (jizoPos) drawJizo(jizoPos.x, jizoPos.y);
// 引いた線の描画
if (drawnPoints.length > 0) {
ctx.beginPath();
ctx.moveTo(drawnPoints[0].x, drawnPoints[0].y);
for (let p of drawnPoints) {
ctx.lineTo(p.x, p.y);
}
ctx.lineWidth = 4;
ctx.strokeStyle = '#f1c40f';
ctx.stroke();
}
}
if (state === 'START') {
drawText('クリックしてゲームスタート!', 400, 300, 40, '#fff');
}
else if (state === 'DRAWING') {
timeLeft -= 1000 / 60;
if (timeLeft <= 0) startRunning();
drawText(`STAGE: ${stage}`, 100, 40, 24, '#fff');
drawTimerBar();
ctx.save();
ctx.translate(30, coursePoints[0].y);
ctx.scale(0.5, 0.5);
drawBicycle(0, 25, false);
drawKappa(0, 0, 3);
ctx.restore();
}
else if (state === 'RUNNING') {
drawText(`STAGE: ${stage}`, 100, 40, 24, '#fff');
// ★ スピードをさらに半分に調整(4 -> 2)
kappaIndex += 2;
if (kappaIndex >= drawnPoints.length - 1) {
kappaIndex = drawnPoints.length - 1;
let lastP = drawnPoints[kappaIndex];
if (lastP.x >= 740) {
state = 'CLEAR';
setTimeout(() => { stage++; initStage(); }, 1000);
} else {
state = 'GAMEOVER';
gameOver = true;
}
} else {
let px = drawnPoints[Math.floor(kappaIndex)].x;
let py = drawnPoints[Math.floor(kappaIndex)].y;
if (isOffCourse(px, py)) {
state = 'GAMEOVER';
gameOver = true;
}
}
frameCount++;
if (frameCount % 12 === 0) { // ポーズ切り替えも少しゆったりに
currentLane = runLanes[Math.floor(Math.random() * runLanes.length)];
}
if (state === 'RUNNING') {
let currentP = drawnPoints[Math.floor(kappaIndex)];
ctx.save();
ctx.translate(currentP.x, currentP.y);
ctx.rotate(Math.sin(frameCount * 0.2) * 0.15); // フラフラの周期もまったりに
ctx.scale(0.5, 0.5);
drawBicycle(0, 25, false);
drawKappa(0, 0, currentLane);
ctx.restore();
}
}
if (state === 'GAMEOVER') {
drawText('ガッシャン!', 400, 280, 60, '#e74c3c');
drawText(`記録:ステージ ${stage}`, 400, 350, 30, '#fff');
drawText('クリックで最初からやり直す', 400, 420, 20, '#bdc3c7');
let crashP = drawnPoints[Math.min(Math.floor(kappaIndex), drawnPoints.length - 1)] || coursePoints[0];
ctx.save();
ctx.translate(crashP.x, crashP.y);
ctx.scale(0.5, 0.5);
drawBicycle(10, 25, true);
drawKappa(-10, 10, 1);
ctx.restore();
}
else if (state === 'CLEAR') {
drawText('川へダイブ!', 400, 300, 50, '#3498db');
let endP = drawnPoints[drawnPoints.length - 1];
ctx.save();
ctx.translate(endP.x, endP.y);
ctx.scale(0.5, 0.5);
drawBicycle(0, 40, false);
drawKappa(0, 0, 0);
ctx.restore();
}
requestAnimationFrame(update);
}
function drawText(text, x, y, size, color) {
ctx.fillStyle = color;
ctx.font = `bold ${size}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.lineWidth = 4;
ctx.strokeStyle = '#000';
ctx.strokeText(text, x, y);
ctx.fillText(text, x, y);
}
function drawTimerBar() {
let barWidth = 300 * (timeLeft / maxTime);
ctx.fillStyle = timeLeft < maxTime * 0.3 ? '#e74c3c' : '#f1c40f';
ctx.fillRect(250, 20, barWidth, 20);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.strokeRect(250, 20, 300, 20);
ctx.fillStyle = '#fff';
ctx.font = '16px sans-serif';
ctx.fillText('TIME', 220, 30);
}
initStage();
update();
</script>
</body>
</html>


■ 今日の学び

今回のゲーム作りには、
実はもうひとつ裏のテーマ(実験)がありました。

それは、「前に作ったゲームのキャラクターコードを、
そのまま新しいゲームに引っ越しさせられるか?」という検証です。

これまで作ってきた「しょうが君」シリーズでも、
同じキャラクターを毎回登場させていましたが、
あの時は毎回AIに特徴を言葉で説明して、ゼロから描いてもらっていました。

そのため、しょうが君の見た目が毎回ほんの少しずつ違っていて、
それはそれで“手作り感”があって楽しかったのですが……(=v=*)

今回は、以前のゲームで実際に使っていた
「アカメカッパの描画プログラム(Canvasコード)」を、
そのままAIにバトンタッチしてみました。

すると、言葉だけでは再現が難しかった
あの独特な「がっちり体格」や
「火花が散るゲームオーバー演出」が、
新しいママチャリゲームへ 一発で、寸分たがわず移植成功!

「過去のコードは、新しいゲームの最高のスプライト(素材)になる」

言葉で特徴を伝えるだけでなく、
すでに動いているコードを“部品”として渡すことで、
キャラクターの個性や世界観をそのまま保ったままシリーズ化できる。

これは、とても大きな発見でした。(^^)

全く違うルールのアクションゲームの中で、 あの時の姿のまま、
アカメカッパがママチャリを漕いで挑発しているのを見たときは、
ちょっと感動してしまいました。(>_<、)

こうして過去の資産を組み合わせながら、
また新しい表現に挑戦していけるのが、
AIと一緒に作るゲーム開発の本当に面白いところですね!

 

■ 次回は、無限の挑戦。筋肉トレ クリックゲームです。

指先だけで挑む、シンプルなのにクセになる筋トレゲームです。

スタートすると……なんだかやめられなくなります。

次回もぜひ遊びに来てくださいね。(*^v^)ノシ

▶ 次の実験はこちら
 筋トレの果てに待つものは…)「マッスル・シンギュラリティ」



 ✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)

~このブログについて~

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

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


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

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

続きを読む