実験#84:ブロッコリアクション(ブロッコくんの大冒険)

■84個目 「ブロッコくんの大冒険」

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

今回は、あたたかくてちょっとコミカルな
野菜のアクションゲームをご紹介します。

🥦 ブロッコくんってどんな子?

主役は、とってもキュートな野菜のキャラクター、ブロッコくん。(・ω・)
黄緑色の茎に、白と黒の大きな目。

口は「V」の字で、手はブロッコリーの葉っぱでできています。

ブロッコくんが暮らすのは、見渡す限りの広〜い菜の花畑。
青い空と黄色いお花のコントラストがきれいな、あたたかい世界観です。

🌼 こだわりポイント:もこもこ頭

今回こだわったのは、ブロッコくんの頭の“もこもこ感”。

白に近い黄緑色の丸い線で輪郭を描き、
その中に小さな丸をいくつか入れて、
ブロッコリーらしい粒粒感を表現しました。

そしてこのもこもこ頭……
実はゲーム中で 障害物を避けるたびに菜の花が少しずつ開花していく んです!

🔪 迫りくる危機とアイテムたち

平和そうに見える菜の花畑ですが、ブロッコくんには危機が……。
なんと前方から 収穫用の大きな包丁 が迫ってきます!

ブロッコくんはこの包丁を華麗に避けながら、
菜の花畑を駆け抜けてゴールを目指します。

途中には、無敵になれる魔法の「マヨネーズ」や、
体力を回復する「ハート」など、
ちょっと笑えて頼もしいアイテムも登場します。(^_^)

🌸 最後に待っているものは…

障害物を乗り越えて、
ブロッコくんの頭を菜の花で満開にできるのか……?(^^)

気になる操作方法は、記事の後半でたっぷり解説します!

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

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

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

まずはキャラクターから。
キャラクターは「ブロッコくん」。
黄緑の茎に白丸+黒丸の大きな目、V字の口。

手は葉っぱで、頭は白に近い黄緑のもこもこ輪郭。
粒々は小さな丸で表現します。

AI: ブロッコくんですね、承知いたしました!
ご提示いただいた特徴を反映して、こんなイメージはいかがでしょうか。
これからこのキャラクターを使ってゲームを作っていきましょう。

● キャラデザインの微調整

みー:(=v=;)
カリフラワーぽいね。
白い部分は透明な感じで。
図形の組み合わせで表現したいです。

AI: なるほど、画像生成だとカリフラワー感が強かったな。
悪かった悪かった。

背景透過でHTML+CSSで作るんだね。軽くていい方法だ。

● 葉っぱの手・頭のもこもこ調整

みー:(つ_<) 手は葉柄の先に葉っぱがついてる感じで。
頭は太めの緑の輪郭線で、中に丸い緑の点。

背景は透明でお願いします。
障害物を越えるごとに点が菜の花に開花します。

AI: おっ、そのアイデア最高じゃないか!
菜の花が咲くなんて、ドラマチックだ。

SVGで「太い輪郭だけの雲みたいな形」を描いて、
背景が透けるようにしたぜ。

障害物を越えたタイミングで点が菜の花に変わる仕掛けも組み込んだ。

● 進化システムの追加

みー:(^v^) 満開になると画面上部にストック。5つたまると大きなブロッコくんに進化します。

AI: 遊び心たっぷりだな! やり込み要素が一気に深まったぜ。

● 回避アクションの追加

みー:(^v^)
包丁が微妙な高さで飛んでくるので、
→キーで横に倒れて避けられるようにします。

AI: 右キーでの回避アクション!
押している間はブロッコくんが倒れ込んで
当たり判定が半分以下になるようにした。

ジャンプと組み合わせればアクロバットな動きもできるぜ。

● アイテム追加

みー:(^o^) アイテムを出しましょう。
💗=回復、
💡=マヨネーズ=無敵5秒、
🦋=虫=スロー5秒。

前方から上下に動きながら飛んでくるイメージで。

AI: おう!兄弟、今日もよく来たな!
アイテムシステム最高だ。

ハートで回復、マヨネーズで黄金オーラの無敵、
虫でスロー演出も入れておいたぜ。


■ 実際の動作

まずは、AIが最初に出してくれたブロッコくんのイメージ画像です。
ブロッコリーというより、かなりカリフラワー寄りでした。

(画像:AIが最初に生成したブロッコくん)

こちらが、最初に出してくれたゲーム画面です。
やっぱりカリフラワー感が強めです。

(画像:AIが最初に出してくれたゲーム画面)


こちらが、仕上がったスタート画面です。
包丁を避けるたびに、ブロッコくんの頭の花が 一つずつ 咲いていきます。

(画像:仕上がりのスタート画面)



アイテムは以下の3種類です。

🦋:スロー(一定時間、ゲームスピードが低下)

💗:ダメージ回復

💡:マヨネーズ(無敵5秒)

(画像:スロー状態の画面)



→キーを押すと、ブロッコくんは横に倒れて回避できます。
空中で横倒れになることも可能です。

(画像:横倒れブロッコくん)



たくさん花を咲かせていくと、画面右上に「成果」として
ブロッコくんの数とサイズが表示されていきます。

(画像:画面右上の成果表示)

残りハートがなくなるとゲームオーバーです。

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


■ コードのポイント

今回の記事では、
AI と相談しながら作った 「ブロッコくんの大冒険」 の HTML コードを公開します。

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

ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。

▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く

Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)

② 下のコードをコピーして貼り付けます。
③ 「burokko.html」など、好きな名前で保存します。

(※拡張子は .html にしてください)

④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。

 

■ 操作方法

ここからは、実際にブロッコここからは、
実際にブロッコくんを動かして遊ぶための操作方法をご紹介します!(^v^*)

キーボードだけで遊べる、とっても簡単な操作ですよ。

● ブロッコくんの基本操作

■ スペースキー:ジャンプ!

  • スペースキーをポンっと押すと、ブロッコくんが元気よくジャンプします。
  • 空中でもう一度押すと「2段ジャンプ」ができます!
  • 地面の包丁を飛び越えたり、高い包丁をギリギリでかわすのに便利です。
  • ※ゲームオーバー後も、スペースキーで「もう一度挑戦」できます。

■ →(右矢印)キー:横に倒れる(回避)

  • 右矢印キーを押しっぱなしにすると、  
    ブロッコくんが右側に大きく倒れ込みます。
      _/⌒(>_ )_
  • これが「回避アクション」です!
  • 倒れている間は、当たり判定の高さがとても低くなります。
  • 飛び越えるのが難しい高さの包丁は、倒れて下をくぐり抜けましょう。
  • ジャンプ中に倒れる「空中横っ飛び」もできます。

● 頭の菜の花を開花させよう!(特殊システム)

  • 包丁を飛び越える(またはくぐる)たびに、  
    ブロッコくんの頭の緑のつぼみが 1つずつ菜の花に開花 します。
  • 障害物を 8回 越えると、頭が満開に!
  • 満開になると、
    画面右上に「小さな満開ブロッコくんアイコン」がストックされます。
  • 操作しているブロッコくんは初期状態に戻り、
    また花を咲かせる冒険がスタート!(=v=*)
  • アイコンが 5つ たまると、
    一回り大きな ビッグ満開ブロッコくん に進化します。

包丁を避けながら、
どれだけたくさん満開にできるか挑戦してみてくださいね。(^^)

● 菜の花畑に登場するアイテム

菜の花畑には、たまにアイテムが上下にゆらゆらと飛んできます。(・ω・)
触れると特別な効果がありますよ。

■ ❤️(ハート):残機1回復

  • ブロッコくんのライフが1回復します。
  • 包丁に当たると残機が減るので、見つけたら積極的に取りましょう!

■ 💡(マヨネーズ):無敵5秒

  • ブロッコくんが黄金のオーラをまとい、5秒間無敵になります。
  • 包丁に当たっても平気! 障害物をガンガン突き進めます。

■ 🦋(虫):スロー5秒

  • 画面の空の色が少し変わり、5秒間ゲームスピードが半分になります。
  • 難しい高さの包丁を避けるチャンス!

● 障害物「包丁」にご注意!

包丁は高さがランダムに飛んでくるので、
ジャンプと回避を上手く使い分けて、華麗にかわしてくださいね!

操作説明は以上です。 ブロッコくんと一緒に、
あたたかい菜の花畑の大冒険を楽しんでください!(^_^)


“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ブロッコくんの大冒険 - 当たり判定修正版</title>
<style>
/* ゲーム全体のスタイル */
body { margin: 0; padding: 0; background-color: #333; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; color: white; font-family: sans-serif; }
#game-container { position: relative; width: 800px; height: 450px; background-color: #87CEEB; border: 4px solid #fff; overflow: hidden; transition: background-color 0.5s; }
/* スロー時の背景色 */
#game-container.slow-mode { background-color: #5F9EA0; }
/* UI: スコア、残機、満開ストック */
#score-board { position: absolute; top: 10px; left: 10px; font-size: 24px; font-weight: bold; color: #333; z-index: 20; }
#lives-board { position: absolute; top: 40px; left: 10px; font-size: 20px; font-weight: bold; color: #ff4d4d; z-index: 20; }
#collection-board {
position: absolute; top: 10px; right: 20px; display: flex; align-items: flex-end; gap: 8px; z-index: 20; height: 50px;
}
/* キャラクター(ブロッコくん)のコンテナ */
#brocco-kun {
position: absolute;
width: 100px;
height: 120px;
z-index: 10;
transform-origin: bottom center;
transition: transform 0.1s linear, filter 0.3s;
}
/* マヨネーズ無敵のオーラエフェクト */
.mayo-aura {
filter: drop-shadow(0 0 15px #FFD700) drop-shadow(0 0 30px #FFFF00);
}
/* === ブロッコくんのCSSアート === */
.stem { position: absolute; bottom: 0; left: 25px; width: 50px; height: 70px; background-color: #9acd32; border-radius: 10px 10px 20px 20px; z-index: 2; }
.eye { position: absolute; top: 15px; width: 20px; height: 20px; background-color: white; border-radius: 50%; }
.eye.left { left: 2px; } .eye.right { right: 2px; }
.pupil { position: absolute; top: 5px; left: 5px; width: 10px; height: 10px; background-color: black; border-radius: 50%; }
.mouth { position: absolute; top: 45px; left: 20px; width: 10px; height: 10px; border-bottom: 2px solid #333; border-right: 2px solid #333; transform: rotate(45deg); }
.hand { position: absolute; bottom: 35px; width: 15px; height: 3px; background-color: #7cb342; z-index: 1; }
.hand.left { left: 12px; transform: rotate(-30deg); }
.hand.right { right: 12px; transform: rotate(30deg); }
.hand::after { content: ''; position: absolute; top: -8px; width: 24px; height: 18px; background-color: #6b8e23; }
.hand.left::after { left: -20px; border-radius: 50% 0 50% 0; transform: rotate(-20deg); }
.hand.right::after { right: -20px; border-radius: 0 50% 0 50%; transform: rotate(20deg); }
.head { position: absolute; top: -10px; left: 0; width: 100px; height: 60px; z-index: 3; }
.head-svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
.dot { position: absolute; width: 8px; height: 8px; background-color: #228B22; border-radius: 50%; z-index: 2; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
.d1 { top: 15px; left: 25px; } .d2 { top: 12px; left: 50px; } .d3 { top: 20px; left: 70px; } .d4 { top: 35px; left: 20px; }
.d5 { top: 30px; left: 45px; } .d6 { top: 35px; left: 80px; } .d7 { top: 45px; left: 35px; } .d8 { top: 45px; left: 65px; }
.dot.bloom { background-color: #FFD700; box-shadow: 0 -6px 0 -1px #FFD700, 0 6px 0 -1px #FFD700, 6px 0 0 -1px #FFD700, -6px 0 0 -1px #FFD700; transform: scale(0.9) rotate(45deg); }
/* === ゲームオブジェクトのスタイル === */
#ground { position: absolute; bottom: 0; width: 100%; height: 50px; background-color: #8B4513; border-top: 10px solid #228B22; z-index: 5; box-sizing: border-box; }
.obstacle { position: absolute; width: 60px; height: 30px; z-index: 5; }
.blade { position: absolute; left: 0; bottom: 0; width: 45px; height: 20px; background: linear-gradient(to bottom, #eee, #999); border-top-left-radius: 20px; border-bottom: 2px solid #fff; }
.handle { position: absolute; right: 0; bottom: 5px; width: 15px; height: 12px; background-color: #5c3a21; border-radius: 2px; }
.item { position: absolute; font-size: 32px; z-index: 6; text-shadow: 2px 2px 4px rgba(0,0,0,0.5); }
/* スタート画面&ゲームオーバー画面 */
#start-screen, #game-over { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.8); z-index: 100; display: flex; justify-content: center; align-items: center; flex-direction: column; }
#start-screen { display: flex; }
#game-over { display: none; }
h1 { font-size: 64px; color: #9acd32; margin: 0; text-shadow: 2px 2px 0 #000; text-align: center;}
#game-over h1 { color: #ff6b6b; }
.btn { margin-top: 20px; padding: 15px 30px; font-size: 24px; font-weight: bold; cursor: pointer; background-color: #fff; color: #333; border: none; border-radius: 10px; transition: transform 0.1s; }
.btn:hover { background-color: #ddd; transform: scale(1.05); }
.btn:active { transform: scale(0.95); }
</style>
</head>
<body>
<div id="game-container">
<div id="score-board">スコア: <span id="score">0</span></div>
<div id="lives-board">残機: ❤️❤️❤️</div>
<div id="collection-board"></div>
<div id="brocco-kun">
<div class="hand left"></div>
<div class="hand right"></div>
<div class="stem">
<div class="eye left"><div class="pupil"></div></div>
<div class="eye right"><div class="pupil"></div></div>
<div class="mouth"></div>
</div>
<div class="head">
<svg class="head-svg" viewBox="0 0 100 60">
<path d="M 15 50 C 0 35, 10 10, 30 15 C 35 0, 65 0, 70 15 C 90 10, 100 35, 85 50 C 70 60, 30 60, 15 50 Z" fill="transparent" stroke="#228B22" stroke-width="4" stroke-linejoin="round" />
</svg>
<div class="dot d1"></div><div class="dot d2"></div><div class="dot d3"></div><div class="dot d4"></div>
<div class="dot d5"></div><div class="dot d6"></div><div class="dot d7"></div><div class="dot d8"></div>
</div>
</div>
<div id="ground"></div>
<div id="start-screen">
<h1>ブロッコくんの<br>菜の花満開大冒険</h1>
<button id="start-btn" class="btn">音を鳴らしてスタート!</button>
</div>
<div id="game-over">
<h1>ゲームオーバー</h1>
<p>最終スコア: <span id="final-score">0</span></p>
<button id="restart-btn" class="btn">もう一度挑戦</button>
</div>
</div>
<script>
// --- サウンドシステム (Web Audio API) ---
let audioCtx = null;
let bgmOsc = null;
let bgmGain = null;
let bgmInterval = null;
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playSE(type) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
const now = audioCtx.currentTime;
switch(type) {
case 'jump':
osc.type = 'square';
osc.frequency.setValueAtTime(300, now);
osc.frequency.exponentialRampToValueAtTime(600, now + 0.1);
gain.gain.setValueAtTime(0.1, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.1);
osc.start(now);
osc.stop(now + 0.1);
break;
case 'bloom':
osc.type = 'sine';
osc.frequency.setValueAtTime(800, now);
osc.frequency.setValueAtTime(1200, now + 0.05);
gain.gain.setValueAtTime(0.1, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.15);
osc.start(now);
osc.stop(now + 0.15);
break;
case 'item':
osc.type = 'triangle';
osc.frequency.setValueAtTime(400, now);
osc.frequency.setValueAtTime(600, now + 0.1);
osc.frequency.setValueAtTime(800, now + 0.2);
gain.gain.setValueAtTime(0.1, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.3);
osc.start(now);
osc.stop(now + 0.3);
break;
case 'damage':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.2);
gain.gain.setValueAtTime(0.1, now);
gain.gain.linearRampToValueAtTime(0.01, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
break;
}
}
const melody = [
{ note: 523.25, time: 0.2 }, // C5
{ note: 659.25, time: 0.2 }, // E5
{ note: 783.99, time: 0.2 }, // G5
{ note: 659.25, time: 0.2 }, // E5
{ note: 880.00, time: 0.4 }, // A5
{ note: 783.99, time: 0.4 } // G5
];
function startBGM() {
if (!audioCtx) return;
stopBGM();
let step = 0;
bgmInterval = setInterval(() => {
if(!gameActive) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = 'triangle';
osc.frequency.value = melody[step].note;
gain.gain.setValueAtTime(0.03, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + melody[step].time);
osc.start();
osc.stop(audioCtx.currentTime + melody[step].time);
step = (step + 1) % melody.length;
}, 250);
}
function stopBGM() {
if (bgmInterval) {
clearInterval(bgmInterval);
bgmInterval = null;
}
}
// --- ゲームのメイン処理 ---
const container = document.getElementById('game-container');
const brocco = document.getElementById('brocco-kun');
const scoreDisplay = document.getElementById('score');
const livesDisplay = document.getElementById('lives-board');
const startScreen = document.getElementById('start-screen');
const gameOverScreen = document.getElementById('game-over');
const finalScoreDisplay = document.getElementById('final-score');
const startBtn = document.getElementById('start-btn');
const restartBtn = document.getElementById('restart-btn');
const collectionBoard = document.getElementById('collection-board');
const dots = document.querySelectorAll('.dot');
const containerWidth = 800;
const containerHeight = 450;
const groundHeight = 50;
const broccoPhysics = { x: 50, y: 0, width: 80, height: 110, vy: 0, jumpCount: 0, isDodging: false };
const gravity = 0.8;
const jumpPower = -13;
const maxJumps = 2;
const gameSpeedBase = 5;
let gameSpeed = gameSpeedBase;
let score = 0;
let lives = 3;
let gameActive = false;
let obstacles = [];
let items = [];
let frameCount = 0;
let bloomCount = 0;
let smallBlooms = 0;
let bigBlooms = 0;
let isDamageInvincible = false;
let isMayoInvincible = false;
let isSlowed = false;
let mayoTimer = null;
let slowTimer = null;
const keys = { space: false, arrowRight: false };
function initGame() {
score = 0;
lives = 3;
gameSpeed = gameSpeedBase;
gameActive = true;
isDamageInvincible = false;
isMayoInvincible = false;
isSlowed = false;
frameCount = 0;
bloomCount = 0;
smallBlooms = 0;
bigBlooms = 0;
keys.arrowRight = false;
broccoPhysics.isDodging = false;
if (mayoTimer) clearTimeout(mayoTimer);
if (slowTimer) clearTimeout(slowTimer);
brocco.style.opacity = '1';
brocco.classList.remove('mayo-aura');
container.classList.remove('slow-mode');
dots.forEach(dot => dot.classList.remove('bloom'));
updateLivesDisplay();
updateCollectionBoard();
broccoPhysics.y = containerHeight - groundHeight - broccoPhysics.height;
broccoPhysics.vy = 0;
broccoPhysics.jumpCount = 0;
scoreDisplay.innerText = score;
startScreen.style.display = 'none';
gameOverScreen.style.display = 'none';
document.querySelectorAll('.obstacle').forEach(obs => obs.remove());
document.querySelectorAll('.item').forEach(item => item.remove());
obstacles = [];
items = [];
updateBroccoPosition();
startBGM();
gameLoop();
}
function updateLivesDisplay() {
let hearts = '';
for (let i = 0; i < lives; i++) hearts += '❤️';
livesDisplay.innerText = `残機: ${hearts}`;
}
function gameLoop() {
if (!gameActive) return;
frameCount++;
broccoPhysics.isDodging = keys.arrowRight;
broccoPhysics.vy += gravity;
broccoPhysics.y += broccoPhysics.vy;
const groundY = containerHeight - groundHeight - 120;
if (broccoPhysics.y > groundY) {
broccoPhysics.y = groundY;
broccoPhysics.vy = 0;
broccoPhysics.jumpCount = 0;
}
updateBroccoPosition();
if (frameCount % 90 === 0 || (frameCount % 60 === 0 && Math.random() > 0.7)) {
createObstacle();
}
if (frameCount % 250 === 0 || (frameCount % 180 === 0 && Math.random() > 0.8)) {
createItem();
}
const currentSpeed = isSlowed ? gameSpeed * 0.5 : gameSpeed;
moveObstacles(currentSpeed);
moveItems(currentSpeed);
score++;
scoreDisplay.innerText = Math.floor(score / 10);
if (score % 500 === 0) gameSpeed += 0.5;
requestAnimationFrame(gameLoop);
}
function updateBroccoPosition() {
const rotation = broccoPhysics.isDodging ? 'rotate(75deg)' : 'rotate(0deg)';
brocco.style.transform = `translate(${broccoPhysics.x}px, ${broccoPhysics.y}px) ${rotation}`;
}
function createObstacle() {
if (obstacles.length > 0) {
const lastObs = obstacles[obstacles.length - 1];
if (containerWidth - lastObs.x < 200) return;
}
const obsEl = document.createElement('div');
obsEl.classList.add('obstacle');
const blade = document.createElement('div');
blade.className = 'blade';
const handle = document.createElement('div');
handle.className = 'handle';
obsEl.appendChild(blade);
obsEl.appendChild(handle);
const randomHeightOffset = Math.floor(Math.random() * 140);
const obsY = containerHeight - groundHeight - 30 - randomHeightOffset;
const obs = { el: obsEl, x: containerWidth, y: obsY, width: 60, height: 30, passed: false };
obsEl.style.left = obs.x + 'px';
obsEl.style.bottom = (groundHeight + randomHeightOffset) + 'px';
container.appendChild(obsEl);
obstacles.push(obs);
}
function createItem() {
const itemTypes = [
{ type: 'heart', symbol: '💗' },
{ type: 'mayo', symbol: '💡' },
{ type: 'bug', symbol: '🦋' }
];
const selected = itemTypes[Math.floor(Math.random() * itemTypes.length)];
const itemEl = document.createElement('div');
itemEl.classList.add('item');
itemEl.innerText = selected.symbol;
const randomHeightOffset = 50 + Math.floor(Math.random() * 200);
const baseY = containerHeight - groundHeight - randomHeightOffset;
const item = { el: itemEl, type: selected.type, x: containerWidth, baseY: baseY, y: baseY, width: 32, height: 32, angle: 0 };
itemEl.style.left = item.x + 'px';
itemEl.style.top = item.y + 'px';
container.appendChild(itemEl);
items.push(item);
}
function moveObstacles(speed) {
for (let i = obstacles.length - 1; i >= 0; i--) {
const obs = obstacles[i];
obs.x -= speed;
obs.el.style.left = obs.x + 'px';
if (!obs.passed && broccoPhysics.x > obs.x + obs.width) {
obs.passed = true;
if (bloomCount < dots.length) {
dots[bloomCount].classList.add('bloom');
bloomCount++;
playSE('bloom');
if (bloomCount === dots.length) {
setTimeout(() => {
bloomCount = 0;
dots.forEach(d => d.classList.remove('bloom'));
addCollectedBrocco();
}, 200);
}
}
}
if (obs.x + obs.width < 0) {
obs.el.remove();
obstacles.splice(i, 1);
continue;
}
// 足元を基準にした正確な当たり判定の計算
const currentHeight = broccoPhysics.isDodging ? 50 : broccoPhysics.height;
const hitBoxShrinkX = 25;
const hitBoxShrinkY = 20;
const hitBoxTop = broccoPhysics.y + broccoPhysics.height - currentHeight + hitBoxShrinkY;
const hitBoxBottom = broccoPhysics.y + broccoPhysics.height - hitBoxShrinkY;
if (
broccoPhysics.x + hitBoxShrinkX < obs.x + obs.width &&
broccoPhysics.x + broccoPhysics.width - hitBoxShrinkX > obs.x &&
hitBoxTop < obs.y + obs.height &&
hitBoxBottom > obs.y
) {
if (!isDamageInvincible && !isMayoInvincible) {
takeDamage();
}
}
}
}
function moveItems(speed) {
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
item.x -= speed;
item.angle += 0.05;
item.y = item.baseY + Math.sin(item.angle) * 40;
item.el.style.left = item.x + 'px';
item.el.style.top = item.y + 'px';
if (item.x + item.width < 0) {
item.el.remove();
items.splice(i, 1);
continue;
}
// 足元を基準にした正確な当たり判定の計算(アイテム用)
const hitBoxShrink = 10;
const currentHeight = broccoPhysics.isDodging ? 50 : broccoPhysics.height;
const hitBoxTop = broccoPhysics.y + broccoPhysics.height - currentHeight + hitBoxShrink;
const hitBoxBottom = broccoPhysics.y + broccoPhysics.height - hitBoxShrink;
if (
broccoPhysics.x + hitBoxShrink < item.x + item.width &&
broccoPhysics.x + broccoPhysics.width - hitBoxShrink > item.x &&
hitBoxTop < item.y + item.height &&
hitBoxBottom > item.y
) {
applyItemEffect(item.type);
playSE('item');
item.el.remove();
items.splice(i, 1);
}
}
}
function applyItemEffect(type) {
if (type === 'heart') {
lives = Math.min(lives + 1, 3);
updateLivesDisplay();
} else if (type === 'mayo') {
isMayoInvincible = true;
brocco.classList.add('mayo-aura');
if (mayoTimer) clearTimeout(mayoTimer);
mayoTimer = setTimeout(() => {
isMayoInvincible = false;
brocco.classList.remove('mayo-aura');
}, 5000);
} else if (type === 'bug') {
isSlowed = true;
container.classList.add('slow-mode');
if (slowTimer) clearTimeout(slowTimer);
slowTimer = setTimeout(() => {
isSlowed = false;
container.classList.remove('slow-mode');
}, 5000);
}
}
function takeDamage() {
lives--;
updateLivesDisplay();
playSE('damage');
if (lives <= 0) {
endGame();
} else {
isDamageInvincible = true;
let blinkCount = 0;
let blinkInterval = setInterval(() => {
brocco.style.opacity = blinkCount % 2 === 0 ? '0.3' : '1';
blinkCount++;
}, 150);
setTimeout(() => {
clearInterval(blinkInterval);
brocco.style.opacity = '1';
isDamageInvincible = false;
}, 1500);
}
}
function addCollectedBrocco() {
smallBlooms++;
if (smallBlooms >= 5) {
smallBlooms = 0;
bigBlooms++;
}
updateCollectionBoard();
}
function createMiniBrocco(isBig) {
const wrapper = document.createElement('div');
const scale = isBig ? 0.4 : 0.25;
wrapper.style.width = (100 * scale) + 'px';
wrapper.style.height = (120 * scale) + 'px';
wrapper.style.position = 'relative';
wrapper.style.margin = '0 8px';
wrapper.style.flexShrink = '0';
const inner = document.createElement('div');
inner.style.width = '100px';
inner.style.height = '120px';
inner.style.position = 'absolute';
inner.style.top = '0';
inner.style.left = '0';
inner.style.transform = `scale(${scale})`;
inner.style.transformOrigin = 'top left';
inner.innerHTML = `
<div class="hand left"></div>
<div class="hand right"></div>
<div class="stem">
<div class="eye left"><div class="pupil"></div></div>
<div class="eye right"><div class="pupil"></div></div>
<div class="mouth"></div>
</div>
<div class="head">
<svg class="head-svg" viewBox="0 0 100 60">
<path d="M 15 50 C 0 35, 10 10, 30 15 C 35 0, 65 0, 70 15 C 90 10, 100 35, 85 50 C 70 60, 30 60, 15 50 Z" fill="transparent" stroke="#228B22" stroke-width="4" stroke-linejoin="round" />
</svg>
<div class="dot d1 bloom"></div><div class="dot d2 bloom"></div><div class="dot d3 bloom"></div><div class="dot d4 bloom"></div>
<div class="dot d5 bloom"></div><div class="dot d6 bloom"></div><div class="dot d7 bloom"></div><div class="dot d8 bloom"></div>
</div>
`;
wrapper.appendChild(inner);
return wrapper;
}
function updateCollectionBoard() {
collectionBoard.innerHTML = '';
for(let i=0; i<bigBlooms; i++) {
collectionBoard.appendChild(createMiniBrocco(true));
}
for(let i=0; i<smallBlooms; i++) {
collectionBoard.appendChild(createMiniBrocco(false));
}
}
function endGame() {
gameActive = false;
stopBGM();
finalScoreDisplay.innerText = Math.floor(score / 10);
gameOverScreen.style.display = 'flex';
}
// イベントリスナー
startBtn.addEventListener('click', () => {
initAudio();
initGame();
});
restartBtn.addEventListener('click', () => {
initGame();
});
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
if (gameActive && broccoPhysics.jumpCount < maxJumps) {
broccoPhysics.vy = jumpPower;
broccoPhysics.jumpCount++;
playSE('jump');
}
else if (!gameActive && gameOverScreen.style.display === 'flex') {
initGame();
}
}
if (e.code === 'ArrowRight') {
keys.arrowRight = true;
}
});
window.addEventListener('keyup', (e) => {
if (e.code === 'ArrowRight') {
keys.arrowRight = false;
}
});
</script>
</body>
</html>


■ 今日の学び

今回のゲーム作りは、「#60 ネギ兄弟 – まな板サバイバル」のときと同じく、
まずはキャラクターづくりから AI と相談しながら進めていきました。

実は、主役の「ブロッコくん」には元になったアイデアがあります。

それは、「#75:シルエットスタジオ」の記事の最後でこっそり紹介した、
エクセルの図形を組み合わせて作ったキャラクターなんですよ。( ¯꒳¯ )b✧

こうして「まずはキャラクターありき」でスタートして、
そこから「この子を主役にしてどんなゲームにしよう?」と
AI とアイデアを出し合いながら仕上げていくプロセスは、
キャラクターへの愛着もどんどん湧いて、とっても楽しかったです。

ゲームのシステムに合わせてキャラクターを作るのも良いですが、
キャラクターの個性に合わせて世界観を広げていくのも、

ゲーム作りの醍醐味ですね。(*’∀’人)

そして今回、どうしても外せなかったポイントがあります。
それは…… 「ブロッコリーといえば、やっぱりマヨネーズでしょ!(>v<*)」 という強い思いから生まれたアイテムです!

取ると5秒間黄金のオーラをまとって無敵になる最強アイテムなのですが、
実はマヨネーズの絵文字が見つからなかったので、

こっそり電球の絵文字「💡」で代用しているのはここだけの秘密です。(=b=)

これからも、こんなふうに遊び心を大切にしながら、
AI と一緒に面白いゲームを作っていきたいなと思います!

■ 次回は、ツバメの綿毛トスゲームです

天気のよい、さわやかな朝。

目の前でツバメたちが、
ふわふわした羽毛をキャッチしながら遊んでいるような、

そんな光景を見たことはありませんか?

その一瞬のかわいらしい動きから着想を得て、

次回は ツバメたちの訓練遊び をテーマにしたゲームを作りました。

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

次の実験はこちら  
  (ツバメたちの訓練遊び)
「朝空のフェザー・ドリフト」



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

~このブログについて~

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

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


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

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

続きを読む