Drag&Drop for UI elements in Unity, the simple(-ish) way.

Jonas Hundertmark
medialesson
Published in
5 min readAug 21, 2022
Photo by Kelly Sikkema on Unsplash

Today, we’ll be building a lightweight and easy-to-implement way for Drag&Drop in Unity’s UI system. We’ll learn about drag layers and PointerEventHandlers and why Unity’s own Event Trigger component is the worst thing you could possibly put in your hierarchies.

Let’s start with the basics.

The setup

Suppose we have a simple Canvas Group with multiple elements randomly placed on it, like magnets on a fridge or post-it notes on a whiteboard. We want to be able to individually drag every element across the screen, with elements being able to overlap each other freely. Whenever we grab an object, it should be rendered above any other objects on that plane (as we’re “holding” it up over the other ones.) Objects should stack in the order we place them, so the most recent object we dragged is always on top. It is also not allowed to drag an object over the edge of our Canvas Group.

We’ll start by creating a Canvas and a Canvas Group that acts as our bounding box for all drag-and-drop operations. You can also add a background image, if you like.

Next, create two empty GameObjects as children of our Canvas Group. Let’s call them Default Layer and Drag Layer. Strech both of them to be the same size as our bounding box. Note that Drag Layer is below Default Layer in our hierarchy, meaning it (and its children) will be rendered second.

Next up, let’s create some objects for us to drag around! These can be anything, as long as one of their components inherits from Graphic. Set them as children of DefaultLayer. We’ll also create a new MonoBehaviour on those objects and call it “DragObject”. Now, implement the following interfaces on your DragObject script: IDragHandler, IBeginDragHandler, IEndDragHandler.

Sometimes, you might want your DragObjects to have a slightly larger area where they register drag events, so they become easier to grab. You can achieve this by adding another transparent Image or RawImage component as a child of your DragObject, which acts as an invisible hitbox.

Once you’re happy, create another MonoBehaviour on the Bounding Box itself, and call it DragManager. This one doesn’t need any special interfaces. We’ll use it to assign Layers to our DragObjects later.

Building our Drag&Drop Logic

Before we continue, let’s look at a high level overview of what we’re trying to achieve in the first place:

All of our DragObjects are placed on the DefaultLayer, where they sit until the user tries to grab them. We then move the object to the above DragLayer, where we can guarantee it will be rendered above everything else. Once the user lets go of the object, we’ll set our transform’s parent to be the DefaultLayer again. Since our dragged GameObject is now the most recent sibling added to the layer, it’s also placed above any other objects below it. You could, in theory achieve the same result with Transform.SetSiblingIndex, but I feel this approach is much more elegant and will keep unwanted interactions (e.g. Collider2D) from happening while you’re dragging objects around.

The next part is not required, but I strongly encourage using WorldSpace coordinates when checking whether an object is inside of bounds. We will need to jump through some hoops in order to get world coordinate Rects for our bounding box, but we’ll get to that in a moment.

The DragObject script is fairly simple. Since our DragManager class will handle most of the calculations later, all our DragObject needs to know is whether it is allowed to move in that specific direction. If yes, we translate its Transform component. If not, no action is taken. The easiest way for us to do this, is to add PointerEventData.delta to the world space TransformPoint of our DragObject and check whether the bounding box (still) contains that point. There’s a bit of boilerplate code to quickly access the world space center of our Rect. Don’t worry too much about it.

Next up, the DragManager. We’ll hold a reference to our currently dragged DragObject in case we want to access it from somewhere else in the future. To get a world space Rect, that we can check the position of our DragObject against, we’ll use RectTransform.GetWorldCorners() and build our Rect that way. We’ll also save that Rect as a reference, so we don’t need to create a new object every time we need to check a DragObject’s position.

A quick tangent on EventTriggers

The way that EventHandlers (like IDragHandler etc.) work in Unity is that on every user input, Unity will go through your scene tree in reverse, starting from the object directly below your pointer, and try to call the corresponding function from that interface, if it finds any. Once Unity has found a component with the correct EventHandler implemented, it will stop bubbling the corresponding event.

You may have noticed already that instead of writing our own DragObject MonoBehaviour, we could also just as well place an EventTrigger Component on each of our DragObjects and call some monolithic controller class to take care of all the objects for us. The problem is, that EventTriggers implement every EventHandler at once, even those you have no interest in. This is terrible for maintaining complex UIs as you will find some of your inputs just magically vanish with no way to properly trace why you weren’t able to click a Button or drag a ScrollView. It is generally advised to never use EventTriggers and write your own implementations instead, even if you’re just using them to call behaviours on other GameObjects, to maintain better control over your Scenes.

Conclusion

With only these two simple C# scripts we have implemented an easy-to-use behaviour system for Drag & Drop in our UI canvases, which can be expanded at will. We’ve learned about EventHandlers and how to use them, and we learned why relying on EventTriggers too much can be detrimental to the quality of our Application.

Until next time!

--

--