Devlog 64: How to add drag and drop to inventory in UE

inventory drag
Drag item in inventory

Handling inventory is a relatively complicated topic. It’s been covered in multiple posts and here are some of them:

In this post, we will be covering how I added drag and drop functionality to my inventory items.

I will try reference most of the required widgets in this post, but it may not be everything as you can see there’s been a lot of content covered already – it will be too large to try include everything here.

Inventory overview and drag & drop on item

Inventory Component

This is one of the more complex components that I have linked to my player.

It processes many things, sending data to server as well as receiving the data.

Let’s begin with Event Begin Play to see what we do at the beginning.

Constructor for inventory component

The functions called are:

  • link to inventory widget
  • link to equipped item slots widgets
  • and link to equipped items widget
  • request to fetch inventory
  • request to fetch equipped items
  • register user inputs

Let’s reference those functions:

Link to inventory widget
Link to equipped items slots
Link to equipped item widget
Fetch inventory request
Fetch Equipped items request
Register user inputs

I won’t go into much details on above as they’re not directly related to drag & drop functionality. The reason I need to register the component is because this actor didn’t exist when the HUD was created, so I need to link it after the actor is created.

The main one that will be of interest is part of Link to inventory widget, there’s function Link player to Inventory:

Bind widget to inventory component events

This binds events from Inventory component to the widget – we will cover the functions below.

Processing inventory update

I will receive messages from the server indicating inventory updates. This is handled by my Process Message function.

Process message - process inventory update

I will focus specifically on the inventory update.

Process inventory update

For now, I am only updating 2 parts

  • the inventory size, which creates the slots for us to use
  • inventory items, which go inside the slots

Process inventory size

Process inventory size

My server doesn’t send delta updates for inventory at this point, which means it will send me the entire inventory on each update. This is quite inefficient but in this function I try reduce the load on cascading functions.

I check what the max size of the inventory is, which is just a struct containing 2 numbers, representing rows and columns.

If this number is different to what is currently promoted in local variable, then I will send an event dispatcher, broadcasting the new inventory size. The widget will be listening to this event.

Process Changed Inventory Slots

This just takes an array of character items and converts them to a map, where the key is the location of the item. This is useful because I use the key to locate the slot and the item that belongs to the slot.

The character item structure is:

Character item structure

Location2d structure is:

Location 2D structure

Item instance structure is:

Item instance structure

And the item structure is:

Item structure

Note that the widget blueprints will consume the event dispatchers, which we’ll see in chapters below.

Inventory Widget

The overall inventory widget is composed of 2 main things. The equip items panel and the inventory contents.

Inventory widget which uses equip item widget and inventory panelm

I will focus on the inventory panel, which is the bottom half of this widget.

The inventory panel widget is a relatively simple one:

The main thing here is that I have a Scroll Box and inside that a Grid Panel.

Inside the grid panel, I will create all the inventory slots that the character has available.

There are many ways of representing an inventory. Modern inventories are using a 1 dimensional array to store and present items. This is much simpler to implement and perhaps if I re-design it, I’d go with 1D approach.

However I went with a more classic 2D approach.

The inventory size is controlled through the Java server, so the client will get a message from server telling the widget how many slots it needs to create.

This is one of the event dispatchers that we bind on, that we introduced in ‘Process inventory size‘ section above.

Update max inventory slots

Update max inventory slots in inventory

This is first half of the function. This is a classic nested FOR loop. It looks a little inflated in blueprints.

The server will basically provide the client the 2D size of the inventory, for example 2×2 will build me a inventory of 2×2 slots (4 slots total).

I will likely have logic to increase inventory capacity, so its dynamic – yours may be static which will make this a bit simpler.

Creating slots for the inventory widget

Here’s the second half of the function. Essentially the nested for loop will increment the current row and column. These variables are used to know in which position to put the slot in, inside uniform grid component.

Position (row/column) information is also used to locate the slot, you can see its referenced inside Inventory Slot Widget itself as well as the Item Slots Map which contains the widget references.

So as we iterate over the slots, for each row and column entry, we add it to the uniform grid which keeps things nice and neat for us. We also store the widget as a reference in a map Item Slots variable.

This map will allow us to find the slot we need to update quickly, when we get a inventory slot update.

Update Inventory Items

Remember that this function is actually bound as part of event begin play of the Inventory Component, see Link player to Inventory (ctrl + F to find it above).

This function is a bit longer, mainly because we get an entire state of inventory, rather than individual updates.

The state also does NOT include null items, i.e. it does not include empty slot entries – this will be relevant when we look at some of the implementation below.

Update inventory items part 1

Update inventory items, part 1
  • Iterate over Map of character items, where the key represents the Location2D of where the item is (the location of the slot)
  • Promote the processing location in local variable
  • Add this location to a Set (Present Item Locations). This is because we will need to process the remainder slots and clear them out after. Set is more efficient than Array for lookup.
  • Ignore character items where location is negative, this is because equipped items have been provided a -1, -1 location in my implementation.

Update inventory items part 2

Process inventory items part 2
  • get the character items from function param, using the key
  • get the item information we need from the character item
  • find the item slot, using location information (covered part of Update max inventory slots)
  • Update slot with item
  • if slot doesn’t exist, retry again after delay

I introduced a small retry function, in case somehow my slot wasn’t created before I receive this inventory update.

Retry implementation if slot doesn't exist

It’s worth noting that I’ve not yet come across this issue.

Update inventory items part 3

Last part of function is connected to Completed on the FOR loop above.

Null out slots with no items

This nulls out the rest of the slots. Remember that the items I receive don’t include empty slots? This means that I don’t know which ones previously had an item and now no longer have one.

Therefore I iterate over all of them, while filtering the Preset Item Locations and update slot with item of null.

Also, I added a small optimization to Update Slot to only do something if the item reference is different to before:

Update slot optimization

The slot itself has a reference of the Item, so its able to check if the old == new and only update if its changed. This is valid even on nulls.

Inventory item slot widgets

Finally we get to the item slot widgets.

Item widget
Button set visibility to not hit - testable

These can be designed in numerous ways. There’s one important factor to note. For Drag and drop functionality, you will need one of the following widgets included:

  • Canvas panel
  • Border
  • Overlay
  • Vertical box / Horizontal box / Uniform grid
  • Size box
  • Button

This is because those components are able to detect the OnDrop event.

It’s worth noting that I had some issues with nested components.

To fix my issues, I have a root border, which has Visibility set to visible:

Set root component to Visibility: Visible

And as you can see from initial image, the Button has Not Hit – Testable (Self & All Children). This has a drawback that I can’t use the button itself – but I found the performance of overlay was better without it.

Updating slot icon

This was introduced above, but let’s reference it again.

Update slot with item data
Update slot with icon from item

If you’re interested in how I designed my Item references, they can be found here

Mouse events & Drag and drop

Mouse events and drag & drop

I experimented with different events for click & double click, from button and overlays and I found the overlay to perform better when combined with drop operations.

Mouse events

On Mouse Button Down:

Note that this is an override function for the widget, which will default be applied to the root component.

When mouse is down, we will begin to detect Drag event.

On Mouse Button Double Click:

Equip item on double click event

Interestingly, we have events for double click, where as buttons for example, don’t have this available.

Here’s how I call this function and you can see I experimented with few other methods.

Equip item function called

And just for reference, the equip item function is a request to java server:

Equip item request to java server

On Drag Detected

When we pressed mouse, we started to detect drag. If its detected, we call this function:

On Drag Detected, create drag drop operation

I decided to simply use the default drag and drop operation methods. I think they are flexible enough to scale.

The main thing is that the Tag is set to item so that I can distinguish it on the drop event.

Payload is set to Item object (custom object – this can be whatever you need it to be, post introducing this object is referenced at start of post).

Default Drag Visual – this is the widget which will be visible as you’re dragging this slot. I simply re-used the Item Icon that’s already part of my widget.

On Drop Operation

I will prepare this to potentially handle multiple different operations, though I expect I should only really handle item drop events here.

Handle on drop events in inventory

You can see that I receive the operation and I just check the Tag is equal to item.

If its item drop operation, then I will request inventory component to handle this as a request to the java server.

Handle item move request

As you can see, in order to move an item, I need the Item Instance ID and the location where it will go to. For now I am supporting to moving it to another slot, but next I will also allow you to move it to equip slot, which will be referred to as Category (e.g. helmet, gloves, etc).

Move item request to Java server

Processing Move Item Request in Java

Just for reference, the main part of the Java server which processes this request can be found in InventoryService.java: custom java server : inventory service.

At the time of writing, the function looks like this:

    public Single<Inventory> moveItem(String actorId, String itemInstanceId, Location2D to) {
        return getInventory(actorId)
                .doOnError(er -> log.error(er.getMessage()))
                .map(inventory -> {
                    CharacterItem movingItem = inventory.getItemByInstanceId(itemInstanceId);
                    CharacterItem itemAtLocation = inventory.getItemAtLocation(to);
                    // if item exists at location, let's swap their locations

                    if (movingItem == null) {
                        log.error("error moving item, failed to find item to move");

                        throw new InventoryException("Failed to move item, item not found");
                    }

                    if (itemAtLocation != null) {
                        itemAtLocation.setLocation(movingItem.getLocation());
                    }

                    movingItem.setLocation(to);

                    inventoryRepository.updateInventoryItems(actorId, inventory.getCharacterItems()).subscribe();

                    return inventory;
                });
    }

This function will simply update the location of the item to the new desired location. If another item exists in the new slot, it will swap the two locations.

After update, it will return the inventory back to the user, which is why we can see the inventory update itself.