3d Hand/Cursor grab and drop Item object

So prototyped a mechanic idea last night and it “works”. Well, it actually works quite well but the code is horrendous and I’m trying to determine if I’m doing things correctly.

You can test the prototype here: https://theoddpotion.itch.io/project-trade-v0

Basically I have a “Hand” that is controlled laterally with the Horizontal and Vertical axes. The Hand can Grab() and Drop() an Item from a Slot.The “Item” just has a few properties like name, cost, stackable, count, etc. And I have a “Slot” which holds a reference to an Item that it is currently holding. You can AddItem(), RemoveItem(), GetItem() and check if HasItem().

All of these Scripts are MonoBehaviours and attached to relative prefabs. I created a grid of Slots for a makeshift “inventory” and with this setup I am able to move the Hand around, hold a button down to grab an Item from the Slot below the Hand and release the button to drop the Item into the Slot below the Hand. And with some hacking I’ve managed to implement a basic stacking setup where if you Grab() a stackable Item you pick up the whole stack, and you can press another button while grabbing a stack of Items to Drop() one Item.

The whole thing works but right now there is too much inter-dependency and I’m not exactly sure how to solve it. Maybe using coroutines for Grab() and Drop()? I’m not really sure.

Note: I haven’t implemented an Item database yet, so Item is a MonoBehaviour. But I intend to use ScriptableObjects to make that easier than just having a separate prefab for each Item.

using UnityEngine;
using System.Collections;

public class Item : MonoBehaviour {
    public string itemName = "Default Item";
    public int cost;
    public bool stackable;
    public int count = 1;
    public Slot lastSlot;
}
using UnityEngine;
using System.Collections;

public class Slot : MonoBehaviour {
    public Item heldItem;

    //We are assuming here that: the item passed in
    // 1) Either we don't have a heldItem -or-
    //the item passed in is the same type of item as the heldItem
    public void AddItem(Item item){
        //if we aren't holding an item
        if (heldItem == null) {
            //set our reference to passed in item
            heldItem = item;
            //set position of new held item to ItemPoint
            //which is just an empty GameObject attached to the Slot object.
            heldItem.transform.position = transform.FindChild ("ItemPoint").transform.position;
        } else { //if we are holding an item
            //destroy the passed in item, basically deleting the item from the Hand
            //because we already have an item in the slot to represent the items in the slot
            Destroy (item.gameObject);
            //and increase the count of the heldItem
            heldItem.count += item.count;
        }
    }

    //When we Grab an Item from a Slot we RemoveItem
    //which just sets a reference to this Slot inside the heldItem and then cuts the tie
    public void RemoveItem(){
        heldItem.lastSlot = this;
        heldItem = null;
    }

    public Item GetItem(){
        return heldItem;
    }

    public bool HasItem(){
        if (heldItem) {
            return true;
        }
        return false;
    }

    public void IncreaseItemCount(int amount){
        heldItem.count += amount;
    }
}
using UnityEngine;
using System.Collections;

public class Hand : MonoBehaviour {

    public float moveSpeed = 5f;
    public Item grabbedItem;
    public bool isGrabbingItem;
    public int grabbedCount = 0;
    public Slot slotBelow;
    public LayerMask slotLayerMask;

    //item prefabs. Just a gameobject with an Item component
    public Item redItemPrefab;
    public Item blueItemPrefab;

    private Vector3 hitPoint = Vector3.zero;

    void Update(){
        //BASIC MOVING
        Vector3 moveDir = new Vector3 (Input.GetAxis ("Horizontal"), 0f, Input.GetAxis ("Vertical"));
        moveDir.Normalize ();
        Move (moveDir);
        //

        //if null we are not over a slot
        slotBelow = GetSlotBelow ();

        //allows to press and hold button to grab item and release to drop
        if (Input.GetButtonDown ("Fire6")) {
            Grab ();
        }
        if (Input.GetButtonUp ("Fire6")) {
            Drop ();
        }
          
        if (Input.GetButtonDown ("Fire3")) {
            DropOne ();
        }

        //testing stuff
        if (Input.GetButtonDown ("Fire2")) {
            AddItemBelow ();
        }

        if (Input.GetButtonDown ("Fire1")) {
            if (isGrabbingItem) {
                DestroyItemInHand ();
            }
        }

        //set grabbed objects position if we have one
        if (grabbedItem && isGrabbingItem) {
            grabbedItem.transform.position = transform.FindChild("ItemPoint").transform.position;
        }
    }

    void LateUpdate(){
        if (slotBelow) {
            //A weird snapping experiment. Idk.
            //Vector3 nextPos = slotBelow.transform.position + Vector3.up * 1f;
            //transform.position = Vector3.Lerp (transform.position, nextPos, 2.5f * Time.deltaTime);
        }
    }

    void Move(Vector3 dir){
        transform.position += dir * moveSpeed * Time.deltaTime;
    }
      
    void Grab(){
        //If we aren't grabbing an item
        if (isGrabbingItem == false) {
            //and there is a slot below with an item in it
            if (slotBelow != null && slotBelow.HasItem()) {
                //get reference to item in slot
                Item item = slotBelow.GetItem ();
                //remove item from slo
                slotBelow.RemoveItem ();

                //we are grabbing the referenced item and store the number of items.
                isGrabbingItem = true;
                grabbedItem = item;
                grabbedCount = grabbedItem.count;

                //Test Debug
                if (grabbedItem.count > 1) {
                    Debug.Log ("Item Name: " + grabbedItem.itemName + " x" + grabbedItem.count + " --- Item Cost: " + grabbedItem.cost);
                } else {
                    Debug.Log ("Item Name: " + grabbedItem.itemName + " --- Item Cost: " + grabbedItem.cost);
                }
            }
        }
    }
      
    void Drop(){
        //if we are grabbing an item
        if (isGrabbingItem) {
            //and there is a slot below us
            if (slotBelow != null) {
                //....and it has an item...
                if(slotBelow.HasItem()){
                    //.....and it is the same type of item as the grabbedItem.. whew.
                    if (grabbedItem.itemName == slotBelow.GetItem().itemName && slotBelow.GetItem().stackable) {
                        //add the item to the slot below
                        slotBelow.AddItem (grabbedItem);
                        //and clear our grabbing stuff
                        ClearGrabbed ();
                    } else { //if it is not the same type of item as the grabbedItem.
                        //get a reference to the slot the grabbedItem was in last and add it to the slot
                        Slot slot = grabbedItem.lastSlot;
                        slot.AddItem (grabbedItem);

                        //and clear our grabbing stuff
                        ClearGrabbed ();
                    }
                }else{ //if the slot below doesn't have an item
                    //add the grabbed item to the slot
                    slotBelow.AddItem (grabbedItem);

                    //and clear our grabbing stuff
                    ClearGrabbed ();
                }                  
            } else { //and if there is not a slot below us
                //get a reference to the slot the grabbedItem was in last and add it to the slot
                Slot slot = grabbedItem.lastSlot;
                slot.AddItem (grabbedItem);

                //and clear our grabbing stuff
                ClearGrabbed ();
            }
        }
    }

    void DropOne(){
        //if we are grabbing and item and it's stackable
        if (isGrabbingItem && grabbedItem.stackable) {
            //and we have a slot below us
            if (slotBelow != null) {
                //...and it has an item..
                if(slotBelow.HasItem()){
                    //...and its the same type of item as the grabbed item...
                    if (grabbedItem.itemName == slotBelow.GetItem().itemName) {
                        //...and we have more than 1 item in our Hand
                        if (grabbedItem.count > 1) {
                            //decrease the amount of grabbed items by 1
                            grabbedItem.count--;
                            //and increase the amoutn of items in the slot by 1
                            slotBelow.IncreaseItemCount (1);
                        } else { //if this is the last item in our hand
                            //destroy the object in hand
                            Destroy (grabbedItem.gameObject);

                            ClearGrabbed ();
                        }
                    }
                }else{ //if the slot below has no item
                    //create an item and set its count to 1
                    Item item = Instantiate (grabbedItem) as Item;
                    item.count = 1;
                    //and add the item to the slot
                    slotBelow.AddItem (item);

                    //and decrease the amount of items we are grabbing by 1
                    grabbedItem.count--;
                    //check if that was the last item
                    if (grabbedItem.count <= 0) {
                        //destroy item in hand
                        Destroy (grabbedItem.gameObject);
                        ClearGrabbed ();
                    }


                }                  
            }
        }
    }

    void DestroyItemInHand(){
        ClearGrabbed ();
        Destroy (grabbedItem.gameObject);
    }

    //shoot raycast down, if hits a Slot
    Slot GetSlotBelow(){
        RaycastHit hit;
        if (Physics.Raycast (transform.position, Vector3.down, out hit, 2f, slotLayerMask)) {
            hitPoint = hit.point;
            return hit.transform.GetComponent<Slot> ();
        }
        Debug.DrawLine (transform.position, transform.position + Vector3.down * 2f, Color.red);
        return null;
    }

    //draws a sphere on the Slot below the hand
    void OnDrawGizmos(){
        Gizmos.color = Color.red;
        Gizmos.DrawSphere (hitPoint, 0.05f);
    }

    //testing
    void AddItemBelow(){
        if (slotBelow) {
            if (slotBelow.HasItem () == false) {
                if (Random.Range (0, 100) > 50) {
                    Item item = Instantiate (redItemPrefab) as Item;
                    slotBelow.AddItem (item);
                } else {
                    Item item = Instantiate (blueItemPrefab) as Item;
                    slotBelow.AddItem (item);
                }
            }
        }
    }

    void ClearGrabbed(){
        isGrabbingItem = false;
        grabbedItem = null;
        grabbedCount = 0;
    }
}

All you should have to do if you were to want to quickly test this is create a Cube object named Slot. Scale it on y to 0.05 or something small and create an empty child object called ItemPoint and set its y position to 5(because of the scaling it will actually be 0.5 units above the Slot). Attach the Slot script to the Slot object and make sure to create a new layer named Slots and assign the slot to that layer. Then create a grid of these Slots in the scene view.

Then create another Cube object named RedItem, set its scale to 0.25 on all axes. Then attach the Item script to it. Set its name to RedItem, cost to whatever, stackable to true and 2 for count. Leave lastSlot blank. Do the same thing again for a BlueItem but scale it just a bit bigger and set stackable to false.

Then create another Cube object named Hand. Set it’s scale to (0.5, 0.1, 0.5) and create an empty child object and set it’s position somewhere just below the hand and name it ItemPoint. Attach the Hand script to it and set the RedItemPrefab and BlueItemPrefab to the item prefabs that were just made and make sure to set the slotLayerMask to the Slots layer. And that should be it. Just make sure to have a Hand and some Slots in your Hierarchy and thats it. You may also need to change the input buttons. During play just press whatever button to AddItemBelow() to get some items in there to play with.

I just don’t know how to make it better… I know it’s a lot but any help would be greatly appreciated. =)

Your code seems mostly fine – the only part I don’t like is that half the logic for controlling the slots is on the Hand and the other half is on the Slot. Instead of those monstrous if statements inside the Hand, just have functions like GetItem() AddItem() maintain the slot’s own internal state and return appropriate results (items, nulls, booleans).

For example, inside hand:

void Grab()
{
   //If we aren't grabbing an item
   if (isGrabbingItem == false) {
     //and there is a slot below
     if (slotBelow != null) {
       Item item = slowBelow.GetItem();
       
       if(item != null)
       {
         isGrabbingItem = true;
         grabbedItem = item;
         grabbedCount = grabbedItem.count;

         //Test Debug
         if (grabbedItem.count > 1) {
           Debug.Log ("Item Name: " + grabbedItem.itemName + " x" + grabbedItem.count + " --- Item Cost: " + grabbedItem.cost);
         } else {
           Debug.Log ("Item Name: " + grabbedItem.itemName + " --- Item Cost: " + grabbedItem.cost);
         }       
       }
     }
   }
}

and then slot equivalent:

  public Item GetItem() {
     Item result = heldItem; // store a temporary reference
     
     RemoveItem();
     
     return result;
   }

Now both scripts are responsible for maintaining their own internal state, which is much cleaner and easier to maintain.

Thanks for your reply!

Since writing the post I have created quite a few prototypes to figure out the best way to do what I’m wanting to do. I do agree with how crazy the logic is in the example above. As of now the Hand has a simple state machine for states like Idle, Grabbing, Dropping, etc. I’ll drop the code I have now but… it’s just as messy if not more so than before and that’s after I cleaned it up for this post. lol

using UnityEngine;
using System.Collections;

public class Hand : MonoBehaviour {

    public enum Side
    {
        LEFT,
        RIGHT
    }
    public Side side;

    public enum State
    {
        IDLE,
        GRABBING,
        DROPPING,
        SET_PRICE
    }
    public State state = State.IDLE;

    public LayerMask slotLayerMask;
    private Slot slotBelow;
    private Slot oldSlot;

    #region Properties (public)
    public float moveSpeed = 5f;
    public Item grabbedItem;
    public Vector3 itemDisplayPosition;
    #endregion

    #region Variables (private)
    private float heldTime = 0f;
    private bool canMove = true;
    #endregion

    #region Unity Events
    void Awake(){
        //
    }

    void Start(){
        itemDisplayPosition = Vector3.up * -0.5f;
    }

    void Update(){
        slotBelow = GetSlotBelow ();

        if (canMove) {
            Vector3 moveDir = new Vector3 (Input.GetAxis ("Horizontal" + (int)side), 0f, Input.GetAxis ("Vertical" + (int)side));
            moveDir.Normalize ();
            Move (moveDir);
        }

        switch (state) {

        case State.IDLE:
            canMove = true;
            if (Input.GetButtonDown ("Grab" + (int)side)) {
                //Debug.Log ("Button down" + " " + heldTime);
                heldTime = 0f;
            }
            if (Input.GetButton ("Grab" + (int)side)) {
                heldTime += Time.deltaTime;
                //Debug.Log (heldTime);

                if (heldTime > 0.15) {
                    Debug.Log ("Grab Item");
                    state = State.GRABBING;
                }
            }
            if (Input.GetButtonUp ("Grab" + (int)side)) {
                if (heldTime < 0.15) {
                    Debug.Log ("Set Price");
                    state = State.SET_PRICE;
                }
            }

            //developer cheat. add item to slot below right hand to have some items to play with.
            if (Input.GetKeyDown (KeyCode.Space)) {
                if (side == Side.RIGHT) {
                    slotBelow.SetItem (ItemDB.GetItem ("Health Potion"));
                }
            }
            break;


        case State.GRABBING:
            Slot closestSlot = GetClosestSlot ();
            StartCoroutine (MoveToAndGrab (closestSlot));
               
            if (Input.GetButtonUp ("Grab" + (int)side)) {
                Debug.Log ("Drop Item");
                state = State.DROPPING;
            }
            break;


        case State.DROPPING:
            Debug.Log ("Dropping");

            closestSlot = GetClosestSlot();
            StartCoroutine (MoveToAndDrop (closestSlot));

            state = State.IDLE;
            break;


        case State.SET_PRICE:
            canMove = false;
            if (Input.GetButtonDown ("Grab" + (int)side)) {
                heldTime = 0f;
            }
            if (Input.GetButtonUp ("Grab" + (int)side)) {
                if (heldTime < 0.25) {
                    Debug.Log ("Set Price Done");
                    state = State.IDLE;
                }
            }
            break;


        default:
            break;
        }
    }
    #endregion

    IEnumerator MoveToAndDrop(Slot slot){
        Vector3 newPos = new Vector3 (slot.transform.position.x, transform.position.y, slot.transform.position.z);
        while(Vector3.Distance(transform.position, newPos) > 0.05f){
            transform.position = Vector3.Lerp (transform.position, newPos, 10f * Time.deltaTime);
            slot.SetDisplayPos (transform.position + itemDisplayPosition);
            yield return null;
        }

        Vector3 nextPos = newPos + Vector3.up * -1;

        while (Vector3.Distance (transform.position, nextPos) > 0.05f) {
            transform.position = Vector3.Lerp (transform.position, nextPos, 20f * Time.deltaTime);
            slot.SetDisplayPos (transform.position + itemDisplayPosition);
            yield return null;
        }

        slot.item = grabbedItem;
        grabbedItem = null;

        while (Vector3.Distance (transform.position, newPos) > 0.05f) {
            transform.position = Vector3.Slerp (transform.position, newPos, 20f * Time.deltaTime);
            yield return null;
        }
    }

    IEnumerator MoveToAndGrab(Slot slot){
        Vector3 newPos = new Vector3 (slot.transform.position.x, transform.position.y, slot.transform.position.z);
        while(Vector3.Distance(transform.position, newPos) > 0.05f){
            transform.position = Vector3.Lerp (transform.position, newPos, 10f * Time.deltaTime);
            slot.SetDisplayPos (transform.position + itemDisplayPosition);
            yield return null;
        }

        Vector3 nextPos = newPos + Vector3.up * -1;

        while (Vector3.Distance (transform.position, nextPos) > 0.05f) {
            transform.position = Vector3.Lerp (transform.position, nextPos, 20f * Time.deltaTime);
            slot.SetDisplayPos (transform.position + itemDisplayPosition);
            yield return null;
        }

        grabbedItem = slot.item;
        slot.item = null;

        while (Vector3.Distance (transform.position, newPos) > 0.05f) {
            transform.position = Vector3.Slerp (transform.position, newPos, 20f * Time.deltaTime);
            yield return null;
        }
    }
    #region Methods
    void Move(Vector3 dir){
        transform.position += dir * moveSpeed * Time.deltaTime;
    }

    Slot GetSlotBelow(){
        RaycastHit hit;
        if (Physics.Raycast (transform.position, Vector3.down, out hit, 2f, slotLayerMask)) {
            //hitPoint = hit.point;
            return hit.transform.parent.GetComponent<Slot> ();
        }
        return null;
    }

    Slot GetClosestSlot(){
        float closestDistSqr = Mathf.Infinity;
        GameObject[] allSlotObjs = GameObject.FindGameObjectsWithTag ("Slot");
        Slot closestSlot = null;

        foreach (GameObject slotObj in allSlotObjs) {
            Vector3 slotPos = slotObj.transform.position;
            float distSqr = (slotPos - transform.position).sqrMagnitude;

            if (distSqr < closestDistSqr) {
                closestSlot = slotObj.GetComponent<Slot> ();
                closestDistSqr = distSqr;
            }
        }

        return closestSlot;
    }
    #endregion
}

Note that the MoveToAndGrab() coroutine really shouldnt be a coroutine. Since the grab button is being actively held while in the GRABBING state it is starting the coroutine each frame. Which bugs the hand out… Not exactly sure how to do it without a coroutine but maybe a check or flag that sets the state to GRABBING after the grab button is held for an amount of time and then doesn’t check for input from the grab button except only when releasing. (Which is what I thought the code was doing… xD)

Also… it does seem like the Hand is pulling some hard work the Slot should do when swapping items inside the MoveToAndDrop and MoveToAndGrab. This is because of the way my Slot class is now implemented, though this is about to change after typing this…

Basically I thought of my Slot being a “display case” which holds an Item data and a display item. Basically the slot would change it’s display item to match the Items field for DisplayPrefab. Which is just a prefab that is the visual of the items. So each item has a DisplayPrefab and the Slot would instantiate this prefab when it’s holding an item and destroy it when not.

public class Slot : MonoBehaviour {

    #region Properties (public)
    public Item item;
    public int itemAmount;
    #endregion

    #region Variables (private)
    private Transform displayPos;
    private GameObject itemDisplay;
    #endregion

    void Start(){
        displayPos = transform.FindChild("DisplayPos");
    }

    void Update(){
    }

    #region Methods
    public void SetItem(Item itemType){
        item = itemType;
        if (itemDisplay == null) {
            itemDisplay = Instantiate (item.itemDisplay);
        }
        itemDisplay.transform.position = displayPos.position;
        itemAmount = 1;
    }

    public void SetAmount(int amount){
        itemAmount = amount;
    }
    public void Add(){
        itemAmount += 1;
    }

    public void Remove(){
        itemAmount -= 1;

        if (itemAmount == 0) {
            item = null;
        }
    }

    public void SetDisplayPos(Vector3 pos){
        if (itemDisplay) {
            itemDisplay.transform.position = pos;
        }
    }
    #endregion
}

And then when my Hand object “grabbed” an item from a slot it would just move the Slots itemDisplay object… This works to an extent but isn’t fool proof and bugs out under some circumstances.

Right now I am thinking about creating a ItemDisplay object which the Hand and the Slot objects will have that is given a prefab and then instantiates it or destroys it. So then I can give the Slot objects and the Hand objects each an ItemDisplay component so that when the Hand grabs an item from a slot it doesnt move an object that is parented to a Slot.

I have NO idea if this makes sense at all. lol I apologize if not. Again thanks for replying!