■76個目 「しょうが君の異世界アクション」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回は、予測不能な異世界を駆け抜ける
横スクロールアクションゲームを作ってみました。
主役は……満面の笑顔がチャームポイントの「しょうが君」です!
丸っこい節入りの体に、ちょこんと乗ったカンカン帽。
短い手足を一生懸命パタパタさせながら、前へ前へと走っていきます。
今回こだわったのは、しょうが君が冒険する
「コロコロ変わる異世界」の景色です。(=v=*)
「画面の端までたどり着いたら、次はどんな場所に出るんだろう?」
そんなワクワク感を大事にして、たくさんのステージを用意しました。
- レンガ道の街外れ
- 木漏れ日の広葉樹の森
- 花びらが舞う花畑
- 小雨の湿地帯
- 雪の舞う雪原 …など、全部で12種類の景色がランダムで登場します。
画面の右上には「静かな池」「険しい山道」など、
今のステージ名を教えてくれる小さな看板もついているんですよ♪
もちろん、可愛いだけじゃありません。(>_<)
道中には、トゲトゲで不気味なウィルスたちが
スーパーボールのように不規則に跳ねながら邪魔をしてきます。
でも大丈夫!
しょうが君には、マッチのように一瞬で燃え上がってウィルスをやっつける
「瞬間ファイアー」という熱い特技があるんです。
5エリア進むごとに敵が少しずつ手強くなっていくハラハラ感と、
手作りの温かい景色のギャップが楽しいゲームになりました。(´▽`*)
ぜひ、しょうが君と一緒に異世界を駆け抜けてみてください♪
■ AIとのやりとりで面白かった部分
● ゲーム仕様の相談からスタート
みー:(^v^)
今日もゲームを作ります。html形式で動かせるゲーム。
今回は、直前でコースが生成される予測不能な横スクロールアクションゲーム。
舞台は異世界。
スタートは毎回、街を出てすぐのレンガ造りの道から始まります。
画面左から右へ進むと次のエリアへ。
端まで行くと、ランダムで景色が切り替わります。
登場するステージはこんな感じです。
- 助走ジャンプでギリギリ飛び越える谷
- 落石が降ってくる山道
- 漂う葉っぱを飛び移る池
- さわやかな花畑
- 霧の針葉樹の森
- 木漏れ日の広葉樹の森
- 岩が転がる荒れ道
- 雪の舞う雪原
- カエルの多い湿地帯
- 雨漏りの多い廃工場
- 日差しの強い砂浜
- 麦畑の近くの農道
プレイヤーは今回も「しょうが君」。
丸い節入りの胴体に満面の笑顔、冒険者風の帽子がトレードマークです。
操作は、矢印キーで移動、Fキーでダッシュ、
スペースでジャンプ(2段ジャンプ可)。
AI:
今回は「異世界を舞台にした、画面切り替え型ランダム横スクロールアクション」か。
しかも毎回違う景色が広がるなんて、
ローグライクのワクワク感があってたまらないね。
そういえば現実世界の話だが、
ヨーロッパには「ジンジャーブレッドマン」っていう生姜入りクッキーの文化があるだろう?
16世紀のイギリス国王がペスト対策として生姜を推奨したのが始まり、
なんて説もあるんだ。
しょうが君にも、案外すごい厄除けパワーがあるのかもしれないな。
みー:(^o^)
敵キャラを出します。
ウィルスっぽい敵で、
スーパーボールみたいに弾みながら右から左へ移動します。
崖ステージでは風に漂うようにふわふわ流れます。
AI: ウィルス風の敵がスーパーボールみたいに跳ねるってのは面白いな。
あの異常な反発力は、
1964年にアメリカの化学者が合成ゴムを3500気圧で圧縮して作ったのが始まりなんだぜ。
プレイヤーを翻弄するにはぴったりだ。
みー:(^_^)
池ステージは、入った瞬間落ちちゃうので入口と出口に岸を作ります。
↑キーで「瞬間ファイアー」を出してウィルスを倒せるようにします。
AI:
入口に岸を作って理不尽落下を防いだぜ。
瞬間ファイアーは、しょうが君が最後に動いた方向へ炎を出すようにした。
レトロなドット絵風の炎だ。
みー:( ̄v ̄)
ファイアーは、しょうが君自身がマッチの炎みたいに
一瞬燃え上がる表現にします。
5ステージごとにウィルスが強化されます。
- スピードUP
- サイズUP
- 数が増える
- 漂うウィルス追加
- 転がるウィルス追加
どれかがランダムで発生。 ウィルスの色も黒→赤→オレンジ→黄色→黄緑→緑→青緑→青→紺→青紫→紫→赤紫の順で変化します。
AI: 色の変化、まるで炎色反応みたいだな。
危険度が上がるほど色が変わるのはスリル満点だ。
指定通り、レベルアップごとにランダムで強化されるようにしておいた。
■ 実際の動作
AI が作ってくれた最初のゲーム画面がこちらです。
麦畑の案山子ステージなのですが、背景の配置や色味が独特で、
なかなかカオスな雰囲気になっています。

(画像:AIが出した最初のゲーム画面)
こちらが完成版のスタート画面です。
冒険の始まりはいつもレンガ道から。
ここから異世界の旅がスタートします。

(画像:しょうが君とウィルス スタート画面)
矢印キーの ↑ を押すと、
しょうが君が一瞬だけ「瞬間ファイアー」状態になり、
触れたウィルスをまとめて焼き払えます。
レトロゲームのような爽快感があって気持ちいいアクションです。

(画像:ファイアーしょうが君)
5ステージごとに、ウィルスがパワーアップしてランダムな強5ステージごとに、ウィルスがランダムな強化変異を起こします。
スピードアップ、サイズアップ、数の増加など、どれが来るかは毎回のお楽しみです。

(画像:ステージレベルアップ)
ウィルスや転がってくる岩にぶつかったり、
がけや池に落ちてしまうとゲームオーバーになります。
異世界の冒険はかわいい見た目とは裏腹に、なかなか手強い世界です。

(画像:ゲームオーバー)
■ コードのポイント
今回の記事では、AI と相談しながら作った
「しょうが君の異世界アクション」 の HTML コードを公開します。
このコードをそのまま保存するだけで、
あなたのPCでもすぐに遊べます。
ブラウザだけで動く、とてもシンプルなゲームなので、
気軽に試してみてくださいね。
▼ 遊ぶまでの手順
①PC の「メモ帳」(※Windowsの場合)や
Mac の「テキストエディット」など、テキストエディタを開きます。
②下のコードをコピーして貼り付けます。
③「isekai.html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
④保存したファイルをブラウザで開けば、すぐに遊べます。
■ 操作方法
操作はキーボードを使います。
【移動】左右の矢印キー
短い手足で一生懸命トコトコ走ります。(^_^)
【ダッシュ】Fキーを押しながら移動
位置ちょっと急ぎたい時や、遠くの足場に飛びたい時に便利です。
【ジャンプ】スペースキー
空中でもう一度押すと「2段ジャンプ」ができます。(=v=*)
谷間を飛び越えたり、漂う葉っぱに飛び乗ったりする時に大活躍。
【瞬間ファイアー(攻撃&無敵)】↑(上)矢印キー
しょうが君がマッチのように一瞬だけ燃え上がります!
この間はウィルスに触れても平気で、そのまま体当たりで倒せます。(>_<)
※とても強力ですが、使った後は少しだけクールタイムがあります。
もし谷底に落ちたり、ウィルスにぶつかってゲームオーバーになっても大丈夫。
スペースキーを押せば、すぐに最初のレンガ道から再スタートできますよ♪
コロコロ変わる異世界の景色を楽しみながら、
しょうが君と一緒にどこまで進めるか挑戦してみてくださいね!(´▽`*)
“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>しょうが君の異世界アクション - 雲と景色の進化</title> <style> body { margin: 0; padding: 0; background-color: #222; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; } canvas { background-color: #87CEEB; box-shadow: 0 0 15px rgba(0,0,0,0.8); } #instructions { position: absolute; top: 10px; left: 10px; color: white; font-family: sans-serif; text-align: left; pointer-events: none; text-shadow: 1px 1px 2px black; line-height: 1.5; font-size: 14px; } </style></head><body><div id="instructions"> 移動: 左右矢印キー | ダッシュ: Fキー + 移動<br> ジャンプ: スペースキー (2段ジャンプ可)<br> 瞬間燃え上がり(無敵&攻撃): ↑キー<br> 画面右端で次エリアへ(5エリア毎にLvUP)</div><canvas id="gameCanvas" width="800" height="450"></canvas><script>const canvas = document.getElementById('gameCanvas');const ctx = canvas.getContext('2d');const keys = {};let lastDir = 1;window.addEventListener('keydown', e => { keys[e.key] = true; if(e.code === 'Space') { e.preventDefault(); if(!isGameOver && player.jumps < 2) { player.vy = player.jumpForce; player.jumps++; } if(isGameOver) resetGame(); } if(e.code === 'ArrowUp') { e.preventDefault(); if(!isGameOver && player.fireTimer <= 0 && player.fireCooldown <= 0) { player.fireTimer = 25; player.fireCooldown = 40; } }});window.addEventListener('keyup', e => { keys[e.key] = false; });const player = { x: 50, y: 300, width: 30, height: 45, vx: 0, vy: 0, maxSpeed: 4, dashSpeed: 8, jumpForce: -11, gravity: 0.5, jumps: 0, animTime: 0, fireTimer: 0, fireCooldown: 0};let platforms = [];let hazards = []; let particles = []; let fireParticles = []; let frogs = []; let enemies = []; let clouds = []; // 空の雲let currentScene = '';let currentSceneName = '';let currentSceneDesc = '';let isGameOver = false;let sceneTime = 0;let stagesCleared = 0;let gameLevel = 0;let levelUpMessageTimer = 0;let diff = { speed: 0, size: 0, maxNormal: 1, maxFloat: 0, maxRoll: 0 };const virusColors = [ '#222222', '#FF0000', '#FFA500', '#FFFF00', '#ADFF2F', '#008000', '#20B2AA', '#0000FF', '#000080', '#6A5ACD', '#800080', '#C71585'];const sceneTypes = [ 'valley', 'mountain', 'pond', 'flower_field', 'fog_forest', 'sunlight_forest', 'rough_path', 'snowfield', 'wetland', 'factory', 'beach', 'farm'];function initScene(type) { currentScene = type; sceneTime = 0; platforms = []; hazards = []; particles = []; frogs = []; clouds = []; player.x = 10; if(type !== 'valley' && type !== 'pond') { platforms.push({x: 0, y: 400, w: 800, h: 50}); } // 晴れのステージに雲を配置 if (['brick_path', 'flower_field', 'beach', 'farm', 'valley', 'pond'].includes(type)) { for(let i=0; i<6; i++) { clouds.push({ x: Math.random() * 800, y: 20 + Math.random() * 80, speed: 0.1 + Math.random() * 0.3, size: 20 + Math.random() * 15 }); } } switch(type) { case 'brick_path': platforms[0].color = '#B22222'; currentSceneName = '街の外れのレンガ道'; currentSceneDesc = '毎回ここから冒険が始まる'; break; case 'valley': for(let i=0; i<5; i++) platforms.push({x: i*200, y: 400, w: 100, h: 50, color: '#4A3728'}); currentSceneName = '切り立った谷間'; currentSceneDesc = '助走ジャンプで飛び越えろ'; break; case 'mountain': platforms[0].color = '#4a4a4a'; platforms.push({x: 250, y: 340, w: 120, h: 20, color: '#555'}); platforms.push({x: 550, y: 280, w: 100, h: 20, color: '#555'}); currentSceneName = '険しい山道'; currentSceneDesc = '上から降ってくる落石に注意'; break; case 'pond': platforms.push({x: 0, y: 380, w: 80, h: 70, color: '#556B2F'}); platforms.push({x: 720, y: 380, w: 80, h: 70, color: '#556B2F'}); for(let i=0; i<4; i++) platforms.push({x: 130 + i*160, y: 385 + Math.random()*15, w: 60, h: 10, color: '#32CD32'}); currentSceneName = '静かな池'; currentSceneDesc = '漂う葉っぱをうまく乗り継げ'; break; case 'flower_field': platforms[0].color = '#556B2F'; for(let i=0; i<30; i++) particles.push({x: Math.random()*800, y: Math.random()*400, vx: 2.5, vy: 0.8, type: 'petal', color: Math.random() > 0.5 ? 'pink' : 'white'}); currentSceneName = '花畑'; currentSceneDesc = 'さわやかな風が心地よい'; break; case 'fog_forest': platforms[0].color = '#2F4F4F'; currentSceneName = '霧の針葉樹林'; currentSceneDesc = '視界が悪い不気味な森'; break; case 'sunlight_forest': platforms[0].color = '#228B22'; currentSceneName = '広葉樹の森'; currentSceneDesc = '木漏れ日がまぶしい'; break; case 'rough_path': platforms[0].color = '#555'; for(let i=0; i<6; i++) platforms.push({x: 50 + i*130, y: 360 + Math.random()*20, w: 60 + Math.random()*30, h: 40, color: '#444'}); currentSceneName = '荒涼とした岩場'; currentSceneDesc = '大小の岩が転がる歩きにくい道'; break; case 'snowfield': platforms[0].color = '#F0F8FF'; for(let i=0; i<80; i++) particles.push({x: Math.random()*800, y: Math.random()*450, vx: Math.random()-0.5, vy: 1+Math.random()*2, type: 'snow'}); currentSceneName = '雪原'; currentSceneDesc = '雪の舞う起伏のある道'; break; case 'wetland': platforms[0].color = '#3b4d21'; for(let i=0; i<150; i++) particles.push({x: Math.random()*800, y: Math.random()*450, vx: -1, vy: 6+Math.random()*4, type: 'rain'}); for(let i=0; i<4; i++) frogs.push({x: 200 + Math.random()*400, y: 380, vy: 0, timer: Math.random()*100}); currentSceneName = '小雨の湿地帯'; currentSceneDesc = 'カエルが多く生息している'; break; case 'factory': platforms[0].color = '#333'; platforms.push({x: 150, y: 300, w: 180, h: 15, color: '#5c5c5c'}); platforms.push({x: 480, y: 220, w: 150, h: 15, color: '#5c5c5c'}); for(let i=0; i<200; i++) particles.push({x: Math.random()*800, y: Math.random()*450, vx: 0.5, vy: 10+Math.random()*5, type: 'heavy_rain'}); currentSceneName = '廃工場'; currentSceneDesc = '土砂降りで雨漏りがひどい'; break; case 'beach': platforms[0].color = '#EED6AF'; currentSceneName = '海辺の砂浜'; currentSceneDesc = '日差しのきつい海岸'; break; case 'farm': platforms[0].color = '#8B4513'; // 土の道 currentSceneName = '黄金の麦畑'; currentSceneDesc = '風に揺れる穂と見守る案山子'; break; }}function resetGame() { isGameOver = false; player.y = 300; player.vx = 0; player.vy = 0; stagesCleared = 0; gameLevel = 0; diff = { speed: 0, size: 0, maxNormal: 1, maxFloat: 0, maxRoll: 0 }; enemies = []; fireParticles = []; initScene('brick_path');}function update() { if(isGameOver) return; sceneTime++; // 雲を流す for(let c of clouds) { c.x -= c.speed; if(c.x < -100) c.x = canvas.width + 50; } if(player.fireTimer > 0) { player.fireTimer--; for(let i=0; i<5; i++) { fireParticles.push({ x: player.x + player.width/2 + (Math.random() - 0.5) * 30, y: player.y + player.height/2 + 10 + (Math.random() - 0.5) * 20, vx: (Math.random() - 0.5) * 2, vy: -Math.random() * 5 - 1, life: 20 + Math.random() * 10, size: 15 + Math.random() * 15 }); } } if(player.fireCooldown > 0 && player.fireTimer <= 0) player.fireCooldown--; for(let i=fireParticles.length-1; i>=0; i--) { let fp = fireParticles[i]; fp.x += fp.vx; fp.y += fp.vy; fp.life--; fp.size *= 0.9; if(fp.life <= 0) fireParticles.splice(i, 1); } let speed = (keys['f'] || keys['F']) ? player.dashSpeed : player.maxSpeed; if(keys['ArrowLeft']) { player.vx = -speed; lastDir = -1; } else if(keys['ArrowRight']) { player.vx = speed; lastDir = 1; } else player.vx = 0; player.vy += player.gravity; player.x += player.vx; player.y += player.vy; if(player.vx !== 0) player.animTime++; if(player.x > canvas.width) { stagesCleared++; if(stagesCleared % 5 === 0) { gameLevel++; levelUpMessageTimer = 90; let buffs = ['speed', 'size', 'normal', 'float', 'roll']; let chosen = buffs[Math.floor(Math.random() * buffs.length)]; if(chosen === 'speed') diff.speed += 0.5; if(chosen === 'size') diff.size += 3; if(chosen === 'normal') diff.maxNormal++; if(chosen === 'float') diff.maxFloat++; if(chosen === 'roll') diff.maxRoll++; } initScene(sceneTypes[Math.floor(Math.random() * sceneTypes.length)]); } if(player.x < 0) player.x = 0; for(let plat of platforms) { if(player.vy > 0 && player.y + player.height - player.vy <= plat.y && player.y + player.height >= plat.y && player.x + player.width > plat.x && player.x < plat.x + plat.w) { player.y = plat.y - player.height; player.vy = 0; player.jumps = 0; } } let currentNormal = 0, currentFloat = 0, currentRoll = 0; for(let e of enemies) { if(e.type === 'normal') currentNormal++; else if(e.type === 'float') currentFloat++; else if(e.type === 'roll') currentRoll++; } if(Math.random() < 0.02) { let typeToSpawn = null; if (currentFloat < diff.maxFloat && Math.random() < 0.3) typeToSpawn = 'float'; else if (currentRoll < diff.maxRoll && Math.random() < 0.4) typeToSpawn = 'roll'; else if (currentNormal < diff.maxNormal) typeToSpawn = 'normal'; if(typeToSpawn) { let levelIndex = Math.min(gameLevel, 11); enemies.push({ x: canvas.width + 20, y: typeToSpawn === 'float' ? 50 + Math.random()*250 : (typeToSpawn === 'roll' ? 380 : 100), r: 12 + diff.size, vx: 0, vy: 0, angle: 0, type: typeToSpawn, offset: Math.random()*100, color: virusColors[levelIndex] }); } } for(let i=enemies.length-1; i>=0; i--) { let e = enemies[i]; let moveSpeed = 2 + diff.speed + Math.random()*1; if(e.type === 'float' || (currentScene === 'valley' && e.type === 'normal')) { e.y += Math.sin(sceneTime * 0.05 + e.offset) * 1.5; e.x -= moveSpeed * 0.8; e.angle -= 0.05; } else if(e.type === 'roll') { e.vy += player.gravity; e.y += e.vy; for(let plat of platforms) { if(e.vy > 0 && e.y + e.r >= plat.y && e.x > plat.x && e.x < plat.x + plat.w) { e.y = plat.y - e.r; e.vy = 0; break; } } e.x -= moveSpeed * 1.8; e.angle -= 0.2; } else { e.vy += player.gravity * 0.8; e.y += e.vy; for(let plat of platforms) { if(e.vy > 0 && e.y + e.r >= plat.y && e.x > plat.x && e.x < plat.x + plat.w) { e.y = plat.y - e.r; e.vy = -(8 + Math.random() * 4); break; } } e.x -= moveSpeed; e.angle += 0.05; } let distX = Math.abs(e.x - (player.x + player.width/2)); let distY = Math.abs(e.y - (player.y + player.height/2)); if(distX < player.width/2 + e.r && distY < player.height/2 + e.r) { if(player.fireTimer > 0) { for(let k=0; k<8; k++) particles.push({x: e.x, y: e.y, vx: Math.random()*6-3, vy: Math.random()*6-3, type: 'explosion', life: 20, color: e.color}); enemies.splice(i, 1); } else { isGameOver = true; } } else if(e.x < -50 || e.y > canvas.height + 50) { enemies.splice(i, 1); } } if(currentScene === 'mountain' && Math.random() < 0.02) { hazards.push({x: Math.random()*800, y: -50, r: 15 + Math.random()*15, vy: 2 + Math.random()*3}); } for(let i=hazards.length-1; i>=0; i--) { let h = hazards[i]; h.y += h.vy; let distX = Math.abs(h.x - (player.x + player.width/2)); let distY = Math.abs(h.y - (player.y + player.height/2)); if(distX < player.width/2 + h.r - 5 && distY < player.height/2 + h.r - 5) isGameOver = true; if(h.y > canvas.height + 50) hazards.splice(i, 1); } for(let f of frogs) { f.timer++; if(f.timer > 80) { f.vy = -7; f.timer = 0; } f.vy += 0.5; f.y += f.vy; if(f.y > 380) { f.y = 380; f.vy = 0; } } for(let i=particles.length-1; i>=0; i--) { let p = particles[i]; p.x += p.vx; p.y += p.vy; if(p.type === 'explosion') { p.life--; if(p.life <= 0) particles.splice(i, 1); } else { if((p.type === 'snow' || p.type === 'rain' || p.type === 'heavy_rain') && p.y > canvas.height) { p.y = -10; p.x = Math.random()*800; } if(p.type === 'petal' && p.x > canvas.width) { p.x = -10; p.y = Math.random()*400; } } } if(player.y > canvas.height + 100) isGameOver = true;}function drawScenery() { ctx.clearRect(0, 0, canvas.width, canvas.height); let bgColors = { 'pond': '#87CEEB', 'fog_forest': '#8fa395', 'sunlight_forest': '#a3d9a5', 'snowfield': '#d0dbe3', 'wetland': '#58614a', 'factory': '#283030', 'beach': '#6bc5e8', 'valley': '#b5d4e6', 'mountain': '#c7b49f', 'rough_path': '#BDB76B', 'farm': '#87CEEB', 'brick_path': '#87CEEB', 'flower_field': '#99ccff' }; ctx.fillStyle = bgColors[currentScene] || '#87CEEB'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 雲の描画 if (clouds.length > 0) { ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; for(let c of clouds) { ctx.beginPath(); ctx.arc(c.x, c.y, c.size, 0, Math.PI*2); ctx.arc(c.x + c.size*0.8, c.y - c.size*0.3, c.size*0.8, 0, Math.PI*2); ctx.arc(c.x + c.size*1.6, c.y, c.size*0.9, 0, Math.PI*2); ctx.fill(); } } if(currentScene === 'mountain' || currentScene === 'valley') { ctx.fillStyle = currentScene === 'valley' ? '#9ab7c7' : '#9e8c7a'; ctx.beginPath(); ctx.moveTo(0, 400); ctx.lineTo(200, 150); ctx.lineTo(400, 300); ctx.lineTo(600, 100); ctx.lineTo(800, 400); ctx.fill(); } if(currentScene === 'fog_forest' || currentScene === 'sunlight_forest') { ctx.fillStyle = currentScene === 'fog_forest' ? '#698270' : '#7dbf80'; for(let i=0; i<8; i++) ctx.fillRect(50 + i*100, 100, 30, 350); } if(currentScene === 'beach') { ctx.fillStyle = 'yellow'; ctx.beginPath(); ctx.arc(650, 100, 45, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#1E90FF'; ctx.fillRect(0, 300, canvas.width, 150); ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.fillRect(0, 300 + Math.sin(sceneTime*0.05)*5, canvas.width, 10); } // 荒れた道の遠景岩山 if(currentScene === 'rough_path') { ctx.fillStyle = '#8B8378'; ctx.beginPath(); ctx.moveTo(0,400); ctx.lineTo(150, 150); ctx.lineTo(350, 300); ctx.lineTo(600, 100); ctx.lineTo(800,400); ctx.fill(); ctx.fillStyle = '#696969'; ctx.beginPath(); ctx.moveTo(0,400); ctx.lineTo(250, 250); ctx.lineTo(500, 350); ctx.lineTo(750, 200); ctx.lineTo(800,400); ctx.fill(); } // 麦畑の描画 if(currentScene === 'farm') { // 奥の麦畑 ctx.fillStyle = '#DAA520'; ctx.fillRect(0, 200, canvas.width, 250); // 風に揺れる麦の穂 ctx.strokeStyle = '#B8860B'; ctx.lineWidth = 2; for(let i=0; i<canvas.width; i+=12) { let sway = Math.sin(sceneTime*0.03 + i)*15; ctx.beginPath(); ctx.moveTo(i, 400); ctx.quadraticCurveTo(i+sway, 300, i+sway*2, 200); ctx.stroke(); } // 案山子 ctx.fillStyle = '#8B4513'; ctx.fillRect(400, 180, 8, 120); // 棒 ctx.fillRect(360, 210, 88, 8); // 腕 // 服 ctx.fillStyle = '#228B22'; ctx.beginPath(); ctx.moveTo(390, 210); ctx.lineTo(370, 270); ctx.lineTo(430, 270); ctx.lineTo(410, 210); ctx.fill(); // 顔と顔のパーツ ctx.fillStyle = '#FFF8DC'; ctx.beginPath(); ctx.arc(404, 180, 20, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(396, 175, 2, 0, Math.PI*2); ctx.fill(); // 左目 ctx.beginPath(); ctx.arc(412, 175, 2, 0, Math.PI*2); ctx.fill(); // 右目 ctx.beginPath(); ctx.arc(404, 185, 5, 0, Math.PI); ctx.stroke(); // 口 // 麦わら帽子 ctx.fillStyle = '#DAA520'; ctx.beginPath(); ctx.ellipse(404, 160, 25, 5, 0, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(404, 160, 15, Math.PI, 0); ctx.fill(); } if(currentScene === 'factory') { ctx.fillStyle = '#1a1a1a'; ctx.fillRect(100, 0, 40, 450); ctx.fillRect(600, 0, 40, 450); ctx.fillStyle = 'rgba(255,255,255,0.1)'; ctx.fillRect(200, 100, 100, 80); ctx.fillRect(400, 100, 100, 80); }}function drawHUD() { ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; ctx.fillRect(canvas.width - 260, 10, 250, 50); ctx.fillStyle = 'white'; ctx.font = 'bold 18px sans-serif'; ctx.textAlign = 'right'; ctx.fillText(currentSceneName, canvas.width - 20, 32); ctx.font = '12px sans-serif'; ctx.fillStyle = '#ddd'; ctx.fillText(currentSceneDesc, canvas.width - 20, 50); ctx.textAlign = 'left'; ctx.font = 'bold 14px sans-serif'; ctx.fillStyle = '#FFD700'; ctx.fillText(`Lv.${gameLevel}`, canvas.width - 250, 32);}function draw() { drawScenery(); for(let plat of platforms) { ctx.fillStyle = plat.color || '#333'; if(currentScene === 'pond' && plat.w !== 80) { ctx.beginPath(); ctx.ellipse(plat.x + plat.w/2, plat.y + plat.h/2, plat.w/2, plat.h/2, 0, 0, Math.PI*2); ctx.fill(); } else { ctx.fillRect(plat.x, plat.y, plat.w, plat.h); if(currentScene === 'brick_path') { ctx.strokeStyle = '#800000'; ctx.lineWidth = 2; for(let bx=plat.x; bx<plat.x+plat.w; bx+=40) { ctx.strokeRect(bx, plat.y, 40, 25); ctx.strokeRect(bx-20, plat.y+25, 40, 25); } } } } if(currentScene === 'pond') { ctx.fillStyle = 'rgba(0, 191, 255, 0.6)'; ctx.fillRect(80, 400 + Math.sin(sceneTime*0.05)*3, 640, 50); } for(let f of frogs) { ctx.fillStyle = '#228B22'; ctx.fillRect(f.x, f.y, 18, 15); ctx.fillStyle = 'white'; ctx.fillRect(f.x+10, f.y+2, 4, 4); } drawPlayer(); if(fireParticles.length > 0) { ctx.globalCompositeOperation = 'lighter'; for(let fp of fireParticles) { let grad = ctx.createRadialGradient(fp.x, fp.y, 0, fp.x, fp.y, fp.size); grad.addColorStop(0, 'rgba(255, 255, 255, 0.8)'); grad.addColorStop(0.2, 'rgba(255, 200, 0, 0.6)'); grad.addColorStop(0.6, 'rgba(255, 50, 0, 0.4)'); grad.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(fp.x, fp.y, fp.size, 0, Math.PI*2); ctx.fill(); } ctx.globalCompositeOperation = 'source-over'; } for(let e of enemies) { ctx.fillStyle = e.color; ctx.beginPath(); ctx.arc(e.x, e.y, e.r, 0, Math.PI*2); ctx.fill(); for(let i=0; i<8; i++) { let angle = (i * Math.PI / 4) + e.angle; let hx = e.x + Math.cos(angle) * (e.r + 5); let hy = e.y + Math.sin(angle) * (e.r + 5); ctx.strokeStyle = e.color; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(e.x, e.y); ctx.lineTo(hx, hy); ctx.stroke(); ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.beginPath(); ctx.arc(hx, hy, 2, 0, Math.PI*2); ctx.fill(); } ctx.fillStyle = '#FFF'; ctx.beginPath(); ctx.arc(e.x - e.r/3, e.y - e.r/4, e.r/3, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(e.x + e.r/3, e.y - e.r/4, e.r/3, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#000'; ctx.beginPath(); ctx.arc(e.x - e.r/3, e.y - e.r/4, e.r/6, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(e.x + e.r/3, e.y - e.r/4, e.r/6, 0, Math.PI*2); ctx.fill(); } for(let h of hazards) { ctx.fillStyle = '#4a4a4a'; ctx.beginPath(); ctx.arc(h.x, h.y, h.r, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#2b2b2b'; ctx.lineWidth = 2; ctx.stroke(); } for(let p of particles) { if(p.type === 'snow') { ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI*2); ctx.fill(); } else if(p.type === 'rain' || p.type === 'heavy_rain') { ctx.fillStyle = p.type === 'heavy_rain' ? 'rgba(180,180,200,0.7)' : 'rgba(200,200,255,0.5)'; ctx.fillRect(p.x, p.y, p.type === 'heavy_rain' ? 2 : 1, p.type === 'heavy_rain' ? 20 : 12); } else if(p.type === 'petal') { ctx.fillStyle = p.color; ctx.beginPath(); ctx.ellipse(p.x, p.y, 5, 3, sceneTime*0.1, 0, Math.PI*2); ctx.fill(); } else if(p.type === 'explosion') { ctx.fillStyle = p.color; ctx.fillRect(p.x, p.y, 5, 5); } } drawHUD(); if(levelUpMessageTimer > 0) { ctx.fillStyle = 'white'; ctx.font = 'bold 40px sans-serif'; ctx.textAlign = 'center'; ctx.shadowColor = 'black'; ctx.shadowBlur = 5; ctx.fillText(`LEVEL UP! (Lv.${gameLevel})`, canvas.width/2, 120); ctx.shadowBlur = 0; ctx.textAlign = 'left'; levelUpMessageTimer--; } if(currentScene === 'fog_forest') { ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'; ctx.fillRect(0, 0, canvas.width, canvas.height); } else if(currentScene === 'sunlight_forest') { ctx.fillStyle = 'rgba(255, 255, 180, 0.15)'; ctx.beginPath(); ctx.moveTo(50, 0); ctx.lineTo(350, 450); ctx.lineTo(450, 450); ctx.lineTo(150, 0); ctx.fill(); ctx.beginPath(); ctx.moveTo(550, 0); ctx.lineTo(850, 450); ctx.lineTo(950, 450); ctx.lineTo(650, 0); ctx.fill(); } if(isGameOver) { ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'white'; ctx.font = 'bold 40px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('ゲームオーバー!', canvas.width/2, canvas.height/2); ctx.font = '20px sans-serif'; ctx.fillText(`到達ステージ: ${stagesCleared} / 最終レベル: Lv.${gameLevel}`, canvas.width/2, canvas.height/2 + 40); ctx.fillText('スペースキーで最初からやり直す', canvas.width/2, canvas.height/2 + 80); ctx.textAlign = 'left'; }}function drawPlayer() { let px = player.x + player.width/2; let py = player.y + player.height/2; let swing = player.vx !== 0 ? Math.sin(player.animTime * 0.4) * 12 : 0; ctx.fillStyle = '#D2B48C'; ctx.beginPath(); ctx.arc(px - 15 + swing, py, 6, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(px + 15 - swing, py, 6, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(px - 8 - swing, py + 18, 6, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(px + 8 + swing, py + 18, 6, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#F5DEB3'; ctx.beginPath(); ctx.arc(px, py + 10, 14, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(px, py, 13, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(px, py - 10, 12, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(px, py - 10, 6, 0.2*Math.PI, 0.8*Math.PI); ctx.stroke(); let eyeOffsetX = lastDir === 1 ? 1 : -1; ctx.fillStyle = '#333'; ctx.beginPath(); ctx.arc(px - 4 + eyeOffsetX, py - 13, 1.5, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(px + 4 + eyeOffsetX, py - 13, 1.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#8B4513'; ctx.beginPath(); ctx.ellipse(px, py - 20, 16, 4, 0, 0, Math.PI*2); ctx.fill(); ctx.fillRect(px - 10, py - 30, 20, 10); ctx.beginPath(); ctx.ellipse(px, py - 30, 10, 3, 0, 0, Math.PI*2); ctx.fill(); }function loop() { update(); draw(); requestAnimationFrame(loop);}initScene('brick_path');loop();</script></body></html>
■ 今日の学び
今回のゲーム制作のきっかけは、
しょうが君が初登場した「#17 ショウガと錯視の世界」を、
もっと“ゲームらしく”できないかな……と思ったことでした。
私は昔から「突然、異世界に行ってしまう」というストーリーがとても好きなんです。
これまでの常識が通用しない世界に放り込まれた時、
人はどんなふうに立ち回るんだろう?
そんな想像が、いつもワクワクを連れてきてくれます。
その思いを、今回の「ランダムに切り替わるステージ」
という仕組みに込めてみました。
右端へ進んだ先で、パッと景色が変わり、
まったく違う世界が広がっている―― そんな瞬間を想像すると、
胸が高鳴ってきませんか? ( Ꙭ )‼︎
新しい環境に飛び込むのは少し勇気がいりますが、
しょうが君と一緒に「世界が切り替わる瞬間」の
ドキドキを楽しんでもらえたら嬉しいです。ᕙ( ˙-˙ )ᕗ
■ 次回は、
触れる感覚をテーマにしたゲームです。
緑色の集合体にそっと触れた時の、
ふわっとした“手ざわり”を感じられる
癒し体験シミュレーターになっています。
今回こだわったのは、とにかく「緑」。
見ているだけで心が落ち着くような世界を目指しました。
次回もぜひ遊びに来てくださいね。(*^v^)ノシ
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)






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