Double click swapping between Equipement and Inventory

Already mentionned here Double click instead of dragging but the topic is closed. ^^

It is a pretty common QoL feature to clicky swap an item by double clicking on it, instead of always drag and drop it.

Here how I impleted it.
First we need to add a method in EquipementSlotUI

        public EquipLocation GetEquipLocation()
        {
            return equipLocation;
        }

After that we need to go to DragItem and change the method DropItemIntoContainer from private to protected. The class is in GameDevTV.Core.UI.Dragging .

And with that, the preparations are complete, we can begin to modify InventoryDragItem inside GameDevTV.UI.Inventories

using System.Collections.Generic;
using GameDevTV.Core.UI.Dragging;
using GameDevTV.Inventories;
using UnityEngine;
using UnityEngine.EventSystems;

namespace GameDevTV.UI.Inventories
{
    /// <summary>
    /// To be placed on icons representing the item in a slot. Allows the item
    /// to be dragged into other slots.
    /// </summary>
    public class InventoryDragItem : DragItem<InventoryItem>, IPointerClickHandler
    {
        private const float DOUBLE_CLICK_TIME = 0.3f;
        private float lastClickTime;
        
        public void OnPointerClick(PointerEventData eventData)
        {
            float timeSinceLastClick = Time.time - lastClickTime;
            // Double click ! We wil try to swap between the equipement and inventory.
            if (timeSinceLastClick <= DOUBLE_CLICK_TIME)
            {
                EquipableItem removedSourceItem = source.GetItem() as EquipableItem;
                // If not equipable, don't bother.
                if (removedSourceItem == null) return;

                IDragContainer<InventoryItem> destination = null;
                if (source is InventorySlotUI)
                { 
                  // We clicked on an InventorySlot. Lets try to find a corresponding EquipementSlot.
                  IEnumerable<EquipmentSlotUI> equipementSlots = FindObjectsOfType<EquipmentSlotUI>();
                  foreach (EquipmentSlotUI equipementSlot in equipementSlots)
                  {
                      if (equipementSlot.GetEquipLocation() == removedSourceItem.GetAllowedEquipLocation())
                      {
                          destination = equipementSlot;
                          break;
                      }
                  }
                }

                if (source is EquipmentSlotUI)
                {
                    // We clicked on an EquipementSlot. Lets try to find a empty InventorySlot.
                    IEnumerable<InventorySlotUI> inventorySlots = FindObjectsOfType<InventorySlotUI>();

                    foreach (InventorySlotUI inventorySlot in inventorySlots)
                    {
                        if (inventorySlot.GetItem() == null)
                        {
                            // we find an empty slot.
                            destination = inventorySlot;
                            break;
                        }
                    }
                }
                
                // We find a candidate slot for swaping.
                if (destination != null)
                {
                    DropItemIntoContainer(destination);
                }
            }
            lastClickTime = Time.time;
        }
    }
}

We could have changed DragItem directly, but that’s a bad idea in my opinion, because it should not have to know the logic for Equipement, or Inventory, and will introduce circular dependencies.

It could probably also handled to assign an ActionItem inside an empty ActionSlot, but I will wrap up here for my part.

Don’t hesitate to point ou improvements or mistakes.
Thanks.

A clever solution.
I’m going to make one suggestion when dealing with the click delay… Time.time-lastClickTime is unreliable due to the nature of floats and loss of precision as the value increases.

private bool canDoubleClick;


private IEnumerator CountDown(float targetTime)
{
    float timer = 0;
    while(canDoubleClick && timer<targetTime)
    {
         timer+=Time.deltaTime;
         yield return null;
     }
     canDoubleClick=false;
}

public void OnPointerClick(PointerEventData eventData)
{
      if(!canDoubleClick)
      {
            StartCoroutine(CountDown(DOUBLE_CLICK_TIME));
      }  
      else
      {
            canDoubleClick=false; //this will kill the coroutine
            //The rest of your code from the if(timeSinceLastClick<=DOUBLE_CLICK_TIME)

So what I’ve done is set up a float timer that is not subject to float resolution issues. I’ve also made it so that it has no Update() instead only acting when needed (technically, CountDown will behave the same as an Update() method, but will only run for 0.3 seconds on the specific object you’ve clicked.

I usually avoid double-click things in Unity because my experience is that it’s a little unreliable.

That being said, Unity already does some of the legwork for you. The PointerEventData contains a clickCount property that holds how many clicks occurred within a certain time threshold, which I believe is 0.3f as well. If this count is greater than 1, you can consider it a double-click.

But - as always - there are many ways to do something and there’s nothing wrong with anything posted above

Thanks for the tip Brian, but I think there is something missing, it don’t work anymore.

@bixarrio thanks you. It make sense that Unity have someting for a very common usage, I should have checked.
It works now just fine without any timer variable, a simple if(eventData.clickCount > 1) at the begining suffice.
Can you share what kind of experience you have with the double-clicking probem with Unity ? ^^

All this was before I knew about the IPointer... interfaces and have just been avoiding it because of older struggles when I would try to determine double-clicks by counting Input.GetMouseButtonDown(0) within a time threshold, etc. It doesn’t mean that the PointerEventData.clickCount is unreliable. I don’t have experience with that, simply because I’ve been avoiding anything that requires double-clicks.

Hmmm… I was on my shop computer (no code editor/Unity) when I wrote the post. It’s a scheme I use often enough that I typed it from memory. I’ll check it later tonight to see where I went wrong.

In any event, I completely forgot about the clickCount property in PointerEventData. I would recommend using this.

Alright this is great. I really wanted to do this myself. I prefer right-click so I get to avoid the controversy of how to time double clicks, thankfully.

However…

It seems to work to equip items, but right clicking an already equipped item goes to the LAST inventory spots for me. I had thought they were being yeeted into oblivion at first but as I was typing this I went to double check and sure enough they are at the bottom of my substantial-sized inventory. Any thoughts on how to go to first available?

Also… and this is for @Brian_Trotter specifically(but I bet he helps above anyway). I have implemented your secondary inventory system. I love it. I used it for a storage chest in the RPG course but for my main project it will get to wear many hats. It will be the loot container and static word chests holding items. I haven’t implemented loot drops yet and fear when I try to because of the serialized fields I seem to need to make it work properly… BUT my static chests are working like champs. My biggest use from the double/right click automation here will be to right click on items in the secondary inventory UI that pops up for the chests (and eventually loot). So far they dutifully equip directly from the container, but ideally I just want them to go right to inventory. I am not quite sure how to figure out what inventory container I am accessing to code this logic. As this code just looks for Equip and Inventory Slots respectively. Loot containers are Inventory Slots and treated like the player inventory essentially.

Thanks ahead of time. I am busy for awhile before I can dive back into this anyway.

(Edit 1: Well… If I remove the break in the EquipSlot logic it allows it to keep replacing the destination over and over until you get to the last element. Which is inefficient but works so far. I’d like to think there is a better way)

(Edit 2: I added a string tag for Inventory Name to InventoryUI so I can detect when I clicked an element from the secondary inventory weapon and then treat it basically like it was Equipment… Just send it to inventory slots. It actually sort of works. The problem is… and I Can’t believe it took me this long to notice… This code just looks for ALL InventorySlotUI objects in the scene. Which is going to grab the loot window as well. So sometimes it shoots stuff right back to the loot/container window. Same goes for unequipping while the loot window is open. I am happy I got this much working, but clearly a different way of grabbing the InventorySlotUI list is a must :/)

Edit 3: Well… we should all be used to this now. I had a “DUH!” moment. Since I added that Inventory Name string I can go ahead and give the main player inventory a name as well. So the destination add also checks to make sure the parent object’s copy of InventoryUI’s name matches the Player inventory before adding. I still have to skip the break, though. Keep replacing the destination throughout the whole foreach. I had hoped this workaround would fix it but I guess the giant IEnumerator of all the InventorySlotsUI still includes EVERYTHING so that is probably why for me specifically it is behaving this way…

All in all… any insight is still appreciated. If there is a better way or if you have any fixes or concerns for me.

It’s been a minute since I looked at that Secondary Inventory system… I’ll have to go through it and I’ll see what I can come up with.

Cool. No rush since it appears I actually having it working like I need it to. Just maybe not super efficient. I’d still be thrilled to find a better way to grab all the slots, too. But performance seems okay.

The real help I’ll need down the line is spawning loot. Making a pre-set container is super easy. I can serialize the show/hide and make an array of items serialized as well. Start populates the inventory on it by adding all the items. Voila. Done.

I just wouldn’t know how to do a random loot bag like this in real time. So you can think on that too while you review your secondary inventory too :joy:

Privacy & Terms