30分で作る「育てる」ゲーム:時間で変わる一枚の絵
遊びをつくる執筆にClaudeを使用しています。

「育てるゲーム」は、何があれば成り立つのでしょうか。キャラクターも、ショップも、レベル表も要りません。必要なのは、時間が進むと状態が変わり、その変化が絵に出ることだけです。種の絵を前に、80秒ほど何もしないで待つ場面を考えてみます。芽が出て、茎が伸び、葉が付き、最後に花が開けば、それだけで「育てた」という感触が生まれます。 この記事では、その最小構成を30分で作ります。使うのは1枚のHTMLファイルだけで、外部ライブラリもサーバーも不要です。仕組みは「経過時間を足し込む変数」「その変数から段階を決める表」「段階ごとに絵を描く関数」の3つです。この3つを分けて持てば、あとから絵を差し替えても、成長の速さを変えても、壊れにくくなります。 結果はコードを動かして確かめてください。この記事に実測値は載せていません。確認の手順は、後半の「自分で確かめる」にまとめてあります。
作る前に決める3つのこと
最初の5分は、コードを書かずに次の3つを紙かメモに書き出します。ここが決まると、残りの25分で迷う箇所がほとんどなくなります。
- 状態:時間で増える数値を1つ決めます。ここでは growth(成長の秒数)です。もう1つ、成長を止める条件として water(水の残り)を持ちます。
- 段階表:growth がいくつになったら絵が変わるかを、配列で決めます。ここでは「種・芽・双葉・葉が増える・つぼみ・花」の6段階とし、境界を 0, 5, 15, 30, 50, 80 秒にします。
- 絵:段階ごとに何を描くかを決めます。茎の高さだけは段階に関係なく growth に比例させ、滑らかに伸びる部分と、段階でぱっと切り替わる部分を両方持たせます。
水を足したのには理由があります。時間だけで勝手に育つ絵は、見ているだけの「アニメーション」になりがちです。水が切れると成長が止まるという一つの条件を足すと、画面に戻る理由と触る理由が生まれ、ゲームになります。
30分の配分は、次の目安で進めます。
- 0〜5分、やること:状態・段階表・絵の3つを書き出す
- 5〜15分、やること:コードを貼り、動かし、段階が切り替わるのを見る
- 15〜25分、やること:絵と数値を自分の好みに差し替える
- 25〜30分、やること:通しで確認し、公開前の準備をする
手順:動くところまで作る
1. ファイルを作る
任意のフォルダに index.html という名前で空のファイルを作ります。アソバースにアップロードする場合は、ZIPの直下に置いた index.html が入口になるので、最初からその名前にしておくと後で困りません。
2. コードを貼る
次のコードをそのまま貼ります。約45行で、単独で動きます。
<!DOCTYPE html>
<meta charset="utf-8">
<canvas id="c" width="300" height="360"></canvas>
<p><button id="w">水をやる</button> <span id="s"></span></p>
<script>
const g = document.getElementById('c').getContext('2d');
const STAGES = [0, 5, 15, 30, 50, 80];
const SPEED = 1, MAXW = 20;
let growth = 0, water = MAXW, last = performance.now();
const dot = (x, y, r, col) => {
g.fillStyle = col; g.beginPath(); g.arc(x, y, r, 0, 7); g.fill();
};
const stage = () => STAGES.filter(t => growth >= t).length - 1;
function draw() {
const st = stage(), h = Math.min(160, growth * 2), top = 300 - h;
g.clearRect(0, 0, 300, 360);
g.fillStyle = water > 0 ? '#8b5a2b' : '#b59a78';
g.fillRect(0, 300, 300, 60);
if (st === 0) dot(150, 300, 6, '#5a3a1a');
else {
g.strokeStyle = water > 0 ? '#3a8a3a' : '#9a9a4a';
g.lineWidth = 6; g.beginPath();
g.moveTo(150, 300); g.lineTo(150, top); g.stroke();
if (st >= 2) dot(164, 300 - h * 0.5, 8, '#4caf50');
if (st >= 3) dot(136, 300 - h * 0.7, 8, '#4caf50');
if (st === 4) dot(150, top, 9, '#e57aa0');
if (st === 5) {
for (let i = 0; i < 5; i++) {
const a = i * 1.2566;
dot(150 + Math.cos(a) * 14, top + Math.sin(a) * 14, 9, '#f48fb1');
}
dot(150, top, 7, '#ffd54f');
}
}
document.getElementById('s').textContent =
`段階 ${st} / 成長 ${growth.toFixed(0)} / 水 ${water.toFixed(1)}`;
}
function tick(now) {
const dt = Math.min((now - last) / 1000, 0.1);
last = now;
water = Math.max(0, water - dt);
if (water > 0) growth += dt * SPEED;
draw();
requestAnimationFrame(tick);
}
document.getElementById('w').onclick = () => { water = Math.min(MAXW, water + 10); };
requestAnimationFrame(tick);
</script>3. ブラウザで開く
ファイルをブラウザで開きます。土の上に小さな種が見え、画面下の文字に「段階 0」と出れば成功です。
4. 倍速で全段階を通す
いきなり80秒待たず、SPEED を 10 に書き換えて開き直します。成長が10倍の速さで進むので、花までの流れを短時間で通せます。確認が終わったら 1 に戻します。戻し忘れが、この種の試作でいちばん多い失敗です。
5. 段階表を触る
STAGES の数字を変えて、絵が変わるタイミングがずれることを確かめます。たとえば [0, 5, 15, 30, 50, 80] を [0, 3, 6, 9, 12, 15] にすれば、全段階が短い間隔で並びます。段階表と絵の描画が別々になっているので、描画のコードには一切手を入れずに調整できます。
6. 自分の絵に差し替える
draw の中の dot を使った部分が絵です。葉の色や位置、花びらの数を変えてみます。花びらは i < 5 と 1.2566(円を5等分した角度)の組で決まっています。6枚にしたいなら、i < 6 と 1.0472 に変えます。円を等分するときは、ループ回数と角度刻みを必ず揃えます。
読みどころ:なぜ「経過時間」で書くのか
このコードで大事なのは、tick の中にある次の2行です。
const dt = Math.min((now - last) / 1000, 0.1);
if (water > 0) growth += dt * SPEED;dt は前のフレームから今のフレームまでの経過秒数です。成長を「1フレームにつき+1」のように書くと、画面の更新回数が多い端末ほど早く育ってしまいます。秒数を足す書き方なら、端末が違っても育つ速さは同じになります。
もう一つ見てほしいのは Math.min(..., 0.1) です。一般に、ブラウザはタブが隠れている間、requestAnimationFrame の呼び出しを止めます。タブに戻った最初のフレームでは、now - last が非常に大きくなります。上限を付けないと、離れていた時間が一度に加算され、戻った瞬間に花が咲いたり、水が一気に空になったりします。上限を付けると、離席中は時間が進みません。「見ている間だけ育つ」という設計の選択です。
反対に「離れている間も育つ」ゲームにしたい場合は、前回の時刻を覚えておき、戻ったときに差分を計算し直す必要があります。その場合は、水が途中で切れたかどうかも差分から割り出す必要があります。難しさが一段上がるので、30分の試作では「見ている間だけ」にとどめるのが現実的です。
段階を決める stage は、STAGES の中で growth 以下の値がいくつあるかを数えて1を引いているだけです。if を6本並べる書き方と違い、段階を増やしたいときは配列に数字を足せば済みます。ただし draw の側にも、その段階の絵を足す必要があります。
自分で確かめる:記録の取り方と読み方
動いた気がするだけで終わらせないために、次の確認を行います。結果はこの記事には書いていません。手元で記録してください。
- SPEED = 1 で花が咲くまでの時間、記録する単位:ストップウォッチの秒、回数:2回
- 水をやらずに放置したとき、成長が止まるまでの時間、記録する単位:画面の水の値が0になった時点の成長値、回数:2回
- タブを10秒隠して戻ったときの成長値の差、記録する単位:隠す前と戻った直後の成長値、回数:2回
読み方の基準は次のとおりです。
- 花までの時間が、水やりの間隔を除いても80秒より大きくずれる場合:水が切れて止まっている時間が混ざっていないかを疑います。水の値が0になっていた時間を差し引いて、もう一度見ます。
- 放置したときに成長が止まらない場合:if (water > 0) の条件が消えていないかを見ます。
- タブを隠した前後で成長値が大きく増えている場合:Math.min の上限が外れている可能性があります。増えていなければ、設計どおりに動いています。
起こりやすい誤りも挙げておきます。
- SPEED を戻し忘れる。
- growth と h(茎の高さ)の比例係数を変えたのに、上限の160を変えず、茎の伸びが途中で止まって見える。
- STAGES を昇順で書かず、stage の結果がおかしくなる。
遊びとして面白くするための調整
動いたら、残りの時間で「待っていて気持ちいいか」を調整します。変えられるのは主に次の数値です。
- 水の持ちと水やりの量:MAXW と水やりの加算量です。水が短時間で切れると、ボタンを連打するだけの作業になります。長すぎると、触る理由がなくなります。
- 段階の間隔:序盤は短く、終盤は長くすると、期待感が保てます。逆に等間隔だと、変化のリズムが単調に感じられるかもしれません。どちらが心地よいかは、自分で通して確かめます。
- 水切れの見た目:コードでは、水がないと土と茎の色が変わります。色が変わるだけでなく、葉が下がる、茎が細くなるなど、「困っている」ことが伝わる変化を一つ足すと、水をやる動機がはっきりします。
さらに伸ばすなら、段階が切り替わる瞬間に、少しだけ絵を大きくして戻すような演出を入れる手があります。これは growth が STAGES の境界を越えたフレームを検出して、短いタイマーを動かすことで作れます。別の種類の植物を選べるようにするなら、STAGES と絵の関数を植物ごとにまとめた配列を作り、選んだものを参照します。
時間の流れを絵で見せる作品が気になる場合は、アソバースの公式作品「余白の庭」を開いてみてください。四季と昼夜の切り替えがあります。何がどう変わるかは、作品を開いて確かめるのがいちばん確実です。
公開前に確認すること
アソバースに出す場合は、次の点を見ておきます。
- index.html をZIPの直下に置きます。画像などを足すなら、相対パスで指定します。
- 外部通信やサーバー用のコードは使いません。この記事のコードは、その条件を満たしています。
- 記録の保存については注意が必要です。アップロードした作品の中の localStorage と sessionStorage は一時メモリーの互換処理なので、画面を閉じた後の育ち具合を保つ仕組みとしては頼れません。この記事のコードも、画面を開いている間だけ育つ設計にしてあります。
- 続きから育てられるようにしたい場合は、アソバースのクラウドセーブ(作品ごとにJSONで128KiBまで)が使えます。ただし、読み込み方や使い方の詳細は、投稿画面や現在の案内で確認してください。
次の行動
まず、貼ったコードを SPEED = 10 で通し、6つの段階が順に出ることを確かめてください。次に SPEED = 1 に戻し、水を切らした状態と、こまめにやった状態で、花までの時間を2回ずつ記録します。そのうえで、STAGES の数字と花の形を自分の好みに変えれば、あなたの植物が1つ完成します。
