Search Unity

Angle property drawer

Discussion in 'Scripting' started by FREEZX, Mar 25, 2014.

  1. FREEZX

    FREEZX

    Joined:
    Apr 2, 2013
    Posts:
    64
    Hi,
    From the unity blog i found this gem:
    http://blogs.unity3d.com/2012/09/07/property-drawers-in-unity-4/
    I would really want to integrate the Angle attribute in my custom scripts, but i cannot find it anywhere.
    In which namespace does it exist in unity, and if it's not included in unity, would it be possible to get the PropertyDrawer code for it?
    Thanks!
     
  2. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    I don't think it exists within Unity. Sameway that "Compact" or "Multiline" doesn't exist.
     
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    That's a pretty nice property drawer for angle. I like that.

    I might write something similar when I get some time to do it. If I do, I'll post it here.
     
  4. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Because I liked the idea and I had time...



    Code (csharp):
    1.  
    2.         private static Vector2 mousePosition;
    3.  
    4.         public static float FloatAngle(Rect rect, float value, bool showValue)
    5.         {
    6.             int id = GUIUtility.GetControlID(FocusType.Native, rect);
    7.  
    8.             Rect knobRect = new Rect(rect.x, rect.y, rect.height, rect.height);
    9.  
    10.             if (Event.current != null)
    11.             {
    12.                 if (Event.current.type == EventType.MouseDown  knobRect.Contains(Event.current.mousePosition))
    13.                 {
    14.                     GUIUtility.hotControl = id;
    15.                     mousePosition = Event.current.mousePosition;
    16.                 }
    17.                 else if (Event.current.type == EventType.MouseUp  GUIUtility.hotControl == id)
    18.                     GUIUtility.hotControl = -1;
    19.                 else if (Event.current.type == EventType.MouseDrag  GUIUtility.hotControl == id)
    20.                 {
    21.                     Vector2 move = mousePosition - Event.current.mousePosition;
    22.                     value += -move.x - move.y;
    23.                     mousePosition = Event.current.mousePosition;
    24.                     GUI.changed = true;
    25.                 }
    26.             }
    27.  
    28.             GUI.DrawTexture(knobRect, KnobBack);
    29.             Matrix4x4 matrix = GUI.matrix;
    30.             GUIUtility.RotateAroundPivot(value, knobRect.center);
    31.             GUI.DrawTexture(knobRect, Knob);
    32.             GUI.matrix = matrix;
    33.  
    34.             if (showValue)
    35.             {
    36.                 Rect label = new Rect(rect.x + rect.height, rect.y, rect.width, rect.height);
    37.                 GUI.Label(label, value.ToString());
    38.             }
    39.  
    40.             return value;
    41.         }
    42.  
    Will probably add some snap every X angle... or clamp the value between 0 and 360 (or better, value from the Angle attribute)
     
    Mehrdad995 and rakkarage like this.
  5. FREEZX

    FREEZX

    Joined:
    Apr 2, 2013
    Posts:
    64
    Awesome. I'll try it out :D
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Awesome, save me having to do it tonight after work. Thanks dude.
     
  7. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    With snap and min/max range... If you want to do a volume that goes up to 11.
    Turned the label into a floatfield.
    Without min/max, the knob is 0 to 360.

    Code (csharp):
    1.  
    2.         private static Vector2 mousePosition;
    3.  
    4.         public static float FloatAngle(Rect rect, float value)
    5.         {
    6.             return FloatAngle(rect, value, -1, -1, -1);
    7.         }
    8.  
    9.         public static float FloatAngle(Rect rect, float value, float snap)
    10.         {
    11.             return FloatAngle(rect, value, snap, -1, -1);
    12.         }
    13.  
    14.         public static float FloatAngle(Rect rect, float value, float snap, float min, float max)
    15.         {
    16.             int id = GUIUtility.GetControlID(FocusType.Native, rect);
    17.  
    18.             Rect knobRect = new Rect(rect.x, rect.y, rect.height, rect.height);
    19.  
    20.             float delta;
    21.             if (min != max)
    22.                 delta = ((max - min) / 360);
    23.             else
    24.                 delta = 1;
    25.  
    26.             if (Event.current != null)
    27.             {
    28.                 if (Event.current.type == EventType.MouseDown  knobRect.Contains(Event.current.mousePosition))
    29.                 {
    30.                     GUIUtility.hotControl = id;
    31.                     mousePosition = Event.current.mousePosition;
    32.                 }
    33.                 else if (Event.current.type == EventType.MouseUp  GUIUtility.hotControl == id)
    34.                     GUIUtility.hotControl = -1;
    35.                 else if (Event.current.type == EventType.MouseDrag  GUIUtility.hotControl == id)
    36.                 {
    37.                     Vector2 move = mousePosition - Event.current.mousePosition;
    38.                     value += delta * (-move.x - move.y);
    39.  
    40.                     if (snap > 0)
    41.                     {
    42.                         float mod = value % snap;
    43.  
    44.                         if (mod < (delta * 3) || Mathf.Abs(mod - snap) < (delta * 3))
    45.                             value = Mathf.Round(value / snap) * snap;
    46.                     }
    47.  
    48.                     mousePosition = Event.current.mousePosition;
    49.                     GUI.changed = true;
    50.                 }
    51.             }
    52.  
    53.             GUI.DrawTexture(knobRect, KnobBack);
    54.             Matrix4x4 matrix = GUI.matrix;
    55.  
    56.             if (min != max)
    57.                 GUIUtility.RotateAroundPivot(value * (360 / (max - min)), knobRect.center);
    58.             else
    59.                 GUIUtility.RotateAroundPivot(value, knobRect.center);
    60.  
    61.             GUI.DrawTexture(knobRect, Knob);
    62.             GUI.matrix = matrix;
    63.  
    64.             Rect label = new Rect(rect.x + rect.height, rect.y + (rect.height / 2) - 9, rect.height, 18);
    65.             value = EditorGUI.FloatField(label, value);
    66.  
    67.             if (min != max)
    68.                 value = Mathf.Clamp(value, min, max);
    69.  
    70.             return value;
    71.         }
    72.  
     
    Last edited: Mar 25, 2014
    rakkarage likes this.
  8. FREEZX

    FREEZX

    Joined:
    Apr 2, 2013
    Posts:
    64


    Tried out both this and the previous version, had a strange bug where i couldn't change to any other tabs until i clicked something in the Project Window.
    Fixed it by removing
    Matrix4x4 matrix = GUI.matrix;

    and setting line 61 to
    GUI.matrix = Matrix4x4.identity;

    Also, at first i forgot to call Repaint whenever the value changed, so let this be a warning to anyone who attempts to use this control.

    Thanks for the component!
     
  9. FREEZX

    FREEZX

    Joined:
    Apr 2, 2013
    Posts:
    64
    Or actually, no, that doesn't quite fix it...
     
  10. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Hmm... Very strange. I admit I'm not within a standard scope here. My inspector is nothing be standard, so that could explain the repaint (which I call automatically whenever a value change).

    However, I don't understand how the matrix issue is solved. Flagging it "identity" would remove any previous transform. Maybe that's acceptable.

    EDIT: Yeah, I have that weird tab issue. Will look into it later, but it's most likely related to the HotControl part.

    REDIT: Yup! I'm a moron who should check the documentation; http://docs.unity3d.com/Documentation/ScriptReference/GUIUtility-hotControl.html

    Change the line #33;

    Code (csharp):
    1.  
    2. GUIUtility.hotControl = -1;
    3.  
    for

    Code (csharp):
    1.  
    2. GUIUtility.hotControl = 0;
    3.  
     
    Last edited: Mar 25, 2014
  11. FREEZX

    FREEZX

    Joined:
    Apr 2, 2013
    Posts:
    64
    Awesome, thanks!
     
  12. Glaskows

    Glaskows

    Joined:
    Mar 14, 2013
    Posts:
    9
    Sorry to intrude... but I am having a problem with GUI.DrawTexture() in general and used your code because it seemed to work, but the problem persist. Inside a PropertyDrawer the DrawTexture seems to flicker and go away after some time when the control looses focus. I came to this problem after searching why my Labels don't rotate inside a PropertyDrawer.
    Are you experience the same issue?
    Thanks!
     
  13. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    I'm sorry... Since I'm not using PropertyDrawer, I can't say I experienced that issue. However, I'm a bit surprised, as with or without PropertyDrawer, the behaviour should be the same. GUI are usually fairly stable. Are you sure nothing else is try to draw at the same spot? Maybe a group with a style or another GUI.
     
  14. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
    There is a bug (Case 568929) in Unity where GUI.DrawTexture fails to work within custom property drawers for the default inspector implementation. It does work, however, for a custom `Editor`.
     
  15. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Nothing to raise my already low opinion of propertydrawers... :p
     
  16. Pal01

    Pal01

    Joined:
    Aug 14, 2014
    Posts:
    1
    Is it possible for someone to post an example of how to implement this into a custom inspector(or property drawer)?
    Which parts of this code need to go into OnGUI or OnInspectorGUI?
    From where are you getting your texture assets for the knobs?, selection,public variable, loaded from path ,etc...
    thanks for the help
     
  17. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    From the bug reported here, it wouldn't work for PropertyDrawer. Don't know if the bug was fixed.

    As for custom inspector, you should just need to call the FloatAngle method from OnInspectorGUI. You should check existing tutorial about writing inspector editor.

    As for loading asset, there's many different ways to do it... However, I doubt I could help much, since I've came up with my own way; I load them from a DLL. I just hate having my editor texture floating around my assets.
     
  18. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    953
    For anyone interested, I came up with this little workaround:
    Code (csharp):
    1. public static class GUIHelper {
    2.  
    3.     private static GUIStyle s_TempStyle = new GUIStyle();
    4.  
    5.     public static void DrawTexture(Rect position, Texture2D texture) {
    6.         if (Event.current.type != EventType.Repaint)
    7.             return;
    8.  
    9.         s_TempStyle.normal.background = texture;
    10.  
    11.         s_TempStyle.Draw(position, GUIContent.none, false, false, false, false);
    12.     }
    13.  
    14. }
     
    hoekkii likes this.
  19. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    @LightStriker : I tried to add it not only on property drawer but also with combination of attribute drawer for it, first take a look :

    Code (CSharp):
    1. public class AngleAttribute : PropertyAttribute
    2.     {
    3.         public readonly float snap;
    4.         public readonly float min;
    5.         public readonly float max;
    6.  
    7.         public AngleAttribute()
    8.         {
    9.             snap = 1;
    10.             min = -360;
    11.             max = 360;
    12.         }
    13.  
    14.         public AngleAttribute(float snap)
    15.         {
    16.             this.snap = snap;
    17.             min = -360;
    18.             max = 360;
    19.         }
    20.  
    21.         public AngleAttribute(float snap, float min, float max)
    22.         {
    23.             this.snap = snap;
    24.             this.min = min;
    25.             this.max = max;
    26.         }
    27.     }
    Code (CSharp):
    1. [CustomPropertyDrawer(typeof(AngleAttribute))]
    2.     public class AngleDrawer : PropertyDrawer
    3.     {
    4.         private static Vector2 mousePosition;
    5.         private static Texture2D KnobBack = Resources.Load("Textures/Editor/KnobBack") as Texture2D;
    6.         private static Texture2D Knob  = Resources.Load("Textures/Editor/Knob") as Texture2D;
    7.         private static GUIStyle s_TempStyle = new GUIStyle();
    8.         private static float height = 75;
    9.  
    10.         private AngleAttribute angleAttribute
    11.         {
    12.             get { return (AngleAttribute) attribute; }
    13.         }
    14.  
    15.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    16.         {
    17.             property.floatValue = FloatAngle(position, property.floatValue, angleAttribute.snap, angleAttribute.min, angleAttribute.max);
    18.         }
    19.  
    20.         public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    21.         {
    22.             return height;
    23.         }
    24.  
    25.         public static float FloatAngle(Rect rect, float value)
    26.         {
    27.             return FloatAngle(rect, value, -1, -1, -1);
    28.         }
    29.  
    30.         public static float FloatAngle(Rect rect, float value, float snap)
    31.         {
    32.             return FloatAngle(rect, value, snap, -1, -1);
    33.         }
    34.  
    35.         public static float FloatAngle(Rect rect, float value, float snap, float min, float max)
    36.         {
    37.             int id = GUIUtility.GetControlID(FocusType.Native, rect);
    38.  
    39.             Rect knobRect = new Rect(rect.x, rect.y, rect.height, rect.height);
    40.  
    41.             float delta;
    42.             if (min != max)
    43.                 delta = ((max - min) / 360);
    44.             else
    45.                 delta = 1;
    46.  
    47.             if (Event.current != null)
    48.             {
    49.                 if (Event.current.type == EventType.MouseDown && knobRect.Contains(Event.current.mousePosition))
    50.                 {
    51.                     GUIUtility.hotControl = id;
    52.                     mousePosition = Event.current.mousePosition;
    53.                 }
    54.                 else if (Event.current.type == EventType.MouseUp && GUIUtility.hotControl == id)
    55.                     GUIUtility.hotControl = 0;
    56.                 else if (Event.current.type == EventType.MouseDrag && GUIUtility.hotControl == id)
    57.                 {
    58.                     Vector2 move = mousePosition - Event.current.mousePosition;
    59.                     value += delta * (-move.x - move.y);
    60.  
    61.                     if (snap > 0)
    62.                     {
    63.                         float mod = value % snap;
    64.  
    65.                         if (mod < (delta * 3) || Mathf.Abs(mod - snap) < (delta * 3))
    66.                             value = Mathf.Round(value / snap) * snap;
    67.                     }
    68.  
    69.                     mousePosition = Event.current.mousePosition;
    70.                     GUI.changed = true;
    71.                 }
    72.             }
    73.  
    74.             GUI.DrawTexture(knobRect, KnobBack);
    75.             Matrix4x4 matrix = GUI.matrix;
    76.  
    77.             if (min != max)
    78.                 GUIUtility.RotateAroundPivot(value * (360 / (max - min)), knobRect.center);
    79.             else
    80.                 GUIUtility.RotateAroundPivot(value, knobRect.center);
    81.  
    82.             GUI.DrawTexture(knobRect, Knob);
    83.             GUI.matrix = matrix;
    84.  
    85.             Rect label = new Rect(rect.x + rect.height, rect.y + (rect.height / 2) - 9, rect.height, 18);
    86.             value = EditorGUI.FloatField(label, value);
    87.  
    88.             if (min != max)
    89.                 value = Mathf.Clamp(value, min, max);
    90.  
    91.             return value;
    92.         }
    93.  
    94.         private static void DrawTexture(Rect position, Texture2D texture)
    95.         {
    96.             if (Event.current.type != EventType.Repaint)
    97.                 return;
    98.             s_TempStyle.normal.background = texture;
    99.             s_TempStyle.Draw(position, GUIContent.none, false, false, false, false);
    100.         }
    101.     }

    Ok, here is my problems :
    1- how to get rid of the constant float i am declering on top for the size of property?
    2- when i use GUI.DrawTexture, lots of time the images gets disappeared
    3- if i use the DrawTexture method numberCruncher mentioned, the texture stays and won't flicker out, but then when changing the value, the knob won't rotate at all
    4- would be nice if you would share those knob and it's back images, i tried my hand but the result was not as fancy and when it rotates it gives the feeling that the gear is not rotating at it's center :/

    Edit : one more question, when i make some property drawers and during when i tweak the drawing stuff in code and save it back, the inspector gets ruined until i click on it, is there anyway to force it to repaint itself like editor windows?

    Edit 2 : found a solution for the flickering, the property needs to be repainted, how to do it ?
    first, you need to change method definitions like this :

    Code (CSharp):
    1. public static float FloatAngle(Rect rect, float value, SerializedProperty property)
    2.  
    3. public static float FloatAngle(Rect rect, float value, float snap, SerializedProperty property)
    4.  
    5. public static float FloatAngle(Rect rect, float value, float snap, float min, float max, SerializedProperty property)
    as you see i am sending in the serialized property, now, at the end of the main method that is drawing the property, where you have the return value; before that add this :

    Code (CSharp):
    1. EditorUtility.SetDirty(property.serializedObject.targetObject);
    so the last lines on that method will look like this :

    Code (CSharp):
    1. if (min != max)
    2.                 value = Mathf.Clamp(value, min, max);
    3.             EditorUtility.SetDirty(property.serializedObject.targetObject);
    4.             return value;
    Now in the place you would call this method in the OnGUI method, change it to send the property as last param :

    Code (CSharp):
    1. public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    2.         {
    3.             property.floatValue = FloatAngle(position, property.floatValue, angleAttribute.snap, angleAttribute.min, angleAttribute.max, property);
    4.         }
    here is how i use that property attribute :

    Code (CSharp):
    1. [Angle(1f,0,100f)]
    2. [SerializeField]
    3. private float angleTest;
    if you don't specify anything, it will snap to 1, min to -360 and max to 360 :D

    and the property drawer behaves properly, now only if someone share the nice gui pictures? :/
     
    Last edited: Sep 8, 2015
    ZenMicro and DomDom like this.
  20. ZenMicro

    ZenMicro

    Joined:
    Aug 9, 2015
    Posts:
    206
    Hi @Jiraiyah, or perhaps @LightStriker might be able to help but i saw you don't like PropertyDrawers... anyway worth a shot!

    I've attempted to use this PropertyDrawer but am new to these and While i have the AngleAttribute and AngleDrawer scripts correctly updated and saved in appropriate folders i am not sure on how to use it, I am attempting to use it within an EditorWindow that has a scriptable object class that contains a list of another scriptable object class that contains a list of scriptable object class if that makes sense.

    In any case the 3 lines you state at the end of your post as to how you are using them has me stuck, or something else is missing i think? If i use angleTest in my Editor window nothing special happens. It appears as a plain float value which i have been using with EditorGUILayout.Slider. Do i need to create a wrapper class for it and then use that? Appreciate your help here :)

    And Here are some links to some Knob Icons, best double check the licence on them some are Free Creative Commons, some with Attribution, not real sure if you can make a product for profit with them all...
    http://vector4free.com/vector/volume-knob/
    https://greensock.com/draggable
    http://www.ifreepic.com/knob-223415.html
    http://icones.gratuites.web.free.fr/?c=Default Icon&img=media-knob.png
    hope they suit your needs!
     
    Last edited: Nov 2, 2015
  21. Jiraiyah

    Jiraiyah

    Joined:
    Mar 4, 2013
    Posts:
    175
    @ZenMicro Hi,
    If you want to use it in a custom editor window, i would suggest to take out those methods from the drawer into a custom static class as a helper method (float angle and draw texture ones), then feed it with the needed information like what we did in OnGUI, obviously you would need to tweak the code here and there to feed in the rect that it needs to draw it in the editor window but it should be fairly straight forward then.
     
    ZenMicro likes this.
  22. Xelnath

    Xelnath

    Joined:
    Jan 31, 2015
    Posts:
    402
    Is there a clean version of the final code for this anywhere?
     
  23. Xelnath

    Xelnath

    Joined:
    Jan 31, 2015
    Posts:
    402
    Code (csharp):
    1.  
    2.  
    3. using UnityEditor;
    4. using UnityEngine;
    5. public class AngleAttribute : PropertyAttribute
    6. {
    7.     public readonly float snap;
    8.     public readonly float min;
    9.     public readonly float max;
    10.  
    11.     public AngleAttribute()
    12.     {
    13.         snap = 1;
    14.         min = -360;
    15.         max = 360;
    16.     }
    17.  
    18.     public AngleAttribute(float snap)
    19.     {
    20.         this.snap = snap;
    21.         min = -360;
    22.         max = 360;
    23.     }
    24.  
    25.     public AngleAttribute(float snap, float min, float max)
    26.     {
    27.         this.snap = snap;
    28.         this.min = min;
    29.         this.max = max;
    30.     }
    31. }
    32.  
    33. [CustomPropertyDrawer(typeof(AngleAttribute))]
    34. public class AngleDrawer : PropertyDrawer
    35. {
    36.     private static Vector2 mousePosition;
    37.     private static Texture2D KnobBack = Resources.Load("Editor Icons/Dial") as Texture2D;
    38.     private static Texture2D Knob  = Resources.Load("Editor Icons/DialButton") as Texture2D;
    39.     //private static GUIStyle s_TempStyle = new GUIStyle();
    40.     //private static float height = 75;
    41.  
    42.     public static float FloatAngle(Rect rect, float value)
    43.     {
    44.         return FloatAngle(rect, value, -1, -1, -1);
    45.     }
    46.  
    47.     public static float FloatAngle(Rect rect, float value, float snap)
    48.     {
    49.         return FloatAngle(rect, value, snap, -1, -1);
    50.     }
    51.  
    52.     public static float FloatAngle(Rect rect, float value, float snap, float min, float max)
    53.     {
    54.         return FloatAngle(rect, value, snap, min, max, Vector2.up);
    55.     }
    56.  
    57.     public static float FloatAngle(Rect rect, float value, float snap, float min, float max, Vector2 zeroVector)
    58.     {
    59.         int id = GUIUtility.GetControlID(FocusType.Passive, rect);
    60.         float originalValue = value;
    61.         Rect knobRect = new Rect(rect.x, rect.y, rect.height, rect.height);
    62.  
    63.         float delta;
    64.         if (min != max)
    65.             delta = ((max - min) / 360);
    66.         else
    67.             delta = 1;
    68.  
    69.         if (Event.current != null)
    70.         {
    71.             if (Event.current.type == EventType.MouseDown && knobRect.Contains(Event.current.mousePosition))
    72.             {
    73.                 GUIUtility.hotControl = id;
    74.                 mousePosition = Event.current.mousePosition;
    75.             }
    76.             else if (Event.current.type == EventType.MouseUp &&  GUIUtility.hotControl == id)
    77.             {
    78.                 GUIUtility.hotControl = 0;
    79.             }
    80.             else if (Event.current.type == EventType.MouseDrag &&  GUIUtility.hotControl == id)
    81.             {
    82.                 Vector2 move = mousePosition - Event.current.mousePosition;
    83.  
    84.                 //if ( knobRect.Contains(mousePosition)  )
    85.                 {
    86.                     Vector2 mouseStartDirection = (mousePosition - knobRect.center).normalized;
    87.                     float startAngle = CalculateAngle(Vector2.up, mouseStartDirection);
    88.  
    89.                     Vector2 mouseNewDirection = (Event.current.mousePosition - knobRect.center).normalized;
    90.                     float newAngle = CalculateAngle(Vector2.up, mouseNewDirection);
    91.  
    92.  
    93.                     float sign = Mathf.Sign(newAngle-startAngle);
    94.                     float delta2 = Mathf.Min( Mathf.Abs(newAngle - startAngle), Mathf.Abs(newAngle-startAngle+360f), Mathf.Abs(newAngle-startAngle-360f) );
    95.                     value -= delta2 * sign;
    96.                 }
    97.  
    98.                 if (snap > 0)
    99.                 {
    100.                     float mod = value % snap;
    101.  
    102.                     if (mod < (delta * 3) || Mathf.Abs(mod - snap) < (delta * 3))
    103.                         value = Mathf.Round(value / snap) * snap;
    104.                 }
    105.  
    106.                 if ( value != originalValue)
    107.                 {
    108.                     mousePosition = Event.current.mousePosition;
    109.                     GUI.changed = true;
    110.                 }
    111.             }
    112.         }
    113.  
    114.         float angleOffset =  (CalculateAngle(Vector2.up, zeroVector ) + 360f) % 360f;
    115.  
    116.         GUI.DrawTexture(knobRect, KnobBack);
    117.         Matrix4x4 matrix = GUI.matrix;
    118.  
    119.         if (min != max)
    120.             GUIUtility.RotateAroundPivot((angleOffset + value) * (360 / (max - min)), knobRect.center);
    121.         else
    122.             GUIUtility.RotateAroundPivot((angleOffset + value), knobRect.center);
    123.  
    124.         GUI.DrawTexture(knobRect, Knob);
    125.         GUI.matrix = matrix;
    126.  
    127.         Rect label = new Rect(rect.x + rect.height, rect.y + (rect.height / 2) - 9, rect.height, 18);
    128.         value = EditorGUI.FloatField(label, value);
    129.  
    130.         if (min != max)
    131.             value = Mathf.Clamp(value, min, max);
    132.  
    133.         return value;
    134.     }
    135.  
    136.     public static float CalculateAngle(Vector3 from, Vector3 to) {
    137.         Vector3 right = Vector3.right;
    138.         float angle = Vector3.Angle(from, to);
    139.         return (Vector3.Angle(right, to) > 90f) ? 360f - angle : angle;          
    140.  
    141.         //return Quaternion.FromToRotation(Vector3.up, to - from).eulerAngles.z;
    142.     }
    143. }
    144.  
    Here's a cleaned up version where you can click and drag on the knob to turn it.

    You will have to add this to your editor:

    Code (csharp):
    1.  
    2.  
    3.      void OnEnable() { EditorApplication.update += Update; }
    4.      void OnDisable() { EditorApplication.update -= Update; }
    5.  
    6.      void Update()
    7.      {
    8.         Repaint();
    9.      }
    10.  
    And textures:


    Dial.png DialButton.png

    Oh and I forgot, usage:

    Code (csharp):
    1.  
    2.         var rect = GUILayoutUtility.GetRect(64f, 64f);
    3.         float newValue = AngleDrawer.FloatAngle(rect, someValue, 5f, 0f, 360f, Vector2.down );
    4.  
     
    Last edited: Mar 29, 2017
    Ruchir, CruelByte, zvidrih and 2 others like this.
  24. Sezafine

    Sezafine

    Joined:
    Dec 9, 2022
    Posts:
    1
    Performing necromancy on this thread... to get this working as just an attribute applied to a field in 2022 add this method to the AngleDrawer class:

    Code (CSharp):
    1.  
    2. public override void OnGUI(Rect position, SerializedProperty prop, GUIContent label) {
    3.     GUILayout.Label(label);
    4.     var rect = GUILayoutUtility.GetRect(64f, 64f);
    5.     prop.floatValue = AngleDrawer.FloatAngle(rect, prop.floatValue, 5f, 0f, 360f, Vector2.down );
    6. }
    7.  
    Usage, on a prop in a script:
    Code (CSharp):
    1.  
    2. [Angle]
    3. public float angle = 0;
    4.  
     
    Bunny83 likes this.
  25. Hosnkobf

    Hosnkobf

    Joined:
    Aug 23, 2016
    Posts:
    1,096
    I just needed this and was not satisfied with the current state of the code as provided here (wasn't fulfilling my needs and a bit buggy). So, I refactored it a lot. Here is what I came up with:

    AngleAttribute.cs (should be in a runtime assembly)
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. /// <summary>
    4. /// The Angle Atribute draws a powerful angle-control-ui for manipulating a float that represents an angle.
    5. /// </summary>
    6. public class AngleAttribute : PropertyAttribute
    7. {
    8.     public readonly float SnapDegrees;
    9.     public readonly float MinDegrees;
    10.     public readonly float MaxDegrees;
    11.     public readonly float AngleOffset;
    12.     public readonly bool StoreAsRadians;
    13.     public readonly bool RotateClockwise;
    14.  
    15.     /// <summary>
    16.     /// The Angle Atribute draws a powerful angle-control-ui for manipulating a float that represents an angle.
    17.     /// </summary>
    18.     /// <param name="min">The minimum angle in degrees. The user cannot assign a smaller angle value. If this is greater or equal <paramref name="max"/>, there is no restriction.</param>
    19.     /// <param name="max">The maximum angle in degrees. The user cannot assign a greater angle value. If this is smaller or equal <paramref name="min"/>, there is no restriction.</param>
    20.     /// <param name="snap">Defines an enforced angle interval in degrees. The user can only assign angles that are devidable by this defined value. If set to zero or below, there is no angle restriction. However, the control allows to set an optional custom snap-interval in that case.</param>
    21.     /// <param name="angleOffset">The angle offset defines the direction of the "zero-angle". Mathematically, this points to the right. If you want to have it upwards, enter 90 here (or -90 if <paramref name="rotateClockwise"/> is true). Note that this only defines the default rotation of the control. The user is allowed to change it for editing (this has no impact to the actual value and is just a visual help).</param>
    22.     /// <param name="rotateClockwise">On a normal cartesian coordinate system the vertical axis increases in upward direction. Angles increase in counter-clockwise direction in that case. However, screen coordinates (as used in IMGUI, but not in UGUI) increase downwards and causes the rotation to increase in clockwise direction. So, if your coordinate system has an inverted vertical axis (downwards), you should pass true for this parameter (this has no impact to the actual value and is just a visual help).</param>
    23.     /// <param name="storeAsRadians">If true, the defined value will be stored in radians rather than degrees. Note that the angle-control-ui will display everything in degrees regardless if the value is stored in radians or not.</param>
    24.     public AngleAttribute(float min = 0, float max = 0, float snap = 0, float angleOffset = 0, bool rotateClockwise = false, bool storeAsRadians = false)
    25.     {
    26.         this.SnapDegrees = snap;
    27.         this.MinDegrees = min;
    28.         this.MaxDegrees = max;
    29.         this.AngleOffset = angleOffset;
    30.         this.StoreAsRadians = storeAsRadians;
    31.         this.RotateClockwise = rotateClockwise;
    32.     }
    33. }
    34.  

    AngleDrawer.cs (must be in an editor assembly)
    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. [CustomPropertyDrawer(typeof(AngleAttribute))]
    5. public class AngleDrawer : PropertyDrawer
    6. {
    7.     private static Texture2D dial = AngleDrawerImages.GetDialImage();
    8.     private static Texture2D handle = AngleDrawerImages.GetHandleImage();
    9.     private static Texture2D line = AngleDrawerImages.GetLineImage();
    10.  
    11.     private static GUIContent angleOffsetIcon = new GUIContent(AngleDrawerImages.GetAngleOffsetIcon(),
    12.         @"Angle Offset: This defines the ""zero-direction"" of the visual control. It has no impact anything else.");
    13.     private static GUIContent snapIcon = new GUIContent(AngleDrawerImages.GetSnapIcon(),
    14.         "Snap Angle: When enabled, the angle can be set in steps only (this might be forced by code)");
    15.  
    16.     static readonly Vector2 FlipVector = new Vector2(1, -1);
    17.  
    18.     bool isInitialized = false;
    19.     bool isSnapEnabled = true;
    20.     float snapSteps = 15;
    21.     float angleOffset = 0f;
    22.  
    23.     void Init()
    24.     {
    25.         var a = attribute as AngleAttribute;
    26.         if(a.SnapDegrees > 0)
    27.         {
    28.             snapSteps = a.SnapDegrees;
    29.         }
    30.  
    31.         angleOffset = a.AngleOffset;
    32.         isInitialized = true;
    33.     }
    34.  
    35.     public override void OnGUI(Rect position, SerializedProperty prop, GUIContent label)
    36.     {
    37.         if(!isInitialized)
    38.         {
    39.             Init();
    40.         }
    41.  
    42.         GUILayout.Label(label);
    43.         var container = GUILayoutUtility.GetRect(128f, 64f);
    44.  
    45.         var controlRect = container;
    46.         controlRect.width = 64;
    47.         Rect fieldRect = container;
    48.         fieldRect.x += 64 + 4;
    49.         fieldRect.width = container.xMax - fieldRect.x;
    50.         fieldRect.height = 18;
    51.  
    52.         var a = attribute as AngleAttribute;
    53.  
    54.         float value = (a.StoreAsRadians) ? prop.floatValue * Mathf.Rad2Deg : prop.floatValue;
    55.         float snap = (isSnapEnabled) ? snapSteps : 0;
    56.  
    57.         value = DrawAngleControl(container, value, snap, a.MinDegrees, a.MaxDegrees, angleOffset, a.RotateClockwise);
    58.         value = DrawAngleField(fieldRect, value, snap, a.MinDegrees, a.MaxDegrees);
    59.         fieldRect.y += 22;
    60.         snapSteps = DrawSnapControl(fieldRect, snapSteps, a.SnapDegrees > 0);
    61.         fieldRect.y += 20;
    62.         angleOffset = DrawAngleOffsetControl(fieldRect, angleOffset);
    63.  
    64.         if (prop.floatValue != value)
    65.         {
    66.             prop.floatValue = (a.StoreAsRadians) ? value * Mathf.Deg2Rad : value;
    67.             prop.serializedObject.ApplyModifiedProperties();
    68.         }
    69.     }
    70.  
    71.     float DrawSnapControl(Rect rect, float value, bool forceSnap)
    72.     {
    73.         EditorGUI.BeginDisabledGroup(forceSnap);
    74.         Rect snapToggleRect = new Rect(rect.x, rect.y, 18, 18);
    75.         isSnapEnabled = GUI.Toggle(snapToggleRect, isSnapEnabled, snapIcon, EditorStyles.iconButton);
    76.         EditorGUI.EndDisabledGroup();
    77.  
    78.         EditorGUI.BeginDisabledGroup(!isSnapEnabled || forceSnap);
    79.         Rect fieldRect = new Rect(rect.x + 20, rect.y, rect.width - 20, 18);
    80.         float val = EditorGUI.FloatField(fieldRect, (isSnapEnabled) ? value : 0);
    81.         if(isSnapEnabled)
    82.         {
    83.             value = val;
    84.         }
    85.         EditorGUI.EndDisabledGroup();
    86.  
    87.         return value;
    88.     }
    89.     float DrawAngleOffsetControl(Rect rect, float value)
    90.     {
    91.         Rect iconRect = new Rect(rect.x, rect.y, 18, 18);
    92.         GUI.Label(iconRect, angleOffsetIcon);
    93.  
    94.         Rect fieldRect = new Rect(rect.x + 20, rect.y, rect.width - 20, 18);
    95.         value = EditorGUI.FloatField(fieldRect, value);
    96.  
    97.         return value;
    98.     }
    99.  
    100.  
    101.     public static float DrawAngleControl(Rect rect, float previousValue, float snap = 0, float min = 0, float max = 0,  float angleOffset = 0, bool rotateClockwise = false)
    102.     {
    103.         int id = GUIUtility.GetControlID(FocusType.Passive, rect);
    104.         rect = new Rect(rect.x, rect.y, rect.height, rect.height);
    105.         float value = previousValue;
    106.  
    107.         if (Event.current != null)
    108.         {
    109.             switch (Event.current.type)
    110.             {
    111.                 case EventType.MouseDown:
    112.  
    113.                     if (!rect.Contains(Event.current.mousePosition))
    114.                         break;
    115.                  
    116.                     GUIUtility.hotControl = id;
    117.                     break;
    118.  
    119.                 case EventType.MouseUp:
    120.  
    121.                     if (GUIUtility.hotControl != id)
    122.                         break;
    123.                  
    124.                     GUIUtility.hotControl = 0;
    125.                     break;
    126.  
    127.                 case EventType.MouseDrag:
    128.  
    129.                     if (GUIUtility.hotControl != id)
    130.                         break;
    131.  
    132.                     Vector2 dir = (Event.current.mousePosition - rect.center).normalized;
    133.                     value = (rotateClockwise)
    134.                         ? Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - angleOffset
    135.                         : Mathf.Atan2(-dir.y, dir.x) * Mathf.Rad2Deg - angleOffset;
    136.  
    137.                     if (value != previousValue)
    138.                     {
    139.                         // check if the user wants to do "extra turns"
    140.                         while (Mathf.Abs(value - previousValue) > Mathf.Abs((value + 360) - previousValue))
    141.                         {
    142.                             value += 360;
    143.                         }
    144.  
    145.                         while (Mathf.Abs(value - previousValue) > Mathf.Abs((value - 360) - previousValue))
    146.                         {
    147.                             value -= 360;
    148.                         }
    149.  
    150.                         value = SnapAndClamp(value, snap, min, max);
    151.  
    152.                         GUI.changed = true;
    153.                     }
    154.  
    155.                     break;
    156.             }
    157.         }
    158.  
    159.         Matrix4x4 identity = GUI.matrix;
    160.         GUIUtility.RotateAroundPivot(angleOffset, rect.center);
    161.         if (!rotateClockwise)
    162.         {
    163.             GUIUtility.ScaleAroundPivot(FlipVector, rect.center);
    164.         }
    165.  
    166.         GUI.DrawTexture(rect, dial);
    167.  
    168.         Matrix4x4 baseTransform = GUI.matrix;
    169.  
    170.         if(min < max)
    171.         {
    172.             GUIUtility.RotateAroundPivot((rotateClockwise) ? min : -min, rect.center);
    173.             GUI.DrawTexture(rect, line);
    174.             GUI.matrix = baseTransform;
    175.  
    176.             GUIUtility.RotateAroundPivot((rotateClockwise) ? max : -max, rect.center);
    177.             GUI.DrawTexture(rect, line);
    178.             GUI.matrix = baseTransform;
    179.         }
    180.  
    181.         GUIUtility.RotateAroundPivot((rotateClockwise) ? value : -value, rect.center);
    182.         GUI.DrawTexture(rect, handle);
    183.  
    184.         GUI.matrix = identity;
    185.  
    186.         return value;
    187.     }
    188.  
    189.     public static float DrawAngleField(Rect rect, float previousValue, float snap = 0, float min = 0, float max = 0)
    190.     {
    191.         float value = EditorGUI.FloatField(rect, previousValue);
    192.         if (value != previousValue)
    193.         {
    194.             value = SnapAndClamp(value, snap, min, max);
    195.         }
    196.  
    197.         return value;
    198.     }
    199.  
    200.     private static float SnapAndClamp(float angle, float snap, float min, float max)
    201.     {
    202.         angle = SnapAngle(angle, snap);
    203.         angle = ClampAngle(angle, min, max);
    204.         return angle;
    205.     }
    206.  
    207.     private static float SnapAngle(float angle, float snap)
    208.     {
    209.         if (snap <= 0)
    210.             return angle;
    211.  
    212.         float mod = angle % snap;
    213.  
    214.         // round
    215.         float modAmount = mod / snap;
    216.         if (angle < 0)
    217.             mod += snap;
    218.  
    219.         if (modAmount < 0.5f && angle > 0)
    220.             angle -= mod; // floor
    221.         else
    222.             angle += snap - mod; // ceil
    223.  
    224.         // special case: around 0 occure rounding errors sometimes
    225.         if (Mathf.Abs(angle) < 0.5f * snap)
    226.             angle = 0;
    227.  
    228.         return angle;
    229.     }
    230.  
    231.     private static float ClampAngle(float angle, float min, float max)
    232.     {
    233.         if (min >= max)
    234.             return angle;
    235.  
    236.         return Mathf.Clamp(angle, min, max);
    237.     }
    238. }
    239.  

    And instead of having textures in the project, I have them as base64-encoded strings in the code, so you simply have to add another class file:

    AngleDrawerImages.cs (must be in the same assembly as AngleDrawer)
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. public static class AngleDrawerImages
    5. {
    6.     const string DialBase64 =
    7.         #region Dial Picture Data
    8.         @"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";
    9.         #endregion
    10.  
    11.     const string HandleBase64 =
    12.         #region Handle Picture Data
    13.             @"iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsSAAALEgHS3X78AAABd2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjaldC9a1MBGMXh5yZqS610MINIhztUcWihKKijRLBLdEgjJNEluU3SQL649wYproKLQ8FBdLHq4H+gq+CqIAiKIOLo7Nci5TqkkCJ08Ew/zuG8vBxypV7UTw6t0h+kcXmtGFZr9XDmm5wj5uRdaETJ6Or6lYoD9fuDAN6v9KJ+4v90dKOVRASzOB+N4pTgEkq30lFKcA+FaLOxQbCD5bhaqxO8QaE54a8odCb8E4W4Ur5MbhZhZx8393G0GffJncFSvzeO9v4JMN8aXF/HSSxKlK0pCjWNdfWkVnQNOKC3ikXXDIUiQyNbYl0dm1LLQmOJllBbrKWlZ6taq4f/7pm0z52dXJ8vcvhLlv04xcx9drez7M+TLNt9Sv4zrwbT/nCHi7/Ib0+9pccs3OHF66nXfMDLu5z4NGrEDZBHrt3m+3OO1Tj+jrkbk632cs8+UrlN6S0PH3G6zcLNv9GQZ/VOyuV1AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAANnSURBVHja7N2/TxNhAIfx791VQZsy2pALxsSEkOsiMbhoDAP/gAtjg4tJy+JgSJRBWTTGQRdK4oBsTN1cnHCTyOLyVqLEsNwEg4N0IL2+DtyV1waNBEga+nzI5eh7d82l98D9YMCz1gr9y+cjIAAQAAgABAACAAGAAEAAIAAQAAgABAACAAGAAEAAIAAQAAgABAACAAGAAEAAIAAQAAgABAACAAGAAEAAIAAQAAgABAACAAGAAEAAIAAQAAgABAACAAGAAEAAIAAQAAgABAACAAGAAEAAOBu5fv8AGo1Gr+/iNUk30u+/SNp2F0ZRdKI39/r9X8d6nteT+2WMGZX0MI7jyubmpiRpbGxMYRguSXpTKpW+SdKJj5+1tq+nHj34RWNMrVwuW03K6rWsnslqUrZcLltjTM0YUzyN49fXvwEajcYlSfck3ZTU7pqspFY6T5yxxFknW569TtKplf18pa/lbK90zDpjtmudu4uLi49qzZr0rmun70vVy1XNzs6+kvQ+3eanpO0oin5xCjjGwd/f32+ur69ra2tLvu/L8zx5niffP7g2DoKg89pdls2DIDi4knbG3HH3vbLt3fXdse7xysuKmqZ55L7nS3ktP13urJvP51UsFjU4OFg4bgT9fBF4sdVqaXV1Vbu7uz23c83R5l+X7V3Z08LCwuFBzOU0Nzen8fHx25I+EMB/fsYDAwOan5/Xzs5O53rAPT+22225F1pHXT9k67Tb7T/G3PdIkoOzQJIknbFs3r19tk7j07/vTqampjrvMTQ0pOHhYUn6zingeKeBC5KuS7rqPBPxJXnpPBvznMk/4rUkBV1j2RQ4y3LpPOha7m6Tk3RnZmZmcuPtxuENoA5vBCceTGhlZeWjpK/pNruSlqMo+sFdwPm4C5iu1+u2MFKwWpPtfK3JFkYKtl6vW2PMNHcB5/s5wPM4jh/XajXFcSxJCsNQ1WpVYRi+KJVKT07jOUDfB9DjTwJvSaqkTwOVPgVckvSZJ4E4FfwxiABAACAAEAAIAAQAAgABgABAACAAEAAIAAQAAgABgABAACAAEAAIAAQAAgABgABAACAAEAAIAAQAAgABgABAACAAEAAIAAQAAgABgABAACAAEAAIAAQAAgABgABAACAAEAAIAAQAAgABgABwFn4PAENzElz4TuBAAAAAAElFTkSuQmCC";
    14.     #endregion
    15.  
    16.     const string LineBase64 =
    17.         #region Line Picture Data
    18.             @"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";
    19.     #endregion
    20.  
    21.     const string SnapIconBase64 =
    22.         #region Snap Icon Data
    23.             @"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";
    24.     #endregion
    25.  
    26.     const string AngleOffsetIconBase64 =
    27.         #region Angle Offset Icon Data
    28.             @"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";
    29.     #endregion
    30.  
    31.  
    32.     public static Texture2D GetDialImage() => Base64ToTexture(DialBase64);
    33.     public static Texture2D GetHandleImage() => Base64ToTexture(HandleBase64);
    34.     public static Texture2D GetLineImage() => Base64ToTexture(LineBase64);
    35.     public static Texture2D GetSnapIcon() => Base64ToTexture(SnapIconBase64);
    36.     public static Texture2D GetAngleOffsetIcon() => Base64ToTexture(AngleOffsetIconBase64);
    37.  
    38.     static Texture2D Base64ToTexture(string base64)
    39.     {
    40.         byte[] bytes = Convert.FromBase64String(base64);
    41.         Texture2D tex = new Texture2D(1,1);
    42.         tex.LoadImage(bytes);
    43.         tex.Apply();
    44.         return tex;
    45.     }
    46. }
    47.  

    to use it, simply write something like this:
    Code (CSharp):
    1.     [SerializeField, Angle]
    2.     float myAngle;
    3.  
    4.     [SerializeField]
    5.     [Angle(min: -180, max: 180, snap: 60, angleOffset: 90, storeAsRadians: true)]
    6.     float myHexagonalAngleInRadians;
    7.  
    this will display something like this:
    upload_2024-2-7_22-33-18.png
    (note that the user has control over the steps, if not set in code. The user will always have control over the rotation of the displayed control, as upwards may feel more natural while mathematically pointing to the right is the "zero-angle").
     
    Last edited: Feb 8, 2024