ai generated, treasure chest, treasure-8063802.jpg

Get Some Stuff – Loot System

Kill a bad guy, get some stuff, Open a Chest, get some stuff, Complete a quest.. well you get the point. Here is a simple system that I have used to “Get Some Stuff”.

The Design: ( thinking about what would be the minimum/basic Functionality and then the Optimal functionality )

  • Minimum:
    • Spawn a copy of a gameObject at a specific location on demand.
  • Optimal:
    • Spawn any number of copies of a random gameObjects from a list of available objects where each entry is weighted to make some object less likely to drop.
    • Apply a Pop (force) direction to the object
    • Have a HasLoot component to attach to objects to handle the logic

Relative to each other the Optimal system is significantly more work but in absolute terms its not too much work to do. In the same way that $3.00 is 300% more than $1.00 but overall its still not much money. Lets do the optimal system.

The Loot item

Yes we are going to make the system able to copy any GameObject so a not wrong first thought would be to keep a list of GameObjects you want to Spawn. Consider what happens if you want to extend the system to have special specific GameObjects effect when dropped, or in our case weight each entry to make some items drop less than others. In this case a better approach would be to wrap the GameObject in a class that holds a reference to our GameObject and then decorate it with additional functionality when needed. If we just make a Game Object list then later decided we needed to add functionality here unwinding the entire list system and then resetting up each item would be required. Again here Minimum vs Optimal where time required is not much.

[System.Serializable]
public class Loot
{
    public GameObject prefab;
    public int chance = 1;  // higher is more likely - no limit
    // ParticleSystem effect - an example for later
}

The Loot Bag Item set

The next object on our list will be the container and logic for a set of Loots drop from. We will need it to hold a list of Loots, and be able to select a Loot from the list using the chance variable. One aspect of this worth explaining is how to use the chance portion. Since I chose to use int based chance system where the higher the chance of the item to drop the higher the number with virtually no limit we must have to upadate the _maxChanceRange variable anytime we add/remove loots or alter the chance of a loot item. to do this we just need to call UpdateMaxRange()

[System.Serializable]
public class LootBagItems
{
    [SerializeField] List<Loot> _lootList;
    public List<Loot> LootList => _lootList;
    int _maxChanceRange;    // must be updated accumulated range based on the sum chances

    public Loot GetRandomLoot()
    { 
        int dropIndex = Random.Range(0, _maxChanceRange - 1);
        int index = 0;
        foreach (var loot in _lootList)
        {
            index += loot.chance;
            if (dropIndex < index)
                return loot;
        }
        return null;
    }

    public void UpdateMaxRange()
    {
        _maxChanceRange = 0;
        LootList.ForEach(x => { _maxChanceRange += x.chance; });
    }
}

The Loot Bag Handler

Now we need the make the functionality to use the system. as in most cases in life there are lots of ways to accomplish this. we could actually make the LootBagItems class a monobehaviour and put our functionality there. For this solution we are going to do something a little different. Lets code a monobehaviour called LootBagHandler that will hold a reference to LootBag and handles everything we need including

  • Custom Drop Point
  • Drop/Pop Force ( physics )
  • Get and Drop From a separate object that Has Loots

This will also us to make global singleton LootBagHandler / System that we can call and use from any place that uses the same settings. ( There are potentially better approaches that singletons depending on the total situation such as… Channels, static events, Unity Events.. but those are less simple and require more setting up.

/// <summary>
/// Handles dropping of loot and quantity in given locations, 
/// holds an default list but the list can temporarily be set on the drop call
/// </summary>
public class LootBagHandler : MonoBehaviour
{
    // Another option would be to have a lootDropChannel Scriptable object to listen to - or static events
    public static LootBagHandler Instance => _instance; 
    static LootBagHandler _instance;

    [SerializeField] LootBagItems _defaultItems;
    public LootBagItems ActiveItems { get; private set; }
    public Vector3 dropOffset = Vector3.zero;

    // explosion effect
    public float popForce = 10f;

    void Start()
    {
        _instance = this;
        _defaultItems.UpdateMaxRange();
    }

    public void Drop( Vector3 position, int count=1, lootBagItems=null)
    {
        this.transform.position = position;
        Drop(count,lootBagItems);
    }

    public void Drop(int count=1, LootBagItems items = null)
    {
        ActiveItems = items == null ? _defaultItems : items;

        for( int i=0;i<count;i++)
        {
            _Drop();
        }

        ActiveItems = null;
    }

    protected void _Drop()
    {
        Loot loot = ActiveItems.GetRandomLoot();
        if( loot == null)
        {
            Debug.Log("Loot to drop was null, probably means there is no loot setup to drop");
            return;
        }

        // may purposely have empty loot.. ie change for no drop
        if (loot.prefab != null)
            _DropTheLoot(loot);
    }

    protected void _DropTheLoot(Loot loot)
    {
        var drop = Instantiate(loot.prefab);
        drop.transform.SetPositionAndRotation( transform.position + dropOffset, Quaternion.identity );
        var body = drop.GetComponent<Rigidbody>();
        if( body != null)
        {
            float r = Random.Range(0f, 1f);
            body.velocity = new Vector3(r, r * 2, r) * popForce;
            // addExplosion force would never generate a force here...?
        }
    }
}

The Loot Bag

Our Data Items and handler are coded up and ready to go but we need to be able to attach a LootBagItems to our in game Object that we want to have Loot to drop. We are going to make a LootBag component that simply has a serialized LootBagItems and a function called Drop. (this is the function we will call to drop our loot)

public class LootBag : MonoBehaviour
{
    public bool useDefaultItems = true;
    
    [SerializeField] LootBagItems lootItems;
    public LootBagItems LootItems => lootItems;
    
    void Start()
    {
        lootItems.UpdateMaxRange();
    }
    
    public void Drop(int count=1)
    {
        LootBagHandler=>Instance.Drop(transform.position, count, useDefaultItems ? null : lootItems)
    }
}

Okay, now that the coding part is done all we have to do is set it up in the Unity Inspector.

  • Make a new GameObject and name it LootHandler.
  • Attach a LootBagHandler component to it.
  • add Loot Items to the defaultItems list using the + button.
  • On any of your object you want to have loot to drop attach a LootBag component.
  • If you want them to have object to drop other than the default ones in the LootBagHandler uncheck the useDefaultLootItems and add your custom items to the lootItems list using the + button.

Now all that will be left for you to figure out is… How to call the LootBag.Drop(x)

  • Here are two hints
    • Use UnityEvents to have your droppable event ( openchest, onDeath ) call LootBag.Drop()
    • Use GetComponent<LootBag> on your droppable game component to get and call Drop() when needed.

Let me know if you have any questions.

Leave a Comment

Your email address will not be published. Required fields are marked *