Wednesday, March 18, 2020

How to assign items to keys/buttons from inventory menu


What this is:       
The following is a guide on how to expand on MisterTaftCreates inventory system from his Zelda series on youtube. Specifically, this guide shows how to make an inventory system where items can be assigned to two different keys on the keyboard from the inventory menu (just like in classic Zelda games). This method can be expanded upon to work for more buttons if needed.  

When completed, you can expect an inventory system that looks something like: 



Disclaimer: I'm not a coder, my background is physics and math. I'm still learning about C# and unity, but I hope the following guide is helpful regardless to those also learning and starting out. 


Game Plan: 
To accomplish our goal, some changes need to be made to the inventory UI, and we must change some of the scripts we have worked with, as well as add at least one more script. I have summarized these changes below. 

UI Changes: 
  • UI Buttons (or images) should be added to the inventory panel (this can be done anywhere, I recommend in the same area where the use button is).  
Note:
  • The number of buttons/images made should be equal to the number of items you want to be able to assign at one time (example: if you want to be able to use a button with z or x, make two buttons, if you want to use items with z, x, or c, create 3, etc.) 

Scripts that will need some changing: 
    • PlayerMovement.cs
    • InventoryManager.cs 
    • Item.cs (or inventoryItem.cs)

Added Scripts: 
  • ItemManager.cs 
  • EquipmentItem.cs (optional) 

  
First:
  
Let's adjust the item.cs/inventoryItem.cs script (whatever your's is called). I have two suggestions for this: 
  
Option one: 
In the script (in the case below, it's called "InventoryItem"), make a boolean for whether the item is "equippable" (this will make it work similarly to taft's "useable" items) :
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.Events;
  5. [System.Serializable]
  6. [CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Items")]
  7. public class InventoryItem : ScriptableObject
  8. {
  9.     // new
  10.     public bool isEquippable
  11.     public string itemName;
  12.     public string itemDescription;
  13.     public Sprite itemImage;
  14.     public int numberHeld;
  15.     public bool usable;
  16.     public bool unique;
  17.     public UnityEvent thisEvent;
  18.     public void Use()
  19.     {
  20.         thisEvent.Invoke();
  21.     }
  22.     public void DecreaseAmount(int amountToDecrease)
  23.     {
  24.         numberHeld-=amountToDecrease;
  25.         if (numberHeld < 0)
  26.         {
  27.             numberHeld = 0;
  28.         }
  29.     }
  30. }
Click here for easy copy and paste of itemInventory.cs -> PasteBinInventoryItem

Option two (and what I did):
Make an enum in the item/inventoryItem script, have one be "equipment" 
  
  1. public enum ItemType
  2. {
  3.     Equipment,
  4.     // other possible types of equipment you might want (don't have to be the ones below!):
  5.     Health,
  6.     Magic,
  7.     Key,
  8.     Gear,
  9.     Default
  10. }
  

For full Item script -> PasteBinItem

If taking option 2: 
  • Make a new script called something like: "EquipmentItem.cs" (recommended folder placement: Scripts > ScriptableObjects > Inventory > Items).
  • You can copy and paste the below script for it: 
  
  1. using UnityEngine;
  2. [CreateAssetMenu(fileName = "New Equipment Object", menuName = "Inventory/PlayerInventory/Items/Equipment")]
  3. public class EquipmentItem : Item
  4. {
  5.     public void Awake()
  6.     {
  7.         type = ItemType.Equipment;
  8.     }
  9. }
  
Notes About Option 2:
  • For option 2, the item.cs script is just what I called the inventoryItem.cs script
  • The added enum should be adjusted for you're own purposes, the only type necessary for the tutorial is the "Equipment" type. Adjust the rest to your liking.
  • Differences between the two scripts are:
    • item.cs contains the enum, inventoryItem.cs does not
    • the enum contains some item categories I think I'll use later. For this tutorial, only the "Equipment" type is necessary. Remove/add whatever else you think you'll ned
    • item.cs has an "itemID" and a "itemRecievedDescription", both are not necessary for this tutorial.
  
Second: UI setup 
  
We'll need to actually setup the UI showing the equipment items. Ultimately, the code handling the Equipment UI will be very similar to the code handling the Use button UI. I have detailed instructions for setting up the UI below. 
  
step 1) Create an empty gameobject somewhere under the "inventory panel" gameobject in your scene. Name it something like: "equipment menu" 
  

step 2) Under the equipment menu object, make a UI button. (As mentioned earlier, this can be later adjusted to have more buttons, or switched to an image instead).
 
  

The "EquipText" is just some text I added that has "Equip" entered in the text line. It's above the button in my scene:
  
We will refer to this button as an "item button" because it will be showing the current item you have equipped (we will add more later). Place it where-ever you like. 
  
step 3) Above or below the item button, it would be nice to see something which indicates what key the item is assigned to. I've done this by adding text UI as a child of the item button: 
  
  
  
step 4) When the item is equipped, it would be nice to see it's picture appear in the button. Add in an image as a child of the button: 



Note: Above, mine says "SecondItemImage" this is just because I consider the x key to be the second item. You can make it "firstItemImage" instead. doesn't matter too much, as long as you are consistent with what you call the first item vs. second item.

When all is said and done, you should have something like the following:

  
In my example, the Text for the assigned key is over the button, while the image is placed inside the button. You may notice I also have a "z" button, we'll get to this very soon! (Like, the next step actually).


Optional: You can adjust the default color of the image to be same color as the button to make it look "invisible" (below example). Mess with the UI to your heart's content.


step 5) Duplicate (Ctrl + D) the item button you just created, move it to a new spot, and place new text indicating the key/button assigned to the item. In the example shown here, this other item button is assigned to the "z" key.

Inspector:

Scene:

  
Phew, now the UI is all set! Mess around with the layout all you like here, just make sure to keep everything having to do with the equipment items in the equipment menu object.

Important Note!
You may notice that your equipment menu overlaps your "Use" button. It does this for mine as well! This is OK! As long as you don't have an item that is both useable and equippable it will not cause an issue. If (for some reason) you do want an item that's useable and equippable, then move the Equipment menu or Use button somewhere where they wont overlap. When all is said and done, this is what my Inventory looks like in scene mode:





Third: Changing the InventoryManager.cs script to respond to equipment items. 
  
Important Note:

Below, I use "Item" instead of "inventoryItem". If your scriptable object for your items is called inventoryItem, then replace all instances of "item" with "inventoryItem".

 If you took Option 1 in "First":

In InventoryManager.cs, we want to activate the equipment menu when the selected item is equippable. To do this, we will need:
  • A reference to the equipment menu game object 
    • Add the below line to the InventoryManager.cs script, then place the Equipment menu gameobject just created in it:
    • [SerializeField] private GameObject equipmentMenu;
  • Add a void that takes the current item as a parameter, checks to see if the item is equippable, and then enables the Equipment menu if it is. 
    • For this, you can copy the code below and place it in your InventoryManager.cs class:
  1.     public void SetEquipmentMenu(Item newItem)
  2.     {
  3.         if (newItem.isEquippable)
  4.         {
  5.             equipmentMenu.SetActive(true);
  6.         }
  7.         else
  8.         {
  9.             equipmentMenu.SetActive(false);
  10.         }
  11.     }
  • Last, we need to modify SetUpDescriptionAndButton to call the void made above:
  1.     public void SetupDescriptionAndButton(string newDescriptionString,
  2.         bool isButtonUsable, Item newItem)
  3.     {
  4.         currentItem = newItem;
  5.         descriptionText.text = newDescriptionString;
  6.         useButton.SetActive(isButtonUsable);
  7.         SetEquipmentMenu(newItem);
  8.     }


If you took Option 2 in "First":

If you decided to go with Option 2 earlier, you will do the same exact thing as Option 1 (above). The only difference will be your SetEquipmentMenu void. Instead of using the code above, you will want to use:


  1.     public void SetEquipmentMenu(Item newItem)
  2.     {
  3.         if (newItem.type == ItemType.Equipment)
  4.         {
  5.             equipmentMenu.SetActive(true);
  6.         }
  7.         else
  8.         {
  9.             equipmentMenu.SetActive(false);
  10.         }
  11.     }
  
as your SetEquipmentMenu void. Call it in the SetUpDescriptionAndButton as shown above in Option 1.

For Everyone:
Check to see if it works! Make your sword "equippable"(option 1)/change it to an equipment type item (option 2), and select it in the inventory. The Equipment menu should appear when you do so. It won't be assigned to z or x yet, but we'll get to that soon!

Fourth: Let's Add a new script, we'll call it the "Item Manager"

Right now, your playerMovement script might have the sword and bow functions all wrapped within it. I'm going to say we should take those and put it in a new script called "ItemManager". If your not ready for big changes, then comment out your current sword and bow voids in your script with:
 /*
(voids to comment out placed here)
*/

If something goes wrong, just delete the /* and */, and things should be back to normal
  • Make a script called ItemManager, and copy and paste from the link below. You may need to change "tempMovement" to "facingDirection"
Link to ItemManager script: PasteBinItemManager

What the script looks like:


  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;


  4. public class ItemManager : MonoBehaviour
  5. {
  6.     [Header("Player Components")]
  7.     [SerializeField] InventoryManager playerInventoryMan;
  8.     [SerializeField] PlayerMovement playerInfo;
  9.     [SerializeField] AnimatorController anim;
  10.     [SerializeField] StateMachine myState;

  11.     // Note: I think Taft calls "tempMovement" the "facingDirection". (I don't remember which one he used in his PlayerMovement script!). Anyway, they're the same. It shouldn't be very significant in this case. (actually, looking at this now, I'm pretty sure it's not even needed, will update later if it isn't)
  12.     private Vector2 tempMovement = Vector2.down;
  13.     //private Vector2 facingDirection = Vector2.down;

  14.     // In my actual Item manager, I have things for my boomerang like item, bike, and so forth. But the below are relevant items to tafts tutorials:
  15.     [Header("Sword Item")]
  16.     private float WeaponAttackDuration = .33f;

  17.     [Header("Arrow Item")]
  18.     [SerializeField] GameObject arrowProjectile;


  19. /// This is a void that will be called each time the player presses a key associated with using an item (in this tutorial, the z and x keys). It checks the name of the item, and then calls the appropriate void to use that item. I would keep in mind that there is likely a better way to do this with dictionaries or something similar, but I've found it easy to simply add new items to be checked and then implemented this way.///

  20.     public void CheckAndImplementItem(Item thisItem, Vector2 tempMovement)
  21.     {
  22.         if (thisItem.itemName == "Sword")
  23.         
  24.             StartCoroutine(SwordCo());
  25.             tempMovement = Vector2.zero;
  26.             playerInfo.Motion(tempMovement);
  27.         }
  28.         else if (thisItem.itemName == "Bow")
  29.         // if you've followed up to the ability section, you might need an "ability" check here, and to add an ability reference above.

  30.             StartCoroutine(BowCo());
  31.           
  32.         }

  33.     }

  34.     // The next few voids are the actual logic behind the sword and bow attacks. This is why I suggested commenting them out in the PlayerMovement script. It will all be handled here instead now
        // Sword Item
  35.     public IEnumerator SwordCo()
  36.     {
  37.         myState.ChangeState(GenericState.attack);
  38.         anim.SetAnimParameter("attacking"true);
  39.         yield return new WaitForSeconds(WeaponAttackDuration);
  40.         myState.ChangeState(GenericState.idle);
  41.         anim.SetAnimParameter("attacking"false);
  42.     }
  43.     // End Sword Item

  44.     // Bow Item
  45.     private IEnumerator BowCo()
  46.     {
  47.         //animator.SetBool("attacking", true);
  48.         myState.ChangeState(GenericState.attack);
  49.         yield return null;
  50.         MakeArrow();
  51.         //animator.SetBool("attacking", false);
  52.         yield return new WaitForSeconds(.1f);
  53.         if (myState.myState != GenericState.receiveItem && myState.myState != GenericState.stun && myState.myState != GenericState.dead)
  54.         {
  55.             myState.ChangeState(GenericState.walk);
  56.         }

  57.     }
  58.      private void MakeArrow()
  59.      {
  60.         Vector2 temp = new Vector2(anim.GetAnimFloat("moveX"), anim.GetAnimFloat("moveY"));
  61.         Arrow arrow = Instantiate(arrowProjectile, transform.position, Quaternion.identity).GetComponent<Arrow>();
  62.         arrow.Setup(temp, ChooseDirection());
  63.      }
  64.     Vector3 ChooseDirection()
  65.     {
  66.         float temp = Mathf.Atan2(anim.GetAnimFloat("moveY"), anim.GetAnimFloat("moveX")) * Mathf.Rad2Deg;
  67.         return new Vector3(00, temp);
  68.     }
  69.     // End Bow Item


  70. }


Where do I put this script?

This script can attach to just about any gameobject in your scene, but I would recommend (to keep things organized), making an empty child object under your "Player" object, name the object "ItemManager", and place it there. You'll need to fill in the [SerializeField] variables with the appropriate gameobjects. The playerInfo, anim, and mystate, should all be from the "Player" object, the playerInventoryMan is most likely going to be from your "InventoryPanel" object.


Fifth: Adjusting the InventoryManager script and the  PlayerMovement script

Time to make some significant changes to PlayerMovement.cs and InventoryManager.cs. We'll tackle the InventoryManager script first.

InventoryManager.cs

In the inventory manager we need to add quite a few references. We need:
  • The x and z item buttons we created earlier 
  • A reference to the items we want to equip to those buttons
  • A reference to the item images so we can change them when an item is equipped
To do this, add the following references to your InventoryManager:


    // X and Z Button References:
         [SerializeField] private GameObject zButton;
    [SerializeField] private GameObject xButton;

    // Remember to change Item to inventoryItem below if needed!
    // Reference to Items/InventoryItems & item images:
   
public Item[] items;     public Image[] itemImages;

Set up in inspector:
  • Z button created earlier goes in zButton
  • X button created earlier goes in xButton
  • Items remains blank (or you could put a "default" item placeholder in)
  • Place the item images created earlier as children of the z and x buttons into the itemImages list. Important Note: Whatever itemImage you want as your "first" item, should be the first in the list, whatever image you want as your "second" item, should be second in the list. In this tutorial, z is the first item so it's image is first in the list, x is second so it is second in the list

We want to add two public voids for the z and x buttons. If z is pressed, we will set the currentItem to items[0], and change the itemImages[0] to the items image.
    public void zButtonPressed()
    {
        //set current item to z button (FirstItem input)
        items[0] = currentItem;
        itemImages[0].overrideSprite = items[0].itemSprite;
    }

It's the same thing for the x button, except we'll assign to items[1] and itemImages[1]

    public void xButtonPressed()
    {
        //set current item to x button (SecondItem input)
        items[1] = currentItem;
        itemImages[1].overrideSprite = items[1].itemSprite;
    }


In the OnEnable function, it may be a good idea to call the "SetEquipmentMenu" void created earlier:
    void OnEnable()
    {
        ClearInventorySlots();
        MakeInventorySlots();
        SetTextAndButton("", false);
        SetEquipmentMenu(currentItem);
    }

Our inventory Manager should be set now! We're almost ready to change the PlayerMovement script (which is the last step), but first, we need to go to:

  • Edit > ProjectSettings > InputManager
Add two inputs, one called "FirstItem", and the other "SecondItem". Make the first item's "positive button" z, and the second item's" positive button" x:


 


Finally, we move on to make some changes to:

PlayerMovement.cs:

In the PlayerMovement script, we need to add a reference to the ItemManager and the InventoryManager:

  1.     [Header("Item and Equipment Info")]
  2.     [SerializeField] private ItemManager itemManager;
  3.     [SerializeField] private InventoryManager playerInventoryMan;

Assign the ItemManager and Inventory Manager to those spots in your scene. 

Next we need to change the input void in the PlayerMovement. We want to check if either z ("FirstItem") or x ("SecondItem") is being pressed. If they are, we get the items currently in the "items" reference we added in the inventory manager, and we call the "CheckAndImplementItem" void made in the item manager. The resulting input void should look like:

    void GetInput()
    {
        // Remember to use inventoryItem instead of Item below if your scriptable object was named differently!
        Item firstItem = playerInventoryMan.items[0];
        Item secondItem = playerInventoryMan.items[1];
        if (Input.GetButtonDown("FirstItem") || Input.GetButtonDown("SecondItem"))
        {
            if (Input.GetButtonDown("FirstItem"))
            {
                itemManager.CheckAndImplementItem(firstItem, tempMovement);
            }
            else if (Input.GetButtonDown("SecondItem"))
            {
                itemManager.CheckAndImplementItem(secondItem, tempMovement);
            }
            tempMovement = Vector2.zero;
            Motion(tempMovement);
        }
        else if(myState.myState != GenericState.attack)
        {
            tempMovement.x = Input.GetAxisRaw("Horizontal");
            tempMovement.y = Input.GetAxisRaw("Vertical");
            Motion(tempMovement);
        }
    }



Some Notes:

  • Notice there's no "attack" input being checked here. That's because it's all wrapped up in the items now. You can always have the sword attack as a separate button of course (like in OOT), and continue to include it in the input, but this should serve our purposes for now.
  • It's commented in red above, but DO NOT put "Item" if your scriptable object for the inventory is actually named "inventoryItem" it won't work.
  • Optional Addition: My InventoryManager has an Update void that checks to see if the current item selected is equipment, and then proceeds to check if the x or z key is pressed, if they are, the items are assigned to those buttons. It looks like the following:

        private void Update()
        {
            if (currentItem.type == ItemType.Equipment)
            {
                if (Input.GetButtonDown("FirstItem"))
                {
                    zButtonPressed();
                }
                if (Input.GetButtonDown("SecondItem"))
                {
                    xButtonPressed();
                }
            }
            
        }
        ^ Only works for those that chose Option 2 in "First" earlier

  • If you chose option 1 earlier, you can instead do:
    private void Update()
    {
        if (currentItem.isEquippable)
        {
            if (Input.GetButtonDown("FirstItem"))
            {
                zButtonPressed();
            }
            if (Input.GetButtonDown("SecondItem"))
            {
                xButtonPressed();
            }
        }
        
    }

Test it out!

Test it out! You should be able to now use your inventory as shown in the video. When you create more items, you can just add them into the item manager (by adding an if statement for the name of the item, any necessary references, and then a void(s) that controls that item's logic). As an example, you can check out what my current ItemManager looks like in the link below: