実験#38:時を駆けるしょうが君と巨大タンタン(Time & Sky Adventure)

■38個目「Time & Sky Adventure

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

今回は、美しい空の変化と幻想的な世界を楽しむアクションゲームを作ってみました。(^_^
タイトルは「時を駆けるしょうが君と巨大タンタン:Time & Sky Adventure」です!

しょうが君は、過去の
実験 #17:しょうが君錯視ゲーム
実験#32:しょうが君シャボン
に登場した、元気いっぱいのキャラクターです。

(画像:実験 #17:しょうが君錯視ゲーム

(画像:実験#32:しょうが君シャボン

■舞台は、深い森と光るジターリング

黒いシルエットで描かれた何層もの森。 その空中には、不思議な光を放つ「ジターリング」がぽわぽわと浮かんでいます。

ジターリングは実験 #23でも紹介した金属製のシンプルなホビーで、指で回すと独特の振動が生まれる不思議なおもちゃです。

(画像:ジターリング)

■こだわりは「空のグラデーション」

今回のゲームで一番こだわったのが、 時間とともに変化する空の色 です。(=v=*)

「ただ走るだけじゃなくて、アイテムを集めるごとに時間が進んだら素敵かも?」 そんな思いつきから、少しエモーショナルな旅が生まれました。

光るジターリングを1つ取るごとに、ゲーム内の時間が30分進みます。

スタートは夜の21時。 そこから、

  • 夜明けのマジックアワー
  • 昼の青空
  • 夕暮れのオレンジ

と、リングを集めるたびに空の色がなめらかに変化していきます。(´▽`*)

■頼もしい相棒「巨大タンタン」

そして、この旅には心強い仲間がいます。 しょうが君の2倍もある巨大なイシガメ、タンタン です!(^_^)

タンタンは、実験 #19 でも紹介した、家で飼っているイシガメがモデル。 のんびりしているのに、いざというときは頼もしい相棒です。

ゲームでは、ドスドスとすごいスピードで追いかけてきてくれて、 高い場所のリングを取るための「動く足場」にもなってくれます。

(画像:イシガメのタンタン)


■24時間を駆け抜ける旅へ

全部で48個のリングを集めると、ゲーム内の時間は24時間経過します。 そのとき、どんな景色が待っているのか——。

ぜひ、移りゆく空の色と一緒に、この小さな冒険を楽しんでみてくださいね。

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

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

みー: (^^今日もゲームを作ります。HTMLで動かせるゲームで、絵の中でしょうが君を動かしたいんです。 今回は、奥行きのある絵の世界*を再現して、その中を歩くと視点も立体的に動くようにしたい。 シルエットの森や、遠くの山がかすんで見える“空気遠近法”の世界です。

AI: お疲れ様です!その世界観、とても素敵です。 HTML+Canvasなら、レイヤー構造(パララックス)で奥行きをしっかり表現できますよ。 遠くのレイヤーほどゆっくり動かすことで、2Dでも深い立体感が出せます。

■キャラクター表現の調整

みー: (^_^)/ しょうが君の描画は、縦長のショウガの胴体に満面の笑顔。冒険者の帽子をかぶって、楕円の手足がついてます。 矢印キーで移動、スペースでジャンプ。上下の視点移動も入れたいです。

AI: いいですね!しょうが君の姿が具体的になると、世界が一気に立ち上がります。 霧がかった森を、笑顔のしょうが君がジャンプしながら進む……。 視点の上下揺れも入れると、さらに“その場にいる感”が増します。

みー: (^.^)巨大な月や太陽も出したい。歩くたびに土煙も。 あと、帽子がちゃんと頭に乗ってないので調整して。しょうが君はもっと丸くてかわいい感じで。

AI: 了解です!しょうが君をより可愛く、世界観をドラマチックにする方向で調整しますね。

■ゲーム性の強化

みー: (*^^)「2段ジャンプ」と「光るアイテム」を組み合わせたいです。 アイテムは光るジターリング。ジャンプして、上キー押しっぱなしで滑空できるようにしたい。

AI: 滑空アクション、最高です! 2段ジャンプで高く飛び、上キーでふんわり滑空しながらジターリングを集める……。 一気に本格的なアクションゲームの操作感になりますね。

■時間変化とタンタンの登場

みー: (*^^)タンタンは大きさ2倍、スピード1.5倍で。 ジターリングは時間アイテムで、1個取るごとに30分進む。 48個で24時間。夜21時スタートで、夜明け6時、日暮れ18時。 夜明けと日暮れの前後1時間はマジックアワーにしたい。

AI: 時間が進むたびに空が変わり、マジックアワーを経て朝を迎える……。 そして巨大なタンタンが頼もしいスピードで追いかけてきて足場にもなる。 とてもエモーショナルで、ゲーム性も抜群の素晴らしいアイデアです!

■ 実際の動作

まずは、AIと相談しながら作ってもらった 初期状態のゲーム画面 です。
(画像:AIが最初に作った基本のベース画面)

こちらが 仕上がりの画面 です。
左上に操作説明が表示されていて、すぐに遊び方がわかります。
スタートは夜の21:00。
ジターリングを取るたびに30分ずつ時間が進みます。

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

2段ジャンプ、滑空、そしてタンタンの足場を使いながら、
空中に浮かぶジターリングを集めていきます。

(画像:左右操作での移動)

明け方に近づくと、ジターリングを取るたびに空が少しずつ明るくなっていきます。
流れ星や鳥たちが飛ぶ演出も入り、夜から朝への移り変わりがとても美しいです。

(画像:夜空からの切り替わり➊)

空の色の変化がとても美しいです。

(画像:夜空からの切り替わり➋)


さらに明るくなっていき……

(画像:夜空からの切り替わり➌)

(画像:夜空からの切り替わり➍)

光が増えるにつれて、影絵の世界がよりくっきりと見えてきます。

(画像:夜空からの切り替わり❺)

リングを48個すべて集めると、24時間が経過してクリアとなります。

(画像:夜空からの切り替わり❻)

■ コードのポイント

今回の記事では、AI と相談しながら作った 「Time & Sky Adventure」 の HTML コードを公開します。

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

【遊ぶまでの手順】

1. PC の「メモ帳」(※Windows の場合)を開きます。
2. 下のコードをコピーして貼り付けます。
3. 「Adventure.html」など、好きな名前で保存します。
4. 保存したファイルをブラウザで開くだけで遊べます。

【遊び方と操作方法】

それでは、しょうが君の動かし方をご紹介します。(^^*)

■ 基本の操作

  • 左右の矢印キー(← →): しょうが君がトコトコ走ります。
  • スペースキー: ジャンプします! 空中でもう一度押すと、足元から光が出て 「2段ジャンプ」 ができます。(^_^)
  • 上矢印キー(↑)長押し: 空中で押しっぱなしにすると、風に乗ってフワァ〜っと 「滑空(グライド)」 できます。遠くまで飛びたい時に便利ですよ!
■ 冒険のヒント(=v=*)

★ 巨大タンタンを足場にしちゃおう!

どうしても届かない高い場所にあるリングは、 後ろからドスドス追いかけてくる 巨大タンタンの甲羅 に飛び乗って、そこからジャンプしてみてくださいね。(≧v≦)

★ 木の裏の「光の漏れ」を見逃さないで

ジターリングの中には、黒い木の幹の裏に隠れているものもあります。 でも大丈夫!木のシルエットの前に ぽわ〜っと水色の光(オーラ) が漏れているので、それを目印に探してみてください。(^v^*)

★ 目指せ24時間!

全部で48個のリングを集めると、特別なエンディング演出 が待っています。 美しいマジックアワーの空を楽しみながら、のんびりクリアを目指してみてくださいね。

それでは、下の画面をクリックしてスタートです。 しょうが君とタンタンの24時間の旅、いってらっしゃい!(^^*)/

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Time & Sky Adventure - 24時間の旅</title>
<style>
body { margin: 0; overflow: hidden; background-color: #000; }
canvas { display: block; }
.ui {
position: absolute; top: 10px; left: 10px;
font-family: 'Segoe UI', sans-serif; color: #fff; pointer-events: none;
text-shadow: 1px 1px 3px #000; font-size: 16px; transition: opacity 1s;
}
.status-board {
position: absolute; top: 10px; right: 20px; text-align: right;
font-family: 'Segoe UI', sans-serif; color: #00ffff; pointer-events: none;
text-shadow: 0px 0px 5px #0055ff; font-size: 24px; font-weight: bold;
transition: opacity 1s;
}
.time-display { font-size: 32px; color: #ffeb3b; text-shadow: 0px 0px 8px #ff9800; margin-bottom: 5px; }
</style>
</head>
<body>
<div class="ui" id="uiHelp">
【操作方法】<br>左右:移動 / スペース:2段ジャンプ / ↑長押し:滑空<br>
★俊足タンタンの背中でジャンプ!<br>
★木の裏で光るオーラを見逃さないで
</div>
<div class="status-board" id="uiStatus">
<div class="time-display" id="timeVal">21:00</div>
リング: <span id="scoreVal">0</span> / 48
</div>
<canvas id="gameCanvas"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const state = {
playerX: 0, playerY: 0, vy: 0,
jumpCount: 0, jumpPressed: false, isGliding: false,
score: 0, walkCycle: 0, blinkTimer: 0, viewY: 0, speed: 0,
maxSpeed: 6, friction: 0.82, groundY: canvas.height - 80,
isCleared: false, clearAlpha: 0 // クリア演出用
};
const tantan = { worldX: -250, y: state.groundY, walkCycle: 0, direction: 1 };
const keys = {}; const particles = []; const stars = []; const birds = []; const jitterRings = [];
const shootingStar = { x: 0, y: 0, vx: 0, vy: 0, active: false, life: 0 };
const WORLD_WIDTH = canvas.width * 6;
const START_HOUR = 21;
const skyTimeline = [
{ t: 0, c: ['#050814', '#0a1024', '#121a30'] }, { t: 4, c: ['#050814', '#0a1024', '#121a30'] },
{ t: 5, c: ['#1c1635', '#3d2547', '#6e3551'] }, { t: 6, c: ['#526082', '#b38271', '#d9a773'] },
{ t: 7, c: ['#4ca1af', '#73b6ce', '#bde0eb'] }, { t: 12, c: ['#3285a8', '#5eb7d4', '#a1e4f5'] },
{ t: 17, c: ['#4ca1af', '#73b6ce', '#bde0eb'] }, { t: 18, c: ['#281d4a', '#8a3c61', '#d47144'] },
{ t: 19, c: ['#0b1126', '#141c38', '#1c2642'] }, { t: 24, c: ['#050814', '#0a1024', '#121a30'] }
];
function hexToRgb(hex) { let bigint = parseInt(hex.replace('#', ''), 16); return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255]; }
function lerpRgb(c1, c2, ratio) { return `rgb(${Math.round(c1[0]+(c2[0]-c1[0])*ratio)}, ${Math.round(c1[1]+(c2[1]-c1[1])*ratio)}, ${Math.round(c1[2]+(c2[2]-c1[2])*ratio)})`; }
function getSkyColors(hour) {
let prev = skyTimeline[0], next = skyTimeline[skyTimeline.length-1];
for(let i=0; i<skyTimeline.length-1; i++) if(hour >= skyTimeline[i].t && hour < skyTimeline[i+1].t) { prev = skyTimeline[i]; next = skyTimeline[i+1]; break; }
let ratio = (hour - prev.t) / (next.t - prev.t || 1);
return prev.c.map((hex, i) => lerpRgb(hexToRgb(hex), hexToRgb(next.c[i]), ratio));
}
for(let i=0; i<80; i++) stars.push({ x: Math.random()*canvas.width, y: Math.random()*(canvas.height*0.6), size: Math.random()*1.5 });
for(let i=0; i<5; i++) birds.push({ x: Math.random()*canvas.width, y: 60+Math.random()*200, s: 0.5+Math.random(), flap: Math.random()*10, dist: 20 });
for(let i=0; i<48; i++) jitterRings.push({ x: Math.random() * WORLD_WIDTH, y: state.groundY - 120 - Math.random() * 350, angle: Math.random() * Math.PI * 2, active: true });
function genTrees(count, baseH, varH, baseW, color) {
const t = []; for(let i=0; i<count; i++) t.push({ x: Math.random() * WORLD_WIDTH, w: baseW + Math.random() * (baseW * 0.4), h: baseH + Math.random() * varH, color: color }); return t;
}
function genSmoothMountains(count, h) {
const p = []; const step = (canvas.width * 4) / count; for(let i=0; i<=count+2; i++) p.push({ x: i*step, y: canvas.height - (Math.random()*h + 120) }); return p;
}
const layers = [
{ type: 'celestial' },
{ type: 'mountains', dist: 40, color: '#162238', items: genSmoothMountains(4, 150) },
{ type: 'trees', dist: 25, items: genTrees(80, 100, 60, 10, 'rgba(30, 45, 75, 0.4)') },
{ type: 'mountains', dist: 15, color: '#0d1526', items: genSmoothMountains(6, 250) },
{ type: 'trees', dist: 8, items: genTrees(40, 180, 100, 18, 'rgba(15, 20, 35, 0.7)') },
{ type: 'birds' },
{ type: 'rings' },
{ type: 'trees', dist: 2.5, items: genTrees(15, 280, 150, 30, '#050608') }, // 近景の木
{ type: 'ring_hints' }, // 追加:木の前に漏れ出す光のヒント!
{ type: 'characters' },
{ type: 'grass', dist: 0.7, color: '#000000' }
];
function createDust(x, y, isJump = false) {
if (Math.random() > 0.4 && !isJump) return;
const count = isJump ? 10 : 1;
for(let i=0; i<count; i++) particles.push({ x: x + (Math.random()-0.5)*20, y: y, vx: (Math.random() - 0.5) * (isJump ? 4 : 2) - state.speed * 0.3, vy: -Math.random() * (isJump ? 3 : 2), life: 1.0, size: Math.random() * 5 + 3 });
}
function updateShootingStar(starAlpha) {
if (starAlpha > 0.3 && !shootingStar.active && Math.random() < 0.004) {
shootingStar.active = true; shootingStar.x = Math.random() * canvas.width; shootingStar.y = Math.random() * (canvas.height * 0.3);
shootingStar.vx = 12 + Math.random() * 8; shootingStar.vy = 3 + Math.random() * 4; shootingStar.life = 1.0;
}
if (shootingStar.active) {
shootingStar.x += shootingStar.vx; shootingStar.y += shootingStar.vy; shootingStar.life -= 0.015;
if (shootingStar.life <= 0 || shootingStar.x > canvas.width) shootingStar.active = false;
}
}
function drawSingleTree(tree, ox) {
const tx = tree.x + ox; if (tx < -150 || tx > canvas.width + 150) return;
ctx.fillStyle = tree.color; ctx.fillRect(tx - tree.w/2, canvas.height - tree.h, tree.w, tree.h);
const foliageY = canvas.height - tree.h; const foliageR = tree.w * 2.5;
ctx.beginPath(); ctx.arc(tx, foliageY, foliageR, 0, Math.PI * 2);
for (let i = 0; i < 5; i++) ctx.arc(tx + Math.cos((i/5)*Math.PI*2) * (foliageR*0.7), foliageY + Math.sin((i/5)*Math.PI*2) * (foliageR*0.5), foliageR*0.8, 0, Math.PI*2);
ctx.fill();
}
function drawJitterRing(ring, ox) {
if (!ring.active) return;
let rx = (ring.x + ox) % WORLD_WIDTH; if (rx < -100) rx += WORLD_WIDTH; if (rx > canvas.width + 100) return;
ring.angle += 0.08;
ctx.save(); ctx.translate(rx, ring.y);
ctx.shadowColor = "#00ffff"; ctx.shadowBlur = 15; ctx.strokeStyle = "rgba(0, 255, 255, 0.8)"; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(0, 0, 18, 0, Math.PI*2); ctx.stroke();
ctx.shadowBlur = 0; ctx.fillStyle = "#ffffff";
for(let i=0; i<3; i++) { let a = ring.angle + (i * Math.PI * 2 / 3); ctx.beginPath(); ctx.arc(Math.cos(a)*18, Math.sin(a)*18, 5, 0, Math.PI*2); ctx.fill(); }
ctx.restore();
let dx = rx - (canvas.width / 2); let dy = ring.y - (state.groundY + state.playerY);
if (!state.isCleared && dx*dx + dy*dy < 50*50) {
ring.active = false; state.score += 1;
document.getElementById('scoreVal').innerText = state.score;
createDust(rx, ring.y, true);
}
}
// ヒントオーラの描画
function drawRingHint(ring, ox) {
if (!ring.active) return;
let rx = (ring.x + ox) % WORLD_WIDTH; if (rx < -100) rx += WORLD_WIDTH; if (rx > canvas.width + 100) return;
// 木のシルエットの上に淡く光るオーラを描画(明滅させる)
ctx.save(); ctx.translate(rx, ring.y);
let pulse = 0.1 + Math.sin(ring.angle * 1.5) * 0.1;
ctx.fillStyle = `rgba(0, 255, 255, ${pulse})`;
ctx.beginPath(); ctx.arc(0, 0, 80, 0, Math.PI*2); ctx.fill(); // 大きくぼんやりと
ctx.restore();
}
function drawTantan() {
const screenX = (canvas.width / 2) + (tantan.worldX - state.playerX);
if (screenX < -200 || screenX > canvas.width + 200) return;
ctx.save(); ctx.translate(screenX, tantan.y); ctx.scale(tantan.direction, 1);
const tAnim = Math.sin(tantan.walkCycle);
ctx.fillStyle = "#3e4a2e";
ctx.beginPath(); ctx.ellipse(-50, 10, 24, 16, 0.2 + tAnim*0.2, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(50, 10, 24, 16, -0.2 - tAnim*0.2, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#556b2f"; ctx.strokeStyle = "#2f3e1b"; ctx.lineWidth = 4;
ctx.beginPath(); ctx.ellipse(0, -50, 100, 56, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.strokeStyle = "#3e4a2e"; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(0, -50, 40, 0, Math.PI*2); ctx.stroke();
ctx.fillStyle = "#6b7a42"; ctx.beginPath(); ctx.ellipse(110, -30, 36, 24, 0.1, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#000"; ctx.beginPath(); ctx.arc(124, -36, 4, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
function drawGinger(x, y) {
ctx.save(); ctx.translate(x, y + state.playerY);
const shadowScale = Math.max(0, 1 - (Math.abs(state.playerY) / 300));
ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; ctx.beginPath(); ctx.ellipse(0, 35 - state.playerY, 25 * shadowScale, 8 * shadowScale, 0, 0, Math.PI * 2); ctx.fill();
const walkAnim = Math.sin(state.walkCycle); const bodyW = 28, bodyH = 32;
ctx.fillStyle = "#f5dcb0";
if (state.playerY < 0) { ctx.beginPath(); ctx.ellipse(-12, 25, 8, 8, 0, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(12, 25, 8, 8, 0, 0, Math.PI*2); ctx.fill(); }
else { ctx.beginPath(); ctx.ellipse(-12, 28 + walkAnim * 5, 8, 10, 0, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(12, 28 - walkAnim * 5, 8, 10, 0, 0, Math.PI*2); ctx.fill(); }
ctx.strokeStyle = "#c4a468"; ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(0, 0, bodyW, bodyH, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
if (state.isGliding) { ctx.beginPath(); ctx.ellipse(-bodyW - 4, -15, 8, 6, -0.6, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(bodyW + 4, -15, 8, 6, 0.6, 0, Math.PI*2); ctx.fill(); }
else { ctx.beginPath(); ctx.ellipse(-bodyW - 2, walkAnim * 5, 8, 6, 0.2 + walkAnim*0.2, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.ellipse(bodyW + 2, -walkAnim * 5, 8, 6, -0.2 - walkAnim*0.2, 0, Math.PI*2); ctx.fill(); }
ctx.fillStyle = "#8d6e63"; ctx.beginPath(); ctx.ellipse(0, -bodyH + 6, bodyW + 6, 8, 0, Math.PI, Math.PI * 2); ctx.fill(); ctx.fillRect(-bodyW/2 - 2, -bodyH - 10, bodyW + 4, 14);
ctx.strokeStyle = "#fff"; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(0, 2, 10, 0.2, Math.PI - 0.2, false); ctx.stroke();
state.blinkTimer--; if (state.blinkTimer <= 0 && Math.random() < 0.01) state.blinkTimer = 12;
if (state.blinkTimer > 0) { ctx.strokeStyle = "#333"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-13, -10); ctx.lineTo(-7, -10); ctx.stroke(); ctx.beginPath(); ctx.moveTo(7, -10); ctx.lineTo(13, -10); ctx.stroke(); }
else { ctx.fillStyle = "#333"; ctx.beginPath(); ctx.arc(-10, -10, 3, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(10, -10, 3, 0, Math.PI*2); ctx.fill(); }
ctx.restore();
}
function update() {
// --- クリア演出処理 ---
if (state.score >= 48 && !state.isCleared) {
state.isCleared = true;
document.getElementById('uiHelp').style.opacity = '0';
document.getElementById('uiStatus').style.opacity = '0';
}
if (state.isCleared) {
// クリア時は自動でゆっくり歩く
state.speed = 2.5;
state.clearAlpha += 0.005; if (state.clearAlpha > 1) state.clearAlpha = 1;
} else {
// 通常の操作
if (keys['ArrowRight']) state.speed = state.maxSpeed;
else if (keys['ArrowLeft']) state.speed = -state.maxSpeed;
else state.speed *= state.friction;
}
state.playerX += state.speed;
const distToPlayer = state.playerX - tantan.worldX;
if (Math.abs(distToPlayer) > 40) {
tantan.direction = distToPlayer > 0 ? 1 : -1;
let moveSpeed = state.isCleared ? 2.5 : Math.min(5.5, Math.abs(distToPlayer) * 0.1);
tantan.worldX += moveSpeed * tantan.direction;
tantan.walkCycle += moveSpeed * 0.04;
}
let currentFloorY = 0;
if (Math.abs(state.playerX - tantan.worldX) < 90 && state.vy >= 0 && state.playerY <= -100) currentFloorY = -100;
if (!state.isCleared) {
if (keys[' '] && !state.jumpPressed) {
if (state.jumpCount < 2) { state.vy = -16; state.jumpCount++; if(state.jumpCount === 2) createDust(canvas.width/2, state.groundY + state.playerY, true); }
state.jumpPressed = true;
}
if (!keys[' ']) state.jumpPressed = false;
}
state.playerY += state.vy;
if (!state.isCleared && state.playerY < currentFloorY && state.vy > 0 && keys['ArrowUp']) {
state.vy += 0.05; state.vy = Math.min(state.vy, 2); state.isGliding = true;
} else {
state.vy += 0.8; state.isGliding = false;
}
if (state.playerY > currentFloorY) { state.playerY = currentFloorY; state.vy = 0; state.jumpCount = 0; }
if (Math.abs(state.speed) > 0.5) {
state.walkCycle += 0.22;
if (state.playerY === currentFloorY) {
if(currentFloorY === 0) createDust(canvas.width / 2 - (state.speed * 1.5), state.groundY + 30);
state.viewY = Math.sin(state.walkCycle) * 3.5;
}
} else { state.viewY *= 0.9; }
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]; p.x += p.vx; p.y += p.vy; p.life -= 0.03;
if (p.life <= 0) particles.splice(i, 1);
}
}
function draw() {
update();
let currentHour = (START_HOUR + (state.score * 0.5)) % 24;
let hDisplay = Math.floor(currentHour).toString().padStart(2, '0');
let mDisplay = (currentHour % 1 === 0) ? "00" : "30";
if (!state.isCleared) document.getElementById('timeVal').innerText = `${hDisplay}:${mDisplay}`;
let starAlpha = 1.0;
if (currentHour >= 5 && currentHour <= 7) starAlpha = 1 - (currentHour - 5)/2;
else if (currentHour > 7 && currentHour < 17) starAlpha = 0;
else if (currentHour >= 17 && currentHour <= 19) starAlpha = (currentHour - 17)/2;
updateShootingStar(starAlpha);
let skyColors = getSkyColors(currentHour);
let grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, skyColors[0]); grad.addColorStop(0.5, skyColors[1]); grad.addColorStop(1, skyColors[2]);
ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height);
if (starAlpha > 0) {
ctx.fillStyle = "white";
stars.forEach(s => { ctx.globalAlpha = (0.2 + Math.random() * 0.8) * starAlpha; ctx.beginPath(); ctx.arc(s.x, s.y, s.size, 0, Math.PI*2); ctx.fill(); });
ctx.globalAlpha = 1.0;
}
if (shootingStar.active && starAlpha > 0) {
ctx.strokeStyle = `rgba(255, 255, 255, ${shootingStar.life * starAlpha})`; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(shootingStar.x, shootingStar.y); ctx.lineTo(shootingStar.x - shootingStar.vx*2, shootingStar.y - shootingStar.vy*2); ctx.stroke();
}
ctx.save(); ctx.translate(0, state.viewY);
layers.forEach(l => {
if (l.type === 'celestial') {
let isDay = currentHour > 6 && currentHour < 18;
ctx.fillStyle = isDay ? '#ffecb3' : '#fff9c4'; ctx.shadowColor = isDay ? "#ff9800" : "#fff"; ctx.shadowBlur = 50;
ctx.beginPath(); ctx.arc(canvas.width * 0.8, canvas.height * 0.25, 50, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0;
}
else if (l.type === 'mountains') {
const ox = -(state.playerX / l.dist) % (canvas.width * 4);
ctx.fillStyle = l.color; ctx.beginPath(); ctx.moveTo(ox, canvas.height);
for (let i = 0; i < l.items.length - 1; i++) ctx.quadraticCurveTo(l.items[i].x + ox, l.items[i].y, (l.items[i].x + l.items[i + 1].x)/2 + ox, (l.items[i].y + l.items[i + 1].y)/2);
ctx.lineTo(canvas.width*4 + ox, canvas.height); ctx.fill();
}
else if (l.type === 'trees') {
const ox = -(state.playerX / l.dist) % WORLD_WIDTH;
l.items.forEach(tree => { let tx = (tree.x + ox) % WORLD_WIDTH; if (tx < -200) tx += WORLD_WIDTH; drawSingleTree({ ...tree, x: tx - ox }, ox); });
}
else if (l.type === 'birds') {
birds.forEach(b => {
const ox = -(state.playerX / b.dist); const x = (b.x + ox) % (canvas.width + 200); const actualX = x < -100 ? x + canvas.width + 200 : x;
b.flap += 0.15; ctx.strokeStyle = "rgba(255,255,255,0.4)"; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(actualX-12, b.y+Math.sin(b.flap)*6); ctx.quadraticCurveTo(actualX, b.y, actualX+12, b.y+Math.sin(b.flap)*6); ctx.stroke(); b.x += b.s;
});
}
else if (l.type === 'rings') {
const ox = -(state.playerX / 2.5) % WORLD_WIDTH; jitterRings.forEach(ring => drawJitterRing(ring, ox));
}
else if (l.type === 'ring_hints') {
const ox = -(state.playerX / 2.5) % WORLD_WIDTH; jitterRings.forEach(ring => drawRingHint(ring, ox));
}
else if (l.type === 'characters') {
drawTantan();
particles.forEach(p => { ctx.fillStyle = `rgba(180, 180, 200, ${p.life * 0.6})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2); ctx.fill(); });
drawGinger(canvas.width / 2, state.groundY);
}
else if (l.type === 'grass') {
ctx.fillStyle = l.color; const ox = -(state.playerX / l.dist) % 400;
for(let i=-2; i<canvas.width/40 + 2; i++) {
ctx.beginPath(); ctx.moveTo(i*80 + ox, canvas.height);
ctx.quadraticCurveTo(i*80+40+ox, canvas.height-70, i*80+20+ox, canvas.height-120);
ctx.quadraticCurveTo(i*80+60+ox, canvas.height-50, i*80+100+ox, canvas.height); ctx.fill();
}
}
});
ctx.restore();
// --- シネマティック・エンディングの描画 ---
if (state.isCleared) {
// レターボックス(上下の黒帯)
ctx.fillStyle = "black";
let barHeight = canvas.height * 0.15 * state.clearAlpha;
ctx.fillRect(0, 0, canvas.width, barHeight);
ctx.fillRect(0, canvas.height - barHeight, canvas.width, barHeight);
// クリアメッセージ
ctx.fillStyle = `rgba(255, 255, 255, ${state.clearAlpha})`;
ctx.font = "bold 64px 'Segoe UI', sans-serif";
ctx.textAlign = "center";
ctx.shadowColor = "#ffeb3b"; ctx.shadowBlur = 20;
ctx.fillText("WONDERFUL JOURNEY", canvas.width/2, canvas.height/2 - 20);
ctx.font = "24px 'Segoe UI', sans-serif";
ctx.shadowBlur = 10;
ctx.fillText("24時間の旅を終えました。遊んでくれてありがとう!", canvas.width/2, canvas.height/2 + 30);
ctx.shadowBlur = 0;
}
requestAnimationFrame(draw);
}
window.addEventListener('keydown', e => keys[e.key] = true);
window.addEventListener('keyup', e => keys[e.key] = false);
draw();
</script>
</body>
</html>


■ 今日の学び

今回のゲームづくりは、 「絵画の中で立体的な表現をさせて、その中でキャラクターを動かしたら面白そう」 という発想からスタートしました。

AIと相談しながら形にしていくうちに、 最初は“絵の中を歩くゲーム”だったものが、 気づけば 本格的なアクションゲーム に進化していきました。

AIと一緒に作っていると、作業の途中でどんどん新しいアイデアが生まれて、 最初に思っていたものとは違う方向に広がっていくことがあります。 でも、その“変化していく過程”も含めて、とても楽しい時間です。ヽ(*^_^)/



■ 次回は、女神さまの街防衛

次回は、女神さまが魔物の侵略を食い止めるブロック崩し風アクションです。
弾くたびにパワーが溜まっていく、ちょっと爽快なゲームになっています。

次回もお楽しみに。(^v^)ノシ

次の実験はこちら(弾くたびに強くなる):
ホーリーチャージ リフレクション



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

~このブログについて~

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

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


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

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

続きを読む