Custom Random Item Dropper

In many RPG games, like Diablo or Destiny, when an item is spawned it is given random stats. Those random stats could depend on many external factors, and some of them might be inter dependent.

Example: The character kills an enemy that drops items. This enemy has probabilities of dropping an item with random rarity and attack stats. The random Attack stat is dependent on the rarity since you wouldn’t want a legendary item with less attack than a common item.

Unfortunatly there is no easy one solution fits all in this sort of feature. It most be custom made per game.

Below we show how it could be done for the example above.

We subclass the RandomItemDropper class which already gives the option to have a probability table for a list of base items.

By overriding the Drop function we can start modifying the attribute values of the items before dropping them.

Note: To override attribute at runtime, the item must be Mutable & Unique. You can learn more about Attributes here

We first get a random rarity using a weigthed probability table. We then use that rarity to get a randomly distributed attack specific to that rarity.
Animation Curves are a great and easy way to define distribution. All it requires is for the values to be set between 0 and 1 and it can be evaluated using a random t value.

Note: The example below uses the attribute name “Attack” for an Attribute of type float. If you use another attribute make sure to replace both the name and the type for it to match. The float value returned by the “GetAttackForRarity” function will need to be converted to the matching attribute type.

In this example we assume all items can have the same attack and rarity probability. But this can easily be skewed per item by getting some attribute value multipliers on the item before getting the random rarity or attack value.

Note: This is just one example, there are many ways this can be done. It highly depends on your type of game

 

[Serializable]
public enum Rarity
{
    Common,
    Rare,
    Legendary
}

[Serializable]
public struct RarityAmount
{
    [Tooltip("The Rarity.")]
    [SerializeField] public Rarity Rarity;
    [Tooltip("The distribution, must be between 0 and 1 in the time (horizontal axis).")]
    [SerializeField] public int Amount;
    public RarityAmount(Rarity rarity, int amount)
    {
        Rarity = rarity;
        Amount = amount;
    }
}
[Serializable]
public struct RarityDistribution
{
    [Tooltip("The Rarity.")]
    [SerializeField] public Rarity Rarity;
    [Tooltip("The distribution, must be between 0 and 1 in the time (horizontal axis).")]
    [SerializeField] public AnimationCurve Distribution;
}

/// <summary>
/// Custom Random Dropper which give random rarity and attack stats.
/// </summary>
public class CustomRandomRarityStatDropper : RandomItemDropper
{
    [SerializeField] protected RarityAmount[] m_RarityProbability;
    [SerializeField] protected RarityDistribution[] m_AttackProbability;
    protected int m_RarityProbabilitySum;
    protected RarityAmount[] m_RarityProbabilityTable;
    protected const string RarityAttributeName = "Rarity";
    protected const string AttackAttributeName = "Attack";
    
    protected override void Awake()
    {
        base.Awake();
        m_RarityProbabilityTable = new RarityAmount[m_RarityProbability.Length];
        //The probability table uses the sum of probability.
        m_RarityProbabilitySum = 0;
        for (int i = 0; i < m_RarityProbability.Length; i++) {
            m_RarityProbabilitySum += m_RarityProbability[i].Amount;
            m_RarityProbabilityTable[i] = new RarityAmount(m_RarityProbability[i].Rarity, m_RarityProbabilitySum);
        }
    }
    /// <summary>
    /// Drop a random set of item amounts.
    /// </summary>
    public override void Drop()
    {
        //Here we get a random list of items from the probability table.
        var itemsToDrop = GetItemsToDrop();

        AssignRandomStats(itemsToDrop);

        DropItemsInternal(itemsToDrop);
    }
    /// <summary>
    /// This is the function in which we will assign our custom random stats.
    /// </summary>
    /// <param name="itemAmounts">The item list to which we'll assign the random stats.</param>
    protected virtual void AssignRandomStats(ListSlice<ItemInfo> itemAmounts)
    {
        for (int i = 0; i < itemAmounts.Count; i++) {
            var item = itemAmounts[i].Item;
            if(item == null){ continue; }
            
            // If the item does not have rarity then ignore it.
            if (item.HasAttribute(RarityAttributeName) == false) {
                continue;
            }
            //Get a random rarity.
            var itemRarity = GetRandomRarity();
            //Assign the new rarity.
            var rarityAttribute = item.GetAttribute<Attribute<Rarity>>(RarityAttributeName);
            rarityAttribute.SetOverrideValue(itemRarity);
            //Get a random attack from the selected rarity.
            var newAttack = GetAttackForRarity(itemRarity);
            
            //Assign the new attack attribute.
            var attackAttribute = item.GetAttribute<Attribute<float>>(AttackAttributeName);
            attackAttribute.SetOverrideValue(newAttack);
        }
    }
    /// <summary>
    /// Get a random rarity from the probability table.
    /// </summary>
    /// <returns>A random rarity.</returns>
    public Rarity GetRandomRarity()
    {
        var randomProbabilityIndex = Random.Range(0, m_RarityProbabilitySum);
        
        //Binary search because probabilitySum is sorted by default
        var min = 0;
        var max = m_RarityProbabilityTable.Length - 1;
        var mid = 0;
        while (min <= max) {
            mid = (min + max) / 2;
            if (m_RarityProbabilityTable[mid].Amount == randomProbabilityIndex) {
                ++mid;
                break;
            }
            if (randomProbabilityIndex < m_RarityProbabilityTable[mid].Amount
                && (mid == 0 || randomProbabilityIndex > m_RarityProbabilityTable[mid - 1].Amount)) { break; }
            if (randomProbabilityIndex < m_RarityProbabilityTable[mid].Amount) { max = mid - 1; } else {
                min = mid + 1;
            }
        }
        return m_RarityProbabilityTable[mid].Rarity;
    }
    /// <summary>
    /// Get a random attack value from the rarity.
    /// </summary>
    /// <param name="rarity">The rarity to get the attack value from.</param>
    /// <returns>The attack.</returns>
    public float GetAttackForRarity(Rarity rarity)
    {
        for (int i = 0; i < m_AttackProbability.Length; i++) {
            if (rarity == m_AttackProbability[i].Rarity) {
                return m_AttackProbability[i].Distribution.Evaluate(Random.value);
            }
        }
        Debug.LogWarning("No attack distribution found for rarity "+rarity);
        return 0;
    }
}