Idle Income Generator
Passive income system for idle/clicker games with offline earnings calculation, upgrade scaling, and prestige mechanics. Handles large numbers with K/M/B/T formatting.
How to Use
Create an empty GameObject and attach the IdleIncomeGenerator script
Set baseIncomePerSecond and tickInterval for your economy balance
Configure upgrade cost scaling with baseUpgradeCost and upgradeCostMultiplier
Connect onCurrencyChanged to a UI Text to display formatted currency
Call TryUpgrade() from an upgrade button and TryPrestige() from a prestige button
Offline earnings are calculated automatically on game start
Features
- Passive income generation over time
- Offline earnings calculation
- Upgrade system with cost scaling
- Number formatting (K, M, B, T)
- Prestige/reset with bonus multiplier
When to Use This
Built for idle games, clicker games, and any mobile game with passive income mechanics. Use this when your game needs offline earnings calculation, exponential upgrade cost scaling, and prestige resets — the core loop of games like Cookie Clicker or AdVenture Capitalist.
Common Mistakes
Offline earnings use DateTime.UtcNow, so players who change their device clock can exploit the system — validate against a server time for competitive games. The upgradeCostMultiplier of 1.15 compounds exponentially, so costs escalate fast; test your economy balance with 50+ upgrades. SaveData() is called in OnApplicationPause and OnDestroy, but on Android, OnApplicationQuit isn't always reliable — save frequently during gameplay too.
Source Code
using UnityEngine;
using UnityEngine.Events;
using System;
public class IdleIncomeGenerator : MonoBehaviour
{
[Header("Income Settings")]
[SerializeField] private double baseIncomePerSecond = 1.0;
[SerializeField] private float tickInterval = 1f;
[Header("Upgrade Settings")]
[SerializeField] private double baseUpgradeCost = 10.0;
[SerializeField] private double upgradeCostMultiplier = 1.15;
[SerializeField] private double upgradeIncomeBonus = 0.5;
[Header("Prestige")]
[SerializeField] private double prestigeThreshold = 1000000.0;
[SerializeField] private double prestigeBonusMultiplier = 0.1;
[Header("Offline")]
[SerializeField] private float offlineEarningRate = 0.5f;
[SerializeField] private float maxOfflineHours = 8f;
[SerializeField] private string lastPlayedKey = "LastPlayedTime";
[SerializeField] private string currencyKey = "Currency";
[SerializeField] private string levelKey = "IncomeLevel";
[SerializeField] private string prestigeKey = "PrestigeLevel";
[Header("Events")]
public UnityEvent<string> onCurrencyChanged;
public UnityEvent<double> onOfflineEarnings;
public UnityEvent<int> onPrestige;
public double Currency { get; private set; }
public int IncomeLevel { get; private set; }
public int PrestigeLevel { get; private set; }
public double IncomePerSecond => CalculateIncome();
private float tickTimer;
private void Start()
{
LoadData();
CalculateOfflineEarnings();
}
private void Update()
{
tickTimer += Time.deltaTime;
if (tickTimer >= tickInterval)
{
tickTimer -= tickInterval;
EarnIncome(CalculateIncome() * tickInterval);
}
}
private double CalculateIncome()
{
double baseIncome = baseIncomePerSecond + (IncomeLevel * upgradeIncomeBonus);
double prestigeBonus = 1.0 + (PrestigeLevel * prestigeBonusMultiplier);
return baseIncome * prestigeBonus;
}
private void EarnIncome(double amount)
{
Currency += amount;
onCurrencyChanged?.Invoke(FormatNumber(Currency));
}
public bool TryUpgrade()
{
double cost = GetUpgradeCost();
if (Currency < cost) return false;
Currency -= cost;
IncomeLevel++;
onCurrencyChanged?.Invoke(FormatNumber(Currency));
return true;
}
public double GetUpgradeCost()
{
return baseUpgradeCost * Math.Pow(upgradeCostMultiplier, IncomeLevel);
}
public string GetUpgradeCostFormatted()
{
return FormatNumber(GetUpgradeCost());
}
public bool CanPrestige()
{
return Currency >= prestigeThreshold;
}
public void Prestige()
{
if (!CanPrestige()) return;
PrestigeLevel++;
Currency = 0;
IncomeLevel = 0;
onPrestige?.Invoke(PrestigeLevel);
onCurrencyChanged?.Invoke(FormatNumber(Currency));
SaveData();
}
private void CalculateOfflineEarnings()
{
string lastPlayed = PlayerPrefs.GetString(lastPlayedKey, "");
if (string.IsNullOrEmpty(lastPlayed)) return;
DateTime lastTime;
if (!DateTime.TryParse(lastPlayed, out lastTime)) return;
TimeSpan elapsed = DateTime.UtcNow - lastTime;
double offlineSeconds = Math.Min(elapsed.TotalSeconds, maxOfflineHours * 3600);
if (offlineSeconds > 60)
{
double earnings = CalculateIncome() * offlineSeconds * offlineEarningRate;
Currency += earnings;
onOfflineEarnings?.Invoke(earnings);
onCurrencyChanged?.Invoke(FormatNumber(Currency));
}
}
public void SaveData()
{
PlayerPrefs.SetString(lastPlayedKey, DateTime.UtcNow.ToString("o"));
PlayerPrefs.SetString(currencyKey, Currency.ToString());
PlayerPrefs.SetInt(levelKey, IncomeLevel);
PlayerPrefs.SetInt(prestigeKey, PrestigeLevel);
PlayerPrefs.Save();
}
private void LoadData()
{
double.TryParse(PlayerPrefs.GetString(currencyKey, "0"), out double saved);
Currency = saved;
IncomeLevel = PlayerPrefs.GetInt(levelKey, 0);
PrestigeLevel = PlayerPrefs.GetInt(prestigeKey, 0);
}
public void AddCurrency(double amount)
{
Currency += amount;
onCurrencyChanged?.Invoke(FormatNumber(Currency));
}
public static string FormatNumber(double number)
{
if (number >= 1e12) return (number / 1e12).ToString("F1") + "T";
if (number >= 1e9) return (number / 1e9).ToString("F1") + "B";
if (number >= 1e6) return (number / 1e6).ToString("F1") + "M";
if (number >= 1e3) return (number / 1e3).ToString("F1") + "K";
return number.ToString("F0");
}
private void OnApplicationPause(bool paused)
{
if (paused) SaveData();
}
private void OnApplicationQuit()
{
SaveData();
}
private void OnDestroy()
{
SaveData();
}
}