実験#119:究極の猪突猛進を目指す!(猪突猛進ダッシュ!)

■119個目 「猪突猛進ダッシュ!

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

今回は、画面いっぱいの迫力と爽快感にこだわった
アクションゲームを作ってみました。

今回の主役は……ズバリ「イノシシ」です!

これまでの静かでやさしい雰囲気の実験から一転して、
今回は“画面を突き破るような勢い”に全振りしてみました。(=v=*)

イノシシの前には、木や岩、時にはクマや車(!?)まで、
いろんな障害物が上から次々と降ってきます。

それでもイノシシは止まりません。

「ドゴォォォン!!」「バキィィィッ!!」

ぶつかった瞬間には、
画面枠からはみ出すほど大きなマンガ文字が飛び出して、
障害物を豪快に吹き飛ばしながら突き進んでいきます。

“ぶつかっちゃダメ”ではなく、 “ぶつかって吹き飛ばす!”という、
ちょっと変わったストレス発散ゲームになっています。(≧▽≦)

もちろん衝突するとダメージは受けてしまいますが、
道中に落ちてくるお芋(🍠)やカキ(🍊)を食べれば元気回復です。

さらに、連続で食べるとイノシシが虹色に光り出す
無敵の「フィーバーモード」も発動します。(´▽`)

どこか懐かしいレトロなピコピコ音(8bitサウンド)と一緒に、
画面全体が揺れるほどのド迫力と、
一生懸命走るイノシシの姿を楽しんでもらえたら嬉しいです。

……あ、画面の横では「イノシシの豆知識」も読めちゃうので、 走り疲れたらそちらも覗いてみてくださいね。(^^)


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

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

みー:(^v^*) 今日もゲームを作ります。

HTMLで動かせる「猪突猛進」ゲームです。

縦画面で、イノシシは①〜⑤の5つのポジションを矢印キーで移動できます。

それぞれの位置でイノシシの見た目に少し変化をつけて、
走っている感じを表現します。

上からは木や岩、クマや車などの障害物がランダムに落ちてきて、
ぶつかるとスコアが入るけれどダメージも受けます。

カキやイモなどの食べ物も落ちてきて、
こちらはスコアにはならないけれどHPを回復してくれます。

AI:今日も制作の続きだな。
まずはひとつ、イノシシの話をしよう。

「猪突猛進」という言葉のイメージで、
まっすぐしか走れない不器用な動物と思われがちだけど、
実はとても神経質で頭が良いんだ。

罠を覚えて避けたり、
急ブレーキや直角のカーブも得意だったりする。

今回はあえて“ぶつかって進む”ゲームだけど、
そんなギャップも面白いよな。

■ ゲームの仕様:猪突猛進ダッシュ!

回復アイテム(スコアなし)
上から降ってくるカキ(🍊)やイモ(🍠)を食べるとHPが回復します。

操作
左右の矢印キーで5つのレーンを移動。

見た目の変化
レーンの端ではイノシシが体を少し傾け、
上下にバウンドしながら土煙(💨)を上げて走り続けます。

障害物(スコア&ダメージ)
木(🌲)、岩(🪨)、クマ(🐻)、車(🚗)に
ぶつかるとスコア+100。 その代わりHPにダメージを受けます。

みー:(^_^) ここからがポイントです。

障害物にぶつかった瞬間のインパクトを最大限に表現したいです。
〈拡大・集中線・オノマトペ・破片・衝撃のリアリティ〉
多少見えづらくても構いません。勢いのあるゲームにしましょう。

AI: なるほど、振り切った表現だな。

イノシシは首から肩にかけて“鎧”と呼ばれる分厚い皮膚を持っていて、
衝撃にとても強い。

その“生きたミサイル”の迫力を出すために、演出を強化してみた。

■ 強化ポイント:オーバーキル・インパクト仕様

  • ズーム効果:衝突時に一瞬だけ画面を拡大
  • ヒットストップ:数フレームだけ時を止めて重みを演出
  • 画面揺れ:キャンバス全体を激しく揺らす
  • 集中線:黒と赤の集中線が一気に広がる
  • 巨大オノマトペ:「ドゴォォォン!!」などの文字が画面手前に飛び出す
  • 破片パーティクル:障害物が粉々に砕ける演出

みー:(=v=) リトライするとスピードがどんどん上がってしまうので、
スタート時に戻るようにしたいです。

ダメージも少し大きいので、もう少し余裕が欲しいです。

AI: 処理が重なってスピードが上がっていたので修正したよ。
ダメージも30→15に調整して、ぶつかりやすくした。

走り続けるほど最高速に近づく仕様にして、疾走感を強めてある。

それと、イノシシの豆知識をもうひとつ。

子どもの「ウリ坊」のシマシマ模様は、
木漏れ日に紛れるための迷彩柄なんだ。

数ヶ月で模様が消える頃には、あの“鎧”が完成している。
自然はよくできているよな。

みー:( ̄v ̄) オノマトペが画面からはみ出して読めないので、
枠を飛び越えるように出せますか?

AI: 画面内に描いていたから切れていたんだな。

今回は構造を変えて、文字を“画面の手前のレイヤー”に出すようにした。

漫画のコマから飛び出すようなアニメーションで、
どんなに大きくても読めるよ。

みー:ヽ(*^_^*)ノ 芋を連続で食べたら無敵になる
「フィーバーモード」を追加して!

AI: 任せてくれ。 イノシシは実は泳ぎも得意で、
島から島へ何キロも泳ぐこともある。

そのタフさをイメージして、フィーバーモードを作った。

■ フィーバーモード(無敵)

効果
障害物にぶつかってもダメージ0。
ヒットストップなしで粉砕。 スコアは通常の3倍(+1500)。

コンボシステム
回復アイテム(🍠🍊)を連続で3つ取ると発動。
障害物にぶつかるとコンボはリセット。

フィーバー中(約5秒)

イノシシが虹色のオーラを放つ

BGMが倍速&1オクターブ上昇してノリノリに



■ 実際の動作

まずは、AI が最初に出してくれた試作版のゲーム画面をご紹介します。

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


こちらが、仕上がりのスタート画面です。
画面左側には毎回変わるイノシシの雑学、
右側には遊び方とスコアランキングが表示されています。

(画像:仕上がりのゲーム画面)


ゲーム中は、障害物にぶつかるとダメージとポイントが同時に入ります。
お芋やカキを取るとポイントは入りませんが、HPが回復します。

(画像:ゲーム画面)


さらに、カキやお芋を連続で3つ取ると「フィーバーモード」に突入します。
無敵状態で一定時間、思う存分猪突猛進できる特別な時間です。
その間にさらに3つ取ると、フィーバーがそのまま延長されます。

(画像:フィーバーモード)


HPが0になるとゲームオーバーです。
スコアが高かった場合は、ランキングに記録されていきます。

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


■ コードのポイント

今回の記事では、
AI と相談しながら作った猪突猛進ダッシュ! の HTML コードを公開します。

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

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

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

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

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

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

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

■ 遊び方・操作方法

操作はとってもシンプルです。(^^*)
パソコンのキーボードの「左(←)」「右(→)」の矢印キーだけで遊べます。

5つのレーンを猪突猛進に走り続けるイノシシを、
矢印キーで左右に動かして導いてあげてくださいね。

道中には、上からいろんなものが降ってきます!

🌲 木・岩・クマ・クルマ(🚗)
これらの障害物にぶつかると……

「ドゴォォォン!!」と派手な文字が飛び出して、
イノシシが豪快に吹き飛ばします!(≧▽≦)

スコアは大きく入りますが、イノシシの体力が少し減ってしまいます。

🍠 お芋・カキ(🍊)
こちらはイノシシの大好物です。(=v=*)
食べると体力が回復するので、見つけたらしっかり取ってあげてくださいね。

🌟 連続ゲットで無敵の「フィーバーモード」!
お芋やカキを 3つ連続 で食べると、
イノシシが虹色に光り出してフィーバーモードに突入します!(´▽`)

この間は、障害物にぶつかってもダメージゼロ!
そのまま粉砕して、スコアも通常の3倍になります。

テンポアップしたノリノリのBGMと一緒に、
無敵の爆走を楽しんでください。

体力がゼロになるとゲームオーバーです。(;;)

画面の右側には上位5名までのスコアランキングも表示されるので、
ぜひハイスコアを目指して何度でも挑戦してみてくださいね。(^_^)/



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

▼ここからコード▼(クリックで開く)
<!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;
display: flex;
flex-direction: column;
align-items: center;
background-color: #1a1a2e;
color: white;
font-family: 'Impact', 'Arial Black', sans-serif;
overflow-x: hidden; /* 横スクロール防止 */
padding: 20px;
}
h1 {
color: #e74c3c;
text-shadow: 2px 2px 0 #fff;
margin: 0 0 20px 0;
font-size: 36px;
letter-spacing: 2px;
}
/* 3列レイアウト用のラッパー */
#main-wrapper {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 20px;
max-width: 1200px;
width: 100%;
}
/* 左右のパネル共通スタイル */
.side-panel {
flex: 1;
max-width: 300px;
background: rgba(0, 0, 0, 0.6);
border: 4px solid #555;
border-radius: 10px;
padding: 20px;
box-sizing: border-box;
font-family: 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif; /* テキスト部分は読みやすいフォントに */
}
.side-panel h3 {
margin-top: 0;
color: #f1c40f;
border-bottom: 2px solid #f1c40f;
padding-bottom: 10px;
font-family: 'Impact', 'Arial Black', sans-serif;
}
/* 左パネル(雑学) */
#trivia-text {
font-size: 16px;
line-height: 1.6;
text-align: justify;
}
/* 右パネル(ルール&ランキング) */
.rules-list {
padding-left: 20px;
font-size: 14px;
line-height: 1.5;
margin-bottom: 30px;
}
.rules-list li { margin-bottom: 8px; }
#ranking-list {
list-style: none;
padding: 0;
margin: 0;
}
#ranking-list li {
background: #333;
margin-bottom: 5px;
padding: 10px;
border-radius: 5px;
display: flex;
justify-content: space-between;
font-weight: bold;
font-family: 'Impact', sans-serif;
letter-spacing: 1px;
}
#ranking-list li:nth-child(1) { background: #d4af37; color: black; } /* 1位はゴールド */
#ranking-list li:nth-child(2) { background: #c0c0c0; color: black; } /* 2位はシルバー */
#ranking-list li:nth-child(3) { background: #cd7f32; color: black; } /* 3位はブロンズ */
/* 中央(ゲーム画面) */
#game-container {
position: relative;
border: 6px solid #e74c3c;
border-radius: 10px;
background: #4a6e27;
box-shadow: 0 15px 30px rgba(231, 76, 60, 0.4);
flex-shrink: 0;
}
canvas {
display: block;
border-radius: 4px;
}
#ui {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
display: flex;
justify-content: space-between;
font-size: 24px;
text-shadow: 2px 2px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
z-index: 10;
}
#hp-bar-bg {
width: 150px;
height: 20px;
background: #222;
border: 3px solid white;
border-radius: 5px;
overflow: hidden;
margin-top: 5px;
}
#hp-bar {
width: 100%;
height: 100%;
background: #2ecc71;
transition: width 0.1s, background-color 0.2s;
}
.manga-text {
position: absolute;
top: 50%;
left: 50%;
width: 150%;
text-align: center;
font-size: 70px;
font-style: italic;
font-weight: 900;
color: #fff200;
text-shadow:
3px 3px 0 #000, -3px -3px 0 #000, 3px -3px 0 #000, -3px 3px 0 #000,
0 0 15px #e74c3c,
6px 6px 0 #e74c3c, 12px 12px 0px #000;
z-index: 100;
pointer-events: none;
animation: popOut 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.fever-text {
color: #ff00ff;
font-size: 90px;
text-shadow:
3px 3px 0 #fff, -3px -3px 0 #fff, 3px -3px 0 #fff, -3px 3px 0 #fff,
0 0 20px #00ffff,
8px 8px 0 #00ffff, 15px 15px 0px #000;
animation: feverPop 1s ease-out forwards;
}
@keyframes popOut {
0% { transform: translate(-50%, -50%) scale(0) rotate(-15deg); opacity: 1; }
40% { transform: translate(-50%, -50%) scale(1.2) rotate(5deg); opacity: 1; }
80% { transform: translate(-50%, -50%) scale(1) rotate(0deg); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(1.1) rotate(5deg); opacity: 0; }
}
@keyframes feverPop {
0% { transform: translate(-50%, -50%) scale(0) rotate(-20deg); }
20% { transform: translate(-50%, -50%) scale(1.5) rotate(10deg); }
80% { transform: translate(-50%, -50%) scale(1.2) rotate(0deg); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(2) rotate(5deg); opacity: 0; }
}
.screen-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0,0,0,0.9);
padding: 40px;
border-radius: 15px;
width: 85%;
box-sizing: border-box;
z-index: 30;
}
#start-screen { border: 4px solid #f1c40f; }
#game-over { border: 4px solid #e74c3c; display: none; }
button {
margin-top: 20px;
padding: 15px 30px;
font-size: 20px;
font-weight: bold;
cursor: pointer;
color: white;
background: #e74c3c;
border: 2px solid white;
border-radius: 5px;
text-transform: uppercase;
}
button:hover { background: #c0392b; }
#start-screen button { background: #f39c12; }
#start-screen button:hover { background: #d68910; }
</style>
</head>
<body>
<h1>猪突猛進!超インパクトダッシュ</h1>
<div id="main-wrapper">
<!-- 左パネル:雑学 -->
<div id="left-panel" class="side-panel">
<h3>🐗 マスターのイノシシ雑学</h3>
<p id="trivia-text">ゲームをスタートすると、ここに雑学が表示されるぜ。</p>
</div>
<!-- 中央パネル:ゲーム本体 -->
<div id="game-container">
<div id="ui">
<div>
HP
<div id="hp-bar-bg"><div id="hp-bar"></div></div>
</div>
<div id="score">SCORE: 0</div>
</div>
<canvas id="gameCanvas" width="400" height="600"></canvas>
<div id="start-screen" class="screen-overlay">
<h2 style="color: #f1c40f; margin:0 0 20px 0; font-size: 36px; text-shadow: 2px 2px 0 #000;">ARE YOU READY?</h2>
<button onclick="startGame()">GAME START</button>
</div>
<div id="game-over" class="screen-overlay">
<h2 style="color: #e74c3c; margin:0 0 10px 0; font-size: 40px;">GAME OVER</h2>
<p id="final-score" style="margin: 0 0 20px 0; font-size: 28px;">最終スコア: 0</p>
<button onclick="resetGame()">RESTART</button>
</div>
</div>
<!-- 右パネル:遊び方&ランキング -->
<div id="right-panel" class="side-panel">
<h3>📜 遊び方</h3>
<ul class="rules-list">
<li><b>移動:</b> 左右の矢印キー</li>
<li><b>障害物 (🌲🪨🐻🚗):</b><br>ぶつかるとダメージを受けるが、スコアが入る!</li>
<li><b>回復アイテム (🍠🍊):</b><br>食べるとHPが回復する。</li>
<li><b>🌟 フィーバーモード:</b><br>アイテムを連続で3つ取ると無敵状態!障害物をノーダメージで粉砕し、スコアが3倍になるぞ!</li>
</ul>
<h3>🏆 TOP 5 ランキング</h3>
<ul id="ranking-list">
<!-- JavaScriptで生成 -->
</ul>
</div>
</div>
<script>
// --- 雑学(トリビア)システム ---
const allTrivia = [
"イノシシは「猪突猛進」という言葉のせいで前しか見えないと思われがちだが、実はすごく神経質で頭が良く、急ブレーキや直角の急カーブもめちゃくちゃ得意なんだ。",
"イノシシの首から肩にかけては「鎧(シールド)」と呼ばれる分厚い皮膚と脂肪の層がある。猟犬の牙や小口径の銃弾すら跳ね返すほどの硬さだぜ。",
"子供の「ウリ坊」にあるシマシマ模様。あれは可愛いだけじゃなく、森の中で木漏れ日と同化するための完璧な「迷彩柄(カモフラージュ)」なんだ。",
"ずんぐりむっくりな体型だが、実はバネの塊。助走なしのその場跳びで、1メートル以上の柵を軽々と飛び越えるジャンプ力を持ってる。",
"水が苦手そうに見えて、実はめちゃくちゃ泳ぎが得意だ。エサを求めて海を何キロも泳ぎ、島から島へ渡るイノシシもいるくらいタフなんだぜ。",
"鼻先(スナウト)の筋肉が異常に発達していて、50〜60キロある重い岩でもブルドーザーみたいにゴロンとひっくり返してエサを探すことができるんだ。"
];
let availableTrivia = [...allTrivia];
function showRandomTrivia() {
if (availableTrivia.length === 0) {
availableTrivia = [...allTrivia]; // 全部出尽くしたら補充
}
const idx = Math.floor(Math.random() * availableTrivia.length);
document.getElementById('trivia-text').innerText = availableTrivia[idx];
availableTrivia.splice(idx, 1); // 一度出たものはリストから外す
}
// --- ランキングシステム ---
let highScores = [0, 0, 0, 0, 0];
function updateRankingUI() {
const list = document.getElementById('ranking-list');
list.innerHTML = '';
highScores.forEach((score, index) => {
let li = document.createElement('li');
li.innerHTML = `<span>${index + 1}位</span> <span>${score}</span>`;
list.appendChild(li);
});
}
function addScoreToRanking(newScore) {
highScores.push(newScore);
highScores.sort((a, b) => b - a); // 降順にソート
highScores = highScores.slice(0, 5); // 上位5件のみ残す
updateRankingUI();
}
// 初期化時にランキングUIを作成
updateRankingUI();
// --- Audio System ---
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx;
function initAudio() {
if (!audioCtx) audioCtx = new AudioCtx();
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playTone(freq, type = 'square', duration = 0.1, vol = 0.05) {
if (!audioCtx) return;
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
function playHealSound() {
playTone(880, 'square', 0.1, 0.05);
setTimeout(() => playTone(1108, 'square', 0.1, 0.05), 80);
setTimeout(() => playTone(1318, 'square', 0.2, 0.05), 160);
}
function playFeverStartSound() {
let freq = 440;
for(let i=0; i<10; i++) {
setTimeout(() => playTone(freq + (i*100), 'square', 0.1, 0.05), i*50);
}
}
function playDamageSound() {
if (!audioCtx) return;
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(10, audioCtx.currentTime + 0.3);
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.3);
}
function playGameOverSound() {
playTone(440, 'square', 0.2, 0.1);
setTimeout(() => playTone(392, 'square', 0.2, 0.1), 200);
setTimeout(() => playTone(349, 'square', 0.2, 0.1), 400);
setTimeout(() => playTone(329, 'square', 0.8, 0.1), 600);
}
const bassline = [130, 130, 196, 130, 130, 196, 130, 261];
let noteIdx = 0;
let bgmFrameCount = 0;
function updateBGM() {
if (hitStop > 0 || !gameActive) return;
bgmFrameCount++;
let tempo = feverTime > 0 ? 8 : 15;
if (bgmFrameCount % tempo === 0) {
let freq = bassline[noteIdx % bassline.length] * (feverTime > 0 ? 2 : 1);
playTone(freq, 'square', 0.1, 0.04);
noteIdx++;
}
}
// --- Game System ---
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const hpBar = document.getElementById('hp-bar');
const scoreEl = document.getElementById('score');
const startScreen = document.getElementById('start-screen');
const gameOverScreen = document.getElementById('game-over');
const lanes = [40, 120, 200, 280, 360];
let boar = { lane: 2, y: 500, hp: 100, score: 0 };
let objects = [];
let particles = [];
let frameCount = 0;
let gameActive = false;
let gameSpeed = 8;
let animationId;
let hitStop = 0;
let screenShake = 0;
let itemCombo = 0;
let feverTime = 0;
const obstacles = ['🌲', '🪨', '🐻', '🚗'];
const items = ['🍠', '🍊'];
const onomatopoeias = ['ドゴォォォン!!', 'バキィィィッ!!', 'ガッシャァァン!!', 'ズドォォォン!!', 'メキョァッ!!'];
const feverHitWords = ['BREAK!!', 'CRASH!!', '無敵!!', '撃破!!'];
document.addEventListener('keydown', (e) => {
// 矢印キー押下時に画面がスクロールするのを防ぐ
if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].indexOf(e.code) > -1) {
e.preventDefault();
}
if (!gameActive || hitStop > 0) return;
if (e.key === 'ArrowLeft' && boar.lane > 0) boar.lane--;
if (e.key === 'ArrowRight' && boar.lane < 4) boar.lane++;
});
function startGame() {
initAudio();
startScreen.style.display = 'none';
showRandomTrivia(); // 開始時に雑学を更新
resetGame();
}
function spawnObject() {
let isItem = Math.random() < 0.25;
let type = isItem
? items[Math.floor(Math.random() * items.length)]
: obstacles[Math.floor(Math.random() * obstacles.length)];
let lane = Math.floor(Math.random() * 5);
objects.push({ type: type, isItem: isItem, lane: lane, y: -50, hit: false });
}
function spawnParticles(x, y, isItem, isFeverBlast = false) {
let baseColor = isItem ? '#2ecc71' : '#e74c3c';
let count = isFeverBlast ? 80 : 40;
for(let i = 0; i < count; i++) {
let color = isFeverBlast
? `hsl(${Math.random() * 360}, 100%, 50%)`
: (Math.random() > 0.5 ? baseColor : '#ffffff');
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5) * (isFeverBlast ? 50 : 30),
vy: (Math.random() - 0.5) * (isFeverBlast ? 50 : 30),
life: Math.random() * 20 + 10,
size: Math.random() * 15 + 5,
color: color
});
}
}
function showMangaText(text, isFever = false) {
const el = document.createElement('div');
el.className = isFever ? 'manga-text fever-text' : 'manga-text';
el.innerText = text;
document.getElementById('game-container').appendChild(el);
setTimeout(() => {
if(el.parentNode) el.parentNode.removeChild(el);
}, 1000);
}
function update() {
if (!gameActive) return;
if (hitStop > 0) {
hitStop--;
updateParticles();
return;
}
frameCount++;
if (feverTime > 0) feverTime--;
if (frameCount % 200 === 0 && gameSpeed < 25) {
gameSpeed += 1;
}
if (screenShake > 0) screenShake *= 0.9;
let baseRate = feverTime > 0 ? 30 : 45;
let spawnRate = Math.max(8, baseRate - Math.floor(gameSpeed * 1.5));
if (frameCount % spawnRate === 0) spawnObject();
objects.forEach((obj) => {
obj.y += gameSpeed;
if (!obj.hit && obj.lane === boar.lane && obj.y > boar.y - 45 && obj.y < boar.y + 45) {
obj.hit = true;
let hitX = lanes[obj.lane];
let hitY = obj.y;
if (obj.isItem) {
boar.hp = Math.min(100, boar.hp + 20);
spawnParticles(hitX, hitY, true);
playHealSound();
itemCombo++;
if (itemCombo >= 3) {
feverTime = 300;
itemCombo = 0;
showMangaText('FEVER!!', true);
playFeverStartSound();
}
} else {
if (feverTime > 0) {
boar.score += 1500;
screenShake = 15;
let word = feverHitWords[Math.floor(Math.random() * feverHitWords.length)];
showMangaText(word);
spawnParticles(hitX, hitY, false, true);
playDamageSound();
} else {
boar.hp -= 15;
boar.score += 500;
itemCombo = 0;
hitStop = 15;
screenShake = 30;
let word = onomatopoeias[Math.floor(Math.random() * onomatopoeias.length)];
showMangaText(word);
spawnParticles(hitX, hitY, false);
playDamageSound();
}
}
updateUI();
if (boar.hp <= 0) gameOver();
}
});
objects = objects.filter(obj => obj.y < canvas.height + 50 && !obj.hit);
updateParticles();
updateBGM();
}
function updateParticles() {
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.life--;
p.size *= 0.9;
});
particles = particles.filter(p => p.life > 0);
}
function drawMangaLines(centerX, centerY, isFever) {
ctx.save();
ctx.translate(centerX, centerY);
for (let i = 0; i < 60; i++) {
ctx.rotate((Math.PI * 2) / 60);
ctx.beginPath();
ctx.moveTo(40, 0);
ctx.lineTo(800, 0);
if (isFever) {
ctx.strokeStyle = `hsla(${Math.random() * 360}, 100%, 50%, 0.6)`;
} else {
ctx.strokeStyle = Math.random() > 0.5 ? 'rgba(0,0,0,0.8)' : 'rgba(231,76,60,0.6)';
}
ctx.lineWidth = Math.random() * 5 + 1;
ctx.stroke();
}
ctx.restore();
}
function draw() {
ctx.save();
if (screenShake > 0.5) {
let dx = (Math.random() - 0.5) * screenShake;
let dy = (Math.random() - 0.5) * screenShake;
ctx.translate(dx, dy);
}
if (feverTime > 0) {
ctx.fillStyle = `hsl(${frameCount * 15 % 360}, 50%, 20%)`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 4;
ctx.setLineDash([40, 40]);
let visualSpeed = feverTime > 0 ? gameSpeed * 2 : gameSpeed;
for(let i = 1; i < 5; i++) {
ctx.beginPath();
let lineYOffset = ((frameCount * visualSpeed) % 80) - 80;
ctx.moveTo(i * 80, lineYOffset);
ctx.lineTo(i * 80, canvas.height + 80);
ctx.stroke();
}
ctx.setLineDash([]);
ctx.font = '50px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
objects.forEach(obj => {
ctx.fillText(obj.type, lanes[obj.lane], obj.y);
});
drawBoar();
particles.forEach(p => {
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
if (hitStop > 0) {
drawMangaLines(canvas.width/2, canvas.height/2, false);
ctx.fillStyle = 'rgba(231, 76, 60, 0.3)';
ctx.fillRect(-100, -100, canvas.width + 200, canvas.height + 200);
} else if (feverTime > 0 && frameCount % 10 === 0) {
drawMangaLines(canvas.width/2, canvas.height/2, true);
}
ctx.restore();
}
function drawBoar() {
if(!gameActive && startScreen.style.display !== 'none') return;
ctx.save();
let x = lanes[boar.lane];
let bounce = hitStop > 0 ? 0 : Math.sin(frameCount * (feverTime > 0 ? 1.5 : 0.8)) * 8;
let y = boar.y + bounce;
ctx.translate(x, y);
let angle = (boar.lane - 2) * 15;
ctx.rotate(angle * Math.PI / 180);
if (feverTime > 0) {
ctx.shadowColor = `hsl(${frameCount * 20 % 360}, 100%, 60%)`;
ctx.shadowBlur = 30;
ctx.scale(1.2, 1.2);
}
ctx.font = '65px sans-serif';
ctx.fillText('🐗', 0, 0);
ctx.shadowBlur = 0;
if (feverTime > 0) ctx.scale(1/1.2, 1/1.2);
if (hitStop === 0) {
ctx.font = '30px sans-serif';
ctx.fillText(feverTime > 0 ? '🔥' : '💨', -20, 35);
}
if (itemCombo > 0 && feverTime === 0) {
ctx.rotate(-angle * Math.PI / 180);
ctx.font = 'bold 20px "Arial Black"';
ctx.fillStyle = '#f1c40f';
ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
ctx.strokeText(`${itemCombo} COMBO`, 0, -50);
ctx.fillText(`${itemCombo} COMBO`, 0, -50);
}
ctx.restore();
}
function updateUI() {
hpBar.style.width = Math.max(0, boar.hp) + '%';
if (feverTime > 0) {
hpBar.style.background = `hsl(${frameCount * 20 % 360}, 100%, 50%)`;
} else {
if (boar.hp <= 30) hpBar.style.background = '#e74c3c';
else if (boar.hp <= 60) hpBar.style.background = '#f1c40f';
else hpBar.style.background = '#2ecc71';
}
document.getElementById('score').innerText = 'SCORE: ' + boar.score;
}
function gameOver() {
gameActive = false;
playGameOverSound();
gameOverScreen.style.display = 'block';
document.getElementById('final-score').innerText = '最終スコア: ' + boar.score;
// ゲームオーバー時にスコアをランキングに追加
addScoreToRanking(boar.score);
}
function resetGame() {
cancelAnimationFrame(animationId);
document.querySelectorAll('.manga-text').forEach(el => el.remove());
showRandomTrivia(); // リスタート時にも新しい雑学を表示
boar = { lane: 2, y: 500, hp: 100, score: 0 };
objects = [];
particles = [];
hitStop = 0;
screenShake = 0;
itemCombo = 0;
feverTime = 0;
frameCount = 0;
bgmFrameCount = 0;
gameSpeed = 8;
gameActive = true;
gameOverScreen.style.display = 'none';
updateUI();
loop();
}
function loop() {
if (!gameActive && hitStop === 0) return;
update();
draw();
animationId = requestAnimationFrame(loop);
}
updateUI();
</script>
</body>
</html>


■ 今日の学び

今まで作ってきたゲームたちの中で、
実は「イノシシ」って森の敵キャラや背景として、
ちょくちょく脇役で登場していたんです。

でも今回は、満を持しての主役デビュー!

イノシシならではの「突進の迫力」と「力強さ」を
どう表現するかを考えて、
画面いっぱいにド派手な演出を詰め込んでみました。

ただ、テストプレイで岩や車を粉砕して突き進む姿を見ているうちに、
ふと 「……いや、これ絶対痛いよね? ( ꒪⌓꒪)」
と冷静になる瞬間があって。

そこから、ただ点数が入るだけではなく、
「豪快にぶつかってポイントは稼げるけれど、自分もちゃんとダメージを受ける」
という、ちょっと不思議な“痛みとスコアの共存ルール”にたどり着きました。

システム自体はとてもシンプルですが、
「いかに迫力を出すか」だけに全振りした今回の実験は、
画面を揺らしたり爆発させたりする工程そのものがすごく楽しくて……

作り終えたあと、
なんだか私自身も気持ちがスッと軽くなっていました。
=͟͟͞͞( ๑`・ω・´)💨 ドゴォォン!!

たまにはこういう、理屈抜きでドカン!
とぶつかっていく実験も良いものですね。


■ 次回は、波打ち際をギリギリで攻めるヤドカリのサバイバルゲームです。

次回は、「リアルな波の満ち引き」と
「スリルある駆け引き」をテーマにした

ちょっとドキドキのアクションゲームを作ってみました。

主人公は、砂浜をとことこ歩く小さなヤドカリ。

波に引き込まれないように距離を保ちながら、
色とりどりのシーグラスや貝殻を集めていきます。

波打ち際ギリギリほどレアなお宝が眠っているけれど、
油断すると波にさらわれてゲームオーバー……!

集めた貝殻にお引っ越ししながら高得点を目指す、
かわいくて少しスリルのあるゲームになっています。

次回もぜひ楽しみにしていてくださいね。(*^v^)ノシ


次の実験はこちら
波を読んでお宝を集めろ!
「ヤドカリの波打ち際サバイバル」




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

 

~このブログについて~

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

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


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

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

続きを読む