I've posted this a tutorial a little while ago in another forum as a reply, but thought a bit more about the issue and come up with some changes, and thought it could be helpful for others.

I'm no expert in Unity or ORK so this can also be seen as a learning exercise on avoiding crappy code :)

The goal of this exercise is produce a western style grid based Drag and Drop Inventory and Character screen like the following (excuse programmer graphics):

image

This is not a tutorial on Drag and Drop and just includes a base class you can inherit from to give any mono-develop class some dragging capability. The base class is pretty useful for adding Drag/Drop to almost anything in Unity.

The drag/drop may look like a lot of code, but it is generic (Does not rely on ORK) and handles a few things including leaving a faded icon in place when dragging and dealing with layer issues (always dragged on top).

That code is a mixture of code I discovered, tutorials I did and my changes over time. I hope it works as well as I think it does.

Change Log
Ver 0.1: Initial post in a Support forums as a reply to another user
Ver 0.2: New post to Tips, Tricks and Tutorials
Changed code to read the ORK inventory quantity limit settings and removed inventory limit property from scripts
Cleared up "the idea" text a bit
Ver 0.3: Added ORK Status.ResetStatus when Equipment is dropped onto a new slot to ensure ORK status values are applied (Thanks to orcish_sailor)

The idea
We have three things we are working with to achieve a inventory system:
1) A prefab image for the inventory item with an attached script for dragging it around
2) A scroll view with a grid component and a control script attached that populates the grid with the inventory items prefabs
3) A prefab image for each equipment slot with attached script to deal with the DROP event

Basically I wanted to be able to really do whatever I wanted with the GUI while leaving ORK to do the heavy lifting.

The Systems
- The Inventory image prefab at a basic level has two images, the one being a border image, the second being the actual inventory item. If there is no item for that slot, the inventory image is hidden and we just see the border
- Likewise the equipment slot works in a similar way. If you have baked in border images already as part of a background for example, you could just use one image
- The scroll view has a controller script on it that clears and populates the list on demand
- The inventory item prefab has a drag script on it that allows dragging that image (and leaving a ghost behind)
- The inventory item, when dragged, contains a link to the ORK.IInventoryShortcut so that when dropped, the receiving slot can decide if this is a good idea or not.
- A second "equipment slot" prefab is created with a script handling drag *AND* drop. It has two images same as the Inventory Item.
- You add as many prefabed equipment slots to your screen as you want and lay them out as you want. Each one has a property with a ID for the ORK equipment slot it is matched to
- The work deciding if something is going to be equipped happens in the DROP event, the dragged item is fairly "dumb" and just knows how to be dragged around

Riders:
- I did not see for a way in ORK to know inventory item A is in inventory slot X (and so on) it just seemed to be a linear list. So this code doesn't allow you to drag items around the inventory and place in specific slots.
- You don't *ACTUALLY* need the border images, this can all be done with one image but it may look a little odd.
- Likewise you can add a lot to the prefabs, item counts, names etc
- More can be done with reading ORK settings, such as greying out AVAILABLE but LOCKED equipment slots.

Enhancements:
- You can easily reduce the list of inventory items returned from ORK by items/weapons/equipment etc. So you can add filter buttons to the GUI to allow the user to JUST see Weapons for example
- You can determine if something is an Item and add a counter text to the item so the user can see how many items are in the stack.
- Add more images to the inventory or equipment slots prefabs to do stuff like masks or special effects. As long as the script is attached to the image that is ACTUALLY the inventory icon, all should work fine. (that could be changed too if you really wanted by making a property for the icon image in the script)
- If you want to drag the equipment off their slots to remove equipped items, you can add a OnDrop event on the Inventory script, *or* just code it in the Equipment drag script that *any* drag off is removing that item.
- You can also enhance the equipment slot OnDrop events to handle an Equipment drag event so that you can drag from equipment slot to another equipment slot.
- Maybe add a text box to each equipment slot to describe what it is? Really lots you can do.

Step -1: Make sure ORK actually works
Whoops, got half way through this and realised that my new scene did not have ORK in it!

Create a empty scene for all this as usual and add a new ORK Game Starter with Quick Testing enabled.

Add a spawn point that spawns your Player Combatant that actually has an inventory

Step 0: Base class to handle dragging

Create a script called DragDropBaseClass.cs and paste in the following code.

The idea here is we capture the begin, end drags etc and create a duplicate image to the one we are wanting to drag, and ensure that image is on the top canvas layer so while dragging it does not fall behind other UI objects.

It also reduces the Alpha of the image you left behind to ghost it a bit (Could FX that up a bit with a material to make the image grey or something too.)

On "drop" all this code does is destroy the dragged image and reset the original back to full Alpha.

It is up to the surface that was "dropped on" to decide if anything needs to actually change.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class DragDropBaseClass : MonoBehaviour, IEndDragHandler, IDragHandler, IBeginDragHandler
{
public bool dragOnSurfaces = true;

public GameObject m_DraggingIcon;
internal RectTransform m_DraggingPlane;

public void OnEndDrag(PointerEventData eventData)
{
if (m_DraggingIcon != null)
{
Destroy(m_DraggingIcon);
Image img = transform.GetComponent<Image>();
img.color = new Color(img.color.r, img.color.g, img.color.b, 1f);
}
}

public void OnDrag(PointerEventData data)
{
if (m_DraggingIcon != null)
SetDraggedPosition(data);
}

public virtual void OnBeginDrag(PointerEventData eventData)
{
var canvas = FindInParents<Canvas>(gameObject);
if (canvas == null)
return;

Image img = transform.GetComponent<Image>();
img.color = new Color(img.color.r, img.color.g, img.color.b, 0.2f);

// We have clicked something that can be dragged.
// What we want to do is create an icon for this.
m_DraggingIcon = new GameObject("icon");

m_DraggingIcon.transform.SetParent(canvas.transform, false);
m_DraggingIcon.transform.SetAsLastSibling();

m_DraggingIcon.AddComponent<CanvasGroup>();
m_DraggingIcon.GetComponent<CanvasGroup>().blocksRaycasts = false;

var image = m_DraggingIcon.AddComponent<Image>();

image.sprite = GetComponent<Image>().sprite;

if (dragOnSurfaces)
m_DraggingPlane = transform as RectTransform;
else
m_DraggingPlane = canvas.transform as RectTransform;

SetDraggedPosition(eventData);
}

private void SetDraggedPosition(PointerEventData data)
{
if (dragOnSurfaces && data.pointerEnter != null && data.pointerEnter.transform as RectTransform != null)
m_DraggingPlane = data.pointerEnter.transform as RectTransform;

var rt = m_DraggingIcon.GetComponent<RectTransform>();
Vector3 globalMousePos;
if (RectTransformUtility.ScreenPointToWorldPointInRectangle(m_DraggingPlane, data.position, data.pressEventCamera, out globalMousePos))
{
rt.position = globalMousePos;
rt.rotation = m_DraggingPlane.rotation;
}
}

private T FindInParents<T>(GameObject go) where T : Component
{
if (go == null) return null;
var comp = go.GetComponent<T>();

if (comp != null)
return comp;

Transform t = go.transform.parent;
while (t != null && comp == null)
{
comp = t.gameObject.GetComponent<T>();
t = t.parent;
}
return comp;
}
}


Step 1: Create an inventory icon

Add a canvas and add a UI Image GameObject. Call this object "InventoryBorder". Add a child to this object, call it "InventoryItem"
image

Ideally, you'd make the "InventoryBorder" some sort of square border, and resize the "InventoryItem" to a smaller size than the border (85x85 maybe). You can also give the "InventoryItem" a different Source Image just to test it out.

You can also add a third image and a mask to ensure the Inventory item is constrained within the border if it is an odd shape etc. sky is the limit.

Create a new script called "InventoryDrag.cs" and add to the "InventoryItem" gameobject.

The code for this script is as follows. This inherits from the DragDrop base class and gives the Image you attached the script to full Drag capability is most cases.

using ORKFramework;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class InventoryDrag : DragDropBaseClass
{
public IInventoryShortcut inventoryItem = null;

public override void OnBeginDrag(PointerEventData eventData)
{
// don't drag if this is an empty slot
if (inventoryItem == null)
return;

base.OnBeginDrag(eventData);
}
}


You probably won't be able to drag the image yet since we have not allocated "inventory item". you can comment out the "if (inventoryItem == null) return;" bit and you should be able to drag as a test.

Now create a prefab from "InventoryBorder" + child and delete the original from the scene. (prefab will probably be called "InventoryBorder")

Step 2: Grid Inventory using Scroll View

Create a new Scroll View (UI -> Scroll View) - If you haven't used them before it looks like a lot, as it creates a tree of probably half a dozen game objects but you really only need to be interested in one, the "Content" (Scroll View | Viewport | Content)

Resize the Scroll View (Top level GameObject) to suite your needs.

Go to content Game Object and add three components

a) Grid Layout Group
b) Content Size Fitter
c) a new script called "InventoryControl.cs"

All you really want to change is Grid Layout Group : Spacing X=3 and Y=3
And you can also change Content Size Fitter : Vertical Fit = Preferred Size

But these are really optional and just for looks. Play around.

image

In the script "InventoryControl.cs" you will need the following code. First use of ORK framework!

Note this code populates the Scroll View grid list with one prefab per slot requested and links the actual ORK.IInventoryShortcut when available to the GameObject. When this dragged we have access to the actual item at the drop event.

using ORKFramework;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class InventoryControl : MonoBehaviour
{
private Combatant playerCombatant;

[Tooltip("The game object prefab that will be created for every inventory item")]
[SerializeField]
private GameObject prefab;

void Start()
{
StartCoroutine(GetPlayer());
}

private IEnumerator GetPlayer()
{
do
{
playerCombatant = ORK.Game.ActiveGroup.Leader;
yield return null;
}
while (this.playerCombatant == null);

PopulateInventory();
}

public void PopulateInventory()
{
if (playerCombatant != null)
{
List<IInventoryShortcut> inventory = new List<IInventoryShortcut>();

// determine at THIS point what we actually want to see, items, weapons, armor, ALL?
playerCombatant.Inventory.GetAll(false, false, true, true, true, false, false, false, -1, false, ref inventory);

// clear the inventory and populate with the new inventory
clearScrollView();

GameObject newObj;

int numberToCreate = 0;
if (ORK.InventorySettings.limit)
numberToCreate = Convert.ToInt32(ORK.InventorySettings.limitValue.value);
else
numberToCreate = inventory.Count;

for (int i = 0; i < numberToCreate; i++)
{
// Create new instances of our prefab until we've created as many as we specified
newObj = (GameObject)Instantiate(prefab, transform);

if (inventory.Count > i)
{
Texture tex = inventory[i].GetIcon();
Transform trItem = newObj.transform.Find("InventoryItem");
Image img = trItem.GetComponent<Image>();

((InventoryDrag)trItem.GetComponent<InventoryDrag>()).inventoryItem = inventory[i];

img.sprite = Sprite.Create(tex as Texture2D, new Rect(0, 0, tex.width, tex.height), new Vector2(.5f, .5f));
img.color = new Color(img.color.r, img.color.g, img.color.b, 1);
}
else
{
// here we are creating a blank border image with no inventrory item inside
Image img = newObj.transform.Find("InventoryItem").GetComponent<Image>();
img.color = new Color(img.color.r, img.color.g, img.color.b, 0); // make transparent
}
}
}
}

private void clearScrollView()
{
foreach (Transform child in transform)
{
Destroy(child.gameObject);
}
}
}


Now drag the "InventoryBorder" prefab we made onto the script property Prefab.

The number of slots created is controlled by ORK. Set in the ORK editor - Inventory | Inventory Settings using the Space Limit and if that is set to TRUE using the Limit Value to limit the number of inventory slots.

Run and test and you should see a grid list of all your inventory items (I hope) and you can drag them about

Step 3: Equipment slots
Now we need somewhere to drop the inventory item.

Create a new Image called "EquipBorder" and add a child to it called "EquipItem" in the same way as the Inventory item

(These names can all be changed of course to suite your naming convention)

Add a script to the "EquipItem" gameobject called "EquipDrag.cs" containing the following code.

using ORKFramework;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using ORKEnums;

public class EquipDrag : DragDropBaseClass, IDropHandler
{
[Tooltip("ORK Framework Equipment slot ID")]
[SerializeField]
private int equipmentSlotID;

private Combatant playerCombatant;

public InventoryControl controller;
public Image borderImage;
public Image equipmentImage;

void Start()
{
StartCoroutine(GetPlayer());
}

private IEnumerator GetPlayer()
{
do
{
playerCombatant = ORK.Game.ActiveGroup.Leader;
yield return null;
}
while (this.playerCombatant == null);

displayEquipmentSlot();
}

private void displayEquipmentSlot()
{
if (playerCombatant.Equipment[(int)equipmentSlotID].Available)
{
this.gameObject.SetActive(true);

if (!playerCombatant.Equipment[(int)equipmentSlotID].Equipped)
{
borderImage.color = new Color(borderImage.color.r, borderImage.color.g, borderImage.color.b, 1);
equipmentImage.color = new Color(equipmentImage.color.r, equipmentImage.color.g, equipmentImage.color.b, 0);
}
else
{
// display this image
borderImage.color = new Color(borderImage.color.r, borderImage.color.g, borderImage.color.b, 1);
equipmentImage.color = new Color(equipmentImage.color.r, equipmentImage.color.g, equipmentImage.color.b, 1);

Texture tex = playerCombatant.Equipment[(int)equipmentSlotID].Equipment.GetIcon();
equipmentImage.sprite = Sprite.Create(tex as Texture2D, new Rect(0, 0, tex.width, tex.height), new Vector2(.5f, .5f));
}

}
else
{
// hide it
borderImage.color = new Color(borderImage.color.r, borderImage.color.g, borderImage.color.b, 0);
equipmentImage.color = new Color(equipmentImage.color.r, equipmentImage.color.g, equipmentImage.color.b, 0);
this.gameObject.SetActive(false);
}
}

public void OnDrop(PointerEventData eventData)
{
if (eventData.pointerDrag != null)
{
InventoryDrag itemScript = ((InventoryDrag)eventData.pointerDrag.GetComponent<InventoryDrag>());

if (itemScript != null && playerCombatant.Equipment[(int)equipmentSlotID].Available && !playerCombatant.Equipment[(int)equipmentSlotID].IsBlocked)
{
if (itemScript.inventoryItem != null)
{
if (itemScript.inventoryItem is ItemShortcut)
{
//; we can't actually *equip* items so we can ignore this (for the moment)

}
else if (itemScript.inventoryItem is EquipShortcut)
{
EquipShortcut eshort = (EquipShortcut)itemScript.inventoryItem;
if (eshort.CanEquip(playerCombatant))
{
playerCombatant.Equipment.Equip((int)equipmentSlotID, eshort, playerCombatant, playerCombatant, true);
playerCombatant.Status.ResetStatus(true);
}
}
}
}

if (itemScript != null && itemScript.m_DraggingIcon != null)
Destroy(itemScript.m_DraggingIcon);

displayEquipmentSlot();
if (controller != null) controller.PopulateInventory();
}
}


public override void OnBeginDrag(PointerEventData eventData)
{
// don't drag if this is an empty slot
if (!playerCombatant.Equipment[(int)equipmentSlotID].Equipped)
{
return;
}

base.OnBeginDrag(eventData);
}
}


You will see this script has multiple properties. I just like doing it this way since these sort of links work even if you change the names of the game object they are linked to (unlike using FindComponent etc even though I see I did it in the InventoryControl script, probably because it was a prefab)

So you will need to link a few things:
a) The EquipBorder gameobject needs to be dragged to "Border Image" property
b) The EquipImage gameobject needs to be dragged to "Equipment Image" property
c) And lastly, and the HARDEST, drag the "Inventory Control (script)" from the Content gameobject onto "Controller" property

image

The last step is so when the item is dragged and dropped, the Equipment Slot can request the controller refresh the inventory grid.

If you have issues linking these, a tip to see two inspectors at the same time:
- Right click the "Inspector" tab
- Click on Add Tab | Inspector
- You now have two inspectors.
- Click on the Content Game object (Both inspectors should show the same stuff at this point)
- Click on the LOCK symbol on one of the tabs (Top right)
- Next click on the EquipImage game object so you can see the inventory control script *and* see the equipment Drag script components allowing you to drag the controller script to link it.

image

The important code here is we handle the OnDrop event then check to see if:
- the object being dropped contains an InventoryDrag script
- the equipment slot is available
- the equipment slot is not blocked (probably already checked above, not sure)
- and finally checks to see if the player can ACTUALLY equip this item

If all that is true, we equip it with playerCombatant.Equipment.Equip letting ORK deal with adding and removing from the inventory and then we refresh the inventory (using that nasty link to the Inventory Controller we had to add)

The other important thing is we have a link to the equipmentSlotID which maps directly to the ORK equipment slots, so we need to change this number for each slot.

Once you have this one Equipment Slot working you can drag it off into a prefab and then add as many as you need, just changing the equipment slot id for each one.

The code does check in DisplayEquipmentSlot if the slots should be shown or not in case your game adds or remove slots as the player progresses.

I actually used an ENUM as the equipmentSlotID generated from Keldryn's resource that can create a nice script automatically from ORK found here.

I highly recommend this for all code.

In this case it allows you to pick from the equipment slot id property "Main Weapon" or "Helmet" rather than 3 or 6

Sorry this went long, never really written a code tutorial before and honestly this is a bit messy

Let me know if you have any questions

Post edited by Fashtas on
  • Good job, great tutorial :)
    Please consider rating/reviewing my products on the Asset Store (hopefully positively), as that helps tremendously with getting found.
    If you're enjoying my products, updates and support, please consider supporting me on patreon.com!
  • Hello! Thank you again! I did my equipment, inventory and shop using your tutorial. And noticed what you need call playerCombatant.Status.ResetStatus after equipping something. Or Status Values will not be applied and can be incorrectly interpretated by methods using them.

    Also, I added bool variable to base class for checking when item actually dragged. All over changes was mostly cosmetic. Everything dragged, dropped, equipped and unequipped now :)
  • Cool! Sounds excellent!

    Great you caught the ResetSatus thing, I hadn't noticed anything since I am still working on various GUI interfaces and not actually *using* any of them in anger yet :)

    I'll have to add add that to my code too!


  • By the way, what do you think about using singlton instances in your UI system?


    public static InventoryManager instance = null;

    void Awake()
    {
    instance = this;
    }


    You can make your UI modular. One manager will enable them or disable. Instances can use OnEnable instead of Awake to initialize.

    And they can be accessed each other by InventoryManager.instance (as exemple) without need using Find or direct links. Only tricky thing here is to enable them in particular order to avoid Null References.
  • I've used Singletons in previous Unity projects and they work well (apart from the endless pits of despair)

    At the moment I am throwing mud at the walls and see what sticks. Just implemented the new Unity Input System (Preview) to fire the Ork shortcuts so the user can change key bindings in game. No idea how to get into the ORK KeyControls stuff properly but what I have works. Part of the implementation of a Diablo/Torchlight mouse click system attack system.

    Maybe singletons would be a way to go with this....

    You could work with something like this.

    Singleton base class
    using UnityEngine;

    /// <summary>
    /// Inherit from this base class to create a singleton.
    /// e.g. public class MyClassName : Singleton<MyClassName> {}
    /// </summary>
    public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
    {
    // Check to see if we're about to be destroyed.
    private static bool m_ShuttingDown = false;
    private static object m_Lock = new object();
    private static T m_Instance;

    /// <summary>
    /// Access singleton instance through this propriety.
    /// </summary>
    public static T Instance
    {
    get
    {
    if (m_ShuttingDown)
    {
    Debug.LogWarning("[Singleton] Instance '" + typeof(T) +
    "' already destroyed. Returning null.");
    return null;
    }

    lock (m_Lock)
    {
    if (m_Instance == null)
    {
    // Search for existing instance.
    m_Instance = (T)FindObjectOfType(typeof(T));

    // Create new instance if one doesn't already exist.
    if (m_Instance == null)
    {
    // Need to create a new GameObject to attach the singleton to.
    var singletonObject = new GameObject();
    m_Instance = singletonObject.AddComponent<T>();
    singletonObject.name = typeof(T).ToString() + " (Singleton)";

    // Make instance persistent.
    DontDestroyOnLoad(singletonObject);
    }
    }

    return m_Instance;
    }
    }
    }


    private void OnApplicationQuit()
    {
    m_ShuttingDown = true;
    }


    private void OnDestroy()
    {
    m_ShuttingDown = true;
    }
    }


    Implemented in a GameControl singleton:

    using ORKFramework;
    using System.Collections;

    public class GameControlSingleton : Singleton<GameControlSingleton>
    {
    // (Optional) Prevent non-singleton constructor use.
    protected GameControlSingleton() { }

    public Combatant PlayerCombatant { get; set; }

    public string ExampleString = "String contents";

    void Start()
    {
    StartCoroutine(GetPlayer());
    }

    private IEnumerator GetPlayer()
    {
    do
    {
    PlayerCombatant = ORK.Game.ActiveGroup.Leader;
    yield return null;
    }
    while (this.PlayerCombatant == null);
    }
    }


    That class can contain LOTS of code and properties and be used elsewhere in the game for other purposes (see the Example string for example)

    Note:

    - To access the properties of the singleton you use GameControlSingleton.Instance.PlayerCombatant
    or GameControlSingleton.Instance.ExampleString

    - The first time you call the singleton this way it is instantiated so the PlayerCombatant will be NULL

    - The second time assuming the co-routine worked, PlayerCombatant will return the player

    Obviously lots of ways to change this and use it.

  • That's singleton of next level! It was an opening for me what I can use singletons with MonoBehaviour and I decided to share it. But you shows class again :)

    I am in debt again! Hope it will help others too :)
  • Glad to help!

    That code, like my drag and drop stuff, is an end result of several of other peoples tutorials and research and bits I fiddled with, can't take too much credit for it.
  • I've tried this out myself but when I equip a two handed weapon it unslots both left and right as expected and updates the Inventory but it does not update both slots, only the slot that you put the weapon in to it. I'm curious to how you've dealt with this issue or if it's a problem my side with how I have the equipment set up within Ork. Any thoughts?
    Legends of Thamia - Coming soon to Android.
  • Awesome tutorial! It has helped a lot :)

    @Talairina Have you solved any related problems?

    According to the main context, is it impossible to implement functions such as slot movement and item sorting in the inventory?
  • edited June 2022
    Can anyone attest if this is still valid on ORK3? Thanks! I don’t need drag and drop, going to try scripting for just the grid layout. Didn’t think it necessary.
    Post edited by theProject on
Sign In or Register to comment.