実験#60:ネギ危機一髪!兄弟の絆と無心の境地(ネギ兄弟 – まな板サバイバル(極限回避編))

■60個目「ネギ兄弟 – まな板サバイバル(極限回避編)

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

今回は、ちょっとシュールなサバイバルアクションゲームを作ってみました。
舞台は……なんと “まな板の上” です!

主役は、2本並んだ緑色のネギのキャラクター「ネギ兄弟」。(=v=*)
左側は頭がまっすぐな弟、右側は先がぴょこんと折れたお兄ちゃん。

こだわったのは、彼らの「絶対に表情を変えない」ところ。
横から巨大な包丁が容赦なく迫ってきても、
目は横棒と四角、口は一本線。 まったく動じない無表情のまま、
2人くっついてトコトコ行動します。

「2人セットだからこそ幅を取ってしまって避けづらい!」
そんな、ちょっとした理不尽さが生むサバイバルです。(>_<)

ただ逃げるだけでなく、時にはスッと体を横に倒して、
包丁と包丁の“すき間”をアクロバティックにすり抜けることもできます。(^^*)

まな板の上で繰り広げられる、シュールなネギ兄弟アクション。
果たして無表情な2人は、みじん切りを回避して
どこまで生き残れるのでしょうか。

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

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

みー:(^v^) 今日もゲームを作ります。HTML形式で動かせるゲームです。

今回は、2本並んだネギのキャラクター「ネギ兄弟」を登場させたいです。

ネギ兄弟は常に一緒に動きます。見た目は、縦長の緑の三角形の胴体に、
黒い横棒+小さな四角の“はみ出した目”、口は一本線の無表情。
左が弟、右がお兄ちゃんで、お兄ちゃんは頭の先が右に折れています。

AI: こんにちは、みーさん!また新しい実験ですね。
この「ネギ兄弟」、
無表情で並んで動くという設定がすでにシュールで最高です。

ご指定のビジュアル要件を満たした、ベースのHTMLゲームを作りました。
まずは矢印キーで兄弟が一緒にトコトコ動くところまで実装しています。

みー:(=v=) 表情その感じです。ネギらしさを出すために、
胴体を1.5倍くらい長くしよう。

AI: 了解です!あの虚無感のある顔、長くなるとさらに味が出ますね。

みー:(*^o^*)このネギ兄弟のキャラクター性、生かしたゲーム作れそうかな。

AI: もちろんです! 無表情で一定距離を保って動くというだけで、
すでに面白いメカニクスになっています。

みー:(つw<*)包丁の当たり判定は切られた瞬間だけで、
判定は包丁の中央の細い線1本分だけ。

←→キーでネギ兄弟の体を横向きにして、
うまく“兄弟のすき間”をすり抜ければセーフ…みたいなことできるかな。

AI: そのアイデア、めちゃくちゃ熱いです!!

「一瞬の判定」「細い線」「体を横にしてすり抜ける」…
これはもう完全にスタイリッシュ死にゲーの文法です。

無表情なネギがアクロバティックに包丁を回避する姿、
想像しただけで最高。

ご提案のシステム、すべて組み込みました!

■ 実際の動作

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


仕上がり版のゲーム画面です。
まな板の上に立つネギ兄弟。
赤いラインは、これから包丁が落ちてくる位置を示しています。

(画像:完成版の画面)



横向きになることができるので、
うまく包丁の“切り口ライン”をすり抜けるとポイントが入ります。

(画像:プレイ中の画面)


包丁の切り口ラインに触れてしまうとゲームオーバーです。

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

■ コードのポイント

今回の記事では、AI と相談しながら作った
「ネギ兄弟 – まな板サバイバル(極限回避編)」 の HTML コードを公開します。

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

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

▼ 遊ぶまでの手順

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

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

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

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

■ 遊び方・操作方法

操作方法はとってもシンプルです!(^^*)

・【 ↑ 】【 ↓ 】キー  ネギ兄弟が上下にトコトコ移動します。

・【 ← 】【 → 】キー  緊急回避! 押した方向にパタンと体を横に倒します。
 倒れていられるのは 1回につき5秒間だけ です。
 (画面左上の緑ゲージが目安)

・【 スペース 】キー  切られてしまっても、すぐにリスタートできます。

画面の横から赤い警告が出たら、包丁が飛んでくる合図です。(>_<)
薄い赤の帯の中央にある 濃い赤の細い線 が、本当の当たり判定です。

「上下の移動じゃ避けきれない!」と思ったら、
左右キーで体を横に倒してみてください。

兄弟の体の“すき間”に赤い線をピタッと通すことができると……
見事 「すり抜け!」 となってボーナス点が入りますよ〜!(=v=*)

無表情なネギ兄弟との、ちょっとスリリングなまな板サバイバル。
ぜひぜひ、遊んでみてくださいね!

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ネギ兄弟 - まな板サバイバル(極限回避・改)</title>
<style>
body {
margin: 0;
padding: 20px;
background-color: #222;
color: #fff;
text-align: center;
font-family: sans-serif;
}
canvas {
background-color: #DEB887;
border: 4px solid #8B4513;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
}
#instructions {
margin-bottom: 10px;
line-height: 1.4;
}
.highlight { color: #FFEB3B; font-weight: bold; }
</style>
</head>
<body>
<h1>まな板サバイバル ~極限回避~</h1>
<p id="instructions">
[↑][↓] 移動 / <span class="highlight">[←]か[→]で、5秒間だけその方向に横たわる!</span><br>
包丁の<span class="highlight">「赤い中心線」</span>を、兄弟の体の<span class="highlight">「すき間」</span>に通してすり抜けろ!
</p>
<canvas id="gameCanvas" width="600" height="600"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const initialPlayerState = {
x: canvas.width / 2,
y: canvas.height / 2,
speed: 5,
tiltDirection: null, // 'left', 'right', または null
tiltTimer: 0 // 横たわっている残りフレーム数
};
let player = { ...initialPlayerState };
const keys = {
ArrowUp: false,
ArrowDown: false,
" ": false
};
let score = 0;
let frameCount = 0;
let isGameOver = false;
let knives = [];
let effects = [];
window.addEventListener('keydown', (e) => {
// 上下とスペースキーの状態管理
if (keys.hasOwnProperty(e.key)) {
keys[e.key] = true;
if(["ArrowUp","ArrowDown"," "].indexOf(e.key) > -1) {
e.preventDefault();
}
}
if (isGameOver && e.key === " ") resetGame();
// 横向きアクションのトリガー(ゲームオーバーじゃなく、立っている時のみ発動)
if (!isGameOver && player.tiltTimer <= 0) {
if (e.key === 'ArrowLeft') {
player.tiltDirection = 'left';
player.tiltTimer = 300; // 60fps * 5秒 = 300フレーム
e.preventDefault();
} else if (e.key === 'ArrowRight') {
player.tiltDirection = 'right';
player.tiltTimer = 300;
e.preventDefault();
}
}
});
window.addEventListener('keyup', (e) => {
if (keys.hasOwnProperty(e.key)) keys[e.key] = false;
});
function resetGame() {
player = { ...initialPlayerState };
score = 0;
frameCount = 0;
knives = [];
effects = [];
isGameOver = false;
keys[" "] = false;
}
function drawNegi(x, y, isOlder) {
const w = 40;
const h = 150;
ctx.fillStyle = '#66BB6A';
ctx.beginPath();
if (isOlder) {
ctx.moveTo(x + w / 2 - 3, y + 18);
ctx.lineTo(x + w / 2 + 15, y - 5);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x, y + h);
} else {
ctx.moveTo(x + w / 2, y);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x, y + h);
}
ctx.closePath();
ctx.fill();
const eyeY = y + 65;
const centerX = x + w / 2;
ctx.strokeStyle = '#111';
ctx.fillStyle = '#111';
ctx.lineWidth = 3;
const eyeBarLen = 20;
const eyeGap = 10;
ctx.beginPath();
ctx.moveTo(centerX - eyeGap / 2 - eyeBarLen, eyeY);
ctx.lineTo(centerX - eyeGap / 2, eyeY);
ctx.stroke();
ctx.fillRect(centerX - eyeGap / 2 - eyeBarLen / 2 - 3, eyeY - 3, 6, 6);
ctx.beginPath();
ctx.moveTo(centerX + eyeGap / 2, eyeY);
ctx.lineTo(centerX + eyeGap / 2 + eyeBarLen, eyeY);
ctx.stroke();
ctx.fillRect(centerX + eyeGap / 2 + eyeBarLen / 2 - 3, eyeY - 3, 6, 6);
ctx.beginPath();
ctx.moveTo(centerX - 8, y + 85);
ctx.lineTo(centerX + 8, y + 85);
ctx.stroke();
}
function update() {
if (isGameOver) return;
frameCount++;
if (frameCount % 60 === 0) score++;
// 横たわりタイマーの更新
if (player.tiltTimer > 0) {
player.tiltTimer--;
if (player.tiltTimer === 0) {
player.tiltDirection = null; // 5秒経ったら起き上がる
}
}
const isHorizontal = player.tiltTimer > 0;
// 移動制限
const halfHeight = isHorizontal ? 50 : 75;
// 上下移動
if (keys.ArrowUp && player.y - halfHeight > 0) player.y -= player.speed;
if (keys.ArrowDown && player.y + halfHeight < canvas.height) player.y += player.speed;
// エフェクトの更新
effects = effects.filter(ef => {
ef.timer--;
ef.y -= 1;
return ef.timer > 0;
});
// 包丁の出現
const spawnInterval = Math.max(20, 60 - Math.floor(score / 3));
if (frameCount % spawnInterval === 0) {
const knifeHeight = 40 + Math.random() * 50;
knives.push({
y: Math.random() * (canvas.height - knifeHeight),
height: knifeHeight,
timer: 0,
state: 'warning'
});
}
// 包丁の更新と当たり判定
for (let i = knives.length - 1; i >= 0; i--) {
let knife = knives[i];
knife.timer++;
if (knife.state === 'warning' && knife.timer > 60) {
knife.state = 'strike';
knife.timer = 0;
const hitLineY = knife.y + knife.height / 2;
const cy = player.y;
let isHit = false;
if (isHorizontal) {
// 横向き時の判定(左右どちらに倒れても兄弟のすき間の位置は同じ)
if (hitLineY >= cy - 50 && hitLineY <= cy - 10) isHit = true;
if (hitLineY >= cy + 10 && hitLineY <= cy + 50) isHit = true;
// すき間を通ったか
if (!isHit && hitLineY > cy - 50 && hitLineY < cy + 50) {
score += 5;
effects.push({ x: player.x, y: player.y, timer: 45, text: "すり抜け! +5" });
}
} else {
// 縦向き時の判定
if (hitLineY >= cy - 75 && hitLineY <= cy + 75) isHit = true;
}
if (isHit) {
isGameOver = true;
}
} else if (knife.state === 'strike') {
if (knife.timer > 10) knives.splice(i, 1);
}
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 包丁の警告表示
knives.forEach(knife => {
if (knife.state === 'warning') {
ctx.fillStyle = 'rgba(255, 0, 0, 0.15)';
ctx.fillRect(0, knife.y, canvas.width, knife.height);
ctx.fillStyle = 'rgba(255, 0, 0, 0.9)';
ctx.fillRect(0, knife.y + knife.height / 2 - 1, canvas.width, 2);
}
});
// ネギ兄弟の描画
ctx.save();
ctx.translate(player.x, player.y);
// 倒れる方向の分岐
if (player.tiltTimer > 0) {
if (player.tiltDirection === 'right') {
ctx.rotate(Math.PI / 2); // 右に90度
} else if (player.tiltDirection === 'left') {
ctx.rotate(-Math.PI / 2); // 左に90度
}
}
drawNegi(-50, -75, false); // 弟
drawNegi(10, -75, true); // 兄
ctx.restore();
// 振り下ろされた包丁の描画
knives.forEach(knife => {
if (knife.state === 'strike') {
ctx.fillStyle = 'rgba(224, 224, 224, 0.8)';
ctx.fillRect(0, knife.y, canvas.width, knife.height);
ctx.fillStyle = '#FFEB3B';
ctx.fillRect(0, knife.y + knife.height / 2 - 2, canvas.width, 4);
}
});
// エフェクトの描画
effects.forEach(ef => {
ctx.fillStyle = `rgba(255, 235, 59, ${ef.timer / 45})`;
ctx.font = 'bold 24px sans-serif';
ctx.textAlign = 'center';
ctx.strokeStyle = `rgba(0, 0, 0, ${ef.timer / 45})`;
ctx.lineWidth = 3;
ctx.strokeText(ef.text, ef.x, ef.y);
ctx.fillText(ef.text, ef.x, ef.y);
});
// UI描画
ctx.fillStyle = '#111';
ctx.font = 'bold 20px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`スコア: ${score}`, 10, 30);
// 残り無敵時間のゲージ表示(横たわっている時のみ)
if (player.tiltTimer > 0) {
ctx.fillStyle = '#333';
ctx.fillRect(10, 40, 100, 10);
// タイマーの残り割合に合わせてゲージを緑色で描画
ctx.fillStyle = '#66BB6A';
ctx.fillRect(10, 40, 100 * (player.tiltTimer / 300), 10);
}
// ゲームオーバー画面
if (isGameOver) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff';
ctx.font = 'bold 40px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('みじん切り!', canvas.width / 2, canvas.height / 2 - 20);
ctx.font = '20px sans-serif';
ctx.fillText(`最終スコア: ${score}`, canvas.width / 2, canvas.height / 2 + 20);
ctx.fillText('スペースキーでもう一度', canvas.width / 2, canvas.height / 2 + 60);
}
}
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
gameLoop();
</script>
</body>
</html>


■ 今日の学び

今回のゲーム作りは、いつもと少し違うアプローチで始めてみました。(^^*)
実は最初に、「どんなキャラクターにするか」を
AI に相談するところからスタートしたんです。

この「ネギ兄弟」のアイデアは、
お庭でネギを収穫している時にふっと思いつきました。(^_^)
そこから、無表情でどこか不思議な彼らのキャラクター性が固まり、
その世界観に引っ張られるように、
ゲームの内容も自然とユニークな方向へ広がっていきました。

いつもは「こんなルールのゲームにしよう」というシステム先行が多いのですが、
今回のように キャラクターの個性からゲームをふくらませていく作り方 も、とても面白かったです。(=v=*)

この“キャラクターから始まるゲーム作り”、新しい発見がたくさんあったので、
また今度、別のキャラクターでも試してみたいと思います!

(画像:お庭で収穫した緑たち)【ネギ、シュンギク、レモンバーム、アップルミント、パセリ、スナップエンドウ、セロリ】

■ 次回は、メダカの水槽です。

イシガメのタンタンに続いて、
飼育ゲームの第2弾。 家で飼っている白いメダカたちを、
やさしい水の世界のゲームにしてみました。

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

▶ 次の実験はこちら(白い3匹のメダカたち)
「天使のしずく – Musical Aquarium」



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


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

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

続きを読む