実験#120:波を読んでお宝を集めろ!(ヤドカリの波打ち際サバイバル)

■120個目 「ヤドカリの波打ち際サバイバル

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

今回は、ちょっぴりスリリングなアクションゲームを作ってみました。

冒険の主人公は……小さな “ヤドカリ” です!

打ち寄せる波からギリギリの距離を保ちながら、
砂浜をトコトコと右へ右へと進んでいきます。

今回こだわったのは、「波打ち際のリスクとリターン」。

波に飲まれたらゲームオーバーだけれど、
危ない場所ほどキラキラしたお宝が落ちていたら……

つい欲張りたくなってしまうかもしれません。(=v=*)

そんな遊び心から生まれた、
ちょっとドキドキのサバイバルゲームになっています。

ヤドカリが拾えるお宝は、色とりどりのシーグラスや貝殻たち。

安全な上の方では白いシーグラスが多いですが、
思い切って波打ち際まで足を伸ばすと――

「虹色のビー玉」やレアな「リュウグウボウシ」など、
素敵なお宝に出会えることも!(@_@*)

良い貝殻を見つけたら、新しいお家へお引っ越しもできます♪

海の奥では小魚たちがのんびり泳いでいるので、
波の満ち引きと一緒にその景色も楽しんでみてくださいね。

制限時間は60秒。

欲張って波に飲まれるか、安全第一で手堅く生き残るか……。

ヤドカリの度胸試し、ぜひ遊んでみてくださいね!

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

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

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

砂浜の波打ち際を、
波に引き込まれないようにギリギリのところを進んでいくヤドカリが主人公。

波の濡れた範囲を見ながら距離を保って移動します。
いろいろな色や形の貝殻が落ちていて、拾うと引っ越し可能。

シーグラスの色別レア率は以前のゲームを参考にして、
ポイントに差をつけます。 制限時間内で進んだ距離もポイントにしましょう。

波打ち際ほどレアなお宝が出やすく、
波に巻き込まれたらゲームオーバーです。

AI: 波の満ち引きを読みながら、
ハイリスク・ハイリターンの波打ち際で
お宝を集めるアクションゲームですね。

HTML/CSS/JavaScriptで基本のゲーム部分を作成しました。

● アイテム説明パネルの追加

みー: (^_^)シーグラスや貝殻の価値が分かりにくかったので、
画面横にポイント表を置きたいです。

良い貝殻を拾ったときは、きらきらして主張する感じにできますか?

波打ち際ほどレア度の高いものが出やすいように調整もお願いします。

貝殻の出現率は少し低めで。リトライボタンもほしいです。

AI: 説明パネルを追加し、レア貝殻にキラキラ演出を入れました。
波打ち際のレア度調整とリトライボタンも実装しています。

● レアアイテムの追加

みー: (=v=)黄色シーグラスを赤と黄色の間に入れましょう。
紫の後に「虹色ビー玉シーグラス(250点)」も追加でお願いします。

AI: 黄色シーグラス(125点)を追加し、
最高レアとして虹色ビー玉シーグラス(250点)も実装しました。

虹色ビー玉は丸い形状で、
波打ち際ギリギリでしか出ない超レアアイテムです。

● 海の奥の表現について

みー: ( ̄v ̄)右の説明欄が縦長になったことで、
ゲーム画面の下側にクリーム色の余白ができてしまいました。
そこを海の色にして、小魚が泳ぐようにできますか?

AI: なるほど、説明欄の拡張でキャンバス外の背景が見えていた部分ですね。

キャンバスの縦幅を広げて余白も海として描画し、
その奥で小魚がのんびり泳ぐように修正しました。


■ 実際の動作

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

今回のゲームは仕様を細かく伝えていたこともあり、
最初の段階からかなり完成形に近いイメージを出してもらえました。

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


こちらが、仕上がりのスタート画面です。

画面左側にはスコアランキング、
右側にはシーグラスや貝殻の得点一覧が表示されています。

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


ヤドカリを操作して、
より高いランクの貝殻に引っ越しながらシーグラスを拾っていきます。

波打ち際ギリギリほど、
レア度(ポイント)の高いシーグラスや貝殻が出やすくなっています。

(画像:ゲーム画面⓵)


制限時間内に、素敵な貝殻へ引っ越ししつつ、シーグラスを集めてハイスコアを目指しましょう。

(画像:ゲーム画面⓶)


虹色のビー玉を発見!

とても高得点ですが、
波打ち際に近いほど波にさらわれてしまう危険も高まります。
ドキドキしながら拾うタイミングを見極めてくださいね。

(画像:虹色ビー玉のゲーム画面)



波に飲まれてしまうとゲームオーバーです。
次はもっと遠くまで進めるように、ぜひリトライしてみてください。

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


■ コードのポイント

今回の記事では、
AI と相談しながら作ったヤドカリの波打ち際サバイバル の HTML コードを公開します。

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

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

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

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

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

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

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

■ 遊び方・操作方法

操作はキーボードだけで、とってもシンプルです!(・v・)b

  • 移動:
    パソコンの「矢印キー(上下左右)」または「WASDキー」
  • 基本ルール:
    右へ進みながら、制限時間「60秒」を生き残ればクリアです!

🌊 波に注意!

画面下から押し寄せる波(深い青色のゾーン)にヤドカリが触れると、
そのまま波に飲まれてゲームオーバーになってしまいます。(>_<)

波の満ち引きをよく観察して、
飲み込まれないように一定の距離を保ちながら進みましょう。

💎 お宝(シーグラス)を集めよう

砂浜には、いろいろな色のシーグラスが落ちています。
拾うとスコアがアップします。

  • 安全な場所:白や茶色(10点)が多め
  • 波打ち際の危険地帯:赤(100点)や、
    めったに見られない虹色(250点)が出やすい!(✧_✧)

リスクとリターンのバランスを見ながら、うまく拾っていきましょう。

🐚 良い貝殻へお引っ越し!

時々落ちている貝殻(巻貝)を拾うと、
ヤドカリがすぽっと新しい貝殻にお引っ越しします。

貝殻ごとに「ボーナス倍率」が決まっていて、
60秒逃げ切ったときの最終スコアがぐんと跳ね上がります。

今の貝殻よりグレードの高い貝殻は キラキラ光ってアピール しているので、
見つけたらぜひ狙ってみてくださいね。(´▽`*)

🏆 ランキングに挑戦

ゲームオーバー、または無事に60秒逃げ切ってクリアすると、
スコアが過去トップ5を上回っている場合、名前を登録できます。

波打ち際のギリギリを攻めて、
ぜひハイスコアを目指してみてくださいね!(≧▽≦)و


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

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ヤドカリの波打ち際サバイバル</title>
<style>
body {
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #e0f7fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #333;
}
#mainContainer {
display: flex;
gap: 20px;
align-items: flex-start;
max-width: 1400px;
}
/* 左パネル(ランキング) */
#leftPanel {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
width: 240px;
display: flex;
flex-direction: column;
box-sizing: border-box;
height: 720px;
}
.ranking-list {
list-style: none;
padding: 0;
margin: 0;
flex-grow: 1;
}
.ranking-list li {
display: flex;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px dashed #ccc;
font-size: 15px;
}
.ranking-list li:first-child { font-weight: bold; color: #d4af37; font-size: 18px; }
.ranking-list li:nth-child(2) { color: #8a8d91; font-weight: bold; }
.ranking-list li:nth-child(3) { color: #b08d57; font-weight: bold; }
/* ゲームコンテナ */
#gameContainer {
position: relative;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
border-radius: 8px;
overflow: hidden;
background-color: #003b73;
}
canvas {
display: block;
background-color: #eeddcc;
}
#ui {
position: absolute;
top: 10px;
left: 10px;
color: #333;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(255,255,255,0.8);
pointer-events: none;
}
.status-text { font-size: 20px; margin-bottom: 5px; }
/* 右パネル(説明) */
#sidePanel {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
width: 280px;
display: flex;
flex-direction: column;
box-sizing: border-box;
height: 720px;
}
h2 {
margin-top: 0;
font-size: 18px;
border-bottom: 2px solid #4682b4;
padding-bottom: 5px;
color: #4682b4;
}
.item-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
font-size: 14px;
}
.item-table th, .item-table td {
border-bottom: 1px solid #eee;
padding: 6px 4px;
text-align: left;
}
.item-table th { background: #f9f9f9; }
.color-box {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 2px;
border: 1px solid #aaa;
margin-right: 5px;
vertical-align: middle;
}
.point-text { font-weight: bold; text-align: right; }
.shell-table { margin-bottom: auto; }
.btn {
background-color: #ff7f50;
color: white;
border: none;
padding: 12px;
font-size: 16px;
font-weight: bold;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
text-align: center;
width: 100%;
margin-top: 10px;
}
.btn:hover { background-color: #ff6347; }
.btn:active { transform: scale(0.98); }
.btn-reset { background-color: #aaa; padding: 8px; font-size: 14px; }
.btn-reset:hover { background-color: #888; }
</style>
</head>
<body>
<div id="mainContainer">
<div id="leftPanel">
<h2>🏆 ハイスコア</h2>
<ul class="ranking-list" id="rankingList"></ul>
<button class="btn btn-reset" onclick="clearRanking()">ランキングをリセット</button>
</div>
<div id="gameContainer">
<canvas id="gameCanvas" width="800" height="720"></canvas>
<div id="ui">
<div class="status-text" id="timeDisplay">残り時間: 60秒</div>
<div class="status-text" id="scoreDisplay">スコア: 0</div>
<div class="status-text" id="shellDisplay">現在の貝殻: ノーマル (x1.0)</div>
</div>
</div>
<div id="sidePanel">
<h2>💎 シーグラス (スコア)</h2>
<table class="item-table">
<tr><th>種類 (色)</th><th style="text-align:right;">ポイント</th></tr>
<tr><td><span class="color-box" style="background:#ffffff;"></span>白・<span class="color-box" style="background:#8b4513;"></span></td><td class="point-text">+10</td></tr>
<tr><td><span class="color-box" style="background:#2e8b57;"></span></td><td class="point-text">+30</td></tr>
<tr><td><span class="color-box" style="background:#00bfff;"></span>水色</td><td class="point-text">+50</td></tr>
<tr><td><span class="color-box" style="background:#dc143c;"></span></td><td class="point-text" style="color:#dc143c;">+100</td></tr>
<tr><td><span class="color-box" style="background:#ffd700;"></span></td><td class="point-text" style="color:#d4af37;">+125</td></tr>
<tr><td><span class="color-box" style="background:#800080;"></span></td><td class="point-text" style="color:#800080;">+150</td></tr>
<tr>
<td><span class="color-box" style="background:linear-gradient(45deg, red, yellow, green, cyan, blue, magenta); border:none;"></span>虹色(ビー玉)</td>
<td class="point-text" style="color:#ff1493;">+250</td>
</tr>
</table>
<h2>🐚 貝殻 (最終倍率)</h2>
<p style="font-size: 11px; margin-top:0; color:#555;">※今の貝より良いものはキラキラ光ります</p>
<table class="item-table shell-table">
<tr><th>名前 (色)</th><th style="text-align:right;">倍率</th></tr>
<tr><td>ノーマル (茶)</td><td class="point-text">x 1.0</td></tr>
<tr><td><span class="color-box" style="background:#f5f5dc;"></span>シロタマガイ</td><td class="point-text">x 1.2</td></tr>
<tr><td><span class="color-box" style="background:#4682b4;"></span>アオイソカニモリ</td><td class="point-text">x 1.5</td></tr>
<tr><td><span class="color-box" style="background:#ffd700;"></span>ホシダカラ</td><td class="point-text" style="color:#d4af37;">x 2.0</td></tr>
<tr><td><span class="color-box" style="background:#ff69b4;"></span>リュウグウボウシ</td><td class="point-text" style="color:#ff1493;">x 3.0</td></tr>
</table>
<button class="btn" onclick="resetGame()">リトライ</button>
</div>
</div>
<script>
// --- ランキングシステム ---
const MAX_RANKING = 5;
const defaultRanking = [
{ name: '波乗り名人', score: 3000 },
{ name: 'ヤドカリ先輩', score: 2000 },
{ name: '貝殻ハンター', score: 1000 },
{ name: 'ルーキー', score: 500 },
{ name: '迷子のカニ', score: 100 }
];
let ranking = JSON.parse(localStorage.getItem('yadokari_ranking')) || [...defaultRanking];
function drawRanking() {
const list = document.getElementById('rankingList');
list.innerHTML = '';
const medals = ['🥇', '🥈', '🥉', '4', '5'];
ranking.forEach((r, i) => {
const li = document.createElement('li');
li.innerHTML = `<span>${medals[i]} ${r.name}</span> <span class="point-text">${r.score}pt</span>`;
list.appendChild(li);
});
}
function checkScoreForRanking(finalScore) {
if (ranking.length < MAX_RANKING || finalScore > ranking[ranking.length - 1].score) {
setTimeout(() => {
const playerName = prompt(`スコア ${finalScore} でランクインしました!\nランキングに登録する名前を入力してください:`, "名無しヤドカリ") || "名無しヤドカリ";
ranking.push({ name: playerName, score: finalScore });
ranking.sort((a, b) => b.score - a.score);
if (ranking.length > MAX_RANKING) ranking.pop();
localStorage.setItem('yadokari_ranking', JSON.stringify(ranking));
drawRanking();
}, 100);
}
}
function clearRanking() {
if(confirm('ランキングをリセットして初期状態に戻しますか?')) {
localStorage.removeItem('yadokari_ranking');
ranking = [...defaultRanking];
drawRanking();
}
}
drawRanking();
// --- ゲームメイン処理 ---
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let isPlaying = false;
let isGameOver = false;
let isGameClear = false;
let timeRemaining = 60;
let distanceTraveled = 0;
let score = 0;
let lastTime = 0;
let gameTimer = 0;
let scoreSaved = false;
// キー入力状態
const keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false, w: false, a: false, s: false, d: false };
window.addEventListener('keydown', e => { if(keys.hasOwnProperty(e.key)) keys[e.key] = true; });
window.addEventListener('keyup', e => { if(keys.hasOwnProperty(e.key)) keys[e.key] = false; });
const defaultShell = { name: 'ノーマル', color: '#8d6e63', mult: 1.0, size: 20 };
let player = { x: 200, y: 300, radius: 15, speed: 4, shell: defaultShell };
let items = [];
const glassTypes = [
{ color: '#ffffff', name: '白', points: 10, alpha: 0.7, rarity: 'common' },
{ color: '#8b4513', name: '茶', points: 10, alpha: 0.7, rarity: 'common' },
{ color: '#2e8b57', name: '緑', points: 30, alpha: 0.8, rarity: 'uncommon' },
{ color: '#00bfff', name: '水色', points: 50, alpha: 0.8, rarity: 'rare' },
{ color: '#dc143c', name: '赤', points: 100, alpha: 0.9, rarity: 'epic' },
{ color: '#ffd700', name: '黄', points: 125, alpha: 0.9, rarity: 'legendary' },
{ color: '#800080', name: '紫', points: 150, alpha: 0.9, rarity: 'mythic' },
{ color: 'rainbow', name: '虹色', points: 250, alpha: 1.0, rarity: 'ultra', isMarble: true }
];
const shellTypes = [
{ name: 'シロタマガイ', color: '#f5f5dc', mult: 1.2, size: 22, rarity: 'common' },
{ name: 'アオイソカニモリ', color: '#4682b4', mult: 1.5, size: 25, rarity: 'uncommon' },
{ name: 'ホシダカラ', color: '#ffd700', mult: 2.0, size: 28, rarity: 'rare' },
{ name: 'リュウグウボウシ', color: '#ff69b4', mult: 3.0, size: 35, rarity: 'epic' }
];
let fishes = [];
for(let i=0; i<8; i++) {
fishes.push({
id: i,
x: Math.random() * 800,
y: 620 + Math.random() * 80,
speed: (Math.random() * 1.5 + 0.5) * (Math.random() < 0.5 ? 1 : -1),
size: Math.random() * 4 + 3,
color: Math.random() < 0.3 ? '#ff7f50' : '#e0ffff'
});
}
// 出現率の再調整(レアアイテムは波打ち際のみに限定)
function getWeight(rarity, riskFactor) {
switch(rarity) {
case 'common': return Math.max(1, 100 - riskFactor * 90);
case 'uncommon': return Math.max(1, 50 - riskFactor * 30);
case 'rare': return 5 + riskFactor * 30;
// 以下、レア度が高いものは riskFactor が一定以上(波打ち際)でないと一切出ない
case 'epic': return riskFactor > 0.7 ? riskFactor * 30 : 0; // 赤
case 'legendary': return riskFactor > 0.8 ? riskFactor * 20 : 0; // 黄
case 'mythic': return riskFactor > 0.9 ? riskFactor * 10 : 0; // 紫
case 'ultra': return riskFactor > 0.95 ? riskFactor * 5 : 0; // 虹
default: return 10;
}
}
function spawnItem() {
// 0(安全)〜1(波打ち際) のうち「1」に近い数値が選ばれやすくなるように偏らせる
let riskFactor = 1 - Math.pow(Math.random(), 2);
// 【重要】絶対に手が届く範囲(Y:200〜500)に制限! 深海には出さない。
const yPos = 200 + riskFactor * 300;
// 貝殻の確率は10%、シーグラスが90%
const isShell = Math.random() < 0.1;
let selectedItem;
let totalWeight = 0;
const list = isShell ? shellTypes : glassTypes;
list.forEach(item => { totalWeight += getWeight(item.rarity, riskFactor); });
let rand = Math.random() * totalWeight;
let currentWeight = 0;
for (let item of list) {
currentWeight += getWeight(item.rarity, riskFactor);
if (rand <= currentWeight) {
selectedItem = item;
break;
}
}
if (isShell) {
items.push({ type: 'shell', x: canvas.width + 50, y: yPos, shell: selectedItem, radius: 12 });
} else {
items.push({ type: 'glass', x: canvas.width + 50, y: yPos, glass: selectedItem, radius: selectedItem.isMarble ? 8 : 6 });
}
}
function resetGame() {
isPlaying = false;
isGameOver = false;
isGameClear = false;
scoreSaved = false;
timeRemaining = 60;
distanceTraveled = 0;
score = 0;
gameTimer = 0;
items = [];
player.x = 200;
player.y = 300;
player.shell = defaultShell;
// リトライ時にキー入力状態をすべてクリアする
for (let key in keys) {
keys[key] = false;
}
updateUI();
}
window.resetGame = resetGame;
function getWaveY(x, time) {
const baseTide = Math.sin(time * 0.0005) * 120;
const surge = Math.sin(time * 0.002 + x * 0.005) * 40;
const ripples = Math.sin(time * 0.005 + x * 0.02) * 10;
return 550 - Math.abs(baseTide) - surge - ripples;
}
function updateUI() {
document.getElementById('timeDisplay').innerText = `残り時間: ${timeRemaining}秒`;
document.getElementById('scoreDisplay').innerText = `スコア: ${Math.floor(score)} (距離: ${Math.floor(distanceTraveled)}m)`;
document.getElementById('shellDisplay').innerText = `現在の貝殻: ${player.shell.name} (x${player.shell.mult})`;
}
function update(timestamp) {
if (!lastTime) lastTime = timestamp;
const dt = timestamp - lastTime;
lastTime = timestamp;
fishes.forEach(f => {
f.x += f.speed;
if (f.speed > 0 && f.x > 900) { f.x = -100; f.y = 620 + Math.random() * 80; }
if (f.speed < 0 && f.x < -100) { f.x = 900; f.y = 620 + Math.random() * 80; }
});
if (isPlaying && !isGameOver && !isGameClear) {
gameTimer += dt;
if (gameTimer > 1000) {
timeRemaining--;
gameTimer = 0;
if (timeRemaining <= 0) {
isGameClear = true;
if (!scoreSaved) {
scoreSaved = true;
checkScoreForRanking(Math.floor(score * player.shell.mult));
}
}
}
let dx = 0; let dy = 0;
if (keys.ArrowUp || keys.w) dy -= player.speed;
if (keys.ArrowDown || keys.s) dy += player.speed;
if (keys.ArrowLeft || keys.a) dx -= player.speed;
if (keys.ArrowRight || keys.d) dx += player.speed;
player.y += dy;
if (player.y < 50) player.y = 50;
if (player.y > 580) player.y = 580;
player.x += dx;
if (player.x < 20) player.x = 20;
if (player.x > 200) {
let scroll = player.x - 200;
player.x = 200;
distanceTraveled += scroll * 0.1;
let riskMultiplier = 1 + (player.y / 580) * 2;
score += 0.5 * riskMultiplier * (scroll / player.speed);
items.forEach(item => item.x -= scroll);
fishes.forEach(f => f.x -= scroll);
}
// 【調整】アイテムの出現確率を大幅アップ!(前進時は約8%の確率で出現)
if (Math.random() < (dx > 0 ? 0.08 : 0.02)) spawnItem();
for (let i = items.length - 1; i >= 0; i--) {
let item = items[i];
let dist = Math.hypot(player.x - item.x, player.y - item.y);
if (dist < player.radius + item.radius + 5) {
if (item.type === 'glass') score += item.glass.points;
else if (item.type === 'shell') player.shell = item.shell;
items.splice(i, 1);
}
}
items = items.filter(item => item.x > -50);
if (player.y > getWaveY(player.x, timestamp)) {
isGameOver = true;
if (!scoreSaved) {
scoreSaved = true;
checkScoreForRanking(Math.floor(score));
}
}
updateUI();
}
draw(timestamp);
requestAnimationFrame(update);
}
function draw(timestamp) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#d4c4b7';
ctx.beginPath();
ctx.moveTo(0, canvas.height);
for (let x = 0; x <= canvas.width; x += 20) {
let wetY = getWaveY(x, timestamp - 2000) - 30;
ctx.lineTo(x, wetY);
}
ctx.lineTo(canvas.width, canvas.height);
ctx.fill();
items.forEach(item => {
ctx.save();
ctx.translate(item.x, item.y);
if (item.type === 'shell' && item.shell.mult > player.shell.mult) {
const time = timestamp * 0.003;
ctx.fillStyle = 'rgba(255, 215, 0, 0.8)';
for(let i=0; i<4; i++){
let angle = time + (i * Math.PI / 2);
let dist = item.radius + 8 + Math.sin(time * 3 + i) * 3;
let px = Math.cos(angle) * dist;
let py = Math.sin(angle) * dist;
ctx.beginPath();
ctx.arc(px, py, 3, 0, Math.PI*2);
ctx.fill();
}
}
if (item.type === 'glass') {
if (item.glass.isMarble) {
let grad = ctx.createRadialGradient(-item.radius/3, -item.radius/3, item.radius*0.1, 0, 0, item.radius);
grad.addColorStop(0, '#fff');
grad.addColorStop(0.2, 'cyan');
grad.addColorStop(0.5, 'yellow');
grad.addColorStop(0.8, 'magenta');
grad.addColorStop(1, 'blue');
ctx.globalAlpha = item.glass.alpha;
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, item.radius + 1, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.9)';
ctx.lineWidth = 1.5;
ctx.stroke();
} else {
ctx.fillStyle = item.glass.color;
ctx.globalAlpha = item.glass.alpha;
ctx.beginPath();
ctx.moveTo(0, -item.radius);
ctx.lineTo(item.radius, item.radius*0.5);
ctx.lineTo(-item.radius*0.8, item.radius);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.lineWidth = 1;
ctx.stroke();
}
} else if (item.type === 'shell') {
ctx.fillStyle = item.shell.color;
ctx.beginPath();
ctx.moveTo(0, -item.radius);
ctx.lineTo(item.radius, item.radius);
ctx.lineTo(-item.radius, item.radius);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#555';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-item.radius*0.5, 0);
ctx.lineTo(item.radius*0.5, 0);
ctx.stroke();
}
ctx.restore();
});
ctx.save();
ctx.translate(player.x, player.y);
ctx.fillStyle = '#cc4433';
ctx.beginPath();
ctx.arc(8, -8, 4, 0, Math.PI*2);
ctx.arc(8, 8, 4, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(5, 0, 6, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.fillRect(8, -3, 2, 2);
ctx.fillRect(8, 2, 2, 2);
ctx.fillStyle = player.shell.color;
ctx.beginPath();
ctx.arc(-5, 0, player.shell.size / 2, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.arc(-5, 0, player.shell.size / 4, 0, Math.PI, false);
ctx.stroke();
ctx.restore();
let waveGrad = ctx.createLinearGradient(0, 350, 0, canvas.height);
waveGrad.addColorStop(0, 'rgba(70, 150, 220, 0.7)');
waveGrad.addColorStop(0.4, 'rgba(30, 120, 190, 0.95)');
waveGrad.addColorStop(1, 'rgba(0, 59, 115, 1)');
ctx.fillStyle = waveGrad;
ctx.beginPath();
ctx.moveTo(0, canvas.height);
for (let x = 0; x <= canvas.width; x += 10) {
ctx.lineTo(x, getWaveY(x, timestamp));
}
ctx.lineTo(canvas.width, canvas.height);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = 0.8;
fishes.forEach(f => {
ctx.save();
ctx.translate(f.x, f.y + Math.sin(timestamp * 0.003 + f.id) * 3);
ctx.fillStyle = f.color;
ctx.beginPath();
ctx.ellipse(0, 0, f.size * 2, f.size, 0, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
let tailX = f.speed > 0 ? -f.size * 1.8 : f.size * 1.8;
let dir = f.speed > 0 ? -1 : 1;
ctx.moveTo(tailX, 0);
ctx.lineTo(tailX + dir * f.size * 1.5, -f.size);
ctx.lineTo(tailX + dir * f.size * 1.5, f.size);
ctx.fill();
ctx.fillStyle = '#333';
let eyeX = f.speed > 0 ? f.size : -f.size;
ctx.beginPath();
ctx.arc(eyeX, -f.size * 0.3, f.size * 0.2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
ctx.globalAlpha = 1.0;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
ctx.lineWidth = 4;
ctx.stroke();
if (!isPlaying && !isGameOver && !isGameClear) {
drawMessage("ヤドカリの波打ち際サバイバル", "矢印キー または WASD で移動 / 右へ進め!", "画面をクリックしてスタート");
} else if (isGameOver) {
drawMessage("波に飲まれた!", `獲得スコア: ${Math.floor(score)}`, "右下の「リトライ」ボタンで再挑戦");
} else if (isGameClear) {
const finalScore = Math.floor(score * player.shell.mult);
drawMessage("タイムアップ! 生還しました",
`ベーススコア: ${Math.floor(score)} x 倍率(${player.shell.mult}) = ${finalScore}`,
"右下の「リトライ」ボタンで再挑戦");
}
}
function drawMessage(title, sub, inst) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.textAlign = 'center';
ctx.fillStyle = '#fff';
ctx.font = 'bold 36px sans-serif';
ctx.fillText(title, canvas.width/2, canvas.height/2 - 40);
ctx.font = '20px sans-serif';
ctx.fillText(sub, canvas.width/2, canvas.height/2 + 10);
ctx.font = '16px sans-serif';
ctx.fillStyle = '#ffd700';
ctx.fillText(inst, canvas.width/2, canvas.height/2 + 60);
}
canvas.addEventListener('click', () => {
if (!isPlaying && !isGameOver && !isGameClear) {
isPlaying = true;
}
});
requestAnimationFrame(update);
</script>
</body>
</html>


■ 今日の学び

最近は日差しが強すぎたり、波が荒かったりで、
大好きなシーグラスを拾いに海へ行けず、おうち時間が続いています。

「海に行きたいなぁ……波打ち際を歩きたいなぁ……」
そんなうずうずした気持ちと、じっと家で過ごしている自分を、
ゲームの中のヤドカリにそっと重ね合わせたのが、
今回の制作のきっかけでした。 (V)( ˶’ᵕ’˶ )(V) カサカサ…

ゲームの中では、
シーグラスの色によって「レア度」や「ポイント」をつけて、
遊びの要素を少し強めていますが、
実際のシーグラス拾いに対する私の気持ちは、また少し違います。

いざ砂浜で探し始めると、もう時間も忘れて夢中になってしまうんですよね。

それはきっと、一つ一つのシーグラスが
「その時、その場所だからこそ」出会えた宝物だから。

とても小さな欠片でも、よく見かける色でも、
現実の海ではレア度なんて関係なく、
つい拾い集めてしまいます。

波に揉まれて角が取れた色とりどりのガラスたちは、
私にとってどれも等しく特別な存在です。

心地よい波の音をBGMに、
ただただ海の広さを肌で感じながら砂浜を見つめる時間は、
本当に楽しくて、なによりの癒しのひとときでもあります。

本当の海へ行くには、もう少し穏やかな日を待たないといけませんが、
このゲームを通して、少しでも波の音を聴きながらお宝を探す
「癒しとワクワク」が皆さんに伝わったら嬉しいです。


■ 次回は、シュワシュワ弾ける
ソーダグラスからの脱出アクションゲームです。

次々と湧き上がっては、
パツンと弾けて消えていく炭酸の細かな気泡たち。

今回は「触り心地の気持ちよさ」と「リアルな泡の動き」にこだわった、
爽快なジャンプアクションゲームを作ってみました。

舞台は、緑あふれるカフェのテラス席に置かれた冷たいソーダグラス。

泡を踏み台にしてエネルギーを溜めながら、
わんぱくな主人公「丸太郎」と一緒に
グラスのふちを飛び越えて脱出を目指します!

耳をすませば、シュワシュワと弾ける音が聞こえてきそうな、
爽やかでクセになるゲームになっています。

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


次の実験はこちら
シュワシュワ泡ジャンプ!
「丸太郎のソーダグラス大脱出」



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

 

~このブログについて~

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

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


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

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

続きを読む