Today I will put together a basic 2D inventory system. This session lasted 2 hours (including the writing of this tutorial).
>
download the unity package.
Resources
First we need artwork. I downloaded a
free pack created by 7Soul1.
The inventory will display (or hide) upon pressing the I key. Since I'm only prototyping I'm not very picky about functionality or appearance. All I need to do is display the inventory and perform a default action upon selecting an item.
I will use the Unity UI system.
Inside the editor, I created a canvas object. This is needed to display UI elements.
I notice that graphics cannot be dragged directly on stage. This is an interesting difference between the 3D and 2D workflow.
A simple, clickable button
I created a button and removed the text element. I configure the button.
- "simple" (not sliced)
- preserve aspect ratio
- change the icon. Notice that we need to change the texture type to "sprite" in order to access imported artwork.
I then create a script named "UseItem". This is to test the button we've just created. The script provides a single function, Use:
public void Use(){
Debug.Log ("Applying default action.");
}
Via the Button script, we now tie the button's OnClick function to the Use function of the UseItem class. Works.
A row of buttons
We don't know in advance (before runtime, that is) how many items the inventory will contain so I prefab the button as "InventoryItemButton".
I then create a
DisplayInventory script. Initially I will attach this to the canvas object. Let's display the same button we just created, but now we're instantiating the button programmatically.
GameObject e = Instantiate(itemUI,
Vector3.zero,
Quaternion.identity) as GameObject;
e.transform.SetParent (transform, false);
The prefabbed button must be attached to a GameObject field named itemUI for this to work.
Now we can create a row of buttons, like this:
int spacing = 34;
for (int i=0; i<3; i++) {
GameObject e = Instantiate (itemUI,
Vector3.zero+Vector3.right*spacing*i,
Quaternion.identity) as GameObject;
e.transform.SetParent (transform, false);
}
A data source
In order to populate the inventory with real data, we need a data source. Minimally, the data source can be a list of strings, with each string representing an item. We will then match each string to an icon.
string[] inventory = new string[]{ "Small healing potion", "Sword", "Mace" };
for (int i=0; i<inventory.Length; i++) {
GameObject e = Instantiate (itemUI,
Vector3.zero+Vector3.right*spacing*i,
Quaternion.identity) as GameObject;
e.GetComponent<Image>().sprite =
Resources.Load (inventory[i],typeof(Sprite)) as Sprite;
e.transform.SetParent (transform, false);
}
As you can see,
Resources.Load() is used to retrieve the sprites at runtime.
Adding functionality
One problem with the above is that our inventory items don't do anything. As "Strings" they don't have any functionality. What if our inventory items were actual game objects? Then, we would have a simple solution (I will discuss later why it makes sense to model inventory items as game objects)
For now, let's do the following:
- Create an inventory game object.
- Add several items (just use empties)
- To each item, we'll attach a component called Item. In fact I have several subclasses of Item, including Consumable and Equipment.
We then update the function used to create the inventory:
Transform inventory = GameObject.Find ("Inventory").transform;
for (int i=0; i<inventory.childCount; i++) {
Item item = inventory.GetChild (i).GetComponent<Item>();
GameObject e = Instantiate (itemUI,
Vector3.zero+Vector3.right*spacing*i,
Quaternion.identity) as GameObject;
e.GetComponent<Image>().sprite = Resources.Load (item.type,typeof(Sprite)) as Sprite;
e.GetComponent<UseItem>().item = item;
e.transform.SetParent (transform, false);
}
And the UseItem script is now updated to look like this:
public class UseItem : MonoBehaviour {
public Item item;
public void Use(){ item.Use (); }
}
Small embellishments.
I reattached the InventoryUI script to a standard UI panel. This takes a little configuring but felt right.
I also wanted consumable items to disappear after use. This requires rebuilding the UI. Unfortunately, Destroy() does not take effect immediately so that, if we query the UI to rebuild right away, the removed item would still show. I solved this with a coroutine but it felt a bit clunky. A better option is to un-parent the item being destroyed (since we're iterating the children of the Inventory object).
Why do we model inventory items as game objects?
Before they become part of the player's inventory, many items begin their life as game objects.
The question is then, what happens when we collect these objects? We probably want them to disappear from the game world.
An effective, but inefficient and inelegant solution to the problem is to just move them out of the way, possibly hiding renderable components and disabling colliders. If you use a model/view split in the hierarchy, you can also destroy the view, and all you have left is the "item logic".
Either way, you can preserve the original object, which preserves the functionality of the item.