Thursday, 5 May 2016

Inventory system

This is something I wanted to do for a while and now I finally got some time to.

Here you will find an example for an inventory system, that kind of inventory where you place items in slots, some of which are stackable and can be place multiple times in a single slot.

Once again, this is not the most efficient code, but it is a good starting point to improve upon. For this project I used this great asset.

Before I post any code, this is what the inventory will look like:

Fig 1
The item "apple" is stackable, which is why it is possible to place multiple of them in one slot. The shield and the sword are not, therefore they will always take one slot for each of them.

To begin with, create a Canvas, then create a Panel and remane it Inventory and also change its tag to InventoryUI. As a child of the canvas, create an empty element call Slot. Then again, place 2 objects as siblings children of this slo object, one is an Image called SlotChild, and the other one is a Text, which you do not need to rename it. For the SlotChild image, use the "f" sprite of the downloaded asset.

This is the hierarchy:

Fig 2


The way I imagined it, we would have an Item script which will serve as a base class for all items in the game. A variable of this script is going to be an object with the script InventoryItem attached to it, which is a prefab of the UI element used to represent the item itself in the inventory.

This is the Item script:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Item : MonoBehaviour {

 public GameObject inventoryItem;

 public string name;
 public bool stackable;

 // Use this for initialization
 void Start () {
 
 }
 
 // Update is called once per frame
 void Update () {
 
 }
}

For our purpose we only need to worry about the InventoryItem public gameobject. This is the representation of the item in the inventory, in form of UI component.

Now, create 3 prefabs (you can empty game objects) called Apple, Shield, Sword. These are supposed to be the intractable objects the player will see in the game. We won't need them for this example, but we will use these objects as items of the player's inventory.

The player's inventory is nothing but a single script, which I won't show you here because it only consists of a List of Item. Make the List public so we can easily access it and populate it.

This list will be used as a "database" for the items, which we will read and translate to a UI inventory.
Attach the PlayerInventory script to an empty object in the hierarchy.

Now, let's focus on the slots for a moment. When the player hover the mouse over the slot, this will change color to red, to signal that the slot is being selected. This will also assign the slot's transform to a Transform type variable in the inventory script, which we haven;t seen yet. This is simply used to keep track of the selected slots.

This is the Slot script, attached to the Slot UI object:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class Slot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler {

 public int quantity;
 public string label;

 InventoryUIController inv;
 Image myImage;
 Text quantityText;

 public GameObject slotchild;

 // Use this for initialization
 void Awake () {

  inv = GameObject.FindGameObjectWithTag("InventoryUI").GetComponent<InventoryUIController> ();
  myImage = GetComponentInChildren < Image> (); // SlotChild image
  quantityText =GetComponentInChildren<Text> (); 
  quantityText.text = ""; 
 
 }

 public void OnPointerEnter (PointerEventData eventData)
 {
  
  myImage.color = Color.red;
  inv.overSlot = slotchild.transform;
 }

 public void OnPointerExit (PointerEventData eventData)
 {
  myImage.color = Color.white;
  inv.overSlot = null;
 }

 public void CountChild()
 {
  int num = slotchild.transform.childCount;

  if (num >= 2)
   quantityText.text = num.ToString ();
  else
   quantityText.text = "";

  if (isSloEmpty ())
   label = "";  
 }

 public bool isSloEmpty()
 {
  
  return slotchild.transform.childCount == 0;
 }

 public void GetChildReady()
 {
  slotchild = transform.GetChild(0).gameObject;
 }
}

OnPointerEnter and OnPointerExit are used to detect when the mouse pointer is over the slot. When it happens, we update the transform variable in the InventoryUIController script, which we'll see soon and we change the color of the SlotChild image.

The CountChild() method on line 35 is used to count all the children of the SlotChild object, to see how many items are in the slot itself. Any time an item is dragged into a slot, it's made a child of it.

The method on line 48 is pretty self explanatory, and the GetChildReady() is used to grab the reference of the SlotChild.

Let's have a look now at the InventoryUIController script, which is attached to the Inventory UI object in the canvas:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class InventoryUIController : MonoBehaviour {

 public Transform overSlot;

 public GameObject slotPrefab;

 [HideInInspector] public List<GameObject> allSlots;

 Vector2 grid;
 RectTransform rect;
 PlayerInventory playerInventory;

 // Use this for initialization
 void Start () {

  grid = new Vector2 (4, 4);
  rect = GetComponent<RectTransform> ();
  allSlots = new List<GameObject> ();
  playerInventory = GameObject.FindGameObjectWithTag ("Player").GetComponent<PlayerInventory> ();

  for (int x = 0; x < 4; x++) {

   for (int y = 0; y < 4; y++) {

    GameObject slot = Instantiate (slotPrefab) as GameObject;
    slot.gameObject.name = "Slot_" + x + "_" + y;
    slot.GetComponent<RectTransform> ().SetParent (rect);

    float posx = (rect.sizeDelta.x/5) * (x+1);
    float posy = -(rect.sizeDelta.y / 5) * (y + 1);

    Vector3 pos = new Vector3(posx, posy,0);

    slot.GetComponent<RectTransform> ().anchoredPosition= pos;
    slot.GetComponent<Slot> ().GetChildReady ();

    allSlots.Add (slot);

   }

  }

  PopulateInvenory ();
 
 }

 void PopulateInvenory()
 {
  foreach (Item it in playerInventory.inventory) {
   GameObject invItem = Instantiate (it.inventoryItem) as GameObject;
   invItem.transform.SetParent (this.gameObject.transform);

   invItem.GetComponent<InventoryItem> ().CheckSlotAndDrop (allSlots[0].GetComponent<Slot>().slotchild.transform);
  }
 }
 
 public GameObject GetFirstEmptySlot()
 {
  GameObject obj = null;
  foreach (GameObject g in allSlots) {

   if (g.GetComponent<Slot> ().isSloEmpty ()) {
    obj = g.GetComponent<Slot> ().slotchild.gameObject;
    break;
   }
  }
 
   return obj;
  
  }
}

The overSlot Transform variable is the one we saw earlier in the Slot script, which containts a reference to the slot that is being selected.

After getting all the reference needed in the Start method, I proceed by creating the grid of slot in the canvas. I instantiate multiple copies of the Slot prefab, set the as children of he Inventory object and add them to a List of Slot.

When it comes to populate the slots, we iterate inside the PlayerInventory list of items and instantiate the inventoryItem gameobject which you have see on on line 3 in the Item script. This will create the UI element for the item, which is then set as a child of the Inventory canvas object  and drop in the slot. We'll see this method in just a moment. Finally, the method on line 57 simply returs the first empty slot available.

Now, the last script: InventoryItem. Before we see it, we need to create the prefab.For each item we need to make a UI representation of it. Simply create an Image in the canvas, and let's called it AppleInv. Attach the InventoryItem script to it and choose the "apple" sprite image. This is the inspector for this object:

Fig 3
Save this as a prefab and do the same thing for shield and sword, and remember to change the image.

This is the script:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class InventoryItem : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler{

 public bool stackable;
 public string name;

 Image im;
 InventoryUIController inv;
 RectTransform rect;

 Transform lastSlot;

 // Use this for initialization
 void Awake () {
 
  im = GetComponent<Image> ();
  inv = GameObject.FindGameObjectWithTag("InventoryUI").GetComponent<InventoryUIController> ();
  rect = GetComponent<RectTransform> ();
 
 }


 public void OnDrag (PointerEventData eventData)
 {
  rect.position = Input.mousePosition;
  rect.SetParent (inv.transform);

  if(lastSlot!=null)
  lastSlot.GetComponentInParent<Slot> ().CountChild ();
 }

 public void OnBeginDrag (PointerEventData eventData)
 {
  im.raycastTarget = false;
 }


 public void OnEndDrag (PointerEventData eventData)
 {
  im.raycastTarget = true;

  CheckSlotAndDrop (inv.overSlot);
   
 }

 public void CheckSlotAndDrop(Transform slot)
 {
  if (slot != null && slot.GetComponentInParent<Slot> ().isSloEmpty () || slot != null && slot.GetComponentInParent<Slot> ().label == name && stackable)
   DropInSlot (slot);
  else if (lastSlot != null)
   DropInSlot (lastSlot);
  else
   DropInSlot (inv.GetFirstEmptySlot ().transform);
 }

 public void DropInSlot(Transform s)
 {
  Debug.Log ("Dropping in " + s.gameObject.name);

  if(rect==null)
   Debug.Log ("NULLLL");
  rect.SetParent (s);
  lastSlot = s;
  s.GetComponentInParent<Slot> ().CountChild ();
  s.GetComponentInParent<Slot> ().label = name;
  rect.anchoredPosition = Vector3.zero;
 }
}

Let's start from the OnDrag method. This is an overridden method used to detect when the UI element is being dragged around. When it happens, we keep updating its position to match the mpuse pointer position, so as to get a dragging effect. We also set its parent to the Inventory object, so it is no longer a child of the slot it was contained in and, finally, we call the CountChild method of the containing slot, so it can update the text.

When the object is being dragged we need to set the raycastTarget variable to false (line 33). This is done so when the UI element of the item is being moved around, we can raycast through it. This is necessary as we need to detect the slot with the mouse pointer and we won;t be able to do it if the item that we are dragging blocks the raycast.

When we stop dragging, OnEndDrag(), we reset this variable to true and we call the CheckSlotAndDrop(...) method.

For this method we pass the transform variable in the InventoryUIController, which should contain the transform of the slot selected, passed by the Slot itself with the method OnPointerEnter, as we saw earlier.

If the slot is empty or if its is populated by an item of type stackable and we are dragging one with the same name, we place the item in it. The dropping, which happens in the method DropInSlot(...), is done by parenting the item to the slot transform. Also, when this happens, we update the label parameter of the slot by making it equal to the item's name. This is used to avoid different items populating the same slot. Here we also call the Count/Child method of the slot, so we can update the text which represents the number of items in one slot.

If the condition on line 47 is not met, it means that we are trying to place a non-stackable item in a slot that is not empty and contains an item of the same type. Basically, place a sword in a slot that already contains a sword. Or, we are dropping the item outside of a slot. If that's the case, we want to place the item back where we got it from. So, we simply drop it in the lastSlot slot, which is a reference to the last slot that was the parent to this item, which it was first assigned when the InventoryUIController populated all the slots. Also, during this process, it will happen that the item swill not have a slot parent first, as they are just being place in the slots for the first time. So, when it happens (line 52), we place these items in the first empty slot available, so as to populate them all correctly.

All this is probably very confusing, but I can assure you it works. To try it out, here an example scene of the project.

No comments:

Post a Comment