Part of these game systems:
intermediate Touch & Mobile

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.

Unity 2022.3+ · 4.5 KB · IdleIncomeGenerator.cs

How to Use

1

Create an empty GameObject and attach the IdleIncomeGenerator script

2

Set baseIncomePerSecond and tickInterval for your economy balance

3

Configure upgrade cost scaling with baseUpgradeCost and upgradeCostMultiplier

4

Connect onCurrencyChanged to a UI Text to display formatted currency

5

Call TryUpgrade() from an upgrade button and TryPrestige() from a prestige button

6

Offline earnings are calculated automatically on game start

Source Code

IdleIncomeGenerator.cs
C#
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();
    }
}
Ready for more? Mobile Safe Area Handler Automatically adjusts UI RectTransform to respect device safe areas on notched phones and tablets.