Search Unity

Non-rectangular button with adequate input-detection area

Discussion in 'Unity UI (uGUI) & TextMesh Pro' started by CanisLupus, Aug 21, 2014.

  1. CanisLupus

    CanisLupus

    Joined:
    Jul 29, 2013
    Posts:
    292
    Is there a way to make a non-rectangular button - a circle, for example - only react to input on its (circular) surface and not on the whole encapsulating rectangle? I haven't found a way to do this.

    Circle buttons are relatively common, and so are buttons that use sprites with some transparent padding (meaning that the image is smaller than the button in the scene, either in x, y or both). Weirdly shaped buttons are also possible, although more uncommon.

    This could be possible by adding a 2D collider with the right shape, somehow intercepting input before it affects the button, raycasting for collision and finally act on the button. This seems complicated, if at all possible, so I was looking for an already integrated solution. Is there one?
     
  2. Tim-C

    Tim-C

    Unity Technologies

    Joined:
    Feb 6, 2010
    Posts:
    2,098
    Yes this is possible, you will need to do a tiny bit of scripting though.

    We have this interface:

    public interface ICanvasRaycastFilter
    {
    bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera);
    }

    If you add a component that extends this interface to a graphic this will be called if the graphic is 'hit'. You can then do custom code to return true or false. So you can do something like write an algorithm to determine if it's in your valid area.
     
    CanisLupus and rakkarage like this.
  3. CanisLupus

    CanisLupus

    Joined:
    Jul 29, 2013
    Posts:
    292
    Thank you, Tim. I successfully got it to work for simple circle buttons with the following:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class CircleGraphic : MonoBehaviour, ICanvasRaycastFilter
    4. {
    5.    public float Radius;
    6.    private RectTransform rectTransform;
    7.  
    8.    void Start()
    9.    {
    10.      rectTransform = this.GetComponent<RectTransform>();
    11.    }
    12.  
    13.    public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
    14.    {
    15.      Vector2 screenPoint = eventCamera.WorldToScreenPoint(rectTransform.position);
    16.      return (Vector2.Distance(sp, screenPoint) < Radius);
    17.    }
    18. }
    Not sure if it's the best way or if it covers problematic cases (probably not). Even so, if the pivot is changed in the RectTransform, rect.position is not enough to get the correct graphic's position, as it seems to return the position of the pivot and not the graphic itself (I expected as much). This results in IsRaycastLocationValid being called when the cursor is over the graphic, but the code returning true or false according to the pivot instead of the graphic, which isn't what I want.

    This should be simple to fix by accounting the offset created by the pivot, but, truth is, I'm very confused by the variables in the RectTransform class. I tried using pivot, anchoredPosition, rect, etc, in various ways and printing values and still can't understand most of it. Or maybe I don't have to use them at all. Any hints? ;)
     
  4. Tim-C

    Tim-C

    Unity Technologies

    Joined:
    Feb 6, 2010
    Posts:
    2,098
    So these should be documented in the script reference (we install one locally when you install the beta, they are not online yet), that is probably a good start.

    So you you want to do is turn your 'press' position into a position in 'Rect' space. This can be done using:
    RectTransformUtility.ScreenPointToLocalPointInRectangle (); Then you should be able to use the radius to see if it's inside.
     
  5. CanisLupus

    CanisLupus

    Joined:
    Jul 29, 2013
    Posts:
    292
    I was already using the offline documentation, but I completely missed the RectTransformUtility class :)

    2 things:
    - ScreenPointToLocalPointInRectangle still gives a position relative to the pivot and not the graphic (also expected). Of course, it's much better than what I was doing, since it allows scaling and rotating the object while keeping the calculations correct.
    - The documentation fails to point that the last argument, Vector2 localPoint, is an "out" parameter. This can be obvious to some people, but should be noted.

    I was able to account for the pivot offset in my script. It wasn't hard, but I was confused previously -.- Again, there might be a better way (I especially dislike the "new Vector2(0.5f, 0.5f)" part), but now it works fine.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class CircleGraphic : MonoBehaviour, ICanvasRaycastFilter
    4. {
    5.     public float Radius;
    6.     private RectTransform rectTransform;
    7.  
    8.     void Start()
    9.     {
    10.         rectTransform = this.GetComponent<RectTransform>();
    11.     }
    12.  
    13.     public bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
    14.     {
    15.         Vector2 pivotToCursorVector;
    16.         RectTransformUtility.ScreenPointToLocalPointInRectangle(
    17.             rectTransform, screenPoint, eventCamera, out pivotToCursorVector);
    18.  
    19.         Vector2 pivotOffsetRatio = rectTransform.pivot - new Vector2(0.5f, 0.5f);
    20.         Vector2 pivotOffset = Vector2.Scale(rectTransform.rect.size, pivotOffsetRatio);
    21.         Vector2 centerToCursorVector = pivotToCursorVector + pivotOffset;
    22.  
    23.         return (centerToCursorVector.magnitude < Radius);
    24.     }
    25. }
    Thanks again :)
     
    Last edited: Aug 22, 2014
    andyz and Tim-C like this.
  6. senritsu

    senritsu

    Joined:
    Dec 12, 2012
    Posts:
    37
    As it is relevant to this thread as well, crossposting my solution from here

    Thank you Tim for your information about the raycast filter interface :)
     
    CanisLupus likes this.
  7. miguelSantirso

    miguelSantirso

    Joined:
    Aug 12, 2013
    Posts:
    3
  8. Elringus

    Elringus

    Joined:
    Oct 3, 2012
    Posts:
    476
    I would like to offer one more solution to the problem: we can use objects texture and check its alpha when deciding whether to respond to input events. That way UI objects will not react to input in any transparent areas, which seems like the most versatile solution.

    I’ve made a plugin that works like that: https://www.assetstore.unity3d.com/en/#!/content/28601

    It also handles any transformations (scale, rotation), anchor setups, works with both orthographic and perspective camera modes (screen/world spaces), supports atlases and all the filled image modes. Been using it myself in several projects for all kind of non-rect buttons\toggles and should say it does it’s job quite good :)

     
  9. Fattie

    Fattie

    Joined:
    Jul 5, 2012
    Posts:
    411
    Hi CanisLupis. I do not know, what to do with your script, to make a circular-area button. Thanks if you can explain, cheers!
     
  10. CanisLupus

    CanisLupus

    Joined:
    Jul 29, 2013
    Posts:
    292
    Hi there, @Fattie. You should place the CircleGraphic script on the object that you want to act as a circle. The one that has the Button component, I assume. Then you have to set a correct radius value for it (in the Unity inspector).

    I should warn you that my script is hardly a good solution because you have to manually define the radius. If your circle button sprites occupy most of the texture square, you can calculate the radius automatically (inside CircleGraphic) to be half the size of the object.

    There might also be other better solutions that I do not know of.

    - Daniel
     
  11. MrAnts

    MrAnts

    Joined:
    Feb 20, 2017
    Posts:
    1
  12. andyz

    andyz

    Joined:
    Jan 5, 2010
    Posts:
    1,195
    Please add an example like the circle hit code above into the UI documentation and better yet add some extended button classes with simple collision shape alternatives to a rectangle - circle/ellipse/rounded-rectangle. It seems so hard to even find info on this subject but let's make it 'out of the box' functionality!?