DEV Community

unity source code
unity source code

Posted on

Designing the Core Loop for an Idle RPG Clicker in Unity: Boss Fights, Loot, and Offline Progression

Idle RPG clickers look deceptively simple from the outside. Tap the screen, watch a number go up, defeat a boss, repeat. But if you've ever tried to actually build one, you know the real complexity isn't the tapping — it's the systems underneath it. Offline progression math, loot balancing, boss difficulty curves, and save-state integrity all have to work together, or the whole game falls apart the moment a player closes the app and comes back six hours later.

In this article I want to break down the core architecture behind a typical idle RPG clicker — boss battles, loot systems, idle/offline earnings, and progression scaling — the way you'd actually structure it in Unity with C#. I'll use patterns pulled from a shipped mobile template as a reference point throughout, because it's easier to reason about real systems than hypothetical ones.

Why Idle + Clicker Hybrids Are Harder Than They Look

A pure clicker game only has to solve one problem: make tapping feel good. A pure idle game only has to solve one problem: make numbers grow in a satisfying curve while the player is away. An idle RPG clicker has to solve both problems simultaneously, and then reconcile them so neither system undermines the other.

If your idle income scales too aggressively, active tapping becomes pointless and players stop engaging. If tapping scales too aggressively, idle income feels like a rounding error and your retention hook (the thing that brings players back after being offline) stops working. Getting this balance right is 80% of what makes these games either addictive or forgettable.

Core System #1: The Tap-Damage Loop

At the most basic level, you need a damage-per-tap system that feeds into a boss health pool. This sounds trivial, but the way you structure it determines how easy the rest of the game is to build on top of.

Here's a minimal but extensible version:

public class BossCombatController : MonoBehaviour
{
    public BossData currentBoss;
    private float currentBossHealth;

    public event Action<float, float> OnBossHealthChanged; // current, max
    public event Action<BossData> OnBossDefeated;

    private void Start()
    {
        currentBossHealth = currentBoss.maxHealth;
    }

    public void ApplyTapDamage(float baseDamage, float critMultiplier = 1f)
    {
        float finalDamage = baseDamage * critMultiplier;
        currentBossHealth = Mathf.Max(0, currentBossHealth - finalDamage);

        OnBossHealthChanged?.Invoke(currentBossHealth, currentBoss.maxHealth);

        if (currentBossHealth <= 0)
        {
            HandleBossDefeat();
        }
    }

    private void HandleBossDefeat()
    {
        OnBossDefeated?.Invoke(currentBoss);
        // Loot roll, next boss load, and reward calculation happen
        // in listeners subscribed to OnBossDefeated, not here.
    }
}
Enter fullscreen mode Exit fullscreen mode

The key design decision here is keeping BossCombatController dumb on purpose. It doesn't know about loot tables, currency, or UI. It just tracks health and fires events. This separation matters more in idle games than most genres, because you'll eventually need the exact same "boss defeated" event to trigger loot drops, idle-rate recalculation, achievement checks, and save-state writes — all independently.

Core System #2: Loot and Reward Loops

The loot loop is what keeps the tap-damage loop from feeling repetitive. Every boss kill needs to produce some combination of currency, item drops, and upgrade materials, ideally with enough randomness that players don't feel like they're grinding a fixed script.

A simple weighted loot table looks like this:

[System.Serializable]
public class LootEntry
{
    public string itemId;
    public float dropWeight;
    public int minQuantity;
    public int maxQuantity;
}

public class LootTable : MonoBehaviour
{
    public List<LootEntry> entries;

    public List<(string itemId, int quantity)> RollLoot(int rolls = 1)
    {
        var results = new List<(string, int)>();
        float totalWeight = entries.Sum(e => e.dropWeight);

        for (int i = 0; i < rolls; i++)
        {
            float roll = UnityEngine.Random.Range(0, totalWeight);
            float cumulative = 0f;

            foreach (var entry in entries)
            {
                cumulative += entry.dropWeight;
                if (roll <= cumulative)
                {
                    int qty = UnityEngine.Random.Range(entry.minQuantity, entry.maxQuantity + 1);
                    results.Add((entry.itemId, qty));
                    break;
                }
            }
        }

        return results;
    }
}
Enter fullscreen mode Exit fullscreen mode

Two things matter here that are easy to overlook:

  1. Weighted, not fixed-probability, rolls. Fixed drop chances (e.g., "10% chance for X") get harder to balance as your loot table grows, because every new item changes the effective probability of every other item. A weighted system self-normalizes as you add or remove entries.
  2. Loot rolling is decoupled from combat. LootTable has no idea a boss even exists. It just responds to "roll N times" and returns results. That decoupling is what lets you reuse the exact same loot system for chests, daily rewards, or event drops later without duplicating logic.

I found this same separation pattern in Lootscape Boss Mania, a Unity idle RPG clicker template that combines tap-based boss combat with an offline idle-earning system. Looking at how a shipped project structures the boundary between combat, loot, and progression is a good sanity check against over-engineering — or under-engineering — your own version of the same systems.

Core System #3: Idle and Offline Progression

This is the system that actually differentiates an "idle RPG" from a plain clicker, and it's also the one most beginners get wrong. The naive approach — just multiply idle-rate by elapsed real-world time — works, but it opens the door to two problems: clock manipulation exploits, and reward curves that don't feel intentional.

Here's a more defensible pattern:

public class IdleProgressionManager : MonoBehaviour
{
    private const float MAX_OFFLINE_HOURS = 8f;

    public float goldPerSecond;

    public void SaveExitTimestamp()
    {
        PlayerPrefs.SetString("last_exit_utc", DateTime.UtcNow.ToString("o"));
        PlayerPrefs.Save();
    }

    public float CalculateOfflineEarnings()
    {
        if (!PlayerPrefs.HasKey("last_exit_utc"))
            return 0f;

        DateTime lastExit = DateTime.Parse(
            PlayerPrefs.GetString("last_exit_utc"),
            null,
            System.Globalization.DateTimeStyles.RoundtripKind
        );

        TimeSpan elapsed = DateTime.UtcNow - lastExit;
        float elapsedSeconds = Mathf.Clamp(
            (float)elapsed.TotalSeconds,
            0,
            MAX_OFFLINE_HOURS * 3600f
        );

        return elapsedSeconds * goldPerSecond;
    }
}
Enter fullscreen mode Exit fullscreen mode

A few implementation notes worth calling out:

  • Always store timestamps in UTC. If you store local time, you'll get silently wrong results the moment a player crosses a daylight-saving boundary or travels between timezones.
  • Cap offline earnings. An uncapped idle system either becomes exploitable (leave the game running for a week, come back absurdly overpowered) or forces you into diminishing-returns math that's harder to communicate to players. A hard cap with a clear UI message ("earning gold for up to 8 hours while away") is usually the simplest honest design.
  • Recalculate goldPerSecond from upgrades, not from a cached value. If you cache the idle rate at the moment the player exits, you lose the ability to retroactively apply upgrades or events that should affect offline earnings.

Core System #4: Progression and Difficulty Scaling

The last piece is tying boss difficulty, loot value, and idle rate together into a single progression curve. This is where a lot of idle games either flatten out (boring) or spike unpredictably (frustrating). A common, safe approach is exponential scaling with a tunable growth rate:

public static class ProgressionCurve
{
    public static float BossHealthForStage(int stage, float baseHealth, float growthRate = 1.15f)
    {
        return baseHealth * Mathf.Pow(growthRate, stage - 1);
    }

    public static float LootValueForStage(int stage, float baseValue, float growthRate = 1.08f)
    {
        return baseValue * Mathf.Pow(growthRate, stage - 1);
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that boss health and loot value scale at different rates (1.15 vs 1.08 in this example). That gap is intentional — it's what creates the sense that later stages require real strategic investment (upgrades, better gear) rather than just more tapping. If both curves scale at the same rate, the game plateaus into a flat grind with no perceived progress.

Putting the Systems Together

None of these four systems — combat, loot, idle progression, and difficulty scaling — are complicated in isolation. The actual engineering challenge is keeping them loosely coupled enough that you can tune, replace, or extend any one of them without breaking the others. In practice that means:

  • Combat fires events; it doesn't call loot or save logic directly
  • Loot tables are content-driven (ScriptableObjects or JSON), not hardcoded
  • Idle earnings are recalculated from current stats, never cached
  • Progression curves are pure functions you can unit test independently of Unity's scene graph

This event-driven separation is a pattern that shows up constantly outside of idle clickers too. I covered a very similar architecture recently while building out a crowd runner combat system with army growth and battle phases — different genre entirely, but the same underlying principle: keep your state-changing systems dumb and event-driven, and let independent listeners handle the consequences.

A Note on Content-Driven Design

One thing that separates a shippable idle RPG from a tech demo is how much of the game lives in data rather than code. Boss stats, loot tables, upgrade costs, and stage requirements should all be defined in ScriptableObjects or external config files, not hardcoded into MonoBehaviours. This matters for two practical reasons:

  1. Balancing becomes a spreadsheet problem, not a recompile problem. You want a designer (even if that's just you, wearing a different hat) to be able to tune a boss's health or a loot drop rate without touching C#.
  2. Reskinning and content expansion become trivial. If your combat, loot, and progression logic never reference specific boss names or item IDs directly, adding a new boss or a new loot tier is a data change, not a code change.

This same content-driven philosophy extends naturally into adjacent genres. Idle mechanics aren't exclusive to RPG clickers — resource-accumulation and management games lean on nearly identical systems: production rates instead of gold-per-second, harvest cycles instead of boss timers, and upgrade trees that scale the same way. If you're exploring how these systems adapt to a slower-paced, management-style game, it's worth looking at how a title like Farm Village structures its resource and progression loop — the underlying math (rate-based accumulation, offline calculation, tiered upgrades) is strikingly similar to what an idle RPG clicker needs, just wrapped in a different theme and pacing.

Common Pitfalls to Avoid

A few mistakes I see repeatedly in idle/clicker prototypes:

Storing floats for currency past a certain scale. Idle games generate very large numbers quickly. Standard float precision starts breaking down well before you hit typical late-game currency values. Switch to double, or better, a custom big-number type (many idle games implement a simple mantissa/exponent pair) once your numbers regularly exceed a few million.

Recalculating idle rate inside Update(). Idle-rate math should only run when something actually changes it — an upgrade purchase, a stage completion, a boss defeat. Recomputing it every frame is wasted work and makes the system harder to reason about.

Tight coupling between UI and game state. If your boss health bar directly polls BossCombatController.currentBossHealth every frame instead of subscribing to OnBossHealthChanged, you'll eventually hit desync bugs the moment you add pause states, background processing, or save/load transitions.

No offline cap communication. If you cap offline earnings at 8 hours but never tell the player, you'll get support messages and negative reviews from people who feel cheated. Surface the cap in the UI explicitly.

Wrapping Up

Idle RPG clickers are a genuinely good genre to study if you're trying to get better at systems-level game architecture, because the constraints are unusually tight: everything has to work correctly even when the player isn't actively playing, every number has to scale predictably over potentially hundreds of stages, and every system has to stay loosely coupled enough to support ongoing content additions without a rewrite.

If you're building your own version of this loop, start with the four systems above — tap combat, loot rolls, offline progression, and difficulty scaling — get each one working in isolation with unit tests where possible, and only then wire them together through events. It's a slower start than jamming everything into one big GameManager class, but it pays off the moment you need to add your fifth boss, your tenth loot tier, or your first live event.

What patterns have you used for offline progression or loot balancing in your own projects? I'd be curious to hear how other people have handled the big-number precision problem in particular — that one tends to bite everyone eventually.

Bonus: A Simple Big-Number Type

Since it came up above, here's a minimal mantissa/exponent implementation you can drop into a project once your currency values start exceeding what double can represent cleanly (roughly 1e15 before precision loss becomes visible, and 1e308 before you hit a hard ceiling):

[System.Serializable]
public struct BigNumber
{
    public double mantissa;
    public int exponent;

    public BigNumber(double mantissa, int exponent = 0)
    {
        this.mantissa = mantissa;
        this.exponent = exponent;
        Normalize();
    }

    private void Normalize()
    {
        if (mantissa == 0) { exponent = 0; return; }

        while (Mathf.Abs((float)mantissa) >= 10)
        {
            mantissa /= 10;
            exponent++;
        }
        while (Mathf.Abs((float)mantissa) < 1 && mantissa != 0)
        {
            mantissa *= 10;
            exponent--;
        }
    }

    public static BigNumber operator +(BigNumber a, BigNumber b)
    {
        int expDiff = a.exponent - b.exponent;
        if (Mathf.Abs(expDiff) > 15)
            return expDiff > 0 ? a : b; // difference too large to matter

        double aligned = expDiff >= 0
            ? a.mantissa + b.mantissa / Math.Pow(10, expDiff)
            : a.mantissa * Math.Pow(10, expDiff) + b.mantissa;

        return new BigNumber(aligned, Math.Max(a.exponent, b.exponent));
    }

    public override string ToString()
    {
        return exponent < 6
            ? Math.Round(mantissa * Math.Pow(10, exponent), 2).ToString()
            : $"{mantissa:F2}e{exponent}";
    }
}
Enter fullscreen mode Exit fullscreen mode

This isn't a full replacement for something like a proper arbitrary-precision library, but for the vast majority of idle games it's more than enough headroom, and it keeps your UI formatting (1.25e47 instead of an unreadable string of digits) clean without extra dependencies.

Testing the Loop Before You Build Content

One more practical tip: before you build out ten bosses and thirty loot items, get the four core systems running with placeholder data — a single boss, three loot entries, one upgrade — and play through several "sessions" manually, closing and reopening the game to simulate offline time. It's much cheaper to catch a broken progression curve or a save-state bug when you have one boss to debug than when you have thirty. Content is easy to add once the systems underneath it are solid; it's expensive to redo once you've built fifty stages on top of a shaky foundation.

Top comments (0)