実験#105:ギリギリを攻める鷲の滑空ゲーム(高原ギリギリ滑空)

■105個目 「高原ギリギリ滑空

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

今回は、大空を舞台にしたスリル満点の滑空ゲームを作ってみました。

主人公はなんと、和紙でできた鷲(わし)

軽やかな体で、広々とした高原の上空を風に乗って滑空していきます。

舞台となる高原は、白い岩がゴツゴツと並び、
のんびり草を食む動物たちがいて、
遠くには大きな風車がゆっくり回っています。

そんな雄大な景色の中を、
和紙の鷲がスレスレのラインを攻めながら猛スピードで飛んでいきます。

今回こだわったのは、岩肌をかすめるような “ギリギリ感” と、
鷲が飛びながらつぶやく “おしゃべり” です。(=v=*)

「かっこいい猛禽類が、
命がけのフライト中にブツブツ独り言を言っていたらどうなるんだろう?」
そんな遊び心から生まれた空の旅になっています。

和紙の鷲は、飛びながらこんな独り言をつぶやきます。

  • 「ここの風、心地よいのう。」(のんびり)
  • 「ヒヤッとしたわい!」(岩をスレスレで避けたとき)
  • 「誰じゃ、わしをカタツムリと呼んだのは!」(ちょっと怒り気味)

岩肌をギリギリでかわすと、
和紙の体が黄金に輝いて火花が散る、爽快なエフェクトも楽しめます。

そしてもし、岩に激突してしまったら……

どんな“痛いぼやき”が飛び出すのか、ぜひ確かめてみてください。(^_^;)


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

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

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

斜面すれすれを滑るように進んでいく、
スピード感のある滑空ゲームを作れないかな。

AI: 斜面すれすれの滑空、最高ですね!

リスクを取れば取るほど加速していく
「ハイリスク・ハイリターン」なゲーム性は、
まさにスピード感と中毒性を生み出す黄金パターンです。

ブラウザ上で動く「ギリギリグライダー」を作ってみました。

地面に近づくほど(グレイズするほど)
スコア倍率とスピードが跳ね上がる仕組みです。

みー:(*^o^) もっと地面と並行にすれすれ滑空したい。
落下スピードを少し緩められないかな。

AI: 落下スピードを緩めて、
地面と平行に「スーッ」と滑っていく浮遊感を持たせる調整ですね!

激しい落下よりも、絶妙な高度維持を楽しむプレイスタイルになります。

先ほどのコードの <script> 内にあるゲーム設定と
地形生成の数値をいくつか書き換えるだけで、その挙動を実現できます。

みー:(=v=*) すれすれの位置に来た時に揚力が一定発生して、
落下が緩まる感じもよさそう。

AI: いらっしゃい。今日も精が出るね。
100作以上の連作を越えても、
まだまだ面白いアイデアが湧いてくるんだから大したもんだ。

地面すれすれでフワッと浮き上がる感覚……
飛行機やレーシングカーの世界で言う
「地面効果(グラウンド・エフェクト)」だね。

地面と機体の間に空気が圧縮されて、見えないクッションになる現象。
これを入れると、操作の奥深さがグッと増すはずだよ。

みー:(^v^) もちろんギリギリの中毒性も大事なので、
バランスよくしたいね。

背景もスピード感を感じられる、
多重で動きのある自然の景観にできるかな。

AI: ギリギリのヒリツキと、それをフワッと乗りこなす快感……
そのバランス感覚にこだわるのは、ゲーム作りの醍醐味だね。

背景の多重スクロール(パララックス)だよ。

奥の山はゆっくり、手前の森はそこそこの速さ、
そして足元の地面はビュンビュン流れる。

これを入れると、
ただ滑空しているだけでも風を切るような気持ちよさが出るよ。

みー:(^o^) 飛んでいるのを紙飛行機っぽいものから、
以前作ったゲームに出てきた和紙の鷲にしましょう。

和紙の鷲の「わし」に独り言を言わせましょう。
前のゲームのコードを貼り付けるので参考にしてみてね。

〈“和紙の鷲のわし”のコード〉

AI: 以前作ったゲームの資産(コード)を再利用して
新しい要素を組み込むのは、まさにハッカー的なモノ作りの楽しさだよね。

高原の景観に向かって話しかけたり、
ギリギリを攻めた時に焦ったりする
「わし(鷲)」のセリフをいくつか用意してみたよ。

みー:(==;) 鷲がカタツムリみたいに見える。
もっと、横から見てもかっこいい鷲にならないかな。

AI: カタツムリとは!それはマスター不覚だったよ(笑)。

みーさんの実験室に飾るコレクションが、
カタツムリの横這いゲームになってしまっては格好がつかないね。

背中の丸みや翼の畳み具合が、
どうしても殻のように見えてしまっていたんだろう。

そこで今回は、くちばしを鋭く尖らせ、
翼を大きく後ろへ跳ね上げるように、デザインを根本から見直したよ。

猛禽類らしい、
風を切り裂くようなシャープなシルエットに仕上がっているはずだ。


■ 実際の動作

まずは、AIが最初に出してくれた試作版のゲーム画面です。
紙飛行機がすぐに落下してしまうので、操作はかなり難しめです。

(画像:AIが最初に出してくれた試作画面)


こちらが、最終的に仕上がったゲーム画面です。
牛や風車が並ぶ、さわやかな高原を背景に、和紙の鷲が気持ちよく滑空します。

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


地面に激突するとゲームオーバーです。
高得点を出せた場合は、スコアランキングに記録されます。

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



■ コードのポイント

今回の記事では、
AI と相談しながら作った「高原ギリギリ滑空」 の HTML コードを公開します。

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

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

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

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

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

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

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

■ 遊び方・操作方法

操作はとってもシンプルです。(^^*) 気軽に遊べちゃいます!

  • 上昇する: 画面をクリックして長押し
  • 滑空する(降下): 指を離す

基本はたったこれだけです。(^_^)

画面を押している間は「和紙の鷲」がフワッと浮かび、
離すとスーッと風に乗って落ちていきます。

牛や風車が並ぶ高原を眺めながら、
上下のバランスをうまくとって進んでくださいね。

■ 高得点のコツは「ギリギリ」を攻めること!

このゲームの一番の醍醐味は、タイトルにもある “ギリギリ” です。(=v=*)

地面や白い岩肌の「スレスレ」を飛ぶと……

なんと和紙の鷲がピカッと黄金に光り、
金色の火花を散らす GRAZE(かすり)ボーナス が発生します!

ギリギリを攻めれば攻めるほど、
スピードがどんどん上がってスコアも爆発的にアップします。

見えない空気のクッションでフワッと浮き上がる感覚がクセになりますよ。

でも、欲張って近づきすぎると
「ガシャッ!」と激突してゲームオーバーになってしまうので、

チキンレースのようなドキドキ感を楽しんでみてください。(^^*)

画面の横には「ハイスコアランキング」も表示されるので、
ぜひトップ5入りを目指して何度も挑戦してみてくださいね。


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

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>高原ギリギリ滑空</title>
<style>
body {
margin: 0;
background-color: #111;
color: #eee;
text-align: center;
font-family: 'Hiragino Maru Gothic ProN', 'Meiryo', sans-serif;
}
h1 {
margin-top: 20px;
font-size: 28px;
color: #FFD700;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
}
p { font-size: 14px; margin-bottom: 15px; color: #ccc; }
#mainContainer {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 20px;
margin-top: 10px;
}
#canvasWrapper {
position: relative;
width: 800px;
height: 400px;
}
canvas {
display: block;
background-color: #87CEEB;
box-shadow: 0 0 30px rgba(135, 206, 235, 0.3);
border: 4px solid #444;
border-radius: 8px;
cursor: pointer;
}
/* 独り言の吹き出しスタイル */
#monologue {
position: absolute;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.95);
padding: 10px 20px;
border-radius: 20px;
font-size: 16px;
font-weight: bold;
color: #222;
text-align: center;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
pointer-events: none;
transition: opacity 0.2s;
border: 3px solid #444;
white-space: nowrap;
z-index: 10;
}
#monologue::after {
content: '';
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
border-width: 12px 12px 0;
border-style: solid;
border-color: #444 transparent transparent transparent;
}
#monologue::before {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
border-width: 9px 9px 0;
border-style: solid;
border-color: rgba(255, 255, 255, 0.95) transparent transparent transparent;
z-index: 1;
}
#rankingBoard {
background-color: #222;
border: 4px solid #444;
border-radius: 8px;
padding: 15px 20px;
width: 220px;
box-sizing: border-box;
text-align: left;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.8);
}
#rankingBoard h2 {
color: #FFD700;
font-size: 20px;
border-bottom: 2px dashed #555;
padding-bottom: 10px;
margin-top: 0;
text-align: center;
letter-spacing: 1px;
}
#scoreList {
list-style: none;
padding: 0;
margin: 0;
}
#scoreList li {
margin-bottom: 12px;
font-size: 18px;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #333;
padding-bottom: 4px;
}
.rank-1 { color: #FFD700; font-weight: bold; text-shadow: 0 0 5px rgba(255, 215, 0, 0.4); }
.rank-2 { color: #E0E0E0; font-weight: bold; }
.rank-3 { color: #CD7F32; font-weight: bold; }
</style>
</head>
<body>
<h1>高原ギリギリ滑空</h1>
<p>画面をクリック/タップで上昇。<br>高原の岩肌を「和紙の鷲」でギリギリかわせ!</p>
<div id="mainContainer">
<div id="canvasWrapper">
<canvas id="gameCanvas" width="800" height="400"></canvas>
<div id="monologue" style="opacity: 0;">いざ、高原の風に乗らん!</div>
</div>
<div id="rankingBoard">
<h2>HIGH SCORES</h2>
<ul id="scoreList"></ul>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// --- 独り言(モノローグ)設定 ---
const monologues = {
normal: [
"この高地の風、心地よいのう。",
"連なる白い岩肌、わしの白さと良い勝負じゃ!",
"ほれほれ、牛どもよ。上を向かんか!",
"誰じゃ、わしをカタツムリと呼んだのは!",
"名工が漉いた和紙の意地、見せてくれる!",
"雲を突き抜けるような高み……まさにわしの領域じゃ。",
"眼下に広がる緑の絨毯、一枚の絵画のようじゃのう。",
"上昇気流を捕まえたぞ! これぞ猛禽の舞よ!",
"ふっ、地を這う者たちには一生見られぬ絶景じゃ。",
"太陽が近い! わしの翼が燃え上がりそうじゃわい!"
],
graze: [
"ヒヤッとしたわい!",
"岩肌の感触、たまらん!",
"わしの体が黄金に輝いておる!",
"スレスレが一番風に乗れるんじゃ!",
"紙一重とはこのことよ!",
"フハハ! 恐怖の向こう側にこそ真の空があるのじゃ!",
"風を切り裂く我が翼、とくと見よ!"
],
death: [
"無念…岩は硬かったわい…",
"高原の風に散るか…",
"ビリッといってしもうた……",
"しゃ、喋りすぎた……",
"わ、わしの美しい翼があぁっ…"
]
};
let monologueTimer = 0;
const monologueEl = document.getElementById('monologue');
function speak(type) {
const lines = monologues[type];
const text = lines[Math.floor(Math.random() * lines.length)];
monologueEl.innerText = text;
monologueEl.style.opacity = 1;
monologueTimer = 120;
}
// --- ランキング管理 ---
const RANKING_KEY = 'kougenGliderHighScores';
let highScores = JSON.parse(localStorage.getItem(RANKING_KEY)) || [0, 0, 0, 0, 0];
function updateRankingDisplay() {
const list = document.getElementById('scoreList');
list.innerHTML = '';
for (let i = 0; i < 5; i++) {
let li = document.createElement('li');
let scoreVal = highScores[i] || 0;
if (i === 0) li.className = 'rank-1';
else if (i === 1) li.className = 'rank-2';
else if (i === 2) li.className = 'rank-3';
li.innerHTML = `<span>${i + 1}位</span> <span>${scoreVal}</span>`;
list.appendChild(li);
}
}
function saveScore(newScore) {
if (newScore === 0) return;
highScores.push(newScore);
highScores.sort((a, b) => b - a);
highScores = highScores.slice(0, 5);
localStorage.setItem(RANKING_KEY, JSON.stringify(highScores));
updateRankingDisplay();
}
updateRankingDisplay();
// --- サウンド設定 ---
let audioCtx;
let isAudioInit = false;
let nextBgmTime = 0;
let bgmStep = 0;
const bgmNotes = [440, 554, 659, 880, 659, 554];
let lastGrazeTime = 0;
function initAudio() {
if (isAudioInit) return;
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext();
isAudioInit = true;
}
function playTone(freq, duration, type = 'square', vol = 0.05) {
if (!isAudioInit) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
function playNoise(duration, vol = 0.3) {
if (!isAudioInit) return;
const bufferSize = audioCtx.sampleRate * duration;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
noise.connect(gain);
gain.connect(audioCtx.destination);
noise.start();
}
// --- ゲーム設定 ---
let player = { x: 150, y: 150, vy: 0, radius: 12 };
const gravity = 0.22;
const lift = -0.42;
let isPullingUp = false;
let terrain = [];
const segmentWidth = 10;
let baseSpeed = 5;
let currentSpeed = baseSpeed;
let terrainOffset = 0;
let bgOffset1 = 0;
let bgOffset2 = 0;
let score = 0;
let gameOver = true;
let isTitle = true;
let lastHeight = 300;
let targetHeight = 300;
let backgroundElements = [];
let windTurbineAngle = 0;
let particles = [];
function initTerrain() {
terrain = [];
backgroundElements = [];
particles = [];
lastHeight = 300;
targetHeight = 300;
for(let i = 0; i <= canvas.width / segmentWidth + 5; i++) {
terrain.push(generateInitialTerrain(i));
}
for (let i = 0; i < 5; i++) {
backgroundElements.push({
x: Math.random() * 1600,
type: Math.random() < 0.4 ? 'turbine' : 'cow'
});
}
}
function generateInitialTerrain(index) {
if (index < 50) return 300;
return generateTerrain(true);
}
function generateTerrain(isInit = false) {
let r = Math.random();
if (r < 0.04) {
targetHeight = 200 + Math.random() * 150;
} else if (r < 0.06 && !isInit) {
targetHeight = 100 + Math.random() * 50;
} else if (r < 0.08 && !isInit) {
targetHeight = 350 - Math.random() * 80;
}
lastHeight += (targetHeight - lastHeight) * 0.02;
return lastHeight;
}
function reset() {
player.y = 100;
player.vy = 0;
score = 0;
currentSpeed = baseSpeed;
gameOver = false;
isTitle = false;
bgOffset1 = 0;
bgOffset2 = 0;
monologueEl.style.opacity = 0;
initTerrain();
if(isAudioInit) nextBgmTime = audioCtx.currentTime;
}
function handleInputStart(e) {
if(e) e.preventDefault();
initAudio();
if (gameOver) {
reset();
} else {
isPullingUp = true;
playTone(330, 0.1, 'square', 0.03);
}
}
canvas.addEventListener('mousedown', handleInputStart);
canvas.addEventListener('touchstart', handleInputStart, {passive: false});
canvas.addEventListener('mouseup', () => isPullingUp = false);
canvas.addEventListener('touchend', () => isPullingUp = false);
function drawTitle() {
let grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, '#1E90FF');
grad.addColorStop(1, '#87CEEB');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#FFD700';
ctx.font = 'bold 46px Courier New';
ctx.textAlign = 'center';
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 10;
ctx.fillText('高原ギリギリ滑空', canvas.width/2, canvas.height/2 - 20);
ctx.fillStyle = '#fff';
ctx.font = 'bold 22px Courier New';
ctx.fillText('CLICK TO START', canvas.width/2, canvas.height/2 + 30);
ctx.shadowBlur = 0;
ctx.textAlign = 'left';
}
function loop() {
requestAnimationFrame(loop);
if (isTitle) {
drawTitle();
return;
}
if (gameOver) return;
if (isAudioInit && audioCtx.currentTime > nextBgmTime) {
playTone(bgmNotes[bgmStep], 0.1, 'square', 0.04);
bgmStep = (bgmStep + 1) % bgmNotes.length;
let speedFactor = currentSpeed / baseSpeed;
nextBgmTime = audioCtx.currentTime + (0.25 / speedFactor);
}
if (isPullingUp) {
player.vy += lift;
} else {
player.vy += gravity;
}
player.vy *= 0.95;
player.y += player.vy;
if (player.y < 20) { player.y = 20; player.vy = 0; }
bgOffset1 += currentSpeed * 0.15;
bgOffset2 += currentSpeed * 0.5;
terrainOffset += currentSpeed;
while (terrainOffset >= segmentWidth) {
terrain.shift();
terrain.push(generateTerrain());
terrainOffset -= segmentWidth;
}
backgroundElements.forEach(el => {
if (el.x - bgOffset2 < -100) el.x += 1600;
});
windTurbineAngle += 0.02;
let exactPos = player.x + terrainOffset;
let tIndex = Math.floor(exactPos / segmentWidth);
let groundY = terrain[tIndex] || 300;
let distanceToGround = groundY - player.y - player.radius;
if (distanceToGround < 0) {
gameOver = true;
playNoise(0.5, 0.4);
speak('death');
monologueEl.style.left = (player.x) + 'px';
monologueEl.style.top = (player.y - 70) + 'px';
saveScore(score);
ctx.fillStyle = 'rgba(200, 0, 0, 0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.font = 'bold 40px Courier New';
ctx.fillText('CRASH!!', canvas.width/2, canvas.height/2 - 10);
ctx.font = '20px Courier New';
ctx.fillText('Click to Retry', canvas.width/2, canvas.height/2 + 30);
ctx.textAlign = 'left';
return;
}
let isGrazing = false;
if (distanceToGround < 45) {
isGrazing = true;
currentSpeed += 0.06;
if (currentSpeed > 24) currentSpeed = 24;
score += Math.floor(80 - distanceToGround);
let groundEffect = (45 - distanceToGround) * 0.016;
player.vy -= groundEffect;
if (Math.random() < 0.6) {
particles.push({
x: player.x,
y: player.y + player.radius - 5,
vx: -currentSpeed * 0.8 - Math.random() * 2,
vy: (Math.random() - 0.5) * 3 - 2,
life: 25
});
}
if (isAudioInit && audioCtx.currentTime > lastGrazeTime + 0.1) {
playTone(1760, 0.05, 'square', 0.06);
lastGrazeTime = audioCtx.currentTime;
if (Math.random() < 0.2 && monologueTimer < 20) {
speak('graze');
}
}
} else {
currentSpeed -= 0.025;
if (currentSpeed < baseSpeed) currentSpeed = baseSpeed;
score += Math.floor(currentSpeed);
}
if (!gameOver) {
monologueTimer--;
if (monologueTimer <= 0) {
monologueEl.style.opacity = 0;
if (Math.random() < 0.005) speak('normal');
} else {
monologueEl.style.left = (player.x) + 'px';
monologueEl.style.top = (player.y - 70) + 'px';
}
}
let grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, '#1E90FF');
grad.addColorStop(1, '#87CEEB');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#A9A9A9';
ctx.beginPath();
ctx.moveTo(0, canvas.height);
for(let x = 0; x <= canvas.width + 20; x += 20) {
let y = 160 + Math.sin((x + bgOffset1) * 0.003) * 60 + Math.cos((x + bgOffset1) * 0.002) * 40;
ctx.lineTo(x, y);
}
ctx.lineTo(canvas.width, canvas.height);
ctx.fill();
ctx.fillStyle = '#FFFAFA';
for(let x = 0; x <= canvas.width + 40; x += 40) {
let y = 160 + Math.sin((x + bgOffset1) * 0.003) * 60 + Math.cos((x + bgOffset1) * 0.002) * 40;
if (Math.sin(x + bgOffset1) > 0.5) ctx.fillRect(x, y - 5, 8, 8);
}
ctx.fillStyle = '#3CB371';
ctx.beginPath();
ctx.moveTo(0, canvas.height);
for(let x = 0; x <= canvas.width + 20; x += 20) {
let y = 220 + Math.sin((x + bgOffset2) * 0.012) * 25 + Math.sin((x + bgOffset2) * 0.02) * 15;
ctx.lineTo(x, y);
}
ctx.lineTo(canvas.width, canvas.height);
ctx.fill();
backgroundElements.forEach(el => {
let screenX = el.x - bgOffset2;
if (screenX > -100 && screenX < canvas.width + 100) {
if (el.type === 'turbine') {
ctx.fillStyle = '#E8E8E8';
ctx.fillRect(screenX, 150, 6, 80);
ctx.save();
ctx.translate(screenX + 3, 150);
ctx.rotate(windTurbineAngle);
ctx.fillStyle = '#FFF';
for (let i = 0; i < 3; i++) {
ctx.rotate((Math.PI * 2) / 3);
ctx.fillRect(-2, -35, 4, 35);
}
ctx.restore();
} else {
ctx.fillStyle = '#222';
ctx.fillRect(screenX, 230, 20, 12);
ctx.fillRect(screenX - 5, 228, 8, 8);
ctx.fillRect(screenX, 242, 3, 6);
ctx.fillRect(screenX + 17, 242, 3, 6);
}
}
});
let groundGrad = ctx.createLinearGradient(0, 100, 0, canvas.height);
groundGrad.addColorStop(0, 'rgba(255, 255, 255, 0.8)');
groundGrad.addColorStop(0.1, '#228B22');
groundGrad.addColorStop(1, '#005000');
ctx.fillStyle = groundGrad;
ctx.beginPath();
ctx.moveTo(0, canvas.height);
for(let i = 0; i < terrain.length; i++) {
ctx.lineTo(i * segmentWidth - terrainOffset, terrain[i]);
}
ctx.lineTo(canvas.width, canvas.height);
ctx.fill();
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += gravity * 0.5;
p.life--;
if (p.life <= 0) {
particles.splice(i, 1);
} else {
ctx.fillStyle = `rgba(255, 215, 0, ${p.life / 25})`;
ctx.fillRect(p.x, p.y, 4, 4);
}
}
if (currentSpeed > 14) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
ctx.lineWidth = 2;
ctx.beginPath();
for(let i = 0; i < Math.floor(currentSpeed * 0.8); i++) {
let lx = Math.random() * canvas.width;
let ly = Math.random() * canvas.height;
ctx.moveTo(lx, ly);
ctx.lineTo(lx + currentSpeed * 5, ly);
}
ctx.stroke();
}
ctx.save();
ctx.translate(player.x, player.y);
ctx.rotate(player.vy * 0.08);
ctx.fillStyle = isGrazing ? '#FFD700' : '#F5F5DC';
ctx.strokeStyle = '#8B4513';
ctx.lineWidth = 1.5;
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(25, 0);
ctx.lineTo(12, -8);
ctx.lineTo(-5, -10);
ctx.lineTo(-20, -25);
ctx.lineTo(-10, -5);
ctx.lineTo(-30, 0);
ctx.lineTo(-35, 10);
ctx.lineTo(-15, 8);
ctx.lineTo(5, 12);
ctx.lineTo(15, 6);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(12, -8);
ctx.lineTo(5, 2);
ctx.lineTo(15, 6);
ctx.moveTo(-5, -10);
ctx.lineTo(5, 2);
ctx.lineTo(-15, 8);
ctx.moveTo(-10, -5);
ctx.lineTo(-25, 5);
ctx.moveTo(12, -3);
ctx.lineTo(16, -2);
ctx.moveTo(25, 0);
ctx.lineTo(15, 2);
ctx.stroke();
ctx.restore();
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.beginPath();
ctx.roundRect(10, 10, 290, 75, 8);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 22px Courier New';
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 4;
ctx.fillText(`SCORE: ${score}`, 25, 40);
if (currentSpeed > 20) {
ctx.fillStyle = '#FF4500';
} else if (currentSpeed > 15) {
ctx.fillStyle = '#FFD700';
}
ctx.font = 'bold 20px Courier New';
ctx.fillText(`SPEED: ${Math.floor(currentSpeed * 10)} km/h`, 25, 70);
ctx.shadowBlur = 0;
}
initTerrain();
drawTitle();
requestAnimationFrame(loop);
</script>
</body>
</html>


■ 今日の学び

今回は、「地面スレスレのギリギリ滑空感」をどう再現するか、
というところから作り始めたゲームでした。

高原の上空を風を切って飛んだら、
きっとすごく涼しいんだろうなぁ……と、

そんな心地よさを想像しながら、
AIと相談してゲーム作りを進めました。( ˘꒳˘)🎐

実は最初のプロトタイプでは、
飛んでいるのはただの「紙飛行機」だったんです。

でも、せっかくなら以前の実験で作った資産を活かそうと思い立ち、
#74『和紙の鷲のわし』で登場したあの鷲に、
もう一度大空を飛んでもらうことにしました。

同じ「空を飛ぶゲーム」でも、
重力や慣性の数値を少し変えるだけで、
まったく違う操作感やスリルが生まれるのがとても興味深かったです。

そして、前作の和紙の鷲といえば……
やっぱり「独り言」ですよね。(≖ᴗ≖ )フフフ

「猛スピードで岩を避けながらブツブツぼやいていたら、きっとシュールで面白いはず!」と、
制作中のいたずら心も一気に加速してしまいました。

過去の実験で作ったキャラクターやコードが、
新しいルールや舞台の中でまた違った遊びに生まれ変わる。

これも連作を続けているからこそ味わえる、
ゲーム開発の醍醐味ですね!╰( º∀º )╯✨

■ 次回は、無重力空間をシミュレーションしたゲームです。

宇宙船の内部で、ふわりと漂う物体。

うっすら丸く青く燃える炎。

その炎を、水鉄砲で消していく――

そんな無重力空間の実験ゲームです。

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

次の実験はこちら
(宇宙ステーション無重力射撃)
  「無重力 ウォーターガン・スナイパー」



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

~このブログについて~

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

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


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

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

続きを読む