17

These days it's incredibly easy to drag UI elements in Unity: Make a few UI items. Add Component -> Event -> Event Trigger. Drop on the script below. Click to add the four obvious triggers. You're done.

However.

I'm totally lost in the relationship between pointer coordinates and UI coordinates (as seen in RectTransform and so on).

In DragIt below: how the hell do you move a UI panel correctly under the finger?

Say you have one large panel, with ten UIButton sitting in the panel with Dragster on the buttons. What is the relationship between the RectTransform coords and the mouse pointer ...

in short how do you move one of the button around at DragIt() below?

/* modern Unity drag of UI element */
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using UnityEngine.EventSystems;
public class Dragster:MonoBehaviour
    {
    public int index; // number each of your UI items
    static bool beingDragged = false;
    static int dragFrom;
    public void DragStart()
        {
        beingDragged = true; dragFrom = index;
        }
    public void DragIt()
        {
        ? ? W T F ? ?
        }
    public void DragEnd()
        {
        beingDragged = false;
        }
    public void DroppedBra()
        {
        Debig.Log("Drag: from/to " +dragFrom +" --> " +index);
        }
    }
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • For converting the pointer data to UI coordinates you could look into the RectTransformUtilty. – Vinod Vijayan May 27 '16 at 11:31
  • So after Typing all these, I read this again and realized that you attached `Dragster` to each UI which made it easier to code. My solution was totally a different stuff. It uses one script only to detect any panel/image and button and then drag them. You don't attach it it to each UI each UI. It has to be attached to the parent Canvas. This is all waste. Forgive me for wasting your time. I will read the question right next time.... – Programmer May 27 '16 at 17:31
  • Its only useful if you only want to use one script. Otherwise its a waste of your time. – Programmer May 27 '16 at 17:32
  • Thanks. I don''t know what you mean by **AND YOU FIGURED OUT HOW TO CONVERT**. – Programmer May 28 '16 at 00:27
  • Isn't the ScrollRect component doing what you want? – Everts May 28 '16 at 12:53
  • Alright then, yep not the same at all. Misread the thing as just button should move around. – Everts May 28 '16 at 14:08
  • If you ever need to do drag and drop, the code below in Colton's answer is amazingly useful. Cheers – Fattie May 28 '16 at 15:38

4 Answers4

12

I would make your script implement the drag interfaces

public class Dragster:MonoBehaviour,IBeginDragHandler, IEndDragHandler, IDragHandler

Which will make your DragIt function become

public void OnDrag(PointerEventData eventData)
{
    transform.position += (Vector3)eventData.delta;
}

giving you access to the delta of that event (how much the mouse has moved) to be able to move your object.

If you would still rather use the EventTrigger component (less prefered way), you just need to change your DragIt function to DragIt(PointerEventData eventData) and use the Dynamic EvenData option in the drop down for the trigger to receive the PointerEventData to access the delta information


Here's actually a total, complete, solution for drag and drop 'UnityEngine.UI` items, based on Uri & Colton's code. Just copy and paste.

Astounding copy and paste no-brainer perfect drag and drop for Unity UI, wtt Colton & Uri:

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class UNCDraggable:MonoBehaviour,
IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler
    {
    public Image ghost;
    // note DON'T try to drag the actual item: it's not worth the hassle.
    // a problem arises where you can't have it on top (as you would want
    // visually), and still easily get the drops. always use a ghost.
    // even if you want the "original invisible" while dragging,
    // simply hide it and use a ghost. everything is tremendously
    // easier if you do not move the originals.
    
    void Awake()
        {
        ghost.raycastTarget = false;
        // (just in case you forgot to do that in the Editor)
        ghost.enabled = false;
        }
    
    public void OnBeginDrag(PointerEventData eventData)
        {
        ghost.transform.position = transform.position;
        ghost.enabled = true;
        }

    public void OnDrag(PointerEventData eventData)
        {
        ghost.transform.position += (Vector3)eventData.delta;
        }

    public void OnEndDrag(PointerEventData eventData)
        {
        ghost.enabled = false;
        }
    
    public void OnDrop(PointerEventData data)
        {
        GameObject fromItem = data.pointerDrag;
        if (data.pointerDrag == null) return; // (will never happen)
        
        UNCDraggable d = fromItem.GetComponent<UNCDraggable>();
        if (d == null)
          {
          // means something unrelated to our system was dragged from.
          // for example, just an unrelated scrolling area, etc.
          // simply completely ignore these.
          return;
          // note, if very unusually you have more than one "system"
          // of UNCDraggable items on the same screen, be careful to
          // distinguish them! Example solution, check parents are same.
          }
        
        Debug.Log ("dropped  " + fromItem.name +" onto " +gameObject.name);
        
        // your code would look probably like this:
        YourThings fromThing = fromItem.GetComponent<YourButtons>().info;
        YourThings untoThing = gameObject.GetComponent<YourButtons>().info;
        
        yourBossyObject.dragHappenedFromTo(fromThing, untoThing);
        }
    }
Community
  • 1
  • 1
Colton White
  • 976
  • 5
  • 17
  • 1
    Also @JoeBlow yea the unity event system is tied into the UI system. by using the delta and not just setting the transform position you maintain the offset to where the user actually clicked, so if they click one corner and your pivot is in another corner it will jump to that new position. – Colton White May 27 '16 at 15:14
  • @JoeBlow Tried Uri's answer and it works. It did work. Do you still need my answer? I solved some problems too lol – Programmer May 27 '16 at 15:22
  • @ColtonWhite Your code did not compile. transform.position += eventData.delta; did not work. I changed it and removed the + sign and it the UI was not even moving to the correct position. It jumped to the bottom down of the screen. Uri's answer worked though. Maybe I was doing it wrong... – Programmer May 27 '16 at 15:24
  • @Programmer yea sorry i didnt realize that it wouldn't be able to add that directly without the cast, i wrote my answer without having the editor open in front of me. JoeBlow did fix my compiler error though which should fix the issue and it should work correctly. – Colton White May 27 '16 at 15:29
  • @Programmer that does seem to work for me: it does need the cast though or you get a warning about Vector3/Vector2 ... `ghost.transform.position += (Vector3)eventData.delta;` – Fattie May 27 '16 at 15:31
  • This would have to be one of the most useful QA on the entire internet. – Fattie May 27 '16 at 15:49
  • @JoeBlow Lol ok. Still typing – Programmer May 27 '16 at 16:13
  • if you guys are following this I added a small fix to the code in `OnDrop`. Awesome. – Fattie Jun 02 '16 at 21:36
10

First of all, All the other anwers in this post are working very well. I worked on this for so long and just wanted to post it here. It adds a way prevent other unwanted UI objects to be dragged around.

My official goal was to provide a way to do this without using bool beingDragged = false;. You just won't know which Button or Image is being dragged if you do it like that.

Dragging UI:

Convert Screenpoint to Local point in the RectTransform with help of RectTransformUtility then use Canvas.transform.TransformPoint to find out where exactly the child UI is.

public Canvas parentCanvasOfImageToMove;
Vector2 pos;
RectTransformUtility.ScreenPointToLocalPointInRectangle(parentCanvasOfImageToMove.transform as RectTransform, eventData.position, parentCanvasOfImageToMove.worldCamera, out pos);
UIToMove.transform.position = parentCanvasOfImageToMove.transform.TransformPoint(pos);

The drag code looks more complicated than other drag code in other answers but it seems to be working in every Canvas camera mode.

Detecting which Object is about to be dragged:

The easiest way of doing this is to create a global variable you can use to save which object user wants to drag in the OnBeginDrag function then you can drag that object in the OnDrag. Set that object to null when OnEndDrag is called.

objectToBeDragged = eventData.pointerCurrentRaycast.gameObject;

This must be done once in the OnBeginDrag function then saved to a global variable.

You can't do the following in the OnDrag function

if (eventData.pointerCurrentRaycast.gameObject == someOtherUI)
{
   someOtherUI....drag
}

Even though it is suppose to work, it doesn't sometimes. It even return null sometimes during OnDrag. That's why it has to be done in the OnBeginDrag function.

Detecting and Dragging Button Vs Image:

Detecting if the UI is just an Image and Dragging an Image is very easy.

objectToBeDragged  = eventData.pointerCurrentRaycast.gameObject;
Button tempButton = objectToBeDragged.GetComponent<Button>();
Image tempImage = objectToBeDragged.GetComponent<Image>();

If tempImage is not null and tempButton is null then that is an Image.

Detecting if the UI is just a Button and Dragging a Button is NOT easy. When a Button is clicked on the side/edge, the name of the Button is returned which is fine. But most of the times, a click on a Button happens in the middle of the Button which does not return the instance or name of the Button but instead returns the Text(Child Object). You CANNOT move a text as a Button. It wont work.

objectToBeDragged  = eventData.pointerCurrentRaycast.gameObject;
Button tempButton = objectToBeDragged.GetComponent<Button>();
Image tempImage = objectToBeDragged.GetComponent<Image>();
Text tempText = objectToBeDragged.GetComponent<Text>();

if tempText is not null, get GetComponentInParent of Image and Button component of the Text. If the Image is not null and Button is not null then it is a Button.

if (tempText != null)
{
    tempButton = tempText.GetComponentInParent<Button>();
    tempImage = tempText.GetComponentInParent<Image>();
    if (tempButton != null && tempImage != null)
    {
        //This is a Button
    }
}

Below is the complete script of dragging UI Image/Panel and Button. Any Button that should be dragged should be put in the UIButtons array and any Panel/Image that should be dragged should be put in the UIPanels array. It will ignore other UI that are not in the Array.

public class UIDRAGGER : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    public Canvas parentCanvasOfImageToMove;

    //10 UI Buttons (Assign in Editor)
    public Button[] UIButtons;

    //2 UI Panels/Images (Assign in Editor)
    public Image[] UIPanels;

    //Hold which Button or Image is selected
    private Button selectedButton;
    private Image selectedUIPanels;

    //Used to make sure that the UI is position exactly where mouse was clicked intead of the default center of the UI
    Vector3 moveOffset;

    //Used to decide which mode we are in. Button Drag or Image/Panel Mode
    private DragType dragType = DragType.NONE;


    void Start()
    {
        parentCanvasOfImageToMove = gameObject.GetComponent<Canvas>();
    }

    //Checks if the Button passed in is in the array
    bool buttonIsAvailableInArray(Button button)
    {
        bool _isAValidButton = false;
        for (int i = 0; i < UIButtons.Length; i++)
        {
            if (UIButtons[i] == button)
            {
                _isAValidButton = true;
                break;
            }
        }
        return _isAValidButton;
    }

    //Checks if the Panel/Image passed in is in the array
    bool imageIsAvailableInArray(Image image)
    {
        bool _isAValidImage = false;
        for (int i = 0; i < UIPanels.Length; i++)
        {
            if (UIPanels[i] == image)
            {
                _isAValidImage = true;
                break;
            }
        }
        return _isAValidImage;
    }

    void selectButton(Button button, Vector3 currentPos)
    {
        //check if it is in the image array that is allowed to be moved
        if (buttonIsAvailableInArray(button))
        {
            //Make the image the current selected image
            selectedButton = button;
            dragType = DragType.BUTTONS;
            moveOffset = selectedButton.transform.position - currentPos;
        }
        else
        {
            //Clear the selected Button
            selectedButton = null;
            dragType = DragType.NONE;
        }
    }

    void selectImage(Image image, Vector3 currentPos)
    {
        //check if it is in the image array that is allowed to be moved
        if (imageIsAvailableInArray(image))
        {
            //Make the image the current selected image
            selectedUIPanels = image;
            dragType = DragType.IMAGES;
            moveOffset = selectedUIPanels.transform.position - currentPos;
        }
        else
        {
            //Clear the selected Button
            selectedUIPanels = null;
            dragType = DragType.NONE;
        }
    }


    public void OnBeginDrag(PointerEventData eventData)
    {
        GameObject tempObj = eventData.pointerCurrentRaycast.gameObject;

        if (tempObj == null)
        {
            return;
        }

        Button tempButton = tempObj.GetComponent<Button>();
        Image tempImage = tempObj.GetComponent<Image>();
        Text tempText = tempObj.GetComponent<Text>();

        //For Offeset Position
        Vector2 pos;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(parentCanvasOfImageToMove.transform as RectTransform, eventData.position, parentCanvasOfImageToMove.worldCamera, out pos);


        //Button must contain Text then Image and Button as parant
        //Check if this is an image
        if (tempButton == null || tempImage == null)
        {
            //Button not detected. Check if Button's text was detected
            if (tempText != null)
            {
                //Text detected. Now Look for Button and Image in the text's parent Object
                tempButton = tempText.GetComponentInParent<Button>();
                tempImage = tempText.GetComponentInParent<Image>();

                //Since child is text, check if parents are Button and Image
                if (tempButton != null && tempImage != null)
                {
                    //This is a Button
                    selectButton(tempButton, parentCanvasOfImageToMove.transform.TransformPoint(pos));
                }
                //Check if there is just an image
                else if (tempImage != null)
                {
                    //This is an Image
                    selectImage(tempImage, parentCanvasOfImageToMove.transform.TransformPoint(pos));
                }
            }
            else
            {
                //This is an Image
                selectImage(tempImage, parentCanvasOfImageToMove.transform.TransformPoint(pos));
            }
        }
        //Check if there is just an image
        else if (tempImage != null)
        {
            selectImage(tempImage, parentCanvasOfImageToMove.transform.TransformPoint(pos));
        }
    }

    public void OnDrag(PointerEventData eventData)
    {
        Vector2 pos;
        if (dragType == DragType.BUTTONS)
        {
            RectTransformUtility.ScreenPointToLocalPointInRectangle(parentCanvasOfImageToMove.transform as RectTransform, eventData.position, parentCanvasOfImageToMove.worldCamera, out pos);
            selectedButton.transform.position = parentCanvasOfImageToMove.transform.TransformPoint(pos) + moveOffset;
        }
        else if (dragType == DragType.IMAGES)
        {
            RectTransformUtility.ScreenPointToLocalPointInRectangle(parentCanvasOfImageToMove.transform as RectTransform, eventData.position, parentCanvasOfImageToMove.worldCamera, out pos);
            selectedUIPanels.transform.position = parentCanvasOfImageToMove.transform.TransformPoint(pos) + moveOffset;
        }
    }


    public void OnEndDrag(PointerEventData eventData)
    {
        //Buttons
        if (dragType == DragType.BUTTONS || dragType == DragType.IMAGES)
        {
            selectedButton = null;
            selectedUIPanels = null;
            dragType = DragType.NONE;
        }
    }

    DragType getCurrentDragType()
    {
        return dragType;
    }

    private enum DragType { NONE, BUTTONS, IMAGES };
}
Programmer
  • 121,791
  • 22
  • 236
  • 328
  • @ina "this value seems to always be 0" What value? It would be good to explain what you are doing and how it's not working – Programmer Jun 18 '18 at 06:09
  • @programmer both `parentCanvasOfImageToMove.transform.TransformPoint(pos)` and `pos` seem to always be 0 vecs? – ina Jun 18 '18 at 18:10
  • nevermind, i just relized that everything was dependent on worldCamera being assigned. added a if(worldCamera==null) worldCamera=Camera.main – ina Jun 18 '18 at 18:11
7

For Draging stuff I just do this :

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class Draggable : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler {
    
    
    
    public void OnBeginDrag(PointerEventData eventData) {
        
    }
    
    public void OnDrag(PointerEventData eventData) {
        //Debug.Log ("OnDrag");
        
        this.transform.position = eventData.position;

        }

    public void OnEndDrag(PointerEventData eventData) {
        Debug.Log ("OnEndDrag");
    
    }
}

Here's the identical amazing URIPOPOV CODE with two simple features which you always need when dragging:

// allow dragging with two basic problem fixes:
// - limit drag to the parent box
// - don't "jump" based on where you hold down

100% tested:

Note that you MUST USE the scale conversion code from #programmer below.

using UnityEngine;
using UnityEngine.EventSystems;

public class AmazingUPDrag : MonoBehaviour,
               IBeginDragHandler, IDragHandler, IEndDragHandler
{

RectTransform rt;
Vector2 dragOffset = Vector2.zero;
Vector2 limits = Vector2.zero;

Canvas canvas;

void Awake()
{
    rt = GetComponent<RectTransform>();
    canvas = GetComponentInParent<Canvas>();
}

public void OnBeginDrag(PointerEventData eventData)
{
    dragOffset = ActualPos(eventData.position) - (Vector2)transform.position;
    limits = transform.parent.GetComponent<RectTransform>().rect.max;

    limits.x = limits.x - 100f;
    limits.y = limits.y - 100f;
}

public void OnDrag(PointerEventData eventData)
{
    transform.position = ActualPos(eventData.position) - dragOffset;

    var p = transform.localPosition;
    if (p.x < -limits.x) { p.x = -limits.x; }
    if (p.x > limits.x) { p.x = limits.x; }
    if (p.y < -limits.y) { p.y = -limits.y; }
    if (p.y > limits.y) { p.y = limits.y; }
    transform.localPosition = p;
}

public void OnEndDrag(PointerEventData eventData)
{
    dragOffset = Vector2.zero;
}

Vector2 ActualPos(Vector2 unityDelusionPos)
{
    Vector2 p;
    RectTransformUtility.ScreenPointToLocalPointInRectangle(
        (RectTransform)(canvas.transform),
        unityDelusionPos,
        canvas.worldCamera,
        out p);
    return canvas.transform.TransformPoint(p);
}


}
Fattie
  • 27,874
  • 70
  • 431
  • 719
Uri Popov
  • 2,127
  • 2
  • 25
  • 43
  • @JoeBlow wait so do you want to drag non-UI elements or UI elements ? – Uri Popov May 27 '16 at 13:59
  • @JoeBlow I created a completely empty scene. NO camera. Just a canvas, a panel in the canvas, a button in the panel and A EventSystem object. My code drags the button around just fine. Some weird graphical glitches from the lack of a camera but the draging is fine. – Uri Popov May 27 '16 at 14:30
  • @JoeBlow right I have given up on that and just go with what works. – Uri Popov May 27 '16 at 14:48
  • 1
    I just want to confirm to all readers that this ***does work perfectly*** - three cheers. – Fattie May 27 '16 at 14:55
  • this indead works for ui elements only attach it to the object and it will move – Geomorillo Mar 18 '17 at 16:02
0

OK, so the first solution(s) works for UI elements (Overlay Canvas) The one from @programmer also works for Other Canvases, but has a bit of overhead for my usage. So here I extract what I think is the minimum you need to drag your "current object"

using UnityEngine.EventSystems;

public class SomeObject : MonoBehaviour, IBeginDragHandler, IDragHandler
{
  private Canvas parentCanvas;
  private Vector3 moveOffset;

  void Start()
  {
      // get the "nearest Canvas"
      parentCanvas = GetComponentsInParent<Canvas>()[0];
      // you could get the Camera of this Canvas here as well and like @ina suggests add a null check for that and otherwise use Camera.Main
  }
  public void OnBeginDrag(PointerEventData eventData)
  {

      //For Offset Position so the object won't "jump" to the mouse/touch position
      RectTransformUtility.ScreenPointToLocalPointInRectangle(parentCanvas.transform as RectTransform, eventData.position, parentCanvas.worldCamera, out Vector2 pos);
      moveOffset = transform.position - parentCanvas.transform.TransformPoint(pos);

  }

  public void OnDrag(PointerEventData eventData)
  {
      RectTransformUtility.ScreenPointToLocalPointInRectangle(parentCanvas.transform as RectTransform, eventData.position, parentCanvas.worldCamera, out Vector2 pos);
      transform.position = parentCanvas.transform.TransformPoint(pos) + moveOffset;
  }
}

Code is composed of lines grabbed from an actual class of mine, so it's not a tested script. And of course you need some usings

Niek
  • 989
  • 2
  • 11
  • 23
  • Thanks for the snippet you shared including the move offset with your code. I was running into the issue where the object would snap and it was quite frustrating. This move offset worked like a charm! – Joseph Markiewicz Jun 28 '22 at 02:49