Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Horrors of OnPointerDown versus OnBeginDrag in Unity3D

I'm concerned over the difference between OnPointerDown versus OnBeginDrag in single-finger movement code.

(In the latest Unity paradigm of using a physics raycaster: so, finally, Unity will properly ignore touch on the UI layer.

So from 2015 onwards what you must do is this:

  1. Forget about the crap traditional Input or Touches system which are pointless crap and don't work

  2. Add an empty game object with a usually BoxCollider2D, likely bigger than the screen. Make the layer called say "Draw". Physics settings, "Draw" interacts with nothing

  3. Simply add to the camera, a 2D or 3D physics raycaster. Event mask the "Draw" layer.

enter image description here

Do a script like below and put it on.

(Tip - don't forget to simply add an EventSystem to the scene. Bizarrely, Unity does not do this automatically for you in some situations but Unity does do it automatically for you in other situations, so it's annoying if you forget!)

But here's the problem.

There has got to be some subtle difference between using OnPointerDown versus OnBeginDrag (and the matching end calls). (You can just swap the action in the following code sample.)

Naturally Unity offers no guidance on this; the following code beautifully rejects stray grabs and also flawlessly ignores your UI layer (thanks Unity! at last!) but I am mystified about the difference between the two approaches (begin drag V. begin touch) and I cannot in anyway find the logical difference between the two in unit testing.

What's the answer?

/*

general movement of something by a finger.

*/

using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems;

public class FingerMove:MonoBehaviour,
      IPointerDownHandler,
      IBeginDragHandler,
      IDragHandler,
      IPointerUpHandler,
      IEndDragHandler
  {
  public Transform moveThis;
  
  private Camera theCam;
  private FourLimits thingLimits;
  
  private Vector3 prevPointWorldSpace;
  private Vector3 thisPointWorldSpace;
  private Vector3 realWorldTravel;
  
  public void Awake()
    {
    theCam = Camera.main or whatever;
    }
  
  public void OnMarkersReady() // (would be EVENT DRIVEN for liveness)
    {
    thingLimits = Grid.liveMarkers. your motion limits
    }
  
  private int drawFinger;
  private bool drawFingerAlreadyDown;
  
  public void OnPointerDown (PointerEventData data)
    {
    Debug.Log("    P DOWN "  +data.pointerId.ToString() );
    }
  
  public void OnBeginDrag (PointerEventData data)
    {
    Debug.Log("    BEGIN DRAG "  +data.pointerId.ToString() );
    
    if (drawFingerAlreadyDown == true)
      {
      Debug.Log("    IGNORE THAT DOWN! "  +data.pointerId.ToString() );
      return;
      }
    drawFinger = data.pointerId;
    drawFingerAlreadyDown=true;
    
    prevPointWorldSpace = theCam.ScreenToWorldPoint( data.position );
    }
  
  public void OnDrag (PointerEventData data)
    {
    Debug.Log("    ON DRAG "  +data.pointerId.ToString() );
    
    if (drawFingerAlreadyDown == false)
      {
      Debug.Log("    IGNORE THAT PHANTOM! "  +data.pointerId.ToString() );
      }
    
    if ( drawFinger != data.pointerId )
      {
      Debug.Log("    IGNORE THAT DRAG! "  +data.pointerId.ToString() );
      return;
      }
    
    thisPointWorldSpace = theCam.ScreenToWorldPoint( data.position );
    realWorldTravel = thisPointWorldSpace - prevPointWorldSpace;
    _processRealWorldtravel();
    prevPointWorldSpace = thisPointWorldSpace;
    }
    
  public void OnEndDrag (PointerEventData data)
    {
    Debug.Log("    END DRAG "  +data.pointerId.ToString() );
    
    if ( drawFinger != data.pointerId )
      {
      Debug.Log("    IGNORE THAT UP! "  +data.pointerId.ToString() );
      return;
      }
    
    drawFingerAlreadyDown = false;
    }
  public void OnPointerUp (PointerEventData data)
    {
    Debug.Log("    P UP "  +data.pointerId.ToString() );
    }
  
  private void _processRealWorldtravel()
    {
    if ( Grid. your pause concept .Paused ) return;
    
   // potential new position...
    Vector3 pot = moveThis.position + realWorldTravel;
    
    // almost always, squeeze to a limits box...
    // (whether the live screen size, or some other box)
    
    if (pot.x < thingLimits.left) pot.x = thingLimits.left;
    if (pot.y > thingLimits.top) pot.y = thingLimits.top;
    if (pot.x > thingLimits.right) pot.x = thingLimits.right;
    if (pot.y < thingLimits.bottom) pot.y = thingLimits.bottom;
    
    // kinematic ... moveThis.position = pot;
    // or
    // if pushing around physics bodies ...  rigidbody.MovePosition(pot);
    }
  }

And here's a handy thing. Save typing with the same thing for 3D scenes, using the little-known but exquisite

pointerCurrentRaycast

here's how... notice the excellent

data.pointerCurrentRaycast.worldPosition

call courtesy Unity.

public class FingerDrag .. for 3D scenes:MonoBehaviour,
      IPointerDownHandler,
      IDragHandler,
      IPointerUpHandler
  {
  public Transform moveMe;
  
  private Vector3 prevPointWorldSpace;
  private Vector3 thisPointWorldSpace;
  private Vector3 realWorldTravel;
  
  private int drawFinger;
  private bool drawFingerAlreadyDown;
  
  public void OnPointerDown (PointerEventData data)
    {
    if (drawFingerAlreadyDown == true)
      return;
    drawFinger = data.pointerId;
    drawFingerAlreadyDown=true;
    
    prevPointWorldSpace = data.pointerCurrentRaycast.worldPosition;
    // in this example we'll put it under finger control...
    moveMe.GetComponent<Rigidbody>().isKinematic = false;
    }
  
  public void OnDrag (PointerEventData data)
    {
    if (drawFingerAlreadyDown == false)
      return;
    if ( drawFinger != data.pointerId )
      return;
    
    thisPointWorldSpace = data.pointerCurrentRaycast.worldPosition;
    
    realWorldTravel = thisPointWorldSpace - prevPointWorldSpace;
    _processRealWorldtravel();
    
    prevPointWorldSpace = thisPointWorldSpace;
    }
    
  public void OnPointerUp (PointerEventData data)
    {
    if ( drawFinger != data.pointerId )
      return;
    
    drawFingerAlreadyDown = false;
    moveMe.GetComponent<Rigidbody>().isKinematic = false;
    moveMe = null;
    }
  
  private void _processRealWorldtravel()
    {
    Vector3 pot = moveMe.position;
    
    pot.x += realWorldTravel.x;
    pot.y += realWorldTravel.y;
    moveMe.position = pot;
    }
  }
like image 1000
Fattie Avatar asked Mar 16 '16 22:03

Fattie


2 Answers

I want to start by saying that Input and Touches are not crappy.They are still usefull and were the best way to check for touch on mobile devices before OnPointerDown and OnBeginDrag came along. OnMouseDown() you can call crappy because it was not optimized for mobile. For a beginner who just started to learn Unity, Input and Touches are their options.

As for your question, OnPointerDown and OnBeginDrag are NOT the-same. Although they almost do the-same thing but they were implemented to perform in different ways. Below I will describe most of these:

OnPointerDown: Called when there is press/touch on the screen (when there is a click or finger is pressed down on touch screen)

OnPointerUp: Called when press/touch is released (when click is released or finger is removed from the touch screen)

OnBeginDrag: Called once before a drag is started(when the finger/mouse is moved for the first time while down)

OnDrag : Repeatedly called when user is dragging on the screen (when the finger/mouse is moving on the touch screen)

OnEndDrag: Called when drag stops (when the finger/mouse is no longer moving on the touch screen).

OnPointerDown versus OnBeginDrag and OnEndDrag

OnPointerUp will NOT be called if OnPointerDown has not been called. OnEndDrag will NOT be called if OnBeginDrag has not been called. Its like the curly braces in C++,C#, you open it '{' and you close it '}'.

THE DIFFERENCE: OnPointerDown will be called once and immediately when finger/mouse is on the touch screen. Nothing else will happen until there is a mouse movement or the finger moves on the screen then OnBeginDrag will be called once followed by OnDrag.

These are made for doing advanced usage such such as custom UI with controls that is not included in Unity.

WHEN TO USE EACH ONE:

1. When you have to implement a simple click button, for example, Up,Down, Shoot Button on the screen, you only need OnPointerDown to detect the touch. This should work for Sprite Images.

2. When you have to implement a custom toggle switch and you want it to be realistic so that the player can drag to left/right or up/down to toggle it then you need OnPointerDown , OnBeginDrag , OnDrag , OnEndDrag , OnPointerUp. You need to write your code in this order to have a smooth Sprite/Texture transition on the screen. Some toggle switches are made to be to clicked and it will toggle. Some people prefer to make it look realistic by making it so that you have to drag it in order to toggle it.

3. Also when you want to implement a Generic re-usable pop-up window that is draggable, you also need to use those 5 functions (OnPointerDown , OnBeginDrag , OnDrag , OnEndDrag , OnPointerUp). First detect when there is a click(OnPointerDown), check to make sure that the Sprite clicked is the right one you want to move. Wait for player to move(OnBeginDrag) their finger/mouse. Once they start dragging, maybe you can call a coroutine function with while loop that will start moving the Sprite and inside that coroutine, you can smooth the movement of the Sprite with Time.deltaTime or any other preferred method.

Since OnBeginDrag is called once, it is a good place to start the coroutine. As the player continue to drag the Sprite, OnDrag will be called repeatedly. Use the OnDrag function to get the current location of the finder and update that to a Vector3 that the coroutine that is already running will use to update the position of the Sprite. When the player stops moving their finger/mouse on the screen, OnEndDrag is called and you can boolean variable and tell the coroutine to stop updating the position of the Sprite. Then, when the player releases their finger(OnPointerUp) you can then stop the coroutine with the StopCoroutine function.

Because of OnBeginDrag we we are able to start coroutine once drag started while waiting for drag to end. It wouldn't make sense to start that coroutine in OnPointerDown because that means that each time player touches the screen, a coroutine would be started.

Without OnBeginDrag, we have to use boolean variable to make the coroutine start only once in the OnDrag function which is called every time or else there would be coroutine running everywhere and unexpected movement of the Sprite will occur.

4. When you want to determine how long player moved their finger. Example of this is that famous game called Fruit Ninja. Lets just say you want to determine far the player swiped on the screen.

First, wait until OnPointerDown is called, wait again until OnBeginDrag is called, then you can get the current position of the finger inside OnBeginDrag function because OnBeginDrag is called before the finger starts moving. After the finger is released, OnEndDrag is called. Then you can get the current position of finger again. You can use these two positions to check how far the finger moved by subtracting them.

If you instead decide to use OnPointerDown as the place to get the first position of the finger, you will get a wrong result because if the player swipes right, then waits and swipes left then waits again and swipe up without releasing their finger after each swipe, the only good result you have is the first swipe(right swipe). The left and the up swipe will have invalid values because that first value you got when OnPointerDown was called is the value you are still using. This is because the player never removed their finger from the screen so therefore, OnPointerDown is never called again and the first old old value is still there.

But when you use OnBeginDrag instead of OnPointerDown, this problem will be gone because when the finger stops moving, OnEndDrag is called and when it starts moving again OnBeginDrag is called once again causing the first position to be overwritten with the new one.

like image 160
Programmer Avatar answered Nov 20 '22 18:11

Programmer


The difference is that OnBeginDrag doesn't get called until the touch/mouse has moved a certain minimum distance, the drag threshold. You can set the drag threshold on the Event System component.

This is necessary for when you have a hierarchy of objects with different ways of handling input, especially scrollviews. Imagine you have a scrollview with a vertical stack of cells, each with a button in it. When the touch first starts on one of the buttons, we don't know whether the user is tapping a button or dragging the scrollview. It isn't until the touch gets dragged for the drag threshold that we know it is a drag and not a tap.

like image 21
Matt Rix Avatar answered Nov 20 '22 16:11

Matt Rix