クリッカーゲームの作り方:数値成長の設計と実装
遊びをつくる執筆にClaudeを使用しています。

クリッカーゲームとは、クリックやタップを繰り返してリソース(多くは「数値」)を増やし続けるジャンルです。シンプルに見えますが、「最初は楽しいのにすぐ飽きる」「数値が大きくなりすぎて画面が崩れる」という問題が初心者には頻出します。この記事では、そうした失敗を避けるための数値設計の考え方と、HTMLとJavaScriptだけで動く最小構成の実装手順を示します。 クリッカーゲームの面白さは「成長が見える速度感」にあります。数値がまったく増えなければ退屈で、逆に一瞬で億単位になれば達成感がなくなります。設計の核心は、プレイヤーが「もう少し頑張れば次のステージに届く」と感じ続けられるペースを作ることです。この記事を読めば、その設計を数式レベルで考え、コードに落とし込む手順が理解できます。
数値成長の設計:指数カーブを意図的に使う
クリッカーゲームの数値は指数的に増えます。これは偶然ではなく、設計の選択です。
線形(等差)成長はすぐに「クリック単価と価格が釣り合わなくなる」問題を生じます。たとえば「クリックで1増える」「アップグレードが毎回+10コスト」という設計では、序盤はすぐ買えるのに中盤以降は永遠に買えなくなります。一方、指数(等比)成長はコストも生産量も同じ比率で増やすため、体感速度をほぼ一定に保てます。
基本的な設計式は次の2本です。
- 生産量:baseProduction × multiplier ^ upgradeLevel
- アップグレードコスト:baseCost × costRatio ^ currentLevel
costRatio を 1.15〜1.5 の範囲で選ぶのが出発点です。1.15 に近いほど「頻繁に買える・単調になりやすい」、1.5 に近いほど「買えた瞬間の満足感が大きい・待ち時間も長い」傾向があります。プレイ時間が数分を想定するミニゲームなら 1.2 前後、長時間放置型なら 1.35〜1.5 が一般的な目安です。
multiplier(アップグレードごとの生産倍率)は、コスト比率と揃えると「コストを払っても体感速度が変わらない」フラット感が生まれます。生産倍率をコスト比率より少し低く(例:コスト×1.3、生産×1.2)にすると、初期は速く後半は少し重くなり、長くプレイさせたい設計に向きます。
大きな数値の扱い方:表示と内部値を分ける
クリッカーゲームで詰まりやすい技術的な問題が「数値が大きすぎてJavaScriptの浮動小数点精度が失われる」ことです。JavaScriptの Number は 2^53(約9,000兆)を超えると整数の精度が保証されません。
対策は2つあります。
- 表示だけを変換する:内部はそのまま Number で持ち、表示時に K(千)、M(百万)、B(十億)と変換する関数を一か所に作る。このアプローチは実装が最もシンプルで、数百兆規模まで実用上問題ありません。
- BigInt を使う:JavaScript の BigInt 型は任意精度の整数を扱えます。ただし BigInt と Number は直接演算できないため、コード全体で型を統一する必要があり、慣れていない場合は管理コストが増えます。
ミニゲームやプロトタイプなら、まず Number +表示変換関数で作り始めるのが現実的です。以下は表示変換の最小実装例です。
function formatNumber(n) {
if (n >= 1e12) return (n / 1e12).toFixed(1) + 'T';
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
return Math.floor(n).toString();
}この関数を数値を表示するすべての箇所で使えば、内部の計算ロジックは一切変えずに見た目を整えられます。
最小構成の実装手順
以下の順序で作ると、動くものが最短で得られます。
1. 状態変数を決める
まずゲームの「状態」をオブジェクト一つにまとめます。
const state = {
cookies: 0,
cps: 0, // cookies per second(毎秒自動生産量)
clickPower: 1,
upgrades: [
{ name: 'カーソル', baseCost: 15, costRatio: 1.15, production: 0.1, owned: 0 },
{ name: 'おばあちゃん', baseCost: 100, costRatio: 1.15, production: 0.5, owned: 0 },
{ name: '農場', baseCost: 1100, costRatio: 1.15, production: 4, owned: 0 },
]
};状態を一か所に集めると、セーブ・ロードが JSON.stringify(state) / JSON.parse(saved) だけで完結します。
2. クリック処理と自動生産を書く
document.getElementById('cookie').addEventListener('click', () => {
state.cookies += state.clickPower;
render();
});
setInterval(() => {
state.cookies += state.cps / 20; // 50msごとに1/20秒分加算
render();
}, 50);setInterval の間隔を短くするほど数値が滑らかに増えますが、処理が増えます。50〜100ms が実用的な範囲です。
3. アップグレード購入を実装する
function buyUpgrade(index) {
const u = state.upgrades[index];
const cost = Math.floor(u.baseCost * Math.pow(u.costRatio, u.owned));
if (state.cookies < cost) return;
state.cookies -= cost;
u.owned += 1;
recalcCps();
}
function recalcCps() {
state.cps = state.upgrades.reduce((sum, u) => sum + u.production * u.owned, 0);
}recalcCps は購入のたびに呼ぶだけでよく、毎フレーム計算し直す必要はありません。
4. 描画関数をまとめる
function render() {
document.getElementById('count').textContent = formatNumber(state.cookies);
document.getElementById('cps-display').textContent = formatNumber(state.cps) + '/秒';
state.upgrades.forEach((u, i) => {
const cost = Math.floor(u.baseCost * Math.pow(u.costRatio, u.owned));
const btn = document.getElementById('upgrade-' + i);
btn.textContent = `${u.name}(${formatNumber(cost)}枚)× ${u.owned}`;
btn.disabled = state.cookies < cost;
});
}描画を一つの関数に集めると、どこから呼んでも表示が一貫します。
失敗しやすい点と対処
コストが安すぎて序盤が瞬殺される:baseCost を決めるとき、「最初のアップグレードは何クリックで買えるか」を手で試算してください。10〜20クリックが目安です。
自動生産が速すぎてクリックが無意味になる:序盤の cps を clickPower の2〜3倍以内に抑えると、クリックの意味が残ります。中盤以降はどんどん自動化が上回る設計でも問題ありません。
アップグレードが多すぎて画面が縦に長くなりすぎる:最初は3〜5種類で作り始め、動作確認後に追加します。一覧をスクロール可能な固定高さのコンテナに入れると画面が崩れません。
setInterval の多重登録:ページを再ロードするたびにインターバルが増えるバグが起きやすいです。let tickId = null; if (tickId) clearInterval(tickId); tickId = setInterval(...) と変数で管理してください。
次の行動
この記事の構成を手元にコピーして、まず3種類のアップグレードだけで動く最小版を作ってみてください。数値バランスの調整は動かしながら行う方が早く、最初から完璧を目指す必要はありません。完成したゲームはHTMLファイル一つにまとめてアソバースに投稿し、他のプレイヤーに数値感覚を試してもらうことでフィードバックが得られます。

