I'm trying to figure out a good way of handling a system of upgrades in a game where multiple upgrades (including things the player can't directly purchase, e.g. achievement bonuses) can affect the same value (e.g. "+X% more income" or "Y% faster production").

I've seen at least three different approaches and I'm not keen on any of them and was wondering if there was another pattern that would work.

The approaches I know of:

  • Keep an array of all possible upgrades, loop over them any time the value needs to be retrieved. Computationally expensive, as the value is not cached.

  • Keep a cached value and recompute only when the upgrades are purchased. Requires having a value for every possible modifier as a variable in the code.

  • Keep a list of all achievements/upgrades (likely already done) and if-check only the ones relevant. Requires large blocks of code in cases where multiple upgrades affect the same value.


What you'd generally do is make a system that optimizes along the following criteria:

  • Easy to implement
  • Easy to debug
  • Fast enough

To make it easy to maintain, put it in a class:

class Upgrades
    void Add(Upgrade u)
    void Remove(Upgrade u)
    float GetSpeedBonus()
    float GetProductionBonus()
    float GetIncomeBonus()

As you can see from the class interface, there's never a need to iterate through all possible updates. You need to tell the code if a new update applies, and telling the code means telling the Upgrades class (if you don't want to create a mess of spaghetti code). You do not "enable" an upgrade by setting a magic flag on an achievement. You enable an upgrade by telling the Upgradesclass that the upgrade is enabled.

How you optimize the code inside the class is then pretty much irrelevant: it's a problem you can solve when it becomes a problem. If you want solve it, you solve it by caching values for speed, production, income, etc.

A possible pseudocode implementation looks somewhat like this:

public class Upgrades
    public void Add(Upgrade u)
       // toDo: if some kind of update can or cannot 
       // be added multiple times, implement that here

    public void Remove(Upgrade u)
    public float SpeedBonus() { get { return _speed; } }
    public float ProductionBonus() { get { return _production; } }
    public float IncomeBonus() { get { return _income; } }

    void RecalculateCache 
       // this can be a very complicated function depending on 
       // the rules you have for combining different upgrade types

A tempting optimization is to change the code to only do a partial recalculation of the cache on adding or removing a update. The downside of such an optimization is that changing the formula for aggregating updates will become much more challenging. It is likely that the formula will be changed at some point, e.g. to ensure the effects of upgrade "foo" and upgrade "bar" don't stack. So I advise against this particular optimization, unless updates change excessively often (multiple times per frame). A more worthwhile optimization may be a way that enables/disables the recalculation of the cache while multiple updates are being added.

| improve this answer | |
  • \$\begingroup\$ One thing I might do to improve the pseudo code here would be not to reset the values to zero and re-loop, just apply/remove the logic for the given Upgrade in the Add and Remove functions directly. So if it's supposed to add or multiply by a value, you would add/multiply it to the current value in Add and you would subtract/divide it from the current value in Remove. Though I suppose you may run into issues with order of operations if you do both addition and multiplication (in both cases) \$\endgroup\$ – Richard Marskell - Drackir Apr 2 '17 at 14:02
  • \$\begingroup\$ @RichardMarskell-Drackir Addressed in an edit. \$\endgroup\$ – Peter Apr 2 '17 at 14:36
  • \$\begingroup\$ I think this is pretty close what I ended up with. I've got the values stored elsewhere (the player object) and I don't have a dedicated recalc function (yet) other than serialization (upgrade state is stored in the save file, when read in it re-modifies the values as it deserializes). \$\endgroup\$ – Draco18s no longer trusts SE Apr 3 '17 at 0:12

Have you think about implementing a Decorator pattern in here?

Every unlocked bonus would then "decorate" a specific property regardless of the number of modifiers already unlocked and active.

With such a system you'd be able to add bonuses on the fly, activate them, deactivate them, and have an efficient and precise computation.

Also, even if computation time won't be that long, as you seem to want optimization when possible I guess you can even put the bonus value in cache every time a property gains/loses a layer of decoration.

| improve this answer | |
  • \$\begingroup\$ And have 100 instances created if it was modified 100 times? I mean if you decorate every decorated object. Otherwise, it makes no sense and he would just create with 100 bonuses 200 instances and would have too loop through them to get right value. \$\endgroup\$ – Candid Moon _Max_ Mar 1 '17 at 9:48
  • \$\begingroup\$ I've heard of the decorator pattern, but implementing it confuses me. \$\endgroup\$ – Draco18s no longer trusts SE Mar 1 '17 at 17:06

The best way I know right now is using Dictionary<IUpgrade>. Now you don't have to loop, you just need to get an appropriate key when you need this upgrade. I would suggest using an enum, because you can simply make it [Serializable] if it was private (I mean the field of class, not enum declaration - it should be public) and chose appropriate type for a key in this dictionary. Also, in the editor it's easy to choose the type for an item or some shop or anything, even in code. You can get a key by casting an enum to (int). Performance: very good, as an enum is int after compilation, though don't use an enum for a key without casting - it has much worse performance.

Now you have to implement behavior where you get right data for right event. For example, you have an event public class Payment for which you need to take into acount an income % bonus, you go to Player that has this dictionary and say to him - hey give me a data for income % bonus. IncomeUpgrade incomeUpgrade player.GetUpgrade<IncomeUpgrade>(UpgradeType upgradeType); - though I would do it not in Player class (In some component player has), it's just an example. Now that you have it, you can give player his payment with PayPlayer(Player player) { here you use code above - then : player.TakePayment(standartPayment * incomeUpgrade.percent + incomeUpgrade.bonusGold) } where UpgradeType is an enum.

public T GetUpgrade<T>(UpgradeType upgradeType){
    return upgradeComponent.DictionaryOfUpgrades[(int)upgradeType] as T;
    // If you don't want to cast an object by as T look at this question, 
    // I have the same problem: [Without Object Cast][1] , though you shouldn't really care about casting because it's done once in a time and it will mean nothing to performance. 
    // In my case I have 100000 calls per second, 1666 calls per frame. 
    // Also if you look at this benchmark [Type Cast Performance][1] - casting even 1 million takes only 0.5+- ms.
    // I guess I shouldn't worry about this at all too, but I'm just curious about the ways it can be implemented.

Without Object Cast

Type Cast Performance

I will write here other solution without using enum mb, because for every class you would have to add new upgradeType in enum, but for now it's the most performant way. No O(n) or something like this, just O(1) with Dictionaries and components.

| improve this answer | |
  • \$\begingroup\$ How does this handle multiple upgrades that affect the same value? E.g. I want an upgrade that increases income by 10%, another more expensive one that increases income again by an additional 20%, and a really-really expensive one that increases income again any another 50%. All three would use the same enum value, wouldn't they? Or is this just a cleaner method of storing the cached values? \$\endgroup\$ – Draco18s no longer trusts SE Mar 1 '17 at 17:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.