実験#74:壮大な異世界を飛ぶ(和紙の鷲のわし)

■74個目 「和紙の鷲のわし」

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

今回は、大空を舞台にした、ちょっと不思議で壮大なゲームを作ってみました。

空を飛ぶ主人公は……なんと “和紙でできた鷲(わし)” です!

舞台となるのは、はるか上空に無数の島が浮かび、
眼下にはどこまでも続く大森林と、美しい川が流れるファンタジー世界。

空の島からは無数の滝が降り注ぎ、
地上には神秘的な水霧が立ちこめています。

そんな絶景の中を、 一人称が「わし」の和紙の鷲が、
ずーっと独り言を呟きながら滑空していくんです。(´▽`*)

今回こだわったのは、
“気持ちよく飛ぶ爽快感” と “和紙ゆえのスリル” のギャップ。

体は和紙なので……
滝の水に濡れるとふやけて墜落しちゃうんです!(>_<)

迫りくる滝の隙間を縫いながら、
遠くを飛ぶドラゴンや、滝登りをするお魚たちの影を眺める癒やしのひととき。

そして、鷲のおじいちゃんみたいな可愛い実況(独り言)にも、
ぜひ注目してみてくださいね。

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

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

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

今回は“滑空感”を味わえるゲームが作りたいです。

プレイヤーは 和紙でできた鷲(わし)。 一人称は「わし」。

ファンタジーな世界で、地上と上空には無数の浮島があり、
空の島からは滝が地上へと降り注ぎます。

和紙の鷲のわしは、その間をマウスで方向を定めながら滑空。
滝に濡れると紙なのでやばいです。

独り言実況が多い鷲の設定でお願い。

AI: 和紙の鷲が「わし」と独り言を言いながら滑空する……
言葉遊びも効いていて、世界観と相性抜群ですね!

ご要望に合わせて、 マウス追従で滑空し、
上空から降る滝を避けるアクションゲームを作成しました。

独り言は画面下部に吹き出しで表示され、
滝をギリギリで避けた時や濡れた時の専用セリフもあります。

みー:(つv=`) 独り言はいい感じ。でも下だと目に入りづらいです。
画面中央上あたりに出ると見やすいかも。

独り言が少し邪魔なくらいがいいです。

ゲームの視点は斜め後方のイメージでした。

AI: なるほど、斜め後方視点ですね!
Canvasは2Dですが、擬似3Dのパース計算を組み込めば
立体的な滑空感を表現できます。

独り言も画面中央上に移動し、
フォントも少し大きくして“ちょっと邪魔”な絶妙さに調整しました(笑)。

みー:ヽ(^o^)ノ すごい!イメージ通りです。

ただ、画面端に寄ると滝がなくて安全地帯になってしまいます。
左右をループ状にして、滝の切れ間がないようにできますか?

AI: 安全地帯の発見、さすがです!
ご要望通り、左右をシームレスにループ化しました。

右に進むと左から景色が戻ってくる“円筒世界”になっています。

みー:( ̄~ ̄;) 滝が多すぎて進めない量になりました。
滝の割合を減らしましょう。

あと、滝登りする魚影や、遠くを飛ぶドラゴンの影など
ファンタジー感を出せる演出もお願い。

AI: ……おっと、滝を盛りすぎましたね。
しっかり減らして避けやすく調整します。

そして、滝の透け感を活かして
「滝を登る魚の影」と「遠くを飛ぶドラゴンのシルエット」を追加しました。

世界観がぐっと深まるはずです。

みー:(*^^)地上も表現できますか?

大森林と無数の川が流れる、水の豊かな世界をお願い。

AI: もちろん。 眼下には果てしない緑の海と、
そこを縫うように流れる青い川を敷き詰めました。

空から落ちる滝が地上にぶつかって水しぶきを上げる演出も追加。
コードも書き直したので、

「水の豊かな世界」の滑空をたっぷり味わってください。

■ 実際の動作

最初に AI が作ってくれたゲーム画面がこちらです。
平面視点で、主人公は紙飛行機。
水のかたまりが飛んでくる、シンプルな試作版でした。

(画像:AIが最初に生成したゲーム画面)

こちらが 完成版のスタート画面 です。 和紙の鷲が滑空しながら、
画面中央上に“独り言実況”がバンバン出てきます。

主張が強すぎて、 操作ミスにつながりやすいのもポイント です( ̄~ ̄*)

(画像:完成版ゲーム画面)


● 滝の水に濡れるとゲームオーバー

和紙なので、水に触れると即アウト。
滝のしぶきにかすっただけでも、ふやけて墜落します。

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

● 気を散らす“世界の影たち”

遠くを飛ぶドラゴンの影、 滝を登る魚の影…… 視界の端にふっと見えると、つい気になってしまい、 これもまた操作ミスの原因に。

(画像:ドラゴンの影)

(画像:魚の影と、大きな独り言)



● 壮大な景色に見とれてしまう危険

浮島のシルエット、 眼下に広がる大森林と無数の川。
水量たっぷりの世界が広がっていて、 つい見とれてしまうと……
やっぱり操作ミスにつながります。

(画像:川のたくさん流れる大森林)

(画像:異世界感たっぷりの浮島)


■ コードのポイント

今回の記事では、AI と相談しながら作った
「和紙の鷲のわし」 の HTML コードを公開します。

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

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

▼ 遊ぶまでの手順

①PC の「メモ帳」(※Windowsの場合)や  
 Mac の「テキストエディット」など、テキストエディタを開きます。

②下のコードをコピーして貼り付けます。

③「washi.html」など、好きな名前で保存します。
 (※拡張子は .html にしてください)

④保存したファイルをブラウザで開けば、すぐに遊べます。

 

■ 操作方法

操作はとってもシンプルです!(^_^)

画面の中でマウスを動かすだけで、
和紙の鷲がスィーっとカーソルに合わせて旋回してくれます。

フライトシミュレーターのように、
右へ左へ自由に飛び回ってみてください。(^^*)

ただし、奥から迫ってくる 「滝」にはご用心!(>_<)

和紙でできているので、
水に濡れるとふやけてゲームオーバーになってしまいます。

滝と滝の隙間を上手に見つけて、
華麗にすり抜けてくださいね。(=v=*)

長く飛べば飛ぶほど、飛翔距離(スコア)がどんどん伸びていきます。

鷲のおちゃめな独り言にクスッとしつつ、
ぜひ大記録を目指してみてくださいね!(´▽`*)

それでは、壮大な大森林の空の旅へいってらっしゃいませ♪

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

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>和紙の鷲の滑空譚 - 猛禽の勇姿</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #87CEEB;
font-family: 'Hiragino Maru Gothic ProN', 'Meiryo', sans-serif;
user-select: none;
}
canvas {
display: block;
}
#ui {
position: absolute;
top: 20px;
left: 20px;
pointer-events: none;
}
#score {
font-size: 28px;
color: white;
text-shadow: 2px 2px 0px #333, -1px -1px 0px #333, 1px -1px 0px #333, -1px 1px 0px #333, 1px 1px 0px #333;
font-weight: bold;
}
#monologue {
position: absolute;
top: 15%;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.95);
padding: 20px 40px;
border-radius: 40px;
font-size: 28px;
font-weight: bold;
color: #222;
text-align: center;
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
pointer-events: none;
transition: opacity 0.2s;
border: 4px solid #444;
max-width: 80%;
white-space: nowrap;
z-index: 10;
}
#monologue::after {
content: '';
position: absolute;
bottom: -18px;
left: 50%;
transform: translateX(-50%);
border-width: 18px 18px 0;
border-style: solid;
border-color: #444 transparent transparent transparent;
}
#monologue::before {
content: '';
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
border-width: 14px 14px 0;
border-style: solid;
border-color: rgba(255, 255, 255, 0.95) transparent transparent transparent;
z-index: 1;
}
#gameover {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.85);
color: white;
padding: 50px;
border-radius: 15px;
text-align: center;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
z-index: 20;
}
#gameover h1 {
color: #4da6ff;
margin-top: 0;
font-size: 40px;
}
button {
margin-top: 30px;
padding: 12px 30px;
font-size: 20px;
font-weight: bold;
cursor: pointer;
background: #fff;
border: none;
border-radius: 8px;
transition: 0.2s;
}
button:hover {
background: #ddd;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="score">飛翔距離: 0m</div>
</div>
<div id="monologue" style="opacity: 1;">わしの新しき翼、とくと見よ!</div>
<div id="gameover">
<h1>ゲームオーバー</h1>
<p style="font-size: 20px;">和紙が水を吸って重くなり…大森林へ墜落した…</p>
<h2 id="finalScore" style="font-size: 32px;"></h2>
<button onclick="location.reload()">もう一度飛ぶ</button>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let width = canvas.width = window.innerWidth;
let height = canvas.height = window.innerHeight;
const focalLength = 400;
const WORLD_WIDTH = 4000;
const GROUND_Y = 2500;
let cameraZ = 0;
let baseSpeed = 18;
const player = {
x: 0,
y: 0,
targetY: 0,
width: 120, // 翼が広がった分、当たり判定も少し調整
height: 40,
turnSpeed: 0,
isDead: false
};
let mouseX = width / 2;
let mouseY = height / 2;
let score = 0;
let objects = [];
let frameCount = 0;
const rivers = [];
for(let i=0; i<6; i++) {
rivers.push({
baseX: (WORLD_WIDTH / 6) * i,
width: 150 + Math.random() * 250,
phase: Math.random() * Math.PI * 2,
wobbleSpeed: 0.0004 + Math.random() * 0.0003
});
}
const monologues = {
normal: [
"どうじゃ!わしのこの翼、格好良かろう!",
"眼下には大森林と無数の川…落ちたら森の肥料じゃな!",
"滝が落ちて霧になる…美しい景色じゃ。",
"遠くにドラゴンがおるのう。わしの方が美しく飛べるがな!",
"おおっ、滝の中に魚の影が…美味そうじゃ。",
"和紙のシワは人生の年輪じゃ。…濡れるのはNGじゃがな。",
"フハハハ!この壮大な空はわしの庭よ!"
],
closeCall: [
"ヒヤッとしたわい!",
"今のはアウトかと思うたぞ!",
"針の穴を通すような見事な滑空じゃ!",
"あぶなっ!美しい翼が濡れるところじゃった!"
],
death: [
"ああっ!濡れる!わしの体が溶けてゆくぅぅ…!",
"無念…ただの紙くずとなって森に散るか…",
"しゃ、喋りすぎた……"
]
};
let monologueTimer = 200;
const monologueEl = document.getElementById('monologue');
window.addEventListener('resize', () => {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
});
canvas.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
function speak(type) {
const lines = monologues[type];
const text = lines[Math.floor(Math.random() * lines.length)];
monologueEl.innerText = text;
monologueEl.style.opacity = 1;
monologueTimer = 180;
}
function getRelativeX(objX) {
let dx = (objX - player.x) % WORLD_WIDTH;
if (dx > WORLD_WIDTH / 2) dx -= WORLD_WIDTH;
if (dx < -WORLD_WIDTH / 2) dx += WORLD_WIDTH;
return dx;
}
function project(objX, y, z) {
if (z < 1) z = 1;
const scale = focalLength / z;
const relX = getRelativeX(objX);
const screenX = width / 2 + relX * scale;
const screenY = height / 2 + (y - player.y) * scale;
return { x: screenX, y: screenY, scale: scale };
}
class Waterfall {
constructor(z) {
this.x = Math.random() * WORLD_WIDTH;
this.z = z;
this.radius = 50 + Math.random() * 100;
this.passed = false;
this.fishes = [];
let fishCount = Math.floor(Math.random() * 3);
for(let i=0; i<fishCount; i++) {
this.fishes.push({
xOffset: (Math.random() - 0.5) * this.radius * 1.2,
y: 0 + Math.random() * 2000,
speed: 10 + Math.random() * 10
});
}
}
update(speed) {
this.z -= speed;
for(let f of this.fishes) {
f.y -= f.speed;
if(f.y < -1500) f.y = GROUND_Y;
}
}
draw() {
if (this.z < 10) return;
const top = project(this.x, -2000, this.z);
const bottom = project(this.x, GROUND_Y, this.z);
const drawWidth = this.radius * 2 * top.scale;
if (top.x + drawWidth < 0 || top.x - drawWidth > width) return;
ctx.fillStyle = 'rgba(230, 245, 255, 0.15)';
ctx.beginPath();
ctx.ellipse(bottom.x, bottom.y, drawWidth * 1.5, drawWidth * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 1.5 * top.scale;
ctx.beginPath();
ctx.ellipse(bottom.x, bottom.y, drawWidth * 0.9, drawWidth * 0.25, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.fillStyle = 'rgba(30, 50, 80, 0.4)';
for(let f of this.fishes) {
const fp = project(this.x + f.xOffset, f.y, this.z);
if(fp.y > top.y && fp.y < bottom.y) {
ctx.beginPath();
ctx.ellipse(fp.x, fp.y, 8 * fp.scale, 25 * fp.scale, 0, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.fillStyle = 'rgba(135, 206, 250, 0.7)';
ctx.fillRect(top.x - drawWidth / 2, top.y, drawWidth, bottom.y - top.y);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 2 * top.scale;
ctx.beginPath();
ctx.moveTo(top.x - drawWidth/4, top.y);
ctx.lineTo(top.x - drawWidth/4, bottom.y);
ctx.moveTo(top.x + drawWidth/4, top.y);
ctx.lineTo(top.x + drawWidth/4, bottom.y);
ctx.stroke();
}
checkCollision() {
if (this.z < 100 && this.z > -50) {
let relX = Math.abs(getRelativeX(this.x));
if (relX < this.radius * 0.8 + player.width/2) return true;
if (!this.passed && relX < this.radius + player.width + 50) {
this.passed = true;
if(Math.random() < 0.4) speak('closeCall');
}
}
return false;
}
}
class Island {
constructor(z) {
this.x = Math.random() * WORLD_WIDTH;
this.y = -1000 - Math.random() * 800;
this.z = z;
this.radiusX = 300 + Math.random() * 500;
this.radiusY = 100 + Math.random() * 150;
}
update(speed) { this.z -= speed; }
draw() {
if (this.z < 10) return;
const p = project(this.x, this.y, this.z);
const rx = this.radiusX * p.scale;
const ry = this.radiusY * p.scale;
if (p.x + rx < 0 || p.x - rx > width) return;
ctx.fillStyle = '#4CAF50';
ctx.beginPath();
ctx.ellipse(p.x, p.y, rx, ry, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#795548';
ctx.beginPath();
ctx.arc(p.x, p.y + (10 * p.scale), rx * 0.85, 0, Math.PI);
ctx.fill();
}
}
class GroundTree {
constructor(z) {
this.x = Math.random() * WORLD_WIDTH;
this.z = z;
this.size = 60 + Math.random() * 80;
}
update(speed) { this.z -= speed; }
draw() {
if (this.z < 10) return;
const p = project(this.x, GROUND_Y, this.z);
if (p.x < -200 || p.x > width + 200) return;
let scale = p.scale;
let w = this.size * scale;
let h = this.size * 2 * scale;
ctx.fillStyle = '#0a2a0a';
ctx.beginPath();
ctx.moveTo(p.x, p.y - h*0.7);
ctx.lineTo(p.x - w/2, p.y);
ctx.lineTo(p.x + w/2, p.y);
ctx.fill();
ctx.fillStyle = '#064010';
ctx.beginPath();
ctx.moveTo(p.x, p.y - h);
ctx.lineTo(p.x - w*0.4, p.y - h*0.3);
ctx.lineTo(p.x + w*0.4, p.y - h*0.3);
ctx.fill();
}
}
class Dragon {
constructor(z) {
this.x = Math.random() * WORLD_WIDTH;
this.y = -1500 - Math.random() * 1500;
this.z = z;
this.vx = (Math.random() > 0.5 ? 1 : -1) * (15 + Math.random() * 10);
this.size = 2 + Math.random() * 1.5;
this.offsetY = Math.random() * 100;
}
update(speed) {
this.z -= speed;
this.x += this.vx;
}
draw() {
if (this.z < 10) return;
let currentY = this.y + Math.sin(frameCount * 0.03 + this.offsetY) * 100;
const p = project(this.x, currentY, this.z);
if (p.x < -200 || p.x > width + 200) return;
ctx.save();
ctx.translate(p.x, p.y);
if (this.vx < 0) ctx.scale(-1, 1);
let s = p.scale * this.size;
ctx.scale(s, s);
ctx.fillStyle = 'rgba(60, 80, 100, 0.4)';
let flap = Math.sin(frameCount * 0.1) * 30;
ctx.beginPath();
ctx.moveTo(30, 0);
ctx.lineTo(10, -5);
ctx.lineTo(-30, -5);
ctx.lineTo(-40, -10);
ctx.lineTo(-25, 0);
ctx.lineTo(10, 5);
ctx.fill();
ctx.beginPath();
ctx.moveTo(5, -2);
ctx.lineTo(-10, -30 + flap);
ctx.lineTo(20, -25 + flap);
ctx.fill();
ctx.restore();
}
}
function update() {
if(player.isDead) return;
let targetTurnSpeed = (mouseX - width / 2) * 0.08;
player.turnSpeed += (targetTurnSpeed - player.turnSpeed) * 0.1;
player.x += player.turnSpeed;
player.targetY = (mouseY - height / 2) * 3;
player.y += (player.targetY - player.y) * 0.1;
baseSpeed = 18 + (score / 400);
cameraZ += baseSpeed;
let spawnInterval = Math.max(25, 50 - Math.floor(score/500));
if (frameCount % spawnInterval === 0) {
let spawnCount = Math.random() < 0.8 ? 1 : 2;
for(let i=0; i<spawnCount; i++){
objects.push(new Waterfall(5000));
}
}
if (frameCount % 60 === 0) {
objects.push(new Island(5500));
}
if (frameCount % 180 === 0) {
objects.push(new Dragon(6000));
}
if (frameCount % 5 === 0) {
objects.push(new GroundTree(5000));
}
for (let i = objects.length - 1; i >= 0; i--) {
let obj = objects[i];
obj.update(baseSpeed);
if (obj instanceof Waterfall && obj.checkCollision()) {
player.isDead = true;
speak('death');
document.getElementById('gameover').style.display = 'block';
document.getElementById('finalScore').innerText = `最終飛翔距離: ${Math.floor(score)}m`;
}
if (obj.z < -200) {
objects.splice(i, 1);
}
}
objects.sort((a, b) => b.z - a.z);
score += baseSpeed * 0.05;
document.getElementById('score').innerText = `飛翔距離: ${Math.floor(score)}m`;
monologueTimer--;
if(monologueTimer <= 0) {
monologueEl.style.opacity = 0;
if(Math.random() < 0.03) speak('normal');
}
frameCount++;
}
// --- アップグレードされた和紙の鷲の描画関数 ---
function drawPlayer() {
ctx.save();
const drawX = width / 2;
const drawY = height / 2 + 150;
ctx.translate(drawX, drawY);
// 旋回速度に合わせて機体をロールさせる
let tiltX = player.turnSpeed * 0.015;
let pitchY = (player.targetY - player.y) * 0.001;
ctx.rotate(tiltX);
ctx.scale(1 - pitchY, 1 + pitchY);
ctx.lineWidth = 1.5;
ctx.lineJoin = 'round';
// 1. 尾羽(一番下層の扇状)
ctx.fillStyle = '#F5DEB3'; // 影になる和紙色
ctx.strokeStyle = '#D2B48C';
ctx.beginPath();
ctx.moveTo(-20, 30);
ctx.lineTo(-40, 65);
ctx.lineTo(-15, 68);
ctx.lineTo(0, 75); // 尾の先端
ctx.lineTo(15, 68);
ctx.lineTo(40, 65);
ctx.lineTo(20, 30);
ctx.closePath();
ctx.fill();
ctx.stroke();
// 2. 左翼(広げた大きな翼と風切羽)
ctx.fillStyle = '#FFFAF0';
ctx.beginPath();
ctx.moveTo(0, 10);
ctx.lineTo(-40, -10); // 翼の前縁
ctx.lineTo(-120, -30); // 翼の先端(鋭く)
ctx.lineTo(-95, -5); // 風切羽のギザギザ
ctx.lineTo(-105, 5);
ctx.lineTo(-75, 15);
ctx.lineTo(-80, 25);
ctx.lineTo(-40, 30); // 翼の後縁
ctx.closePath();
ctx.fill();
ctx.stroke();
// 左翼の折り目(立体感)
ctx.beginPath();
ctx.moveTo(-40, -10);
ctx.lineTo(-75, 15);
ctx.stroke();
// 3. 右翼
ctx.beginPath();
ctx.moveTo(0, 10);
ctx.lineTo(40, -10);
ctx.lineTo(120, -30);
ctx.lineTo(95, -5);
ctx.lineTo(105, 5);
ctx.lineTo(75, 15);
ctx.lineTo(80, 25);
ctx.lineTo(40, 30);
ctx.closePath();
ctx.fill();
ctx.stroke();
// 右翼の折り目
ctx.beginPath();
ctx.moveTo(40, -10);
ctx.lineTo(75, 15);
ctx.stroke();
// 4. 胴体(手前が太く奥が細い、立体的な菱形)
ctx.fillStyle = '#FFF8DC';
ctx.beginPath();
ctx.moveTo(-15, 25);
ctx.lineTo(0, -50); // 頭部へのライン
ctx.lineTo(15, 25);
ctx.lineTo(0, 45); // 背中の後端
ctx.closePath();
ctx.fill();
ctx.stroke();
// 背骨のライン
ctx.beginPath();
ctx.moveTo(0, 45);
ctx.lineTo(0, -50);
ctx.stroke();
// 5. 頭とくちばし(最前面)
ctx.fillStyle = '#DEB887'; // くちばしは少し濃い色
ctx.beginPath();
ctx.moveTo(-8, -40);
ctx.lineTo(0, -65); // 鋭いくちばしの先
ctx.lineTo(8, -40);
ctx.lineTo(0, -35);
ctx.closePath();
ctx.fill();
ctx.stroke();
// 頭の折り目
ctx.beginPath();
ctx.moveTo(0, -35);
ctx.lineTo(0, -65);
ctx.stroke();
ctx.restore();
}
function drawEnvironment() {
let skyGrad = ctx.createLinearGradient(0, 0, 0, height/2);
skyGrad.addColorStop(0, '#1A5276');
skyGrad.addColorStop(1, '#A9DFBF');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, width, height);
let horizonY = height / 2;
let groundGrad = ctx.createLinearGradient(0, horizonY, 0, height);
groundGrad.addColorStop(0, '#0B5345');
groundGrad.addColorStop(1, '#013220');
ctx.fillStyle = groundGrad;
ctx.fillRect(0, horizonY, width, height - horizonY);
const zSteps = [];
for(let z = 5000; z >= 20; z -= (z > 2000 ? 300 : (z > 500 ? 100 : 25))) {
zSteps.push(z);
}
ctx.fillStyle = 'rgba(0, 206, 209, 0.25)';
for(let r of rivers) {
for(let offset = -1; offset <= 1; offset++) {
let baseX = r.baseX + offset * WORLD_WIDTH;
let relBaseX = getRelativeX(baseX);
if (relBaseX < -width * 2 || relBaseX > width * 2) continue;
ctx.beginPath();
for(let i = 0; i < zSteps.length; i++) {
let z = zSteps[i];
let worldZ = z + cameraZ;
let rX = relBaseX + Math.sin(worldZ * r.wobbleSpeed + r.phase) * 500;
let scale = focalLength / Math.max(1, z);
let pX = width / 2 + (rX - r.width/2) * scale;
let pY = height / 2 + (GROUND_Y - player.y) * scale;
if(i === 0) ctx.moveTo(pX, pY);
else ctx.lineTo(pX, pY);
}
for(let i = zSteps.length - 1; i >= 0; i--) {
let z = zSteps[i];
let worldZ = z + cameraZ;
let rX = relBaseX + Math.sin(worldZ * r.wobbleSpeed + r.phase) * 500;
let scale = focalLength / Math.max(1, z);
let pX = width / 2 + (rX + r.width/2) * scale;
let pY = height / 2 + (GROUND_Y - player.y) * scale;
ctx.lineTo(pX, pY);
}
ctx.fill();
}
}
}
function draw() {
drawEnvironment();
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.beginPath();
for(let i=0; i<10; i++) {
ctx.moveTo(width/2, height/2);
let angle = Math.random() * Math.PI * 2;
ctx.lineTo(width/2 + Math.cos(angle)*width, height/2 + Math.sin(angle)*width);
}
ctx.stroke();
objects.forEach(obj => obj.draw());
if(!player.isDead) drawPlayer();
}
function loop() {
update();
draw();
if(!player.isDead) {
requestAnimationFrame(loop);
} else {
draw();
}
}
for(let z = 1000; z < 5000; z += 300) {
if(Math.random() < 0.6) objects.push(new Waterfall(z));
if(Math.random() < 0.3) objects.push(new Island(z));
if(Math.random() < 0.2) objects.push(new Dragon(z));
for(let i=0; i<3; i++) objects.push(new GroundTree(z));
}
speak('normal');
loop();
</script>
</body>
</html>


■ 今日の学び

今回の「和紙の鷲のわし」は、
以前作った #6「筋肉もりもり森盛り盛り」と同じく、
タイトルの言葉遊び から生まれたゲームでした。

「和紙の鷲……これを主人公にして、
    壮大なファンタジー世界を自由に飛びたい!」

そんな思いを胸に AI に相談し、
二人三脚で少しずつ、この不思議な世界観を組み上げていきました。

開発の中で一番苦労したのは、 地上を流れる川の表現 でした。

無限ループする地形の計算がうまく合わず、
画面いっぱいに巨大な水色の板が 上下にビカビカとチラつく大惨事に……。

AI と一緒に原因を探り、計算式を修正して、
最終的には透明感のある美しい水脈を流すことができました。(^^;)

苦労した分、 完成した大森林の景色を眺めながら
滑空できたときの達成感はひとしおです。

今回の実験でも、 3D空間の表現、遠近法、ループ構造 など、
新しくて奥深い学びがたくさんありました。

■ 次回は、
7個目のお絵描きゲームです

ペンの線と図形を組み合わせて、

“影の形” を描いていくお絵描きゲーム。

影絵ならではの ほわっとした柔らかい雰囲気
そのまま画面に描ける、

ちょっと不思議な感覚のアート体験です。

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

次の実験はこちら  
  (影絵の世界を描く)「シルエット・スタジオ」



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

~このブログについて~

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

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


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

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

続きを読む