■51個目「スライス乾燥シミュレーター」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回は、ちょっと本格的な「加工所の職人」になれるシミュレーターを作ってみました。 主役は……なんと 3枚刃の大型スライサー と 温風乾燥機 です!
農業用コンテナにたっぷり入った「レモン」「グリーンレモン」「トマト」を掴んで、 上部の投入口からコロコロッと放り込むところからスタートします。
●こだわりは “スライサーの物理”
「刃が高速回転して野菜を切る時って、どんな抵抗感があるんだろう?」 そんな疑問から、今回は スライサーの手応え をしっかり再現してみました。(=v=*)
スライスの厚さは 1mm〜5mm の間で自由に調整できます。 厚さを変えると、切れる枚数が変わるだけでなく、 刃に当たった時の「ザザザザッ!」という抵抗の重さや、 落ちてくる輪切りの見た目・重さまで変化します。 厚さ調整だけでも、ちょっとした職人の気分が味わえるんですよ。
●乾燥工程は “じっくり眺める楽しさ”
器がいっぱいになったら、次は巨大な 温風乾燥機 の出番です!(・v・*)
棚に並べたスライスに下から温風を当てて、 じっくり水分を飛ばしていきます。 分厚いスライスほど乾くのに時間がかかったり、 水分が抜けてだんだんシワシワに縮んで、 色が淡く変わっていく様子を眺めるのが、なんだかクセになります。(´v`*)
「あ、今ちょうど乾き始めたな…」 そんな変化が目で分かるのが、この工程の面白さです。
●仕上げは “ガサガサッ!” の快感
完全に乾いたら、最後は袋へ「ガサガサッ!」と一気に取り込んで完成。 自分の手で作ったドライフルーツやドライトマトが並ぶと、 ちょっとした達成感があります。
どんな仕上がりになるか、ぜひ楽しんでみてください!
■ AIとのやりとりで面白かった部分
■ゲーム仕様の相談からスタート
みー:(^v^) 今日もゲームを作ります。HTML形式で動かせるゲーム。
今回はスライサーの物理。 大型スライサーへ上部の投入口から材料を流し込むと、下の排出口からスライスされた材料が出てくる仕組みです。
スライサーは時計回りに回転する円盤に3枚の大型刃が付いていて、
投入された材料は重力で転がり落ち、円盤左内側の刃に当たると、
刃の厚さに合わせてスライスされていきます。
スライスされた材料はそのまま重力で排出口へ落ち、
器で受け止められます。
刃に当たる瞬間には「ザザザザッ」と抵抗が生まれ、
材料の落下スピードが少し遅くなるようにしたいです。
材料はレモンとトマト。スライス厚は1〜5mmで調整可能。
材料の投入はマウスでドラッグして入れる方式にしましょう。
スライサーを極めるゲームを目指します。
AI: 今日は「スライサーの物理」を極める、
こだわり満載のキッチンガジェット作りですね。
投入口から丸ごとのレモンやトマトを放り込み、
回転する3枚刃が「ザザザザ」と抵抗を生みながらスライスしていく……。
単なる作業ではなく、その“手応え”を物理演算で表現するのはとても面白い試みです。
みー:(・v・`) 投入がうまくできなくて、スライサーの刃の外周の色が変わるだけになってるよ。
スライサーの刃はもっと高速回転で、材料は横の農業用コンテナから掴んで入れるようにしましょう。
スライスされた材料は輪切りになって器へ流れ落ち、横向きに積もっていく感じで。
AI: ご指摘ありがとうございます。投入口周りの判定がシビアすぎたようですね。
「農業用コンテナから掴んで入れる」という、より直感的で“現場感”のある操作に刷新し、スライサーの回転速度や排出後の挙動もご要望に合わせて調整しました。
☕ マスターのちょっと一息、本日の小ネタ
プロの厨房で使われる平型スライサーには「マンドリン」という呼び名があるのをご存知ですか?
シャカシャカとリズミカルに野菜をスライスする手の動きが、弦楽器のマンドリンを弾く姿に似ていることから名付けられたと言われています。
今日作っている大型スライサーも、テンポよく材料を放り込むと、
ある種の“楽器”のような心地よいリズムを刻んでくれるかもしれませんね。
みー:(^v^*)いい感じだよ。刃の厚さでスライスされる枚数が変わるし、
厚さは外枠の線の太さで表現するといいかな。
AI: 素晴らしい視点です! 「道具を極める」ゲームにおいて、そのこだわりはとても重要です。
スライス厚の設定が単なる数字ではなく、
排出される枚数 と 見た目の重厚感(外枠の線の太さ) に直結するよう、
物理演算と描画ロジックを強化しました。
職人の遊び方
- スライダーを「1mm」にしてレモンを投入
排出口から 細い枠線のスライス が、雪崩のように大量に溢れ出てきます。 - 次に「5mm」で投入
今度は 太い枠線のスライス が、ゴロリゴロリと少ない枚数で出てきます。
厚さによる“リズム”と“見た目”の変化、
かなりスライサーの本質に近づいてきました!
みー:(^o^)/ グリーンレモンも材料に追加しましょう。
あと、スライスがいっぱいになると材料が宙に浮いてバグっぽくなるので、満杯になったら「乾燥機へ投入」ボタンで器を空にできるようにしましょう。
材料のコンテナは3つにして、それぞれの材料を分けたいです。
AI: 器の許容量を超えて空中に積み上がっていく、
物理エンジンあるあるの“無重力バグ”ですね。
あれはあれで面白いですが、職人の現場では整理整頓が大事です。
ご要望通り、爽やかな グリーンレモン を追加し、材料ごとに3つの専用コンテナを右側に配置しました。 さらに、器が満杯になったら「乾燥機へ投入」ボタンで一旦リセットできるように組み直しています。
☕ マスターのちょっと一息、本日の小ネタ
今回追加した「グリーンレモン」、実は黄色いレモンと品種が違うわけではありません。
黄色く熟す前の秋頃に早摘みされたもので、香りが鋭く、酸味もキリッとしているのが特徴。
乾燥させると黄色とのコントラストが生まれ、見た目も華やかになります。
みー:(*^_^)大型の温風乾燥機にしよう。大きな乾燥槽があって、棚の下から温風が吹き上がるタイプ。
スライスが厚いほど乾燥に時間がかかり、乾くにつれてシワシワに縮んで色も淡くなる。 乾燥が終わったら袋にガサガサッと取り込みます。
AI: それはもう、加工所で使われる本格的なプロ仕様の設備ですね。
素晴らしいこだわりです。
スライスされた材料が網棚に敷き詰められ、下からの温風でじっくり水分が飛んでいく。
厚みによって乾燥時間が変わり、徐々にシワシワになり、色が淡く変化していく…… そして最後は「ガサガサッ!」と袋に集める。
その一連の快感を、ひとつのシミュレーターに統合しました。
☕ マスターのちょっと一息、本日の小ネタ
今回の材料のひとつ「トマト」。
実は乾燥させると、旨味成分である グルタミン酸 がギュッと凝縮されます。
これは日本料理の「昆布だし」と同じ成分。
だからドライトマトは、そのまま食べても、スープや煮込みの“天然の旨味調味料”としても大活躍します。
乾燥機の中で赤い実がシワシワになっていくのを眺めると、
「この中に旨味が詰まっていくんだな…」と、ちょっと見方が変わるかもしれません。
■ 実際の動作
最初にAIと相談して作ってもらった 初期バージョン がこちら。
材料をドロップしても、なぜかスライサーの外周の色だけが変わるという、ちょっと不思議な動作になっていました。

(画像:初期のゲーム画面)
こちらが、調整を重ねて仕上がった 完成版のゲーム画面 です。
スライサーはしっかり高速回転し、3種類のカラフルな材料が並んでいます。

(画像:スタート画面)
マウスで好きな材料をドラッグして、スライサー上部の投入口へ。
手を離すと材料が落ちていき、
刃に当たった瞬間に「ザザザッ」とスライスされます。
切られたチップは下の容器にどんどん溜まっていきます。

(画像:2mmスライス)
スライスしたチップを乾燥機に並べ、
温風スイッチをON にすると乾燥がスタートします。

(画像:乾燥機投入直後)
乾燥が進むと、チップはだんだん小さく縮み、色も淡く変化していきます。
完全に乾いたら袋に「ガサガサッ」と回収。
またスライス工程から再スタートできます。

(画像:乾燥しあがり)
スライスの厚さを変えると、仕上がりも大きく変化します。
厚切りにすると見た目がしっかりして、乾燥にも時間がかかります。

(画像:厚切りスライス)
■ コードのポイント
今回の記事では、AI と相談しながら作った「スライス乾燥シミュレーター」の HTML コードを公開します。
このコードをそのまま保存するだけで、あなたのPCでもすぐに遊べるようになります。
ブラウザだけで動く、とてもシンプルなゲームなので、気軽に試してみてくださいね。
▼ 遊ぶまでの手順
1. PC の「メモ帳」(※Windowsの場合)や、
Mac の「テキストエディット」など、テキストエディタを開きます。
2. 下のコードをコピーして貼り付けます。
3. 「suraisu.html」など、好きな名前で保存します。
(※拡張子は .html にしてください)
4. 保存したファイルをブラウザで開けば、すぐに遊べます。
■ 遊び方・操作説明
① まずはスライスの厚さを決めよう!
画面上のスライダーを動かして、1mm〜5mm の間でお好みの厚さを選びます。 薄いとサクサク、厚いとザクザク。切れる感触がガラッと変わります♪
② 材料を投入口へ!
右下のコンテナから、好きな材料(レモン・グリーンレモン・トマト)を マウスで つかんで(ドラッグ) → 左上の投入口へ ポンッ(ドロップ)。
すると…… 「ザザザザッ!」 と勢いよくスライスされて、 下の器にどんどん溜まっていきます。
③ いざ、乾燥機へGO!
器がいっぱいになったら、 「乾燥機へ投入」ボタン をポチッ。
画面が切り替わり、広い網棚の乾燥室へ移動します。
④ 温風スイッチ、オン!
「温風スイッチ」を押すと、 下から ブワァーッ! と熱風が吹き上がります。
⑤ ガサガサッと袋詰め!
すべてのスライスが完全に乾くと、 「袋に回収」ボタン が緑色に光って押せるようになります。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <title>極・大型スライサー&温風乾燥シミュレーター V4</title> <style> body { background: #222; color: white; font-family: 'Hiragino Kaku Gothic ProN', sans-serif; display: flex; flex-direction: column; align-items: center; margin: 0; overflow: hidden; } canvas { background: #eee; box-shadow: 0 0 30px rgba(0,0,0,0.7); cursor: crosshair; touch-action: none; } .controls { background: #333; padding: 15px; width: 100%; display: flex; justify-content: center; gap: 20px; align-items: center; border-bottom: 2px solid #555; height: 40px; } .label { font-weight: bold; color: #ffca28; font-size: 1.1em; } input[type="range"] { vertical-align: middle; cursor: pointer; } #status { font-size: 1.1em; color: #aaa; width: 350px; text-align: center; font-weight: bold; } /* スライサー用ボタン */ .btn-dryer { background: #f57c00; color: white; border: none; padding: 8px 20px; font-size: 1.1em; font-weight: bold; border-radius: 5px; cursor: pointer; transition: 0.2s; } .btn-dryer:hover { background: #ff9800; transform: translateY(-2px); } .btn-dryer:disabled { background: #555; cursor: not-allowed; transform: none; color: #888; } /* 乾燥機用ボタン */ .btn-heater { background: #e53935; color: white; border: none; padding: 8px 20px; font-size: 1.1em; font-weight: bold; border-radius: 5px; cursor: pointer; transition: 0.2s; } .btn-heater.active { background: #ef5350; box-shadow: 0 0 15px #ef5350; } .btn-bag { background: #43a047; color: white; border: none; padding: 8px 20px; font-size: 1.1em; font-weight: bold; border-radius: 5px; cursor: pointer; transition: 0.2s; } .btn-bag:hover:not(:disabled) { background: #4caf50; transform: translateY(-2px); } .btn-bag:disabled { background: #555; cursor: not-allowed; color: #888; } </style></head><body><div id="slicerUI" class="controls"> <div> <span class="label">スライス厚: </span> <input type="range" id="thicknessRange" min="1" max="5" value="2" step="1"> <span id="thicknessVal">2mm</span> </div> <button class="btn-dryer" id="toDryerBtn" onclick="goToDryer()" disabled>乾燥機へ投入</button> <div id="status">コンテナから材料を掴んで投入口へ!</div></div><div id="dryerUI" class="controls" style="display: none;"> <button class="btn-heater" id="heaterBtn" onclick="toggleHeater()">温風スイッチ: OFF</button> <button class="btn-bag" id="bagBtn" onclick="collectToBag()" disabled>袋に回収 (ガサガサッ)</button> <div id="dryerStatus" style="font-size: 1.1em; color: #aaa; width: 300px; text-align: center; font-weight: bold;">温風をONにしてください</div></div><canvas id="gameCanvas"></canvas><script>const canvas = document.getElementById('gameCanvas');const ctx = canvas.getContext('2d');canvas.width = 800;canvas.height = 700;// システム状態let gameState = 'slicer'; // 'slicer', 'dryer', 'bagging'// --- スライサー用設定 ---let sliceThickness = 2; const MAX_SLICES = 100; // 乾燥機に一度に持っていける量const colors = { lemon: { raw: '#fff176', dry: '#fff9c4', skin: '#fbc02d' }, greenLemon: { raw: '#aed581', dry: '#dcedc8', skin: '#558b2f' }, tomato: { raw: '#ff5252', dry: '#ffcdd2', skin: '#d32f2f' }};const hopper = { x: 180, y: 50, w: 120, h: 200 };const slicer = { x: 250, y: 380, radius: 130, angle: 0, speed: 0.2, blades: 3 };const containers = [ { type: 'lemon', name: 'レモン', x: 620, y: 120, w: 160, h: 130, color: '#f57f17' }, { type: 'greenLemon', name: 'グリーンレモン', x: 620, y: 280, w: 160, h: 130, color: '#33691e' }, { type: 'tomato', name: 'トマト', x: 620, y: 440, w: 160, h: 130, color: '#b71c1c' }];let wholeFoods = []; let slices = []; let stackedSlices = []; let grabbedFood = null;let mousePos = { x: 0, y: 0 };// --- 乾燥機用設定 ---let dryingSlices = [];let isHeaterOn = false;let heatWaveOffset = 0;let baggingProgress = 0;// UI連動document.getElementById('thicknessRange').oninput = function() { sliceThickness = parseInt(this.value); document.getElementById('thicknessVal').innerText = sliceThickness + "mm";};// --- クラス定義 ---class WholeFood { constructor(x, y, type, state) { this.x = x; this.y = y; this.type = type; this.radius = 35; this.vy = 0; this.state = state; this.slicedAmount = 0; } update() { if (this.state === 'dropping' || this.state === 'slicing') { let gravity = 0.6; if (this.y + this.radius > slicer.y - slicer.radius + 30 && this.y - this.radius < slicer.y + slicer.radius) { this.state = 'slicing'; gravity = 0.08; this.vy *= 0.7; let spawnChance = 1.1 - (sliceThickness * 0.2); if (Math.random() < spawnChance) { createSlice(this.x + 20 + Math.random()*20, this.y + 10, this.type, sliceThickness); this.slicedAmount += sliceThickness * 2.5; } } else if (this.state === 'slicing') return false; this.vy += gravity; this.y += this.vy; if (this.slicedAmount > 100 || this.y > canvas.height + 100) return false; } return true; } draw() { ctx.save(); ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = colors[this.type].raw; ctx.fill(); ctx.strokeStyle = colors[this.type].skin; ctx.lineWidth = 4; ctx.stroke(); ctx.beginPath(); ctx.arc(this.x - 10, this.y - 10, 10, 0, Math.PI*2); ctx.fillStyle = "rgba(255,255,255,0.3)"; ctx.fill(); ctx.restore(); }}class Slice { constructor(x, y, type, thickness) { this.x = x; this.y = y; this.vx = 2 + Math.random() * 4; this.vy = -3 - Math.random() * 4; this.type = type; this.thickness = thickness; this.rot = Math.random() * Math.PI * 2; } update() { this.vy += 0.5; this.x += this.vx; this.y += this.vy; this.rot += 0.15; if (this.y > 640) { stackSlice(this); return false; } return true; } draw() { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rot); ctx.fillStyle = colors[this.type].raw; ctx.beginPath(); ctx.ellipse(0, 0, 15, 25, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = colors[this.type].skin; ctx.lineWidth = this.thickness; ctx.stroke(); ctx.restore(); }}// 乾燥機用のスライスクラスclass DryingSlice { constructor(type, thickness) { this.type = type; this.thickness = thickness; // 3m x 4mの棚(画面中央の広いエリア)にランダム配置 this.x = 100 + Math.random() * 600; this.y = 100 + Math.random() * 500; this.rot = Math.random() * Math.PI * 2; this.dryness = 0; // 0〜100 // 厚いほど乾燥に時間がかかる(1mm=速い、5mm=遅い) this.drySpeed = 0.5 / this.thickness; } update() { if (isHeaterOn && this.dryness < 100) { this.dryness += this.drySpeed; if (this.dryness > 100) this.dryness = 100; } } draw() { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rot); // 色のブレンド(生の色から乾燥色へ) let rawCol = colors[this.type].raw; let dryCol = colors[this.type].dry; // 乾燥によるシワシワ・縮み表現 // 基本の半径(厚みがあるほど少しだけ大きい) let baseRadius = 22 + (this.thickness * 0.5); // 乾燥すると縮む(最大で半径-6) let shrink = (this.dryness / 100) * 6; ctx.beginPath(); let numPoints = 16; for (let i = 0; i <= numPoints; i++) { let a = (i / numPoints) * Math.PI * 2; let currentRadius = baseRadius - shrink; // シワシワ成分(乾燥するほどフチが波打つ) let wrinkle = (this.dryness / 100) * 2 * Math.sin(i * (3 + this.thickness)); let r = currentRadius + wrinkle; let px = Math.cos(a) * r; let py = Math.sin(a) * r; if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); } // 色の描画(生の色を描いた上から、乾燥色をアルファ値を変えて重ねる) ctx.fillStyle = rawCol; ctx.fill(); ctx.fillStyle = dryCol; ctx.globalAlpha = this.dryness / 100; // 乾燥するほど淡い色が濃くなる ctx.fill(); ctx.globalAlpha = 1.0; ctx.strokeStyle = colors[this.type].skin; ctx.lineWidth = this.thickness; ctx.stroke(); // 水分が抜けていくパーティクル(温風ON時) if (isHeaterOn && this.dryness < 100 && Math.random() < 0.05) { ctx.fillStyle = "rgba(255,255,255,0.5)"; ctx.beginPath(); ctx.arc(Math.random()*20-10, Math.random()*20-10, 2, 0, Math.PI*2); ctx.fill(); } ctx.restore(); }}// --- 関数群 ---function createSlice(x, y, type, thickness) { slices.push(new Slice(x, y, type, thickness)); }function stackSlice(s) { if (stackedSlices.length >= MAX_SLICES) return; s.vx = 0; s.vy = 0; s.x = 200 + Math.random() * 320; let visualStack = Math.min(stackedSlices.length, 100); s.y = 660 - (visualStack * (0.3 + s.thickness * 0.1)); s.x += Math.random() * 30 - 15; s.rot = Math.PI / 2 + (Math.random() * 0.4 - 0.2); stackedSlices.push(s); if (stackedSlices.length > 0) { document.getElementById('toDryerBtn').disabled = false; }}function refillContainer(c) { for (let i = 0; i < 6; i++) { const x = c.x + 30 + Math.random() * (c.w - 60); const y = c.y + 30 + Math.random() * (c.h - 60); wholeFoods.push(new WholeFood(x, y, c.type, 'inContainer')); }}function initContainers() { wholeFoods = []; containers.forEach(c => refillContainer(c)); }initContainers();// --- シーン切り替え制御 ---window.goToDryer = function() { if (stackedSlices.length === 0) return; // スライスを乾燥用オブジェクトに変換 dryingSlices = stackedSlices.map(s => new DryingSlice(s.type, s.thickness)); stackedSlices = []; document.getElementById('toDryerBtn').disabled = true; // UI切り替え document.getElementById('slicerUI').style.display = 'none'; document.getElementById('dryerUI').style.display = 'flex'; document.getElementById('dryerStatus').innerText = "温風をONにしてください"; document.getElementById('bagBtn').disabled = true; isHeaterOn = false; document.getElementById('heaterBtn').innerText = "温風スイッチ: OFF"; document.getElementById('heaterBtn').className = "btn-heater"; gameState = 'dryer';};window.toggleHeater = function() { isHeaterOn = !isHeaterOn; const btn = document.getElementById('heaterBtn'); if (isHeaterOn) { btn.innerText = "温風スイッチ: ON"; btn.className = "btn-heater active"; document.getElementById('dryerStatus').innerText = "乾燥中... (厚みで時間が変わります)"; document.getElementById('dryerStatus').style.color = "#ef5350"; } else { btn.innerText = "温風スイッチ: OFF"; btn.className = "btn-heater"; document.getElementById('dryerStatus').innerText = "乾燥を一時停止中"; document.getElementById('dryerStatus').style.color = "#aaa"; }};window.collectToBag = function() { gameState = 'bagging'; baggingProgress = 0; isHeaterOn = false; document.getElementById('dryerStatus').innerText = "ガサガサッ!!"; document.getElementById('dryerStatus').style.color = "#4caf50";};// --- マウスイベント(スライサー画面のみ) ---function getMousePos(e) { const rect = canvas.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; }canvas.addEventListener('mousedown', (e) => { if (gameState !== 'slicer') return; const m = getMousePos(e); for (let i = wholeFoods.length - 1; i >= 0; i--) { let food = wholeFoods[i]; if (food.state === 'inContainer' && Math.sqrt((food.x - m.x)**2 + (food.y - m.y)**2) < food.radius) { grabbedFood = food; grabbedFood.state = 'grabbed'; wholeFoods.splice(i, 1); wholeFoods.push(grabbedFood); return; } }});canvas.addEventListener('mousemove', (e) => { if (grabbedFood) { mousePos = getMousePos(e); grabbedFood.x = mousePos.x; grabbedFood.y = mousePos.y; }});canvas.addEventListener('mouseup', (e) => { if (grabbedFood) { const m = getMousePos(e); if (m.x > hopper.x && m.x < hopper.x + hopper.w && m.y > 0 && m.y < hopper.y + hopper.h) { grabbedFood.state = 'dropping'; grabbedFood.x = hopper.x + hopper.w/2 + (Math.random()*10-5); grabbedFood.vy = 3; } else { grabbedFood.state = 'inContainer'; } grabbedFood = null; }});// --- 描画と更新ループ ---function update() { ctx.clearRect(0, 0, canvas.width, canvas.height); if (gameState === 'slicer') { // --- スライサー画面描画 --- ctx.fillStyle = "rgba(180, 180, 180, 0.6)"; ctx.beginPath(); ctx.moveTo(150, 680); ctx.lineTo(550, 680); ctx.lineTo(600, 580); ctx.lineTo(100, 580); ctx.fill(); ctx.strokeStyle = "#999"; ctx.lineWidth = 2; ctx.stroke(); containers.forEach(c => { ctx.fillStyle = c.color; ctx.fillRect(c.x, c.y, c.w, c.h); ctx.strokeStyle = "#111"; ctx.lineWidth = 4; ctx.strokeRect(c.x, c.y, c.w, c.h); ctx.fillStyle = "#fff"; ctx.font = "bold 14px sans-serif"; ctx.fillText(c.name, c.x + 10, c.y + 20); }); // スライサー本体描画(省略化) ctx.fillStyle = "rgba(100, 100, 100, 0.4)"; ctx.fillRect(hopper.x, hopper.y, hopper.w, hopper.h); ctx.save(); ctx.translate(slicer.x, slicer.y); ctx.rotate(slicer.angle); ctx.beginPath(); ctx.arc(0, 0, slicer.radius, 0, Math.PI * 2); ctx.fillStyle = "rgba(160, 160, 160, 0.9)"; ctx.fill(); ctx.strokeStyle = "#333"; ctx.lineWidth = 8; for (let i = 0; i < slicer.blades; i++) { let a = (i * Math.PI * 2) / slicer.blades; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(Math.cos(a) * slicer.radius, Math.sin(a) * slicer.radius); ctx.stroke(); } ctx.restore(); slicer.angle += slicer.speed; stackedSlices.forEach(s => { ctx.save(); ctx.translate(s.x, s.y); ctx.rotate(s.rot); ctx.fillStyle = colors[s.type].raw; ctx.beginPath(); ctx.ellipse(0, 0, 15, 25, 0, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = colors[s.type].skin; ctx.lineWidth = s.thickness; ctx.stroke(); ctx.restore(); }); wholeFoods = wholeFoods.filter(food => { let alive = food.update(); if(food.state !== 'grabbed') food.draw(); return alive; }); if (grabbedFood) grabbedFood.draw(); containers.forEach(c => { if (wholeFoods.filter(f => f.state === 'inContainer' && f.type === c.type).length === 0 && !(grabbedFood && grabbedFood.type === c.type)) refillContainer(c); }); slices = slices.filter(s => { let alive = s.update(); s.draw(); return alive; }); // 容量バー let fillRatio = stackedSlices.length / MAX_SLICES; ctx.fillStyle = "#444"; ctx.fillRect(150, 560, 400, 10); ctx.fillStyle = fillRatio > 0.9 ? "#f44336" : "#4caf50"; ctx.fillRect(150, 560, 400 * fillRatio, 10); } else if (gameState === 'dryer' || gameState === 'bagging') { // --- 乾燥機画面描画(真上視点) --- // 温風エフェクト(網棚の下) if (isHeaterOn || gameState === 'dryer') { heatWaveOffset += 2; ctx.fillStyle = "#1a1a1a"; ctx.fillRect(0, 0, canvas.width, canvas.height); // 暗い庫内 if (isHeaterOn) { ctx.save(); ctx.globalCompositeOperation = "lighter"; for(let i=0; i<5; i++) { ctx.fillStyle = `rgba(255, 100, 0, ${0.05 + Math.random()*0.02})`; ctx.beginPath(); ctx.arc(400 + Math.sin(heatWaveOffset*0.05 + i)*100, 350 + Math.cos(heatWaveOffset*0.03 + i)*100, 300, 0, Math.PI*2); ctx.fill(); } ctx.restore(); } } // 金網棚(3m×4mを画面いっぱいに表現) ctx.strokeStyle = "#555"; ctx.lineWidth = 1; for(let i=0; i<800; i+=20) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, 700); ctx.stroke(); } for(let j=0; j<700; j+=20) { ctx.beginPath(); ctx.moveTo(0, j); ctx.lineTo(800, j); ctx.stroke(); } // スライスの更新と描画 let allDry = true; dryingSlices.forEach(ds => { if (gameState === 'dryer') ds.update(); // ガサガサ回収アニメーション if (gameState === 'bagging') { baggingProgress += 0.005; // 速度 // 中央下部(袋の位置)へ吸い込まれるように移動 let targetX = 400; let targetY = 650; ds.x += (targetX - ds.x) * 0.1; ds.y += (targetY - ds.y) * 0.1; ds.rot += 0.2; // 回転しながら入る } ds.draw(); if (ds.dryness < 100) allDry = false; }); // 完了判定とボタン有効化 if (gameState === 'dryer' && allDry && dryingSlices.length > 0) { document.getElementById('bagBtn').disabled = false; document.getElementById('dryerStatus').innerText = "乾燥完了!袋に回収してください。"; document.getElementById('dryerStatus').style.color = "#4caf50"; } // 回収アニメーション終了後、スライサーへ戻る if (gameState === 'bagging') { // 袋の描画 ctx.fillStyle = "#8d6e63"; // 茶色のクラフト紙袋 ctx.fillRect(300, 600, 200, 100); ctx.fillStyle = "#fff"; ctx.font = "bold 24px sans-serif"; ctx.fillText("DRY FRUITS", 330, 650); if (baggingProgress > 1.0) { dryingSlices = []; gameState = 'slicer'; document.getElementById('dryerUI').style.display = 'none'; document.getElementById('slicerUI').style.display = 'flex'; document.getElementById('status').innerText = "コンテナから材料を掴んで投入口へ!"; document.getElementById('status').style.color = "#aaa"; } } } requestAnimationFrame(update);}update();</script></body></html>
■ 今日の学び
今回のゲームは、ファンタジー要素を少し離れて、
“道具の物理をどこまで再現できるか” をテーマにした実験でした。(^o^)
実際に作ってみて感じたのは、
スライサーや乾燥機のような シンプルな道具ほど、物理の調整が奥深い ということです。
●「切る」という動作は、数字以上に“感触”が大事
刃に当たった瞬間の減速、
材料が回転に巻き込まれる角度、
厚みが変わったときのスピードの違い。
どれも小さな数値の調整なのに、
体験としての“手応え”がガラッと変わる のが面白かったポイントです。
● 厚みの違いが、乾燥工程にも影響する
厚さを変えると、乾燥の進み方も変わる。
これは実際の加工所でも同じで、
「薄いとすぐ乾く」「厚いとじっくり時間がかかる」
という 現実の物理がそのままゲーム性になる のが新鮮でした。
乾燥中にフチから少しずつ縮んでいく様子は、
シミュレーターならではの“観察の楽しさ”につながりました。(=v=*)
今回の実験を通して、 「物理の再現=ゲームの気持ちよさ」 につながることを改めて実感しました。
ぜひあなたも、加工所の職人になった気分で、
いろいろな厚さや材料を試してみてくださいね。(*^v^)
■ 次回は、吹くを考えるシミュレーターです。
あちこちで見かける、たんぽぽの綿毛。
ふわふわと風に乗って飛んでいくあの動きは、
よく見るととても不思議なんですよね。
「風を送ると、対象物にはどんな力が働くんだろう?」
そんな小さな疑問から生まれた、
“風と綿毛の物理”をテーマにしたシミュレーターゲーム です。
次回もぜひ遊びに来てくださいね。(*^v^)ノシ
▶ 次の実験はこちら(綿帽子とシャボンと火)
「綿帽子吹き飛ばしシミュレーター」
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)

コメントを残す