Search Unity

Range Attribute

Discussion in 'Editor & General Support' started by GameDevRick, Jan 18, 2017.

  1. GameDevRick

    GameDevRick

    Joined:
    Oct 8, 2011
    Posts:
    269
    Is it possible to make the Range attribute slider increment by 10 instead of 1?
    The following range controlled variable for example:

    Code (CSharp):
    1. // Current Stage
    2. [Range(0, 100)]
    3. public int currentStage = 0;
     
    Kaldrin likes this.
  2. MSplitz-PsychoK

    MSplitz-PsychoK

    Joined:
    May 16, 2015
    Posts:
    1,278
    Not really, unless you make your own PropertyDrawer, but that seems like more work than it's worth.
     
  3. liortal

    liortal

    Joined:
    Oct 17, 2012
    Posts:
    3,562
    Here's the code to roughly do what you're after (it's not perfect, especially when setting the step to weird numbers).
    RangeEx attribute (place with runtime code. This has to be placed over your fields).
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. [AttributeUsage (AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
    5. public sealed class RangeExAttribute : PropertyAttribute
    6. {
    7.     public readonly int min;
    8.     public readonly int max;
    9.     public readonly int step;
    10.  
    11.     public RangeExAttribute (int min, int max, int step)
    12.     {
    13.         this.min = min;
    14.         this.max = max;
    15.         this.step = step;
    16.     }
    17. }
    And the property drawer (place in an editor folder):
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. [CustomPropertyDrawer (typeof(RangeExAttribute))]
    5. internal sealed class RangeExDrawer : PropertyDrawer
    6. {
    7.     private int value;
    8.  
    9.     //
    10.     // Methods
    11.     //
    12.     public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    13.     {
    14.         var rangeAttribute = (RangeExAttribute)base.attribute;
    15.  
    16.         if (property.propertyType == SerializedPropertyType.Integer)
    17.         {
    18.             value = EditorGUI.IntSlider (position, label, value, rangeAttribute.min, rangeAttribute.max);
    19.  
    20.             value = (value / rangeAttribute.step) * rangeAttribute.step;
    21.             property.intValue = value;
    22.         }
    23.         else
    24.         {
    25.             EditorGUI.LabelField (position, label.text, "Use Range with float or int.");
    26.         }
    27.     }
    28. }
    Now you can use this in your code:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class NewBehaviourScript : MonoBehaviour {
    4.  
    5.     [RangeEx(0, 1000, 10)]
    6.     public int myNumber;
    7. }
    8.  
     
  4. nicmarxp

    nicmarxp

    Joined:
    Dec 3, 2017
    Posts:
    406
    This is excellent, thanks a lot! For a noob like me, I just wanted to clarify, the first snipped needs to be placed somewhere outside the Editor folder, or you will get an "assembly missing" error.

    I have a Utils.cs file where I put enums and stuff, and the RangeEx worked great, and this file is in an Utils folder in the root, outside of any Editor folder.
     
    Mehrdad995 likes this.
  5. AnKOu

    AnKOu

    Joined:
    Aug 30, 2013
    Posts:
    123
    There is a useless line :
    Code (CSharp):
    1. value = (value / rangeAttribute.step) * rangeAttribute.step;
    ex: value = 4 / 2 * 2
     
  6. nicmarxp

    nicmarxp

    Joined:
    Dec 3, 2017
    Posts:
    406
    Isnt that for rounding to the wanted step?
    Value = 5
    Step = 2
    5/2 = 2 (since they are ints)
    2*2 = 4

    How would it work otherwise?
     
    Mehrdad995 likes this.
  7. TheWolfNL

    TheWolfNL

    Joined:
    Mar 4, 2014
    Posts:
    3
    In case you have the problem of the value resetting when triggering game mode:
    Use this slightly updated property drawer:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. [CustomPropertyDrawer (typeof(RangeExAttribute))]
    4. internal sealed class RangeExDrawer : PropertyDrawer
    5. {
    6.     private int value;
    7.     //
    8.     // Methods
    9.     //
    10.     public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    11.     {
    12.         var rangeAttribute = (RangeExAttribute)base.attribute;
    13.         if (property.propertyType == SerializedPropertyType.Integer)
    14.         {
    15.             value = EditorGUI.IntSlider (position, label, property.intValue, rangeAttribute.min, rangeAttribute.max);
    16.             value = (value / rangeAttribute.step) * rangeAttribute.step;
    17.             property.intValue = value;
    18.         }
    19.         else
    20.         {
    21.             EditorGUI.LabelField (position, label.text, "Use Range with int.");
    22.         }
    23.     }
    24. }
     
    lokeshvt likes this.
  8. MrDizzle26

    MrDizzle26

    Joined:
    Feb 8, 2015
    Posts:
    36
    Anyone got a MixMax style one for the Integer Slider? Writing property drawers hurts my brain XD
     
  9. fcrloch

    fcrloch

    Joined:
    Nov 1, 2019
    Posts:
    1
    With a few months late ...

    For integer, replace line
    Code (CSharp):
    1. value = (value / rangeAttribute.step) * rangeAttribute.step;
    by
    Code (CSharp):
    1. value -= (value % (int)Math.Round(rangeAttribute.step));
    Work fine ^^

    but it's a bit more complex for floats.
    The process with the modulo to calculate the steps with the floats value does not work perfectly (in every case).

    I suggest this but there may be better ^^
    PS: Step and Label is Optional.
    I did not test all the contexts but it seems quite correct.

    Code (CSharp):
    1.  
    2.      [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
    3.     public sealed class RangeExAttribute : PropertyAttribute
    4.     {
    5.         public readonly float min = .0f;
    6.         public readonly float max = 100.0f;
    7.         public readonly float step = 1.0f;
    8.         public readonly string label = "";
    9.  
    10.         public RangeExAttribute(float min, float max, float step = 1.0f, string label = "")
    11.         {
    12.             this.min = min;
    13.             this.max = max;
    14.             this.step = step;
    15.             this.label = label;
    16.         }
    17.     }
    18.  
    19. #if UNITY_EDITOR
    20.     [CustomPropertyDrawer(typeof(RangeExAttribute))]
    21.     internal sealed class RangeExDrawer : PropertyDrawer
    22.     {
    23.  
    24.         /**
    25.          * Return exact precision of reel decimal
    26.          * ex :
    27.          * 0.01     = 2 digits
    28.          * 0.02001  = 5 digits
    29.          * 0.02000  = 2 digits
    30.          */
    31. private int Precision(float value)
    32. {
    33.     int _precision;
    34.     if (value == .0f) return 0;
    35.     _precision = value.ToString().Length - (((int)value).ToString().Length + 1);
    36.     // Math.Round function get only precision between 0 to 15
    37.     return Mathf.Clamp(_precision, 0, 15);
    38. }
    39.  
    40. /**
    41.  * Return new float value with step calcul (and step decimal precision)
    42.  */
    43. private float Step(float value, float min, float step)
    44. {
    45.     if (step == 0) return value;
    46.     float newValue = min + Mathf.Round((value - min) / step) * step;
    47.     return (float)Math.Round(newValue, Precision(step));
    48. }
    49.  
    50. /**
    51.  * Return new integer value with step calcul
    52.  * (It's more simple ^^)
    53.  */
    54. private int Step(int value, float step)
    55. {
    56.     if (step == 0) return value;
    57.     value -= (value % (int)Math.Round(step));
    58.     return value;
    59. }
    60.  
    61. //
    62. // Methods
    63. //
    64. public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    65. {
    66.     var rangeAttribute = (RangeExAttribute)base.attribute;
    67.  
    68.     if (rangeAttribute.label != "")
    69.         label.text = rangeAttribute.label;
    70.  
    71.     switch (property.propertyType)
    72.     {
    73.         case SerializedPropertyType.Float:
    74.             float _floatValue = EditorGUI.Slider(position, label, property.floatValue, rangeAttribute.min, rangeAttribute.max);
    75.             property.floatValue = Step(_floatValue, rangeAttribute.min, rangeAttribute.step);
    76.             break;
    77.         case SerializedPropertyType.Integer:
    78.             int _intValue = EditorGUI.IntSlider(position, label, property.intValue, (int)rangeAttribute.min, (int)rangeAttribute.max);
    79.             property.intValue = Step(_intValue, rangeAttribute.step);
    80.             break;
    81.         default:
    82.             EditorGUI.LabelField(position, label.text, "Use Range with float or int.");
    83.             break;
    84.     }
    85. }
    86.     }
    87. #endif
    88.  
    Usage
    Code (CSharp):
    1.  
    2.         [RangeEx(-10.0f, 10.0f, 0.5f, "Float With 0.5 Step")]
    3.         public float CurrentFloat = .0f;
    4.  
    5.         [RangeEx(-10, 10, 2, "Integer With 2 Step")]
    6.         public int CurrentInteger = 0;
    7.  

    PS : Sorry for my bad english ^^
     
    Ranger-Ori likes this.
  10. Ranger-Ori

    Ranger-Ori

    Joined:
    Apr 29, 2010
    Posts:
    29

    This works brilliantly!
    The entire code in one file (just some small adjsutments), I called it 'RangeExtension.cs'

    Code (CSharp):
    1. using System;
    2. using UnityEditor;
    3. using UnityEngine;
    4.  
    5. [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
    6. public sealed class RangeExtension : PropertyAttribute
    7. {
    8.     public readonly float min = .0f;
    9.     public readonly float max = 100.0f;
    10.     public readonly float step = 1.0f;
    11.     public readonly string label = "";
    12.  
    13.     public RangeExtension(float min, float max, float step = 1.0f, string label = "")
    14.     {
    15.         this.min = min;
    16.         this.max = max;
    17.         this.step = step;
    18.         this.label = label;
    19.     }
    20. }
    21.  
    22. #if UNITY_EDITOR
    23. [CustomPropertyDrawer(typeof(RangeExtension))]
    24. internal sealed class RangeExDrawer : PropertyDrawer
    25. {
    26.  
    27.     /**
    28.      * Return exact precision of reel decimal
    29.      * ex :
    30.      * 0.01     = 2 digits
    31.      * 0.02001  = 5 digits
    32.      * 0.02000  = 2 digits
    33.      */
    34.     private int Precision(float value)
    35.     {
    36.         int _precision;
    37.         if (value == .0f) return 0;
    38.         _precision = value.ToString().Length - (((int)value).ToString().Length + 1);
    39.         // Math.Round function get only precision between 0 to 15
    40.         return Mathf.Clamp(_precision, 0, 15);
    41.     }
    42.  
    43.     /**
    44.      * Return new float value with step calcul (and step decimal precision)
    45.      */
    46.     private float Step(float value, float min, float step)
    47.     {
    48.         if (step == 0) return value;
    49.         float newValue = min + Mathf.Round((value - min) / step) * step;
    50.         return (float)Math.Round(newValue, Precision(step));
    51.     }
    52.  
    53.     /**
    54.      * Return new integer value with step calcul
    55.      * (It's more simple ^^)
    56.      */
    57.     private int Step(int value, float step)
    58.     {
    59.         if (step == 0) return value;
    60.         value -= (value % (int)Math.Round(step));
    61.         return value;
    62.     }
    63.  
    64.     //
    65.     // Methods
    66.     //
    67.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    68.     {
    69.         var rangeAttribute = (RangeExtension)base.attribute;
    70.  
    71.         if (rangeAttribute.label != "")
    72.             label.text = rangeAttribute.label;
    73.  
    74.         switch (property.propertyType)
    75.         {
    76.             case SerializedPropertyType.Float:
    77.                 float _floatValue = EditorGUI.Slider(position, label, property.floatValue, rangeAttribute.min, rangeAttribute.max);
    78.                 property.floatValue = Step(_floatValue, rangeAttribute.min, rangeAttribute.step);
    79.                 break;
    80.             case SerializedPropertyType.Integer:
    81.                 int _intValue = EditorGUI.IntSlider(position, label, property.intValue, (int)rangeAttribute.min, (int)rangeAttribute.max);
    82.                 property.intValue = Step(_intValue, rangeAttribute.step);
    83.                 break;
    84.             default:
    85.                 EditorGUI.LabelField(position, label.text, "Use Range with float or int.");
    86.                 break;
    87.         }
    88.     }
    89. }
    90. #endif
    91.  
     
  11. VictorHHT

    VictorHHT

    Joined:
    Dec 17, 2021
    Posts:
    3
    Still noticing some weird edge cases using int range attribute. I changed some code according to how the float attribute was constructed, here is my little improvements

    Code (CSharp):
    1. //Add a new int parameter like we did above in the float Step method to access the minimum value of the attribute
    2.     private int Step(int value, int min, int step)
    3.     {
    4.         if (step == 0) return value;
    5.         int newValue = min + (value - min) / step * step;
    6.         return newValue;
    7.     }
    8.  
    9.  
    10. //Then change the Step function call in the second switch case like this
    11. property.intValue = Step(_intValue, (int)rangeAttribute.min, (int)rangeAttribute.step);
     
  12. lokeshvt

    lokeshvt

    Joined:
    Oct 1, 2012
    Posts:
    3
    Amazing thank you so much TheWolfNL i was losing my mind over this. I used a diff tool to see what the change was and notice it was in an assignment statement of the value where the 3rd param was changed from value to property.intValue. Would u know why this fixed it though?
     
    Last edited: Jul 11, 2022
  13. VictorHHT

    VictorHHT

    Joined:
    Dec 17, 2021
    Posts:
    3
    After A LOT of trial and error, I think I finally got a complete version of this.

    Code (CSharp):
    1. using System;
    2. using UnityEditor;
    3. using UnityEngine;
    4.  
    5. // Usage Example 1 (float): [VTRangeStep(0f, 10f, 0.25f)]
    6. // Usage Example 2 (int): [VTRangeStep(1, 100, 25)]
    7. namespace Victor.Tools
    8. {
    9.     [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
    10.     public sealed class VTRangeStep : PropertyAttribute
    11.     {
    12.         internal readonly float m_min = 0f;
    13.         internal readonly float m_max = 100f;
    14.         internal readonly float m_step = 1;
    15.         // Whether a increase that is not the step is allowed (Occurs when we are reaching the end)
    16.         internal readonly bool m_allowNonStepReach = true;
    17.         internal readonly bool m_IsInt = false;
    18.  
    19.         /// <summary>
    20.         /// Allow you to increase a float value in step, make sure the type of the variable matches the the parameters
    21.         /// </summary>
    22.         /// <param name="min"></param>
    23.         /// <param name="max"></param>
    24.         /// <param name="step"></param>
    25.         /// <param name="allowNonStepReach">Whether a increase that is not the step is allowed (Occurs when we are reaching the end)</param>
    26.         public VTRangeStep(float min, float max, float step = 1f, bool allowNonStepReach = true)
    27.         {
    28.             m_min = min;
    29.             m_max = max;
    30.             m_step = step;
    31.             m_allowNonStepReach = allowNonStepReach;
    32.             m_IsInt = false;
    33.         }
    34.  
    35.         /// <summary>
    36.         /// Allow you to increase a int value in step, make sure the type of the variable matches the the parameters
    37.         /// </summary>
    38.         /// <param name="min"></param>
    39.         /// <param name="max"></param>
    40.         /// <param name="step"></param>
    41.         /// <param name="allowNonStepReach"></param>
    42.         public VTRangeStep(int min, int max, int step = 1, bool allowNonStepReach = true)
    43.         {
    44.             m_min = min;
    45.             m_max = max;
    46.             m_step = step;
    47.             m_allowNonStepReach = allowNonStepReach;
    48.             m_IsInt = true;
    49.         }
    50.     }
    51.  
    52.     #if UNITY_EDITOR
    53.     [CustomPropertyDrawer(typeof(VTRangeStep))]
    54.     internal sealed class RangeStepDrawer : PropertyDrawer
    55.     {
    56.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    57.         {
    58.             var rangeAttribute = (VTRangeStep)base.attribute;
    59.  
    60.             if (!rangeAttribute.m_IsInt)
    61.             {
    62.                 float rawFloat = EditorGUI.Slider(position, label, property.floatValue, rangeAttribute.m_min, rangeAttribute.m_max);
    63.                 property.floatValue = Step(rawFloat, rangeAttribute);
    64.             }
    65.             else
    66.             {
    67.                 int rawInt = EditorGUI.IntSlider(position, label, property.intValue, (int)rangeAttribute.m_min, (int)rangeAttribute.m_max);
    68.                 property.intValue = Step(rawInt, rangeAttribute);
    69.             }
    70.            
    71.         }
    72.  
    73.         // This is time tested and bug free!
    74.         // I stayed up late until 2:50 AM in September 23 2022 trying to get this right, relentless curiocity paid off
    75.         internal float Step(float rawValue, VTRangeStep range)
    76.         {
    77.             float f = rawValue;
    78.  
    79.             if (range.m_allowNonStepReach)
    80.             {
    81.                 // In order to ensure a reach, where the difference between rawValue and the max allowed value is less than step
    82.                 float topCap = Mathf.Floor(range.m_max / range.m_step) * range.m_step;
    83.                 float topRemaining = range.m_max - topCap;
    84.  
    85.                 // If this is the special case near the top maximum
    86.                 if (topRemaining < range.m_step && f > topCap)
    87.                 {
    88.                     f = range.m_max;
    89.                 }
    90.                 else
    91.                 {
    92.                     // Otherwise we do a regular snap
    93.                     f = Snap(f, range.m_step);
    94.                 }
    95.             }
    96.             else if(!range.m_allowNonStepReach)
    97.             {
    98.                 f = Snap(f, range.m_step);
    99.                 // Make sure the value doesn't exceed the maximum allowed range
    100.                 if (f > range.m_max)
    101.                 {
    102.                     f -= range.m_step;
    103.                 }
    104.             }
    105.  
    106.             return f;
    107.         }
    108.  
    109.         internal int Step(int rawValue, VTRangeStep range)
    110.         {
    111.             int f = rawValue;
    112.  
    113.             if (range.m_allowNonStepReach)
    114.             {
    115.                 // In order to ensure a reach, where the difference between rawValue and the max allowed value is less than step
    116.                 int topCap = (int)range.m_max / (int)range.m_step * (int)range.m_step;
    117.                 int topRemaining = (int)range.m_max - topCap;
    118.  
    119.                 // If this is the special case near the top maximum
    120.                 if (topRemaining < range.m_step && f > topCap)
    121.                 {
    122.                     f = (int)range.m_max;
    123.                 }
    124.                 else
    125.                 {
    126.                     // Otherwise we do a regular snap
    127.                     f = (int)Snap(f, range.m_step);
    128.                 }
    129.             }
    130.             else if (!range.m_allowNonStepReach)
    131.             {
    132.                 f = (int)Snap(f, range.m_step);
    133.                 // Make sure the value doesn't exceed the maximum allowed range
    134.                 if (f > range.m_max)
    135.                 {
    136.                     f -= (int)range.m_step;
    137.                 }
    138.             }
    139.  
    140.             return f;
    141.         }
    142.  
    143.         /// <summary>
    144.         /// Snap a value to a interval
    145.         /// </summary>
    146.         /// <param name="value"></param>
    147.         /// <param name="snapInterval"></param>
    148.         /// <returns></returns>
    149.         internal static float Snap(float value, float snapInterval)
    150.         {
    151.             return Mathf.Round(value / snapInterval) * snapInterval;
    152.         }
    153.     }
    154.     #endif
    155. }
     
    frumple314 likes this.
  14. frumple314

    frumple314

    Joined:
    Jun 18, 2018
    Posts:
    6
    VictorHHT that is awesome. Works great but I found some rounding errors when using this with floats. If, lets say, I try a range of 1 to 10 with a step of 1.3, the next to the last step is not 9.1 as expected but 9.0999999... So yeah some rounding errors due to floating point math I believe.

    However, I think I fixed it with the following fix on lines 82-94

    Code (CSharp):
    1.                 float topCap = Mathf.Floor(range.m_max / range.m_step) * range.m_step;
    2.                 topCap = (float)Math.Round(topCap, Precision(range.m_step));
    3.  
    4.                 float topRemaining = range.m_max - topCap;
    5.                 topRemaining - (float)Math.Round(topRemaining, Precision(range.m_step));
    6.  
    7.                 // If this is the special case near the top maximum
    8.                 if (topRemaining < range.m_step && f > topCap)
    9.                 {
    10.                     f = range.m_max;
    11.                 }
    12.                 else
    13.                 {
    14.                     // Otherwise we do a regular snap
    15.                     f = (float)Math.Round(Snap(f, range.m_step), Precision(range.m_step));
    16.                 }
    where the Precision method what ever method you like to determine the decimal precision of a value. I use the following

    Code (CSharp):
    1. internal static int Precision(float value) {
    2.     // cast us as a decimal
    3.     var val = (decimal)value;
    4.  
    5.     // convert to a string and trim any trailing zeros
    6.     var stringVal = val.ToString(CultureInfo.InvariantCulture).TrimEnd('0');
    7.  
    8.     // find the decimal point (remember that this is 0 based)
    9.     var point = stringVal.IndexOf('.');
    10.  
    11.     // if we do not have a decimal point, return 0
    12.     if (point < 0) return 0;
    13.  
    14.     // precision will be the length of our number string, minus the position of the decimal, minus 1
    15.     // for example: 1.32 will have length 4, the decimal point is at index 1, from these we calculate the
    16.     // precision to be 4 - 1 - 1 = 2 which is what we expect.
    17.     var precision = stringVal.Length - point - 1;
    18.  
    19.     // since floats have a precision of 7, clamp the precision to be no more than that
    20.     return Mathf.Min(precision, 7);
    21. }

    but there are many other (and probably more elegant) ways to do it.[/CODE]
     
    Last edited: Oct 5, 2022
    VictorHHT likes this.
  15. VictorHHT

    VictorHHT

    Joined:
    Dec 17, 2021
    Posts:
    3
    Thanks for your improvement frumple, and I really like your approach from another angle using string operations, definitely a way to solve the problem.

    The updated version and a carefully crafted MinMaxSlider Attribute(like the one you use to adjust the instruments' velocity in the GarageBand iOS) has been uploaded to my gist in the link below (more stuff in the future).

    https://gist.github.com/VictorHHT
     
    Last edited: Dec 16, 2022
    MrDizzle26 likes this.