Taking Inventory Pro Item Collections To The Extreme

2016-09-08,

Inventory Pro's Item Collections

Item Collections in Inventory Pro are powerful collections that can contain items, stack, unstack, merge items and much, much more.

As we'll soon see, however, these features aren't only useful when creating a traditional RPG inventory system, but can be used in many aspects of your game. 

For example, they also allow you to give the NPCs in your game items like weapons or gear, or as we'll do in this tutorial, setup little minion workers who will gather items from around the map and move them to a specific area.

 

What we'll be building

Each minion will get their own item collection, which will store the items they're gathering. The advantage of using a collection here is that we can use the collection's restrictions to prevent the gatherer (our minion) from picking up certain items.

For example, gatherer A can only pick up consumable items while gatherer B can only pick up resources.

In addition to that, we can limit by item type, stats, total weight and much more.

 

Let's get coding!

To create the minion worker setup, we'll first need a resource area to store our items. This area will contain the items our little minions gather.

using UnityEngine;
using System.Collections.Generic;
using Devdog.InventoryPro; // Don't forget to include the Devdog.InventoryPro namespace

public class ResourceArea : MonoBehaviour
{
    // Create a list of resources. The ItemAmountRow contains a reference to the item and an amount.
    public List resources = new List();
}

The collection

To store our items, we're obviously going to need that collection we talked about before. Making a collection in Inventory Pro is very simple; Just inherit from ItemCollectionBase and override whatever feature you'd like to modify.

using UnityEngine;
using System.Collections;
using Devdog.InventoryPro;

// Inherit from ItemCollectionBase
public class MyItemCollection : ItemCollectionBase
{
    // Override the initialCollectionSize to give our minion a starting collection of 8 slots.
    [SerializeField]
    private uint _initialCollectionSize = 8;
    public override uint initialCollectionSize
    {
        get { return _initialCollectionSize; }
        set { _initialCollectionSize = value; }
    }

    // FillUI is called when the collection is populated.
    protected override void FillUI()
    {
        // Fill the collection with data slots (these are not shown in the editor or UI, as they only reside in memory)
        items = new ICollectionItem[initialCollectionSize];
        for (int i = 0; i < initialCollectionSize; i++)
        {
            items[i] = new ItemCollectionDataObj(); // Fill it using our data object
        }
    }
}

Item collections store a 'slot', which in turn contains the item. The slot is responsible for any UI and input handling.

In our case, we don't want to show any UI elements, or handle any user input, so we're just going to create a ItemCollectionDataObj class "slot", which has no UI and no input handling (it will just wrap the item, that's it).

using UnityEngine;
using System;
using Devdog.General;
using Devdog.InventoryPro;

public partial class ItemCollectionDataObj : ICollectionItem
{
    [NonSerialized]
    private InventoryItemBase _item;

    public virtual InventoryItemBase item
    {
        get
        {
            return _item;
        }
        set
        {
            _item = value;
            if (_item != null && itemCollection != null)
            {
                if (itemCollection.useReferences == false)
                {
                    _item.itemCollection = itemCollection;
                    _item.index = index;
                }
            }
        }
    }

    [NonSerialized]
    private uint _index;

    [HideInInspector]
    public virtual uint index
    {
        get
        {
            return _index;
        }
        set
        {
            _index = value;
            if (item != null && itemCollection && itemCollection.useReferences == false)
                item.index = value;
        }
    }

    [NonSerialized]
    private ItemCollectionBase _itemCollection;

    public virtual ItemCollectionBase itemCollection
    {
        get
        {
            return _itemCollection;
        }
        set
        {
            _itemCollection = value;
            if (item != null && itemCollection != null && itemCollection.useReferences == false)
                item.itemCollection = value;
        }
    }

    public void TriggerContextMenu()
    {
        DevdogLogger.LogWarning("Trigger actions can't be used on data wrapper.");
    }

    public void TriggerUnstack(ItemCollectionBase toCollection, int toIndex = -1)
    {
        DevdogLogger.LogWarning("Trigger actions can't be used on data wrapper.");
    }

    public void TriggerDrop(bool useRaycast = true)
    {
        DevdogLogger.LogWarning("Trigger actions can't be used on data wrapper.");
    }

    public void TriggerUse()
    {
        DevdogLogger.LogWarning("Trigger actions can't be used on data wrapper.");
    }

    public void Repaint()
    {

    }

    public void Select()
    {

    }
}


​

And finally, our minion

Next, we'll need our minion, who is going to run around and gather the items.

Once the minion has grabbed some items, it's going to run back and drop off the items, after which it will go back and gather some more.

For the sake of this tutorial, we'll mix the UI code and movement controller with the gathering; Normally you'd seperate these, but to keep things simple they are combined below.

using UnityEngine;
using System.Collections.Generic;
using Devdog.General;
using Devdog.InventoryPro;
using UnityEngine.UI;

[RequireComponent(typeof(MyItemCollection))]
public sealed class MyResourceGatherer : MonoBehaviour
{
    [Required]
    public Transform carryToLocation;

    [Header("Movement")]
    public float movementSpeed = 2f;

    private Vector3 _movementTarget;
    private MyItemCollection _collection;
    private static ResourceArea[] _resourceAreas;

    private void Awake()
    {
        _collection = GetComponent();
        if (_resourceAreas == null)
        {
            _resourceAreas = FindObjectsOfType();
        }

        UpdateRandomResourceTarget();
    }

    private void UpdateRandomResourceTarget()
    {
        _movementTarget = GetMovementTargetFromRandomResourceArea(_resourceAreas);
    }

    private Vector3 GetMovementTargetFromRandomResourceArea(IList areas)
    {
        // Just to be safe. If there's no area's in the world, return 0,0,0
        if (areas.Count == 0)
        {
            return carryToLocation.position;
        }

        // Grab a random area from the given IList
        return areas[Random.Range(0, areas.Count)].transform.position;
    }

    private void Update()
    {
        // A very nasty way of walking... You would of course write something cleaner, right... right?
        var dir = _movementTarget - transform.position;
        dir.y = 0f; // Make sure we never go up or down...
        dir = dir.normalized;
        transform.position += dir * movementSpeed * Time.deltaTime;
    }

    private void OnTriggerEnter(Collider other)
    {
        var resourceArea = other.GetComponent();
        if (resourceArea != null)
        {
            // Grab some resources and add them to our collection
            var items = GrabOneOfEachResourceAndCreateItemInstances(resourceArea);

            // Add all items to our item collection. This will fail if not all items can be stored!
            foreach (var item in items)
            {
                var stored = _collection.AddItem(item);
                if (stored)
                {
                    Debug.LogFormat(this, "Gatherer gathered {0} item", item.name);
                }
            }

            // Grabbed the resources, now let's head back to deliver these goods
            _movementTarget = carryToLocation.position;
        }

        if (carryToLocation == other.transform)
        {
            // At the carry to location
            DropAllItemsAtLocation(carryToLocation.position + Vector3.up * 2f + Random.insideUnitSphere * 2f);

            // And get back to work, gather more resources.
            UpdateRandomResourceTarget();
        }
    }

    public void DropAllItemsAtLocation(Vector3 pos)
    {
        foreach (var slot in _collection.items)
        {
            if (slot.item != null)
            {
                // Drop the item and set it's position
                slot.item.transform.SetParent(null);
                slot.item.transform.position = pos;

                // And finally, enable the item so it can be seen by the user.
                slot.item.gameObject.SetActive(true);
            }
        }

        _collection.Clear(); // Clear the collection of it's items
    }

    private InventoryItemBase[] GrabOneOfEachResourceAndCreateItemInstances(ResourceArea resourceArea)
    {
        // Grab one of each resource from the resources pile.
        var elements = new List(resourceArea.resources.Count);
        for (int i = 0; i < resourceArea.resources.Count; i++)
        {
            elements.Add(new ItemAmountRow(resourceArea.resources[i].item, 1));
            resourceArea.resources[i] = new ItemAmountRow(resourceArea.resources[i].item, resourceArea.resources[i].amount - 1);
        }

        // Remove depleted resources from the pile, which also avoids other gatherers from taking any.
        resourceArea.resources.RemoveAll(o => o.amount <= 0);

        // Convert the rows to actual items and make sure no items are exceeding the maximum stack size.
        var items = InventoryItemUtility.RowsToItems(elements.ToArray(), true);

        // Disable the items, we don't need them right now.
        foreach (var item in items)
        {
            item.gameObject.SetActive(false);
        }

        return items;
    }
}


And that's all the code we need

Now we can create our gather areas and drop-off location, so that our minions can start gathering. First, let's set up a resource area.

Don't forget to add a box trigger. We will be using this to see if our minion has reached its destination.

And for the minion itself, we'll create a new capsule and add the MyResourceGatherer component to it. 

The "Carry To Location" is the location where the minion will drop off our items and go back to gather more. This should also be a trigger.

Finally, we may want to show what our minions are carrying using some sort of UI.

To do this, we add a world space canvas to each of our minions and add a text field. This text field will display the current weight the minion is carrying.

The ItemCollectionWeightUI is a component built-in to Inventory Pro, which displays the set collection's weight, and automatically updates it if the collection is in any way changed.

 

All done :)

Well done, we're all done!

Care to test it out for yourself? Download the Unity file package here (contains everything we did above, but does require Inventory Pro to work).

New to Inventory Pro? This was just a tiny fraction of what you can do with the most popular inventory system for Unity, which you can check out here.

You May also Like

10:30
7 Things Every Game Developer Needs on Their Website

Prior to Devdog, I ran an online games industry news media for and about Nordic game developers for nearly two years.

During that time, I often reached out to new game developers directly, apart from interviewing the ones who sent a press release directly to me.

10:30
5 Ways to Identify if a Unity Asset is Worth Buying

A few weeks ago, we bought a Unity Asset that really let us down.

In developing our own assets, we spend a lot of time on the Asset Store, researching the market and getting inspired.

About us

We're a developer and publisher of best-in-class tools and asset for the Unity game engine. We develop and sell but also publish products like Odin for talented developers and artists around the world.

Subscribe