Search code examples
c#unity-game-enginerangeclamp

Logic for range sliders: min(min, max) works but max(min, max) doesn't


I have the following code:

public class Test : UnityEngine.MonoBehaviour
{
    [Range(0.0f, 1.0f)] // draws a slider restricted to 0.0 <-> 1.0 range in UI
    public float RangeMin = 0.0f;

    [Range(0.0f, 1.0f)] // draws a slider restricted to 0.0 <-> 1.0 range in UI
    public float RangeMax = 1.0f;

    private void OnValidate() // called at every update in UI to validate/coerce
    {
        RangeMin = math.min(RangeMin, RangeMax);
        RangeMax = math.max(RangeMin, RangeMax);
    }
}

Currently, it does the following:

Changing minimum does never influence maximum: (desired behavior)

enter image description here

Changing maximum does influence minimum: (unwanted behavior)

enter image description here

That very simple piece of code works for minimum slider but not for maximum slider.

Note, please don't suggest using EditorGUI.MinMaxSlider as it doesn't show the values:

enter image description here


Solution

  • Your problem is the order:

    private void OnValidate() // called at every update in UI to validate/coerce
    {
        RangeMin = math.min(RangeMin, RangeMax);
        RangeMax = math.max(RangeMin, RangeMax);
    }
    

    it "works" for the RangeMin because you immediately check and limit it in the moment you changed it.

    However, while changing RangeMax you already influence immediately the RangeMin, before it gets a chance to limit the RangeMax!

    As suggested you should check which of the two values you are currently changing e.g. like

    [HideInInspector] private float lastMin;
    [HideInInspector] private float lastMax;
    
    private void OnValidate() 
    {
        if(!Mathf.Approximately(lastMin, RangeMin))
        {
            RangeMin = Mathf.Min(RangeMin, RangeMax);
            lastMin = RangeMin;
        }
    
        if(!Mathf.Approximately(lastMax, RangeMax))
        {
            RangeMax = Mathf.Max(RangeMin, RangeMax);
            lastMax = RangeMax;
        }
    }
    

    Another alternative also already mentioned in the comments is using local variables to store the clamped values but wait with the assignment until all values are finished clamping like e.g.

    private void OnValidate() 
    {     
        var newMin = Mathf.Min(RangeMin, RangeMax);        
        var newMax = Mathf.Max(RangeMin, RangeMax);
    
        RangeMin = newMin;
        RangeMax = newMax;
    }
    

    Alternatively back to the

    Note, please don't suggest using EditorGUI.MinMaxSlider as it doesn't show the values.

    I guess you could simply make it like it was already done by Naughty Attributes

    namespace NaughtyAttributes
    {
        [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
        public class MinMaxSliderAttribute : DrawerAttribute
        {
            public float MinValue { get; private set; }
            public float MaxValue { get; private set; }
    
            public MinMaxSliderAttribute(float minValue, float maxValue)
            {
                MinValue = minValue;
                MaxValue = maxValue;
            }
        }
    }
    

    And the drawer

    namespace NaughtyAttributes.Editor
    {
        [CustomPropertyDrawer(typeof(MinMaxSliderAttribute))]
        public class MinMaxSliderPropertyDrawer : PropertyDrawerBase
        {
            protected override float GetPropertyHeight_Internal(SerializedProperty property, GUIContent label)
            {
                return (property.propertyType == SerializedPropertyType.Vector2)
                    ? GetPropertyHeight(property)
                    : GetPropertyHeight(property) + GetHelpBoxHeight();
            }
    
            protected override void OnGUI_Internal(Rect rect, SerializedProperty property, GUIContent label)
            {
                EditorGUI.BeginProperty(rect, label, property);
    
                MinMaxSliderAttribute minMaxSliderAttribute = (MinMaxSliderAttribute)attribute;
    
                if (property.propertyType == SerializedPropertyType.Vector2)
                {
                    EditorGUI.BeginProperty(rect, label, property);
    
                    float indentLength = NaughtyEditorGUI.GetIndentLength(rect);
                    float labelWidth = EditorGUIUtility.labelWidth + NaughtyEditorGUI.HorizontalSpacing;
                    float floatFieldWidth = EditorGUIUtility.fieldWidth;
                    float sliderWidth = rect.width - labelWidth - 2.0f * floatFieldWidth;
                    float sliderPadding = 5.0f;
    
                    Rect labelRect = new Rect(
                        rect.x,
                        rect.y,
                        labelWidth,
                        rect.height);
    
                    Rect sliderRect = new Rect(
                        rect.x + labelWidth + floatFieldWidth + sliderPadding - indentLength,
                        rect.y,
                        sliderWidth - 2.0f * sliderPadding + indentLength,
                        rect.height);
    
                    Rect minFloatFieldRect = new Rect(
                        rect.x + labelWidth - indentLength,
                        rect.y,
                        floatFieldWidth + indentLength,
                        rect.height);
    
                    Rect maxFloatFieldRect = new Rect(
                        rect.x + labelWidth + floatFieldWidth + sliderWidth - indentLength,
                        rect.y,
                        floatFieldWidth + indentLength,
                        rect.height);
    
                    // Draw the label
                    EditorGUI.LabelField(labelRect, label.text);
    
                    // Draw the slider
                    EditorGUI.BeginChangeCheck();
    
                    Vector2 sliderValue = property.vector2Value;
                    EditorGUI.MinMaxSlider(sliderRect, ref sliderValue.x, ref sliderValue.y, minMaxSliderAttribute.MinValue, minMaxSliderAttribute.MaxValue);
    
                    sliderValue.x = EditorGUI.FloatField(minFloatFieldRect, sliderValue.x);
                    sliderValue.x = Mathf.Clamp(sliderValue.x, minMaxSliderAttribute.MinValue, Mathf.Min(minMaxSliderAttribute.MaxValue, sliderValue.y));
    
                    sliderValue.y = EditorGUI.FloatField(maxFloatFieldRect, sliderValue.y);
                    sliderValue.y = Mathf.Clamp(sliderValue.y, Mathf.Max(minMaxSliderAttribute.MinValue, sliderValue.x), minMaxSliderAttribute.MaxValue);
    
                    if (EditorGUI.EndChangeCheck())
                    {
                        property.vector2Value = sliderValue;
                    }
    
                    EditorGUI.EndProperty();
                }
                else
                {
                    string message = minMaxSliderAttribute.GetType().Name + " can be used only on Vector2 fields";
                    DrawDefaultPropertyAndHelpBox(rect, property, message, MessageType.Warning);
                }
    
                EditorGUI.EndProperty();
            }
        }
    }
    

    which in the end looks like

    [SerializeField] [MinMaxSlider(0f; 100f)] private float _minMaxSlider;
    

    enter image description here

    Now before copying that code, note that Naughty Attributes Package is available in the Unity Asset Store for free and has a lot more nice enhancements for the editor (ReorderableList, Button, ShowIf, etc) ;)