Range Attribute

Is it possible to make the Range attribute slider increment by 10 instead of 1?
The following range controlled variable for example:

// Current Stage
[Range(0, 100)]
public int currentStage = 0;
1 Like

Not really, unless you make your own PropertyDrawer, but that seems like more work than it’s worth.

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).

using System;
using UnityEngine;

[AttributeUsage (AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public sealed class RangeExAttribute : PropertyAttribute
{
    public readonly int min;
    public readonly int max;
    public readonly int step;

    public RangeExAttribute (int min, int max, int step)
    {
        this.min = min;
        this.max = max;
        this.step = step;
    }
}

And the property drawer (place in an editor folder):

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer (typeof(RangeExAttribute))]
internal sealed class RangeExDrawer : PropertyDrawer
{
    private int value;

    //
    // Methods
    //
    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    {
        var rangeAttribute = (RangeExAttribute)base.attribute;

        if (property.propertyType == SerializedPropertyType.Integer)
        {
            value = EditorGUI.IntSlider (position, label, value, rangeAttribute.min, rangeAttribute.max);

            value = (value / rangeAttribute.step) * rangeAttribute.step;
            property.intValue = value;
        }
        else
        {
            EditorGUI.LabelField (position, label.text, "Use Range with float or int.");
        }
    }
}

Now you can use this in your code:

using UnityEngine;

public class NewBehaviourScript : MonoBehaviour {

    [RangeEx(0, 1000, 10)]
    public int myNumber;
}
9 Likes

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.

1 Like

There is a useless line : value = (value / rangeAttribute.step) * rangeAttribute.step;

ex: value = 4 / 2 * 2

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?

1 Like

In case you have the problem of the value resetting when triggering game mode:
Use this slightly updated property drawer:

using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer (typeof(RangeExAttribute))]
internal sealed class RangeExDrawer : PropertyDrawer
{
    private int value;
    //
    // Methods
    //
    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
    {
        var rangeAttribute = (RangeExAttribute)base.attribute;
        if (property.propertyType == SerializedPropertyType.Integer)
        {
            value = EditorGUI.IntSlider (position, label, property.intValue, rangeAttribute.min, rangeAttribute.max);
            value = (value / rangeAttribute.step) * rangeAttribute.step;
            property.intValue = value;
        }
        else
        {
            EditorGUI.LabelField (position, label.text, "Use Range with int.");
        }
    }
}
1 Like

Anyone got a MixMax style one for the Integer Slider? Writing property drawers hurts my brain XD

With a few months late …

For integer, replace line

value = (value / rangeAttribute.step) * rangeAttribute.step;

by

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.

     [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
    public sealed class RangeExAttribute : PropertyAttribute
    {
        public readonly float min = .0f;
        public readonly float max = 100.0f;
        public readonly float step = 1.0f;
        public readonly string label = "";

        public RangeExAttribute(float min, float max, float step = 1.0f, string label = "")
        {
            this.min = min;
            this.max = max;
            this.step = step;
            this.label = label;
        }
    }

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(RangeExAttribute))]
    internal sealed class RangeExDrawer : PropertyDrawer
    {

        /**
         * Return exact precision of reel decimal
         * ex :
         * 0.01     = 2 digits
         * 0.02001  = 5 digits
         * 0.02000  = 2 digits
         */
private int Precision(float value)
{
    int _precision;
    if (value == .0f) return 0;
    _precision = value.ToString().Length - (((int)value).ToString().Length + 1);
    // Math.Round function get only precision between 0 to 15
    return Mathf.Clamp(_precision, 0, 15);
}

/**
 * Return new float value with step calcul (and step decimal precision)
 */
private float Step(float value, float min, float step)
{
    if (step == 0) return value;
    float newValue = min + Mathf.Round((value - min) / step) * step;
    return (float)Math.Round(newValue, Precision(step));
}

/**
 * Return new integer value with step calcul
 * (It's more simple ^^)
 */
private int Step(int value, float step)
{
    if (step == 0) return value;
    value -= (value % (int)Math.Round(step));
    return value;
}

//
// Methods
//
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    var rangeAttribute = (RangeExAttribute)base.attribute;

    if (rangeAttribute.label != "")
        label.text = rangeAttribute.label;

    switch (property.propertyType)
    {
        case SerializedPropertyType.Float:
            float _floatValue = EditorGUI.Slider(position, label, property.floatValue, rangeAttribute.min, rangeAttribute.max);
            property.floatValue = Step(_floatValue, rangeAttribute.min, rangeAttribute.step);
            break;
        case SerializedPropertyType.Integer:
            int _intValue = EditorGUI.IntSlider(position, label, property.intValue, (int)rangeAttribute.min, (int)rangeAttribute.max);
            property.intValue = Step(_intValue, rangeAttribute.step);
            break;
        default:
            EditorGUI.LabelField(position, label.text, "Use Range with float or int.");
            break;
    }
}
    }
#endif

Usage

        [RangeEx(-10.0f, 10.0f, 0.5f, "Float With 0.5 Step")]
        public float CurrentFloat = .0f;

        [RangeEx(-10, 10, 2, "Integer With 2 Step")]
        public int CurrentInteger = 0;

PS : Sorry for my bad english ^^

1 Like

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

using System;
using UnityEditor;
using UnityEngine;

[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public sealed class RangeExtension : PropertyAttribute
{
    public readonly float min = .0f;
    public readonly float max = 100.0f;
    public readonly float step = 1.0f;
    public readonly string label = "";

    public RangeExtension(float min, float max, float step = 1.0f, string label = "")
    {
        this.min = min;
        this.max = max;
        this.step = step;
        this.label = label;
    }
}

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(RangeExtension))]
internal sealed class RangeExDrawer : PropertyDrawer
{

    /**
     * Return exact precision of reel decimal
     * ex :
     * 0.01     = 2 digits
     * 0.02001  = 5 digits
     * 0.02000  = 2 digits
     */
    private int Precision(float value)
    {
        int _precision;
        if (value == .0f) return 0;
        _precision = value.ToString().Length - (((int)value).ToString().Length + 1);
        // Math.Round function get only precision between 0 to 15
        return Mathf.Clamp(_precision, 0, 15);
    }

    /**
     * Return new float value with step calcul (and step decimal precision)
     */
    private float Step(float value, float min, float step)
    {
        if (step == 0) return value;
        float newValue = min + Mathf.Round((value - min) / step) * step;
        return (float)Math.Round(newValue, Precision(step));
    }

    /**
     * Return new integer value with step calcul
     * (It's more simple ^^)
     */
    private int Step(int value, float step)
    {
        if (step == 0) return value;
        value -= (value % (int)Math.Round(step));
        return value;
    }

    //
    // Methods
    //
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var rangeAttribute = (RangeExtension)base.attribute;

        if (rangeAttribute.label != "")
            label.text = rangeAttribute.label;

        switch (property.propertyType)
        {
            case SerializedPropertyType.Float:
                float _floatValue = EditorGUI.Slider(position, label, property.floatValue, rangeAttribute.min, rangeAttribute.max);
                property.floatValue = Step(_floatValue, rangeAttribute.min, rangeAttribute.step);
                break;
            case SerializedPropertyType.Integer:
                int _intValue = EditorGUI.IntSlider(position, label, property.intValue, (int)rangeAttribute.min, (int)rangeAttribute.max);
                property.intValue = Step(_intValue, rangeAttribute.step);
                break;
            default:
                EditorGUI.LabelField(position, label.text, "Use Range with float or int.");
                break;
        }
    }
}
#endif

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

//Add a new int parameter like we did above in the float Step method to access the minimum value of the attribute
    private int Step(int value, int min, int step)
    {
        if (step == 0) return value;
        int newValue = min + (value - min) / step * step;
        return newValue;
    }


//Then change the Step function call in the second switch case like this
property.intValue = Step(_intValue, (int)rangeAttribute.min, (int)rangeAttribute.step);

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?

After A LOT of trial and error, I think I finally got a complete version of this.

using System;
using UnityEditor;
using UnityEngine;

// Usage Example 1 (float): [VTRangeStep(0f, 10f, 0.25f)]
// Usage Example 2 (int): [VTRangeStep(1, 100, 25)]
namespace Victor.Tools
{
    [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
    public sealed class VTRangeStep : PropertyAttribute
    {
        internal readonly float m_min = 0f;
        internal readonly float m_max = 100f;
        internal readonly float m_step = 1;
        // Whether a increase that is not the step is allowed (Occurs when we are reaching the end)
        internal readonly bool m_allowNonStepReach = true;
        internal readonly bool m_IsInt = false;

        /// <summary>
        /// Allow you to increase a float value in step, make sure the type of the variable matches the the parameters
        /// </summary>
        /// <param name="min"></param>
        /// <param name="max"></param>
        /// <param name="step"></param>
        /// <param name="allowNonStepReach">Whether a increase that is not the step is allowed (Occurs when we are reaching the end)</param>
        public VTRangeStep(float min, float max, float step = 1f, bool allowNonStepReach = true)
        {
            m_min = min;
            m_max = max;
            m_step = step;
            m_allowNonStepReach = allowNonStepReach;
            m_IsInt = false;
        }

        /// <summary>
        /// Allow you to increase a int value in step, make sure the type of the variable matches the the parameters
        /// </summary>
        /// <param name="min"></param>
        /// <param name="max"></param>
        /// <param name="step"></param>
        /// <param name="allowNonStepReach"></param>
        public VTRangeStep(int min, int max, int step = 1, bool allowNonStepReach = true)
        {
            m_min = min;
            m_max = max;
            m_step = step;
            m_allowNonStepReach = allowNonStepReach;
            m_IsInt = true;
        }
    }

    #if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(VTRangeStep))]
    internal sealed class RangeStepDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            var rangeAttribute = (VTRangeStep)base.attribute;

            if (!rangeAttribute.m_IsInt)
            {
                float rawFloat = EditorGUI.Slider(position, label, property.floatValue, rangeAttribute.m_min, rangeAttribute.m_max);
                property.floatValue = Step(rawFloat, rangeAttribute);
            }
            else
            {
                int rawInt = EditorGUI.IntSlider(position, label, property.intValue, (int)rangeAttribute.m_min, (int)rangeAttribute.m_max);
                property.intValue = Step(rawInt, rangeAttribute);
            }
           
        }

        // This is time tested and bug free!
        // I stayed up late until 2:50 AM in September 23 2022 trying to get this right, relentless curiocity paid off
        internal float Step(float rawValue, VTRangeStep range)
        {
            float f = rawValue;

            if (range.m_allowNonStepReach)
            {
                // In order to ensure a reach, where the difference between rawValue and the max allowed value is less than step
                float topCap = Mathf.Floor(range.m_max / range.m_step) * range.m_step;
                float topRemaining = range.m_max - topCap;

                // If this is the special case near the top maximum
                if (topRemaining < range.m_step && f > topCap)
                {
                    f = range.m_max;
                }
                else
                {
                    // Otherwise we do a regular snap
                    f = Snap(f, range.m_step);
                }
            }
            else if(!range.m_allowNonStepReach)
            {
                f = Snap(f, range.m_step);
                // Make sure the value doesn't exceed the maximum allowed range
                if (f > range.m_max)
                {
                    f -= range.m_step;
                }
            }

            return f;
        }

        internal int Step(int rawValue, VTRangeStep range)
        {
            int f = rawValue;

            if (range.m_allowNonStepReach)
            {
                // In order to ensure a reach, where the difference between rawValue and the max allowed value is less than step
                int topCap = (int)range.m_max / (int)range.m_step * (int)range.m_step;
                int topRemaining = (int)range.m_max - topCap;

                // If this is the special case near the top maximum
                if (topRemaining < range.m_step && f > topCap)
                {
                    f = (int)range.m_max;
                }
                else
                {
                    // Otherwise we do a regular snap
                    f = (int)Snap(f, range.m_step);
                }
            }
            else if (!range.m_allowNonStepReach)
            {
                f = (int)Snap(f, range.m_step);
                // Make sure the value doesn't exceed the maximum allowed range
                if (f > range.m_max)
                {
                    f -= (int)range.m_step;
                }
            }

            return f;
        }

        /// <summary>
        /// Snap a value to a interval
        /// </summary>
        /// <param name="value"></param>
        /// <param name="snapInterval"></param>
        /// <returns></returns>
        internal static float Snap(float value, float snapInterval)
        {
            return Mathf.Round(value / snapInterval) * snapInterval;
        }
    }
    #endif
}
1 Like

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

                float topCap = Mathf.Floor(range.m_max / range.m_step) * range.m_step;
                topCap = (float)Math.Round(topCap, Precision(range.m_step));

                float topRemaining = range.m_max - topCap;
                topRemaining - (float)Math.Round(topRemaining, Precision(range.m_step));

                // If this is the special case near the top maximum
                if (topRemaining < range.m_step && f > topCap)
                {
                    f = range.m_max;
                }
                else
                {
                    // Otherwise we do a regular snap
                    f = (float)Math.Round(Snap(f, range.m_step), Precision(range.m_step));
                }

where the Precision method what ever method you like to determine the decimal precision of a value. I use the following

internal static int Precision(float value) {
    // cast us as a decimal
    var val = (decimal)value;
 
    // convert to a string and trim any trailing zeros
    var stringVal = val.ToString(CultureInfo.InvariantCulture).TrimEnd('0');
 
    // find the decimal point (remember that this is 0 based)
    var point = stringVal.IndexOf('.');
 
    // if we do not have a decimal point, return 0
    if (point < 0) return 0;
 
    // precision will be the length of our number string, minus the position of the decimal, minus 1
    // for example: 1.32 will have length 4, the decimal point is at index 1, from these we calculate the
    // precision to be 4 - 1 - 1 = 2 which is what we expect.
    var precision = stringVal.Length - point - 1;

    // since floats have a precision of 7, clamp the precision to be no more than that
    return Mathf.Min(precision, 7);
}

but there are many other (and probably more elegant) ways to do it.[/CODE]

1 Like

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

1 Like