実験#56:吹き飛ばしと重さ(しょうが君のエアダスター)

■56個目「しょうが君のエアダスター

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

今回は、ちょっと変わった「お掃除」をテーマにしたゲームを作ってみました。(^_^) 題して、「エアダスター吹き飛ばし検定」です!

#52の吹き飛ばしをテーマにしたシミュレーターから、
イメージにつながったゲームになります。

このゲームの主役は、冒険者風の帽子をかぶった、
いつも元気な「しょうが君」です。

今回こだわったのは、風の強さと、
落ち葉が「ふぁさふぁさ」と舞うリアリティです。(=v=*)

「もしも、絶対に動かしてはいけない置物があるお庭で、強風のエアダスターを使ったらどうなるんだろう?」

そんな、ちょっとした緊張感と遊び心から生まれたゲームになっています。

舞台は、立派な丸い石や灯籠が置かれた、風情あるお庭。
そこへヒラヒラと落ち葉が舞い降りてきました。

しょうが君の任務は、エアダスターの風を巧みに操って、
お庭の景観を崩さずに落ち葉「だけ」を画面の外へ吹き飛ばすこと。

でも、風を強く当てすぎると、重たい石がゴロゴロと転がってしまったり、
灯籠がズレてしまったりして、
スコアが容赦なく減点されてしまいます……!

「置物はそっとそのまま、落ち葉だけをふぁさふぁさと優しく舞い飛ばす」。

そんな気持ちで風を操りながら、
ぜひ100点満点の「1級清掃員」を目指してみてくださいね!(^-^*)

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

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

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

今回のゲームは、エアダスターによる吹き飛ばし検定。 置物がいくつも置かれた庭園で、落ち葉を吹き飛ばしてきれいにするゲームです。

置物を倒したりずらしたりしないよう、風を当てる方向や距離を気にしながら、 一定の強さでエアーを吹き出す機械を持ったしょうが君が、
落ち葉を画面外へ飛ばしていきます。

100点から引かれていく減点方式で、
制限時間内の落ち葉の残り数と、置物の移動・倒れが減点対象になります。

AI: エアダスターによる落ち葉の吹き飛ばし検定!
その着眼点、最高に面白いです。

「置物を倒してはいけない」という物理演算的な緊張感と、
「しょうが君が一定の風力で庭を掃除している」というシュールな光景が見事に融合していますね。

100点満点からの減点方式も、まさに検定試験らしいプレッシャーがあって、 プレイヤーの熱中度を高めてくれそうです。

みー:(^_^*) 「マウスをクリックしたまま引っ張って風の向きを細かく決める」ような操作に変更してみましょう。 それと、リトライ機能もつけたいです。

しょうが君は、長丸い胴体の上部に口を開けた満面の笑顔。
頭には冒険者風の帽子をかぶっていて、

手足も楕円形のしょうがの手足。 胴体と手足には、
しょうがの表面のうっすら線がある感じです。

ここからどんどんクオリティを高めていきましょう!

AI: しょうが君のビジュアルデザイン、最高にキュートで冒険心をくすぐりますね! その詳細なイメージ、バッチリ共有されました。

さっそく、リトライ機能を搭載し、 しょうが君をテキスト(「姜」)から、ご要望通りのイラスト描画へとアップグレードしたコードを作成しました。

みー:(*^v^)落ち葉の吹き飛ぶ様子をもう少し再現できるかな。 ふぁさふぁさって感じで舞いながら飛ぶ様子を出せるかな。

AI: 「ふぁさふぁさ」と舞う落ち葉、いいですね!
物理的なリアリティーと可愛らしさが共存する、
このゲームの肝となる部分です。

現状の「単純な円が直線的に飛ぶ」動きから、
「落ち葉が不規則に回転し、揺らぎながら舞う」動きへと大幅にアップデートしました。

これで、置物の隙間を縫って落ち葉だけを優しく舞わせるという、
高等テクニックが求められるようになり、ゲーム性がさらに深まりましたね。

みー:(つx<`) いい感じの葉っぱになったけど、
スタート直後に全部画面下方向へ落ちていったよ。

AI: あちゃー、やっちまいました!完全に私のミスです。

「葉っぱがフワフワと落ちる」様子を表現しようとして、
画面の下方向(Y軸のプラス方向)に常に重力をかけてしまったのが原因です。

今回の庭園は「上から見下ろした視点(トップダウン)」なので、
これだと庭全体が急な斜面になっていて、
何もしなくても葉っぱが滑り落ちていく状態になっていました。

そこで、重力を削除し、 代わりに「風で舞い上がっている間だけ、
空気抵抗で軌道が揺らぎ、回転する」仕組みに修正しました。

これで、勝手に画面外へ逃げていくことなく、
風を当てた時だけフワッと舞い散るようになります!

■ 実際の動作

こちらが、AIが最初に作ってくれたゲーム画面です。
シンプルな表現で、しょうが君(生姜の文字)を中心に円状に吹き飛ばす仕組みになっています。
(画像:最初にAIが作ってくれたゲームの画像)

仕上がりのゲーム画面です。
しょうが君が登場し、葉っぱや置物・石が配置されて、
庭の雰囲気がしっかり出ています。

(画像:完成版のスタート画面)


しょうが君の位置と、風を吹かせる方向・強さを調整しながら、
落ち葉だけを画面の外へ飛ばしていきます。

(画像:プレイ中の画面)


石や置物は葉っぱほど風の影響は受けにくいのですが、
大きく動かしてしまうと暗い色に変化し、大きく減点となります。

(画像:置物が動いたときの画面)

葉っぱの枚数と、置物の減点の有無によって、
最終的なスコアが決まります。

(画像:検定終了の画面)

■ コードのポイント

今回の記事では、AI と相談しながら作った「しょうが君のエアダスター」の HTML コードを公開します。

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

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

▼ 遊ぶまでの手順

1. PC の「メモ帳」(※Windowsの場合)や、  
 Mac の「テキストエディット」など、
 テキストエディタを開きます。
2. 下のコードをコピーして貼り付けます。
3. 「fukitobasi.html」など、好きな名前で保存します。
 (※拡張子は .html にしてください)
4. 保存したファイルをブラウザで開けば、すぐに遊べます。

■ 遊び方と操作方法

続いて、ゲームの操作方法をご紹介しますね。(^^*)
今回は 「マウスを引っ張って調整する」 のがポイントです!

① 風を吹き出す

お庭の好きなところをクリックすると、
そこにしょうが君がピタッとスタンバイします。

そのままマウスをドラッグ(クリックしたまま移動)すると、
口を大きく開けて一生懸命に風を吹き出します!(^▽^)=3 🍃

② 風の向きと強さを調整する

クリックした最初の位置から、 マウスを引っ張った方向に風が吹き抜けます。

マウスを遠くに離すほど、風が「強く・遠くまで」届くようになります。
画面に出る白い点線と、水色の扇形を目安にして、
落ち葉を狙ってみてくださいね。

③ 高得点のコツ(検定のルール)

制限時間は45秒。最初は100点満点からのスタートです。

時間切れのときに落ち葉が残っていると減点になりますが、
一番気をつけたいのがお庭の 置物 です。(>_<)

強い風を当てて丸い石がゴロンと転がったり、
灯籠がズレたりすると、容赦なく大きな減点が……!

置物の近くは、マウスを少しだけ引っ張って「優しく舞わせるそよ風」に。
何もない場所は、大きく引っ張って「一気に飛ばす強風」に。

風の強弱をうまく使い分けて、
ふぁさふぁさ~っと綺麗なお掃除を楽しんでみてください。(*^_^)ノ

下のコードをメモ帳などにコピーして、
「〇〇.html」という名前で保存すれば、すぐにブラウザで遊べますよ♪

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>落ち葉吹き飛ばし検定 - しょうが君の冒険(斜面修正版)</title>
<style>
body {
text-align: center;
background-color: #f4f1ea;
font-family: 'Helvetica Neue', 'Arial', 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', sans-serif;
user-select: none;
}
h2 { color: #5d4037; margin-bottom: 5px; }
#ui-board {
width: 800px;
margin: 10px auto;
display: flex;
justify-content: space-between;
align-items: center;
background-color: rgba(255, 255, 255, 0.8);
padding: 10px 20px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.ui-item { font-size: 24px; font-weight: bold; color: #333; }
#score { transition: color 0.2s; }
.penalty { color: #d9534f !important; animation: blink 0.5s step-end 2; }
@keyframes blink { 50% { opacity: 0; } }
#canvas-container {
position: relative;
width: 800px;
height: 500px;
margin: 0 auto;
}
canvas {
background-color: #a3ca9d;
background-image: repeating-linear-gradient(45deg, #a3ca9d 0px, #a3ca9d 10px, #9cc296 10px, #9cc296 20px);
border: 8px solid #8d6e63;
border-radius: 15px;
cursor: crosshair;
box-shadow: 0 6px 12px rgba(0,0,0,0.3);
}
#overlay {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 15px;
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
z-index: 10;
}
#result-title { font-size: 48px; margin-bottom: 10px; }
#result-score { font-size: 36px; margin-bottom: 5px; }
#result-detail { font-size: 20px; color: #ffcccc; margin-bottom: 30px; }
#retry-button {
padding: 15px 40px;
font-size: 24px;
font-weight: bold;
color: #fff;
background-color: #f0ad4e;
border: none;
border-radius: 30px;
cursor: pointer;
box-shadow: 0 4px 0 #d58512;
transition: all 0.1s;
}
#retry-button:hover { background-color: #ec971f; }
#retry-button:active { transform: translateY(3px); box-shadow: 0 1px 0 #d58512; }
</style>
</head>
<body>
<h2>🍃 エアダスター吹き飛ばし検定 🍃</h2>
<div id="ui-board">
<div class="ui-item">スコア: <span id="score">100</span> / 100</div>
<div class="ui-item">残り時間: <span id="time">45</span></div>
</div>
<div id="canvas-container">
<canvas id="gameCanvas" width="800" height="500"></canvas>
<div id="overlay">
<div id="result-title">検定終了!</div>
<div id="result-score">最終スコア: --点</div>
<div id="result-detail">--</div>
<button id="retry-button">再検定を受ける</button>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const timeEl = document.getElementById('time');
const overlayEl = document.getElementById('overlay');
const resScoreEl = document.getElementById('result-score');
const resDetailEl = document.getElementById('result-detail');
const retryBtn = document.getElementById('retry-button');
const GAME_WIDTH = canvas.width;
const GAME_HEIGHT = canvas.height;
const GINGER_COLOR = '#f2e6c9';
const GINGER_LINE_COLOR = '#d2b48c';
// 重力(gravity)を削除し、摩擦と空気抵抗だけで制御
const LEAF_TYPES = [
{
color: '#DAA520', size: 8, friction: 0.94,
draw: function(ctx, size) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(-size, -size/2);
ctx.quadraticCurveTo(0, -size*1.2, size, -size/2);
ctx.closePath();
ctx.fill();
}
},
{
color: '#CD853F', size: 9, friction: 0.92,
draw: function(ctx, size) {
ctx.beginPath();
ctx.moveTo(0, -size);
ctx.lineTo(size/3, -size/3);
ctx.lineTo(size, -size/2);
ctx.lineTo(size/2, size/3);
ctx.lineTo(size, size);
ctx.lineTo(0, size/2);
ctx.lineTo(-size, size);
ctx.lineTo(-size/2, size/3);
ctx.lineTo(-size, -size/2);
ctx.lineTo(-size/3, -size/3);
ctx.closePath();
ctx.fill();
}
},
{
color: '#8B4513', size: 6, friction: 0.96,
draw: function(ctx, size) {
ctx.beginPath();
ctx.ellipse(0, 0, size, size/2, 0, 0, Math.PI * 2);
ctx.fill();
}
}
];
const ORNAMENT_TYPES = {
stone: { radius: 22, color: '#95a5a6', edgeColor: '#7f8c8d', friction: 0.92, penalty: 15, moveThreshold: 15 },
lantern: { width: 35, height: 50, color: '#a1887f', edgeColor: '#5d4037', friction: 0.60, penalty: 30, moveThreshold: 8 }
};
let score = 100;
let timeLeft = 45;
let isBlowing = false;
let mouseX = GAME_WIDTH / 2;
let mouseY = GAME_HEIGHT / 2;
let blowStartX = 0;
let blowStartY = 0;
let gameActive = false;
let penaltyActive = false;
let leaves = [];
let ornaments = [];
let gameTimer;
function initGame() {
score = 100;
timeLeft = 45;
isBlowing = false;
gameActive = true;
leaves = [];
ornaments = [];
penaltyActive = false;
scoreEl.classList.remove('penalty');
scoreEl.innerText = score;
timeEl.innerText = timeLeft;
overlayEl.style.display = 'none';
for(let i = 0; i < 60; i++) {
const type = LEAF_TYPES[Math.floor(Math.random() * LEAF_TYPES.length)];
leaves.push({
x: Math.random() * (GAME_WIDTH - 100) + 50,
y: Math.random() * (GAME_HEIGHT - 100) + 50,
vx: 0, vy: 0,
angle: Math.random() * Math.PI * 2,
va: 0,
...type,
active: true
});
}
for(let i = 0; i < 3; i++) createOrnament('stone');
for(let i = 0; i < 2; i++) createOrnament('lantern');
clearInterval(gameTimer);
gameTimer = setInterval(() => {
if(!gameActive) return;
timeLeft--;
timeEl.innerText = timeLeft;
if(timeLeft <= 0) endGame();
}, 1000);
}
function createOrnament(typeKey) {
const type = ORNAMENT_TYPES[typeKey];
let ox, oy;
let padding = 100;
do {
ox = Math.random() * (GAME_WIDTH - padding * 2) + padding;
oy = Math.random() * (GAME_HEIGHT - padding * 2) + padding;
} while (ornaments.some(o => Math.hypot(o.x - ox, o.y - oy) < 70));
ornaments.push({
type: typeKey,
x: ox, y: oy,
originX: ox, originY: oy,
vx: 0, vy: 0,
...type,
moved: false
});
}
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
});
canvas.addEventListener('mousedown', (e) => {
if(!gameActive) return;
isBlowing = true;
blowStartX = mouseX;
blowStartY = mouseY;
});
window.addEventListener('mouseup', () => isBlowing = false);
retryBtn.addEventListener('click', initGame);
function endGame() {
gameActive = false;
isBlowing = false;
clearInterval(gameTimer);
let remainingLeaves = leaves.filter(l => l.active).length;
let leafPenalty = remainingLeaves * 4;
score -= leafPenalty;
if(score < 0) score = 0;
scoreEl.innerText = score;
resScoreEl.innerText = `最終スコア: ${score}点`;
let movedCount = ornaments.filter(o => o.moved).length;
let msg = `庭園の美しさ: ${60 - remainingLeaves}/60枚 清掃完了\n`;
msg += `置物の移動: ${movedCount}個 (${movedCount * -ORNAMENT_TYPES.lantern.penalty}点相当)`;
resDetailEl.innerText = msg;
overlayEl.style.display = 'flex';
}
function triggerPenalty() {
if(penaltyActive) return;
penaltyActive = true;
scoreEl.classList.add('penalty');
setTimeout(() => {
scoreEl.classList.remove('penalty');
penaltyActive = false;
}, 1000);
}
function drawGingerKun(x, y, blowing) {
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = GINGER_COLOR;
ctx.strokeStyle = GINGER_LINE_COLOR;
ctx.lineWidth = 1;
const limbWidth = 6;
const limbHeight = 12;
const limbs = [
{ dx: -15, dy: 15, rot: 0.3 },
{ dx: 15, dy: 15, rot: -0.3 },
{ dx: -10, dy: 28, rot: 0.1 },
{ dx: 10, dy: 28, rot: -0.1 }
];
limbs.forEach(limb => {
ctx.save();
ctx.translate(limb.dx, limb.dy);
ctx.rotate(limb.rot);
if(blowing) ctx.rotate(Math.sin(Date.now() / 50) * 0.1);
ctx.beginPath();
ctx.ellipse(0, 0, limbWidth, limbHeight, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = 'rgba(210, 180, 140, 0.5)';
ctx.beginPath(); ctx.moveTo(-2, -4); ctx.lineTo(2, 2); ctx.stroke();
ctx.restore();
});
ctx.fillStyle = GINGER_COLOR;
ctx.strokeStyle = GINGER_LINE_COLOR;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(0, 5, 20, 30, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.strokeStyle = 'rgba(210, 180, 140, 0.6)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(-10, -10); ctx.lineTo(5, -15); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-15, 5); ctx.lineTo(-5, 10); ctx.stroke();
ctx.beginPath(); ctx.moveTo(5, 15); ctx.lineTo(15, 10); ctx.stroke();
ctx.fillStyle = '#333';
ctx.beginPath(); ctx.arc(-8, -10, 2.5, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(8, -10, 2.5, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = '#333';
ctx.lineWidth = 2;
ctx.fillStyle = '#8e0000';
ctx.beginPath();
if(blowing) {
ctx.ellipse(0, 2, 8, 10, 0, 0, Math.PI);
ctx.lineTo(8, 2);
ctx.fill();
ctx.stroke();
} else {
ctx.arc(0, 0, 10, 0.2 * Math.PI, 0.8 * Math.PI);
ctx.stroke();
}
ctx.fillStyle = '#8d6e63';
ctx.strokeStyle = '#5d4037';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.ellipse(0, -28, 28, 10, 0, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-15, -28);
ctx.lineTo(-12, -45);
ctx.quadraticCurveTo(0, -50, 12, -45);
ctx.lineTo(15, -28);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.strokeStyle = '#f0ad4e';
ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(-13, -32); ctx.lineTo(13, -32); ctx.stroke();
ctx.restore();
}
function loop() {
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
let blowAngle = 0;
let powerRatio = 0;
let maxReach = 0;
if(isBlowing && gameActive) {
let dx = mouseX - blowStartX;
let dy = mouseY - blowStartY;
let dragDist = Math.hypot(dx, dy);
if (dragDist > 10) {
blowAngle = Math.atan2(dy, dx);
powerRatio = Math.min(dragDist, 200) / 200;
maxReach = 350 * powerRatio;
}
}
// --- 落ち葉の計算と描画 ---
leaves.forEach(leaf => {
if(!leaf.active) return;
// 風による力
if(isBlowing && maxReach > 0) {
let objDx = leaf.x - blowStartX;
let objDy = leaf.y - blowStartY;
let objDist = Math.hypot(objDx, objDy);
if (objDist > 0 && objDist < maxReach) {
let objAngle = Math.atan2(objDy, objDx);
let angleDiff = objAngle - blowAngle;
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
if (Math.abs(angleDiff) < 0.45) {
let turbulence = (Math.random() - 0.5) * 0.4;
let force = (30 * powerRatio) / Math.max(objDist, 30);
// 風で押し出される力+乱流
leaf.vx += Math.cos(blowAngle + turbulence) * force;
leaf.vy += Math.sin(blowAngle + turbulence) * force;
// 風を受けると強く回転する
leaf.va += (Math.random() - 0.5) * 0.8 * powerRatio;
}
}
}
// 動いている時だけ「ふぁさふぁさ(横揺れ)」する処理を追加
let speed = Math.hypot(leaf.vx, leaf.vy);
if(speed > 0.5) {
// スピードに応じて、進行方向とは少し違う方向へランダムにブレる
let wobble = (Math.random() - 0.5) * speed * 0.2;
leaf.x += Math.cos(leaf.angle) * wobble;
leaf.y += Math.sin(leaf.angle) * wobble;
// 空中を舞っている間は少しずつ回転を続ける
leaf.va += (Math.random() - 0.5) * 0.05;
}
// 摩擦と移動(重力による下方向の強制移動は削除)
leaf.vx *= leaf.friction;
leaf.vy *= leaf.friction;
leaf.x += leaf.vx;
leaf.y += leaf.vy;
// 回転の摩擦と適用
leaf.va *= 0.95;
leaf.angle += leaf.va;
// 画面外判定
if(leaf.x < -30 || leaf.x > GAME_WIDTH + 30 || leaf.y < -30 || leaf.y > GAME_HEIGHT + 30) {
leaf.active = false;
}
if(leaf.active) {
ctx.save();
ctx.translate(leaf.x, leaf.y);
ctx.rotate(leaf.angle);
ctx.fillStyle = leaf.color;
leaf.draw(ctx, leaf.size);
ctx.restore();
}
});
// --- 置物の計算と描画 ---
ornaments.forEach(o => {
if(isBlowing && maxReach > 0) {
let objDx = o.type === 'lantern' ? (o.x + o.width/2) - blowStartX : o.x - blowStartX;
let objDy = o.type === 'lantern' ? (o.y + o.height/2) - blowStartY : o.y - blowStartY;
let objDist = Math.hypot(objDx, objDy);
if (objDist > 0 && objDist < maxReach) {
let objAngle = Math.atan2(objDy, objDx);
let angleDiff = objAngle - blowAngle;
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
if (Math.abs(angleDiff) < 0.4) {
let force = (8 * powerRatio) / Math.max(objDist, 40);
o.vx += Math.cos(blowAngle) * force;
o.vy += Math.sin(blowAngle) * force;
}
}
}
o.vx *= o.friction;
o.vy *= o.friction;
o.x += o.vx;
o.y += o.vy;
if(!o.moved && gameActive) {
let moveDist = Math.hypot(o.x - o.originX, o.y - o.originY);
if(moveDist > o.moveThreshold) {
o.moved = true;
score -= o.penalty;
if(score < 0) score = 0;
scoreEl.innerText = score;
triggerPenalty();
}
}
ctx.save();
ctx.lineWidth = 3;
if (o.type === 'stone') {
ctx.beginPath();
ctx.arc(o.x, o.y, o.radius, 0, Math.PI * 2);
ctx.fillStyle = o.moved ? '#6d4c41' : o.color;
ctx.fill();
ctx.strokeStyle = o.edgeColor;
ctx.stroke();
ctx.strokeStyle = 'rgba(0,0,0,0.1)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(o.x - 5, o.y - 5, 8, 0.5, 2.5); ctx.stroke();
} else if (o.type === 'lantern') {
ctx.translate(o.x, o.y);
ctx.fillStyle = o.moved ? '#5d4037' : o.color;
ctx.strokeStyle = o.edgeColor;
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(5, 5, o.width, o.height);
ctx.fillStyle = o.moved ? '#5d4037' : o.color;
ctx.fillRect(0, 0, o.width, o.height);
ctx.strokeRect(0, 0, o.width, o.height);
ctx.fillStyle = o.moved ? '#444' : '#fff9c4';
ctx.fillRect(7, 8, o.width - 14, 15);
ctx.strokeRect(7, 8, o.width - 14, 15);
ctx.fillStyle = o.edgeColor;
ctx.beginPath();
ctx.moveTo(-5, 0); ctx.lineTo(o.width + 5, 0);
ctx.lineTo(o.width/2, -15); ctx.closePath();
ctx.fill(); ctx.stroke();
}
ctx.restore();
});
if(isBlowing && maxReach > 0 && gameActive) {
ctx.beginPath();
ctx.moveTo(blowStartX, blowStartY);
ctx.lineTo(mouseX, mouseY);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
ctx.lineWidth = 3;
ctx.setLineDash([8, 8]);
ctx.stroke();
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(blowStartX, blowStartY);
ctx.arc(blowStartX, blowStartY, maxReach, blowAngle - 0.45, blowAngle + 0.45);
ctx.lineTo(blowStartX, blowStartY);
let gradient = ctx.createRadialGradient(blowStartX, blowStartY, 0, blowStartX, blowStartY, maxReach);
gradient.addColorStop(0, 'rgba(230, 245, 255, 0.5)');
gradient.addColorStop(1, 'rgba(230, 245, 255, 0.0)');
ctx.fillStyle = gradient;
ctx.fill();
}
let shogaX = isBlowing && gameActive ? blowStartX : mouseX;
let shogaY = isBlowing && gameActive ? blowStartY : mouseY;
shogaX = Math.max(30, Math.min(GAME_WIDTH - 30, shogaX));
shogaY = Math.max(50, Math.min(GAME_HEIGHT - 30, shogaY));
drawGingerKun(shogaX, shogaY, isBlowing && gameActive);
requestAnimationFrame(loop);
}
initGame();
loop();
</script>
</body>
</html>


■ 今日の学び

今回のゲーム作りで、ふと気づいたことがあります。(^^*)

今回の落ち葉の動きも「ふぁさふぁさって感じで舞うように」と AIに相談してプログラムを調整してもらったのですが、

以前作った「#10 オノマトペ2角取り」というゲームがありましたが、
思えば、私はゲームのイメージを伝えるのに、
ずっとオノマトペ(擬音語・擬態語)を多用しているんです。

「ツツーイ」とか「ふぁさふぁさ」といった感覚的な言葉を投げかけても、
AIはしっかりとこちらの意図を察してくれて、
頭の中で描いている雰囲気に一生懸命近づけようとしてくれます。(^_^)

日本語は特にオノマトペが豊かな言語だと言われていますよね。
もしかすると、こうやって感覚的な音の響きでAIとイメージを共有しながらゲーム作りを進めていくのって、

ほかの国の言葉とは少し違った、
日本語ならではの面白いAIとのコミュニケーションの形なのかもしれないな……
なんて、ふと思いました。(=v=*)

難しい専門用語や数式がわからなくても、
「こんな感じで!」と音で伝えれば、AIがそれを汲み取って形にしてくれる。
言葉のニュアンスまで共有できる、本当に心強い相棒です♪

■ 次回は、ボウリング風ホラーゲームです。

次回は、実験 #13「背中越しのA〜Zタイピング(THE BACK)」以来となる、 久しぶりのホラー系ゲームです。

前回のホラーから引き継いだ「怖い何か」と対峙するゲーム。 今回も、ちょっと不思議で、ちょっと怖い世界観になっています。

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

▶ 次の実験はこちら(ボウリングと悪夢) 「THE VOID STRIKE」



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

~このブログについて~

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

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


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

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

続きを読む