In the last post I gave a quick overview over the new event system that was introduced with Unity 4.6. Following up on those basics I’d like to give you some insights about the customizations I made to fully utilize the possibilities of the event system in our current project. The first big change I made was adding a generic Drag & Drop system by using an own input module derived from the existing PointerInputModule.
General workflow
Before diving into the implementation details, let’s get a rough idea how a generic drag & drop operation should work.
The following steps should be performed and be able to customize for specific projects:
- Begin Drag
- Get custom drag data from the dragged object
- Continue Drag
- Check, if a drop would be successful currently
- Inform dragged object if the drop would be successful (e.g. for a visual effect)
- End Drag
- Perform drop operation if over a valid drop object
- Inform dragged object about success of operation (e.g. for a visual effect)
This should make up a pretty solid drag & drop framework already, especially with letting the dragged object know about the current state of the operation. This way nice user feedback is possible.
Extending the input module
The current state of the drag & drop operation has to be stored somewhere. This is where I extended Unity’s PointerInputModule as there is some logic (starting the operation, updating the success of the operation during the drag) that has to be performed during every drag operation.
For the handlers though, only the ability to get the current drag & drop operation is required, so I added a small interface that the handlers work with instead of the concrete input module:
1 2 3 4 5 6 7 8 |
public interface IDragDropManager { #region Public Methods and Operators DragDropOperation GetDragDropOperation(int pointerId); #endregion } |
The drag & drop operation itself looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public class DragDropOperation { #region Properties /// <summary> /// Data depending on specific drag drop operation. /// </summary> public object Data { get; set; } /// <summary> /// Dragged game object. /// </summary> public GameObject DragObject { get; set; } /// <summary> /// Game object the dragged one was dropped on. /// </summary> public GameObject DropObject { get; set; } /// <summary> /// Indicats if the drag drop operation will be/was successful. /// </summary> public bool DropSuccessful { get; set; } #endregion #region Public Methods and Operators public void Reset() { this.Data = null; this.DragObject = null; this.DropObject = null; this.DropSuccessful = false; } #endregion } |
To store the drag & drop operations, the ExtPointerInputModule as I called it, contains a dictionary from the pointer id to the operation object:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private readonly Dictionary<int, DragDropOperation> dragDropOperations = new Dictionary<int, DragDropOperation>(); public DragDropOperation GetDragDropOperation(int pointerId) { DragDropOperation operation; this.dragDropOperations.TryGetValue(pointerId, out operation); return operation; } private DragDropOperation GetOrCreateDragDropOperation(int pointerId) { DragDropOperation operation; if (!this.dragDropOperations.TryGetValue(pointerId, out operation)) { operation = new DragDropOperation(); this.dragDropOperations[pointerId] = operation; } return operation; } |
Start of operation
Unfortunately there is no virtual callback in the PointerInputModule, so we have to check on our own if a drag & drop operation should be started:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var moving = pointerEvent.IsPointerMoving(); var beginDragging = moving && !pointerEvent.dragging && ShouldStartDrag( pointerEvent.pressPosition, pointerEvent.position, this.eventSystem.pixelDragThreshold, pointerEvent.useDragThreshold); if (beginDragging) { if (pointerEvent.pointerDrag != null) { // Create new drag drop operation. var dragDropOperation = this.GetOrCreateDragDropOperation(pointerEvent.pointerId); dragDropOperation.Reset(); dragDropOperation.DragObject = pointerEvent.pointerDrag; } } |
The ShouldStartDrag method was just copied from the PointerInputModule as it is private:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private static bool ShouldStartDrag( Vector2 pressPos, Vector2 currentPos, float threshold, bool useDragThreshold) { if (!useDragThreshold) { return true; } return (pressPos - currentPos).sqrMagnitude >= threshold * threshold; } |
During the operation
The input module has another task: To keep the IsSuccessful flag of the drag & drop operation up-to-date during the drag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
if (pointerEvent.dragging) { // Update drop object of drag drop operation. var dragDropOperation = this.GetDragDropOperation(pointerEvent.pointerId); if (dragDropOperation != null) { var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject; var dropObject = ExecuteEvents.GetEventHandler<IDropHandler>(currentOverGo); if (dropObject != dragDropOperation.DropObject) { dragDropOperation.DropObject = dropObject; if (dragDropOperation.DropObject != null) { ExecuteEvents.Execute(dragDropOperation.DropObject, pointerEvent, DragOverHandler); } else { dragDropOperation.DropSuccessful = false; } } } } |
What it does it to search for the IDropHandler interface on the current hovered game object. If it finds one it updates the drag & drop operation object and either performs the DragOver event of the new drop object or sets the flag to false if no valid drop object is hovered.
New event: DragOver
The drag events and the drop events are already covered by Unity, so we just need one new event which is sent to a game object. When a game object that has a IDropHandler is entered while doing a drag & drop operation, its DragOver event is called.
To add a new event, we can check how it is done with the existing events in the ExecuteEvents class. We need the following parts:
- Execute method that calls the correct method on a handler and casts the event data
- Property or field that holds a pointer to the execute method
- Setting the pointer on initialization
This way we can use the already existing ExecuteEvents.Execute method. The easiest way for now is to add the code as static members to the input module:
1 2 3 4 5 6 7 8 9 10 11 |
static ExtPointerInputModule() { DragOverHandler = Execute; } private static ExecuteEvents.EventFunction<IDragOverHandler> DragOverHandler { get; set; } private static void Execute(IDragOverHandler handler, BaseEventData eventData) { handler.OnDragOver(ExecuteEvents.ValidateEventData<PointerEventData>(eventData)); } |
The interface IDragOverHandler can be used exactly as all the basic Unity handlers by letting a mono behaviour implement it:
1 2 3 4 5 6 7 8 9 10 |
using UnityEngine.EventSystems; public interface IDragOverHandler : IEventSystemHandler { #region Public Methods and Operators void OnDragOver(PointerEventData eventData); #endregion } |
Example for drag over handler
A simple example would be to change the color of the drop target when dragged over:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public class ChangeColorOnDragOver : MonoBehaviour, IDragOverHandler, IPointerExitHandler { #region Fields public Color DragOverColor; public Graphic Graphic; private Color initialColor; #endregion #region Public Methods and Operators public void OnDragOver(PointerEventData eventData) { if (this.Graphic != null) { this.Graphic.color = this.DragOverColor; } } public void OnPointerExit(PointerEventData eventData) { if (this.Graphic != null) { this.Graphic.color = this.initialColor; } } #endregion #region Methods protected void Reset() { if (this.Graphic == null) { this.Graphic = this.GetComponent<Graphic>(); } } protected void Start() { if (this.Graphic != null) { this.initialColor = this.Graphic.color; } } #endregion } |
In general though the OnDragOver method should be used to get the current drag & drop operation and update the IsSuccessful flag.
Handler for drag object
The dragged object gets already the input events (BeginDrag, Drag, EndDrag) from the basic Unity input module. We can use those events to initialize the drag data and let the dragged object know if the drop will be successful. Therefore I created a general DragOperationHandler that offers Unity events for the special drag & drop operation events:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
public class DragOperationHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { #region Fields public DragDropOperationEvent DropWillBeSuccessful; public DragDropOperationEvent DropWillBeUnsuccessful; public StartDragDropEvent StartDragDrop; public SuccessfulDragDropEvent SuccessfulDragDrop; public UnsuccessfulDragDropEvent UnsuccessfulDragDrop; /// <summary> /// Indicates if the drop will be successful. /// Stored to know when the drop result changed. /// </summary> private bool dropSuccessful; #endregion #region Public Methods and Operators public void OnBeginDrag(PointerEventData eventData) { this.dropSuccessful = false; // Get drag drop operation. var dragDropOperation = GetDragDropOperation(eventData); if (dragDropOperation != null) { if (this.StartDragDrop != null) { // Start drag drop operation. this.StartDragDrop.Invoke(dragDropOperation); } // Initial call if drop will be successful. this.dropSuccessful = dragDropOperation.DropSuccessful; if (this.dropSuccessful) { this.DropWillBeSuccessful.Invoke(dragDropOperation); } else { this.DropWillBeUnsuccessful.Invoke(dragDropOperation); } } } public void OnDrag(PointerEventData eventData) { // Get drag drop operation. var dragDropOperation = GetDragDropOperation(eventData); if (dragDropOperation != null) { this.UpdateDropSuccessful(dragDropOperation); } } public void OnEndDrag(PointerEventData eventData) { // Get drag drop operation. var dragDropOperation = GetDragDropOperation(eventData); if (dragDropOperation != null) { this.UpdateDropSuccessful(dragDropOperation); if (dragDropOperation.Data != null) { if (dragDropOperation.DropSuccessful) { this.SuccessfulDragDrop.Invoke(dragDropOperation.Data); } else { this.UnsuccessfulDragDrop.Invoke(dragDropOperation.Data); } } } this.dropSuccessful = false; } #endregion #region Methods private static DragDropOperation GetDragDropOperation(PointerEventData eventData) { var dragDropManager = eventData.currentInputModule as IDragDropManager; return dragDropManager != null ? dragDropManager.GetDragDropOperation(eventData.pointerId) : null; } private void UpdateDropSuccessful(DragDropOperation operation) { if (operation.DropSuccessful == this.dropSuccessful) { return; } this.dropSuccessful = operation.DropSuccessful; if (this.dropSuccessful) { this.DropWillBeSuccessful.Invoke(operation); } else { this.DropWillBeUnsuccessful.Invoke(operation); } } #endregion [Serializable] public class DragDropOperationEvent : UnityEvent<DragDropOperation> { } [Serializable] public class StartDragDropEvent : UnityEvent<DragDropOperation> { } [Serializable] public class SuccessfulDragDropEvent : UnityEvent<object> { } [Serializable] public class UnsuccessfulDragDropEvent : UnityEvent<object> { } } |
Starting the operation
The dragged object has to set the data which is dragged in the operation when the drag starts. The StartDragDrop event is triggered when the drag operation begins. The operation is passed as a parameter, so its Data object can be modified.
During the operation
During the drag & drop operation the dragged object will be informed about changes in the success of the (future) drop operation. The events DropWillBeSuccessful and DropWillBeUnsuccessful can be used e.g. to change the visualization of the dragged object when over a valid/invalid drop target.
End of the operation
As we decided at the beginning, the general workflow should also contain informing the dragged object if it was dropped over a valid object. Therefore there are two further events called SuccessfulDragDrop and UnsuccessfulDragDrop . One of those is called at the end of the drag & drop operation.
Handlers for drop object
A drop target for a drag & drop operation can be defined by adding a generic DropOperationHandler . It contains an event called Drop which is invoked when a drag operation is ended above it and a DragOver event that is called when a drag & drop operation is dragged over the object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
public class DropOperationHandler : MonoBehaviour, IDropHandler, IDragOverHandler { #region Fields public DragOverEvent DragOver; public DropEvent Drop; #endregion #region Public Methods and Operators public void OnDragOver(PointerEventData eventData) { // Get drag drop operation. var dragDropOperation = GetDragDropOperation(eventData); if (dragDropOperation != null) { this.DragOver.Invoke(dragDropOperation); } } public void OnDrop(PointerEventData eventData) { // Get drag drop operation. var dragDropOperation = GetDragDropOperation(eventData); if (dragDropOperation != null) { this.Drop.Invoke(dragDropOperation); } } #endregion #region Methods private static DragDropOperation GetDragDropOperation(PointerEventData eventData) { var dragDropManager = eventData.currentInputModule as IDragDropManager; return dragDropManager != null ? dragDropManager.GetDragDropOperation(eventData.pointerId) : null; } #endregion [Serializable] public class DropEvent : UnityEvent<DragDropOperation> { } [Serializable] public class DragOverEvent : UnityEvent<DragDropOperation> { } } |
If a drag & drop operation is available, it is passed as a parameter. This way the IsSuccessful flag can be adjusted and the drag data can be used.
Concrete input modules
Before we can put it all together we have to derive a concrete input module from our ExtPointerInputModule class, e.g. for touch input. The PointerInputModule is also just a base class and the two concrete modules StandaloneInputModule and TouchInputModule are derived from it.
So we added a ExtTouchInputModule for touch (and fake touch) input:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
public class ExtTouchInputModule : ExtPointerInputModule { #region Fields [SerializeField] private bool forceModuleActive; private Vector2 lastMousePosition; private Vector2 mousePosition; #endregion #region Delegates /// <summary> /// Delegate for the Pressed event. /// </summary> /// <param name="pointerId">Id of pressed pointer.</param> public delegate void PressedDelegate(int pointerId); #endregion #region Events /// <summary> /// Called when a pointer is pressed. /// </summary> public event PressedDelegate Pressed; #endregion #region Properties public bool ForceModuleActive { get { return this.forceModuleActive; } set { this.forceModuleActive = value; } } #endregion #region Public Methods and Operators public override void DeactivateModule() { base.DeactivateModule(); this.ClearSelection(); } public override bool IsModuleSupported() { return this.ForceModuleActive || Input.touchSupported; } public override void Process() { if (this.UseFakeInput()) { this.FakeTouches(); } else { this.ProcessTouchEvents(); } } public override bool ShouldActivateModule() { if (!base.ShouldActivateModule()) { return false; } if (this.forceModuleActive) { return true; } if (this.UseFakeInput()) { var wantsEnable = Input.GetMouseButtonDown(0); wantsEnable |= (this.mousePosition - this.lastMousePosition).sqrMagnitude > 0.0f; return wantsEnable; } for (var i = 0; i < Input.touchCount; ++i) { var input = Input.GetTouch(i); if (input.phase == TouchPhase.Began || input.phase == TouchPhase.Moved || input.phase == TouchPhase.Stationary) { return true; } } return false; } public override void UpdateModule() { this.lastMousePosition = this.mousePosition; this.mousePosition = Input.mousePosition; } #endregion #region Methods protected virtual void OnPressed(int pointerId) { var handler = this.Pressed; if (handler != null) { handler(pointerId); } } /// <summary> /// For debugging touch-based devices using the mouse. /// </summary> private void FakeTouches() { var pointerData = this.GetMousePointerEventData(0); var leftPressData = pointerData.GetButtonState(PointerEventData.InputButton.Left).eventData; // fake touches... on press clear delta if (leftPressData.PressedThisFrame()) { leftPressData.buttonData.delta = Vector2.zero; this.OnPressed(leftPressData.buttonData.pointerId); } this.ProcessPress( leftPressData.buttonData, leftPressData.PressedThisFrame(), leftPressData.ReleasedThisFrame()); // only process move if we are pressed... if (Input.GetMouseButton(0)) { this.ProcessMove(leftPressData.buttonData); this.ProcessDragDrop(leftPressData.buttonData); this.ProcessDrag(leftPressData.buttonData); } } /// <summary> /// Process all touch events. /// </summary> private void ProcessTouchEvents() { for (var i = 0; i < Input.touchCount; ++i) { var input = Input.GetTouch(i); bool released; bool pressed; var pointer = this.GetTouchPointerEventData(input, out pressed, out released); if (pressed) { // Invoke event. this.OnPressed(pointer.pointerId); } this.ProcessPress(pointer, pressed, released); if (!released) { this.ProcessMove(pointer); this.ProcessDragDrop(pointer); this.ProcessDrag(pointer); } else { this.RemovePointerData(pointer); } } } private bool UseFakeInput() { return !Input.touchSupported; } #endregion } |
In general it is not much more as Unity’s TouchInputModule implementation, but uses ExtPointerInputModule as a base class and calls ProcessDragDrop in its process method.
Putting the parts together
Alright, we now have a handler for our drag objects, one for our drop targets and a customized input module that manages the drag operations and calls the new input event DragOver .
To assemble a valid drag & drop scene, you should follow those steps:
- Create a new scene and add an EventSystem
- Add a
ExtTouchInputModule to the event system game object
- Enable the ForceModuleActive checkbox, so fake touch input is used in the editor
- Add a drag object, e.g. a UI Image, and put a
DragOperationHandler on it
- You also have to add a custom behaviour that provides a method to set the drag data and link this method to the StartDragDrop event of the handler
- Add a drop object, e.g. an Image again, and put a
DropOperationHandler on it
- You also have to add a custom behaviour that provides methods to handle the DragOver and Drop events
The custom behaviours depend on the specific use case, of course, but should be easy enough to implement. Most of the times the DragOver handler checks if the drag data is of the correct type and may check if the drag data is valid for this specific drop target.
The Drop handler performs the action for the operation. This can be anything depending on your application.
All the other events are meant to easily add feedback about the drag & drop operation for the user, but can be added bit by bit later.
Adding first feedback
There is one kind of feedback though, that is essential for drag & drop and that is really visually dragging the drag object. The handlers are called even if nothing else happens on the screen and are only affected by the input. So if you start the drag over a drag object and end (release) it over a drop target, the operation is performed. But the user may be a bit confused and can’t be sure that the operation has started already.
So I wrote a script that pulls an object when it is dragged, so it is always positioned at the pointer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
public class PullBehaviour : MonoBehaviour { #region Fields public bool IgnoreDragOffset; public Transform Target; private Vector2 currentScreenPosition; private Vector3 dragOffset; private Vector3 initialPosition; private bool isDragging; #endregion #region Public Methods and Operators public void ContinuePull(Vector2 pointerPosition, Vector2 moveDelta) { this.currentScreenPosition = pointerPosition; this.UpdateTargetPosition(); } public void EndPull() { this.isDragging = false; this.ResetPosition(); var canvasGroup = this.GetComponent<CanvasGroup>(); if (canvasGroup != null) { canvasGroup.blocksRaycasts = true; } } public void ResetPosition() { this.Target.position = this.initialPosition; } public void StartPull(Vector2 pointerPosition) { this.isDragging = true; this.currentScreenPosition = pointerPosition; this.initialPosition = this.Target.position; this.dragOffset = this.initialPosition - this.ConvertScreenPosition(pointerPosition); var canvasGroup = this.GetComponent<CanvasGroup>(); if (canvasGroup != null) { canvasGroup.blocksRaycasts = false; } } #endregion #region Methods protected virtual Vector3 ConvertScreenPosition(Vector2 screenPosition) { return new Vector3(screenPosition.x, screenPosition.y); } protected virtual void Reset() { if (this.Target == null) { this.Target = this.transform; } } protected void Update() { if (this.isDragging) { this.UpdateTargetPosition(); } } private void UpdateTargetPosition() { if (this.Target == null) { return; } var targetPosition = this.ConvertScreenPosition(this.currentScreenPosition); if (!this.IgnoreDragOffset) { targetPosition += this.dragOffset; } this.Target.position = targetPosition; } #endregion } |
You just have to connect the methods StartPull , ContinuePull and EndPull to the drag events and it should work out-of-the-box.
You should also add a CanvasGroup script, so raycasts on this object are ignored during the time of the drag. Otherwise underlying objects are not hit and the drop target is not recognized.
Conclusion
Phew, this was quite a technical post, hope you could follow my descriptions. I uploaded all the code we talked about to bitbucket, so you can try it on your own.
I plan to add some more blog posts about the topic Drag & Drop. For example how and which user feedback to add during the operation and how we connected it to our Unity asset Data Bind for Unity. So feel free to ask any questions or give me hints on other topics, so I can consider them for my future posts. Any feedback about this post is very welcome as well, of course! 🙂
Update 07.12.16
Due to some comments on this post I decided to have another look into the example as it should also work with 3D objects out of the box. So I added another example to the bitbucket project. There is also a Unity package you can use directly.
The main issue why it didn’t work for some readers was probably that they forgot to add a PhysicsRaycaster to the main camera, so the event system wasn’t able to detect the touched 3D objects. For UI objects the GraphicsRaycaster does this job. Anyway, thanks @Simon and @Jose for reporting the issue! 🙂