■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=*)
長く飛べば飛ぶほど、飛翔距離(スコア)がどんどん伸びていきます。
鷲のおちゃめな独り言にクスッとしつつ、
ぜひ大記録を目指してみてくださいね!(´▽`*)
それでは、壮大な大森林の空の旅へいってらっしゃいませ♪
“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。”
▼ここからコード▼(クリックで開く)
<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^*)






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