Many games allow players to collect and carry around a large number of items (ex. RTS/MOBA/RPG games, Action Role-playing games, etc.), that's where the Inventory comes into play.
Inventory is a table of elements that provides quick access to player items and a simple way to organize them.
In this post, we will be learning how to program a simple Inventory System with Item Pick up and UI Drag & Drop in Unity.
Step 1: Create the Scripts
This tutorial requires 3 scripts:
SC_CharacterController.cs
//You are free to use this script in Free or Commercial projects//sharpcoderblog.com @2019using UnityEngine;[RequireComponent(typeof(CharacterController))]public class SC_CharacterController : MonoBehaviour{ public float speed = 7.5f; public float jumpSpeed = 8.0f; public float gravity = 20.0f; public Camera playerCamera; public float lookSpeed = 2.0f; public float lookXLimit = 60.0f; CharacterController characterController; Vector3 moveDirection = Vector3.zero; Vector2 rotation = Vector2.zero; [HideInInspector] public bool canMove = true; void Start() { characterController = GetComponent<CharacterController>(); rotation.y = transform.eulerAngles.y; } void Update() { if (characterController.isGrounded) { // We are grounded, so recalculate move direction based on axes Vector3 forward = transform.TransformDirection(Vector3.forward); Vector3 right = transform.TransformDirection(Vector3.right); float curSpeedX = speed * Input.GetAxis("Vertical"); float curSpeedY = speed * Input.GetAxis("Horizontal"); moveDirection = (forward * curSpeedX) + (right * curSpeedY); if (Input.GetButton("Jump")) { moveDirection.y = jumpSpeed; } } // Apply gravity. Gravity is multiplied by deltaTime twice (once here, and once below // when the moveDirection is multiplied by deltaTime). This is because gravity should be applied // as an acceleration (ms^-2) moveDirection.y -= gravity * Time.deltaTime; // Move the controller characterController.Move(moveDirection * Time.deltaTime); // Player and Camera rotation if (canMove) { rotation.y += Input.GetAxis("Mouse X") * lookSpeed; rotation.x += -Input.GetAxis("Mouse Y") * lookSpeed; rotation.x = Mathf.Clamp(rotation.x, -lookXLimit, lookXLimit); playerCamera.transform.localRotation = Quaternion.Euler(rotation.x, 0, 0); transform.eulerAngles = new Vector2(0, rotation.y); } }}
SC_PickItem.cs
//You are free to use this script in Free or Commercial projects//sharpcoderblog.com @2019using UnityEngine;public class SC_PickItem : MonoBehaviour{ public string itemName = "Some Item"; //Each item must have an unique name public Texture itemPreview; void Start() { //Change item tag to Respawn to detect when we look at it gameObject.tag = "Respawn"; } public void PickItem() { Destroy(gameObject); }}
SC_InventorySystem.cs
//You are free to use this script in Free or Commercial projects//sharpcoderblog.com @2019using UnityEngine;public class SC_InventorySystem : MonoBehaviour{ public Texture crosshairTexture; public SC_CharacterController playerController; public SC_PickItem[] availableItems; //List with Prefabs of all the available items //Available items slots int[] itemSlots = new int[12]; bool showInventory = false; float windowAnimation = 1; float animationTimer = 0; //UI Drag & Drop int hoveringOverIndex = -1; int itemIndexToDrag = -1; Vector2 dragOffset = Vector2.zero; //Item Pick up SC_PickItem detectedItem; int detectedItemIndex; // Start is called before the first frame update void Start() { Cursor.visible = false; Cursor.lockState = CursorLockMode.Locked; //Initialize Item Slots for (int i = 0; i < itemSlots.Length; i++) { itemSlots[i] = -1; } } // Update is called once per frame void Update() { //Show/Hide inventory if (Input.GetKeyDown(KeyCode.Tab)) { showInventory = !showInventory; animationTimer = 0; if (showInventory) { Cursor.visible = true; Cursor.lockState = CursorLockMode.None; } else { Cursor.visible = false; Cursor.lockState = CursorLockMode.Locked; } } if (animationTimer < 1) { animationTimer += Time.deltaTime; } if (showInventory) { windowAnimation = Mathf.Lerp(windowAnimation, 0, animationTimer); playerController.canMove = false; } else { windowAnimation = Mathf.Lerp(windowAnimation, 1f, animationTimer); playerController.canMove = true; } //Begin item drag if (Input.GetMouseButtonDown(0) && hoveringOverIndex > -1 && itemSlots[hoveringOverIndex] > -1) { itemIndexToDrag = hoveringOverIndex; } //Release dragged item if (Input.GetMouseButtonUp(0) && itemIndexToDrag > -1) { if (hoveringOverIndex < 0) { //Drop the item outside Instantiate(availableItems[itemSlots[itemIndexToDrag]], playerController.playerCamera.transform.position + (playerController.playerCamera.transform.forward), Quaternion.identity); itemSlots[itemIndexToDrag] = -1; } else { //Switch items between the selected slot and the one we are hovering on int itemIndexTmp = itemSlots[itemIndexToDrag]; itemSlots[itemIndexToDrag] = itemSlots[hoveringOverIndex]; itemSlots[hoveringOverIndex] = itemIndexTmp; } itemIndexToDrag = -1; } //Item pick up if (detectedItem && detectedItemIndex > -1) { if (Input.GetKeyDown(KeyCode.F)) { //Add the item to inventory int slotToAddTo = -1; for (int i = 0; i < itemSlots.Length; i++) { if (itemSlots[i] == -1) { slotToAddTo = i; break; } } if (slotToAddTo > -1) { itemSlots[slotToAddTo] = detectedItemIndex; detectedItem.PickItem(); } } } } void FixedUpdate() { //Detect if the Player is looking at any item RaycastHit hit; Ray ray = playerController.playerCamera.ViewportPointToRay(new Vector3(0.5F, 0.5F, 0)); if (Physics.Raycast(ray, out hit, 2.5f)) { Transform objectHit = hit.transform; if (objectHit.CompareTag("Respawn")) { if ((detectedItem == null || detectedItem.transform != objectHit) && objectHit.GetComponent<SC_PickItem>() != null) { SC_PickItem itemTmp = objectHit.GetComponent<SC_PickItem>(); //Check if item is in availableItemsList for (int i = 0; i < availableItems.Length; i++) { if (availableItems[i].itemName == itemTmp.itemName) { detectedItem = itemTmp; detectedItemIndex = i; } } } } else { detectedItem = null; } } else { detectedItem = null; } } void OnGUI() { //Inventory UI GUI.Label(new Rect(5, 5, 200, 25), "Press 'Tab' to open Inventory"); //Inventory window if (windowAnimation < 1) { GUILayout.BeginArea(new Rect(10 - (430 * windowAnimation), Screen.height / 2 - 200, 302, 430), GUI.skin.GetStyle("box")); GUILayout.Label("Inventory", GUILayout.Height(25)); GUILayout.BeginVertical(); for (int i = 0; i < itemSlots.Length; i += 3) { GUILayout.BeginHorizontal(); //Display 3 items in a row for (int a = 0; a < 3; a++) { if (i + a < itemSlots.Length) { if (itemIndexToDrag == i + a || (itemIndexToDrag > -1 && hoveringOverIndex == i + a)) { GUI.enabled = false; } if (itemSlots[i + a] > -1) { if (availableItems[itemSlots[i + a]].itemPreview) { GUILayout.Box(availableItems[itemSlots[i + a]].itemPreview, GUILayout.Width(95), GUILayout.Height(95)); } else { GUILayout.Box(availableItems[itemSlots[i + a]].itemName, GUILayout.Width(95), GUILayout.Height(95)); } } else { //Empty slot GUILayout.Box("", GUILayout.Width(95), GUILayout.Height(95)); } //Detect if the mouse cursor is hovering over item Rect lastRect = GUILayoutUtility.GetLastRect(); Vector2 eventMousePositon = Event.current.mousePosition; if (Event.current.type == EventType.Repaint && lastRect.Contains(eventMousePositon)) { hoveringOverIndex = i + a; if (itemIndexToDrag < 0) { dragOffset = new Vector2(lastRect.x - eventMousePositon.x, lastRect.y - eventMousePositon.y); } } GUI.enabled = true; } } GUILayout.EndHorizontal(); } GUILayout.EndVertical(); if (Event.current.type == EventType.Repaint && !GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition)) { hoveringOverIndex = -1; } GUILayout.EndArea(); } //Item dragging if (itemIndexToDrag > -1) { if (availableItems[itemSlots[itemIndexToDrag]].itemPreview) { GUI.Box(new Rect(Input.mousePosition.x + dragOffset.x, Screen.height - Input.mousePosition.y + dragOffset.y, 95, 95), availableItems[itemSlots[itemIndexToDrag]].itemPreview); } else { GUI.Box(new Rect(Input.mousePosition.x + dragOffset.x, Screen.height - Input.mousePosition.y + dragOffset.y, 95, 95), availableItems[itemSlots[itemIndexToDrag]].itemName); } } //Display item name when hovering over it if (hoveringOverIndex > -1 && itemSlots[hoveringOverIndex] > -1 && itemIndexToDrag < 0) { GUI.Box(new Rect(Input.mousePosition.x, Screen.height - Input.mousePosition.y - 30, 100, 25), availableItems[itemSlots[hoveringOverIndex]].itemName); } if (!showInventory) { //Player crosshair GUI.color = detectedItem ? Color.green : Color.white; GUI.DrawTexture(new Rect(Screen.width / 2 - 4, Screen.height / 2 - 4, 8, 8), crosshairTexture); GUI.color = Color.white; //Pick up message if (detectedItem) { GUI.color = new Color(0, 0, 0, 0.84f); GUI.Label(new Rect(Screen.width / 2 - 75 + 1, Screen.height / 2 - 50 + 1, 150, 20), "Press 'F' to pick '" + detectedItem.itemName + "'"); GUI.color = Color.green; GUI.Label(new Rect(Screen.width / 2 - 75, Screen.height / 2 - 50, 150, 20), "Press 'F' to pick '" + detectedItem.itemName + "'"); } } }}
Step 2: Set up the Player and Inventory System
Let's begin by setting up our Player:
- Create a new GameObject and call it "Player"
- Create a new Capsule (GameObject -> 3D Object -> Capsule) remove the Capsule Collider component then move the Capsule inside the "Player" Object and lastly change its position to (0, 1, 0)
- Move the Main Camera inside the "Player" Object and change its position to (0, 1.64, 0)
- Attach SC_CharacterController script to "Player" Object (it will automatically add another component called Character Controller, change its center value to (0, 1, 0))
- Assign the Main Camera to a "Player Camera" variable at SC_CharacterController
Now let's setup Pick Up items - these will be Prefabs of the items that can be picked in the game.
For this tutorial, I will be using simple shapes (Cube, Cylinder, and Sphere) but you can add different models, possibly some particles, etc.
- Create a new GameObject and call it "SimpleItem"
- Create a new Cube (GameObject -> 3D Object -> Cube), scale it down to (0.4, 0.4, 0.4) then move it inside "SimpleItem" GameObject
- Select "SimpleItem" and add a Rigidbody component and a SC_PickItem script
You will notice there are 2 variables in SC_PickItem:
Item Name - this should be a unique name.
Item Preview - a Texture that will be displayed in the Inventory UI, preferably you should assign the image that represents the item.
In my case the Item Name is "Cube" and Item Preview is a white square:
Repeat the same steps for the other 2 items.
For Cylinder item:
- Duplicate a "SimpleItem" Object and name it "SimpleItem 2"
- Remove child Cube and create a new Cylinder (GameObject -> 3D Object -> Cylinder). Move it inside "SimpleItem 2" and Scale it to (0.4, 0.4, 0.4).
- Change the Item Name in SC_PickItem to "Cylinder" and the Item Preview to an image of a cylinder
For Sphere Item:
- Duplicate a "SimpleItem" Object and name it "SimpleItem 3"
- Remove the child Cube and create a new Sphere (GameObject -> 3D Object -> Sphere). Move it inside "SimpleItem 3" and Scale it to (0.4, 0.4, 0.4).
- Change the Item Name in SC_PickItem to "Sphere" and Item Preview to an image of a sphere
Now Save each item into Prefab:
The items are now ready.
The last step is to set up the Inventory System:
- Attach SC_InventorySystem to "Player" Object
- Assign a Crosshair Texture variable (You can use the image below or get high-quality crosshair textures from here):
- Assign SC_CharacterController to the "Player Controller" variable in SC_InventorySystem
- For the "Available Items" assign previously created item Prefabs (Note: This should be Prefab instances from the Project view and not Scene objects):
The inventory system is now ready, let's test it:
Everything works as expected!