マガジン

クリッカーゲームの作り方:数値成長の設計と実装

遊びをつくる

執筆にClaudeを使用しています。

さまざまな小さなゲームの世界が並んだ棚を描いたイラスト。
記事のテーマをもとに制作したAI生成イメージ。実際のゲーム画面ではありません。

クリッカーゲームとは、クリックやタップを繰り返してリソース(多くは「数値」)を増やし続けるジャンルです。シンプルに見えますが、「最初は楽しいのにすぐ飽きる」「数値が大きくなりすぎて画面が崩れる」という問題が初心者には頻出します。この記事では、そうした失敗を避けるための数値設計の考え方と、HTMLとJavaScriptだけで動く最小構成の実装手順を示します。 クリッカーゲームの面白さは「成長が見える速度感」にあります。数値がまったく増えなければ退屈で、逆に一瞬で億単位になれば達成感がなくなります。設計の核心は、プレイヤーが「もう少し頑張れば次のステージに届く」と感じ続けられるペースを作ることです。この記事を読めば、その設計を数式レベルで考え、コードに落とし込む手順が理解できます。

この記事の目次
  1. 数値成長の設計:指数カーブを意図的に使う
  2. 大きな数値の扱い方:表示と内部値を分ける
  3. 最小構成の実装手順
  4. 失敗しやすい点と対処
  5. 次の行動

数値成長の設計:指数カーブを意図的に使う

クリッカーゲームの数値は指数的に増えます。これは偶然ではなく、設計の選択です。

線形(等差)成長はすぐに「クリック単価と価格が釣り合わなくなる」問題を生じます。たとえば「クリックで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つあります。

  1. 表示だけを変換する:内部はそのまま Number で持ち、表示時に K(千)、M(百万)、B(十億)と変換する関数を一か所に作る。このアプローチは実装が最もシンプルで、数百兆規模まで実用上問題ありません。
  1. BigInt を使う:JavaScript の BigInt 型は任意精度の整数を扱えます。ただし BigInt と Number は直接演算できないため、コード全体で型を統一する必要があり、慣れていない場合は管理コストが増えます。

ミニゲームやプロトタイプなら、まず Number +表示変換関数で作り始めるのが現実的です。以下は表示変換の最小実装例です。

javascript
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. 状態変数を決める

まずゲームの「状態」をオブジェクト一つにまとめます。

javascript
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. クリック処理と自動生産を書く

javascript
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. アップグレード購入を実装する

javascript
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. 描画関数をまとめる

javascript
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ファイル一つにまとめてアソバースに投稿し、他のプレイヤーに数値感覚を試してもらうことでフィードバックが得られます。