-
-
Save frarees/9791517 to your computer and use it in GitHub Desktop.
// https://frarees.github.io/default-gist-license | |
using System; | |
using UnityEngine; | |
[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)] | |
public class MinMaxSliderAttribute : PropertyAttribute | |
{ | |
public float Min { get; set; } | |
public float Max { get; set; } | |
public bool DataFields { get; set; } = true; | |
public bool FlexibleFields { get; set; } = true; | |
public bool Bound { get; set; } = true; | |
public bool Round { get; set; } = true; | |
public MinMaxSliderAttribute() : this(0, 1) | |
{ | |
} | |
public MinMaxSliderAttribute(float min, float max) | |
{ | |
Min = min; | |
Max = max; | |
} | |
} |
// https://frarees.github.io/default-gist-license | |
using UnityEngine; | |
using UnityEditor; | |
[CustomPropertyDrawer(typeof(MinMaxSliderAttribute))] | |
internal class MinMaxSliderDrawer : PropertyDrawer | |
{ | |
private const string kVectorMinName = "x"; | |
private const string kVectorMaxName = "y"; | |
private const float kFloatFieldWidth = 16f; | |
private const float kSpacing = 2f; | |
private const float kRoundingValue = 100f; | |
private static readonly int controlHash = "Foldout".GetHashCode(); | |
private static readonly GUIContent unsupported = EditorGUIUtility.TrTextContent("Unsupported field type"); | |
private bool pressed; | |
private float pressedMin; | |
private float pressedMax; | |
private float Round(float value, float roundingValue) | |
{ | |
return roundingValue == 0 ? value : Mathf.Round(value * roundingValue) / roundingValue; | |
} | |
private float FlexibleFloatFieldWidth(float min, float max) | |
{ | |
var n = Mathf.Max(Mathf.Abs(min), Mathf.Abs(max)); | |
return 14f + (Mathf.Floor(Mathf.Log10(Mathf.Abs(n)) + 1) * 2.5f); | |
} | |
private void SetVectorValue(SerializedProperty property, ref float min, ref float max, bool round) | |
{ | |
if (!pressed || (pressed && !Mathf.Approximately(min, pressedMin))) | |
{ | |
using (var x = property.FindPropertyRelative(kVectorMinName)) | |
{ | |
SetValue(x, ref min, round); | |
} | |
} | |
if (!pressed || (pressed && !Mathf.Approximately(max, pressedMax))) | |
{ | |
using (var y = property.FindPropertyRelative(kVectorMaxName)) | |
{ | |
SetValue(y, ref max, round); | |
} | |
} | |
} | |
private void SetValue(SerializedProperty property, ref float v, bool round) | |
{ | |
switch (property.propertyType) | |
{ | |
case SerializedPropertyType.Float: | |
{ | |
if (round) | |
{ | |
v = Round(v, kRoundingValue); | |
} | |
property.floatValue = v; | |
} | |
break; | |
case SerializedPropertyType.Integer: | |
{ | |
property.intValue = Mathf.RoundToInt(v); | |
} | |
break; | |
default: | |
break; | |
} | |
} | |
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) | |
{ | |
float min, max; | |
label = EditorGUI.BeginProperty(position, label, property); | |
switch (property.propertyType) | |
{ | |
case SerializedPropertyType.Vector2: | |
{ | |
var v = property.vector2Value; | |
min = v.x; | |
max = v.y; | |
} | |
break; | |
case SerializedPropertyType.Vector2Int: | |
{ | |
var v = property.vector2IntValue; | |
min = v.x; | |
max = v.y; | |
} | |
break; | |
default: | |
EditorGUI.LabelField(position, label, unsupported); | |
return; | |
} | |
var attr = attribute as MinMaxSliderAttribute; | |
float ppp = EditorGUIUtility.pixelsPerPoint; | |
float spacing = kSpacing * ppp; | |
float fieldWidth = ppp * (attr.DataFields && attr.FlexibleFields ? | |
FlexibleFloatFieldWidth(attr.Min, attr.Max) : | |
kFloatFieldWidth); | |
var indent = EditorGUI.indentLevel; | |
int id = GUIUtility.GetControlID(controlHash, FocusType.Keyboard, position); | |
var r = EditorGUI.PrefixLabel(position, id, label); | |
Rect sliderPos = r; | |
if (attr.DataFields) | |
{ | |
sliderPos.x += fieldWidth + spacing; | |
sliderPos.width -= (fieldWidth + spacing) * 2; | |
} | |
if (Event.current.type == EventType.MouseDown && | |
sliderPos.Contains(Event.current.mousePosition)) | |
{ | |
pressed = true; | |
min = Mathf.Clamp(min, attr.Min, attr.Max); | |
max = Mathf.Clamp(max, attr.Min, attr.Max); | |
pressedMin = min; | |
pressedMax = max; | |
SetVectorValue(property, ref min, ref max, attr.Round); | |
GUIUtility.keyboardControl = 0; // TODO keep focus but stop editing | |
} | |
if (pressed && Event.current.type == EventType.MouseUp) | |
{ | |
if (attr.Round) | |
{ | |
SetVectorValue(property, ref min, ref max, true); | |
} | |
pressed = false; | |
} | |
EditorGUI.BeginChangeCheck(); | |
EditorGUI.indentLevel = 0; | |
EditorGUI.MinMaxSlider(sliderPos, ref min, ref max, attr.Min, attr.Max); | |
EditorGUI.indentLevel = indent; | |
if (EditorGUI.EndChangeCheck()) | |
{ | |
SetVectorValue(property, ref min, ref max, false); | |
} | |
if (attr.DataFields) | |
{ | |
Rect minPos = r; | |
minPos.width = fieldWidth; | |
var vectorMinProp = property.FindPropertyRelative(kVectorMinName); | |
EditorGUI.showMixedValue = vectorMinProp.hasMultipleDifferentValues; | |
EditorGUI.BeginChangeCheck(); | |
EditorGUI.indentLevel = 0; | |
min = EditorGUI.DelayedFloatField(minPos, min); | |
EditorGUI.indentLevel = indent; | |
if (EditorGUI.EndChangeCheck()) | |
{ | |
if (attr.Bound) | |
{ | |
min = Mathf.Max(min, attr.Min); | |
min = Mathf.Min(min, max); | |
} | |
SetVectorValue(property, ref min, ref max, attr.Round); | |
} | |
vectorMinProp.Dispose(); | |
Rect maxPos = position; | |
maxPos.x += maxPos.width - fieldWidth; | |
maxPos.width = fieldWidth; | |
var vectorMaxProp = property.FindPropertyRelative(kVectorMaxName); | |
EditorGUI.showMixedValue = vectorMaxProp.hasMultipleDifferentValues; | |
EditorGUI.BeginChangeCheck(); | |
EditorGUI.indentLevel = 0; | |
max = EditorGUI.DelayedFloatField(maxPos, max); | |
EditorGUI.indentLevel = indent; | |
if (EditorGUI.EndChangeCheck()) | |
{ | |
if (attr.Bound) | |
{ | |
max = Mathf.Min(max, attr.Max); | |
max = Mathf.Max(max, min); | |
} | |
SetVectorValue(property, ref min, ref max, attr.Round); | |
} | |
vectorMaxProp.Dispose(); | |
EditorGUI.showMixedValue = false; | |
} | |
EditorGUI.EndProperty(); | |
} | |
} |
I am new to property drawers. What is the usage of this?
Thanks, very useful
Thanks for this script!
Change MinMaxSliderDrawer.cs line 15 to EditorGUI.MinMaxSlider(position, label, ref min, ref max, attr.min, attr.max);
to fix deprecated warning in later Unity versions (I'm using 5.5).
Hello everyone! Thanks for giving this little snippet a try. I didn't know this would draw any attention at all... but as it did, I felt it could use a little update. I've updated the files with a bunch of improvements:
- Changed coding conventions to fit Unity's
- Inspired by @Vilyx, there's now support for float fields
- Support for mixed values
- Fixed issue when the field wasn't a Vector2
- Proper AttributeUsage
- Default constructor for normalized ranges
- Updated deprecated code, thanks @wilderic
- Tested on Unity 2018
MinMaxSlider Property Drawer Usage
- Place
MinMaxSliderDrawer.cs
inside an Editor folder (e.g.Assets/Editor
,Assets/Third Parties/Editor
, ...) - Place
MinMaxSliderAttribute.cs
inside a non-editor folder (e.g.Assets/
,Assets/Scripts
, ...) - Mark a
Vector2
field on your MonoBehaviour or serializable class with[MinMaxSlider]
(for 0-1 range) or[MinMaxSlider(<min>, <max>)]
(where<min>
and<max>
arefloat
values) - Enjoy your new handsome property drawer!
A big thank you!
Great script, thanks very much. It had some problems with indent level when used within foldouts, I hacked a little fix in for that and posted it to a fork if anyone else needs it.
Hi @FishOfTheNorthStar, thanks for your contribution. I'm glad you're finding this little script useful.
I've taken a look at how I could best get around indentation, and worked my way through it. Could you grab the updated gist and see if it works OK for you now?
I took some extra time to improve several aspects of the script:
- Handle indentation
- Consistent rect layouting
- Consistent value fields on retina displays
- Consistency checks on defined bounds
- Make sure min <= max
- Round floats down to 2 decimal places (consist with how Unity handles float fields)
- Support for Vector2Int
- Clearer error when using on unsupported types
- Tried to stick to the eighty column rule
Added license information.
Added an unbounded
property to allow for arbitrary setting of values, unbounded by min/max, in my branch. Thanks for this!
Hey everyone! Extra round of maintenance on this property drawer. This time I didn't keep a formal changelog, sorry about that. But there was a lot going on for this update, emphasizing stability and flexibility. Also, added support for unbounded sliders based on @hyakugei's idea.
Overall, with this update the attribute is way more flexible, providing a number of parameters to fine-tune your MinMaxSlider needs:
DataFields
: Should it draw Min and Max input fields?FlexibleFields
: Should the data fields adapt to how long numbers can get? (e.g. [0, 9] shorter number representation than [10, 100])Bound
: Are the data fields bound to [Min, Max]? Or are users able to override these with custom values?Round
: Should it round to two decimals?
Tested on Unity 2020.3.5f1.
To support Prefab, add MinMaxSliderDrawer.cs
to line 58:
EditorGUI.BeginProperty(position, label, property);
And after line 172:
EditorGUI.EndProperty();
This will allow you to track changes to the prefab.
There is also a bug in the code when editing multiple GameObjects. For example, by changing min, max will become the same for all GameObjects.
To fix this bug you need to set a new value in lines 147 and 168 with prop.floatValue = value.
Approximately like this:
vectorMinProp.floatValue = min;
Hey @viruseg thanks for the suggestions.
Turns out I had a big rework commited locally since last year. Sadly I didn't get to document the changes, so I will just go ahead and publish it. I think I addressed the mixed value behaviour too, but give it a go and let me know if that's the case.
I've also encapsulated the drawer in a BeginProperty/EndProperty scope 👍
Thank you for the source code. It saved me a lot of time. Fixing these bugs will help everyone else who gets here via google.
The bug with multiple selected GameObjects still remains. For example, by changing min, max will become the same for all GameObjects.
In lines 149 and 170 you need to save the value differently. Something like this:
private static void SavePropertyValue(SerializedProperty property, float value)
{
switch (property.propertyType)
{
case SerializedPropertyType.Float:
property.floatValue = value;
break;
case SerializedPropertyType.Integer:
property.intValue = (int) Math.Round(value, 0);
break;
}
}
And there is a bug with negative values for the integer slider. When using the slider, the values are rounded to the wrong side.
[MinMaxSlider(-5, 5)] public Vector2Int test;
Line 47 should be replaced with this
property.vector2IntValue = new Vector2Int((int) Math.Round(min, 0), (int) Math.Round(max, 0));
there is a bug with negative values for the integer slider.
Nice, I'll get a fix.
The bug with multiple selected GameObjects still remains. For example, by changing min, max will become the same for all GameObjects.
Not sure how far we can go while using EditorGUI.MinMaxSlider. There's no way (that I've found) to tell apart when you're grabbing just one handle. In the past I've tried to store the min and max values on press, and compare every edit to see which value haven't changed, so that we edit only one. But both values can get modified. I feel this is an issue with EditorGUI.MinMaxSlider.
For example, look at the max value on this slider, while I grab the min handle:
EDIT: actually, I'm doing some tests now, and comparison through Mathf.Approximately seems to do just fine!
Pushed a fix, give it a go
Platform: Linux
The float fields are too small again in Unity 2021.2.12f1, even with flexible width.
The calculation is done in FlexibleFloatFieldWidth and makes sense, but the coefficients are apparently too small. I'm not even on hi-dpi so I don't see why the pixel units scale would have changed...
In addition, while Vector2Int loses numbers after 3 digits, it happens even earlier with Vector2 since we have 2 decimals (at least).
While evaluation the number of decimals may be hard, we could at least at space for 3 characters when using Vector2, and even more when Round = false... I'll try something, but I'm surprised your GIF from Mar 4 looks so good if you're also using a recent version of Unity!
If it's an OS thing and we can prove a difference between Linux, OSX and WIndows with the same code, then I'll send a bug report to Unity about this.
OK, so this is my revised formula:
private float FlexibleFloatFieldWidth(float min, float max, bool hasDecimals)
{
var n = Mathf.Max(Mathf.Abs(min), Mathf.Abs(max));
float floatFieldWidth = 14f + (Mathf.Floor(Mathf.Log10(Mathf.Abs(n)) + 1) * 8f);
if (hasDecimals)
{
floatFieldWidth += 18f;
}
return floatFieldWidth;
}
8f seems a good estimation of 1 character's max width (I'd rather pass an official constant of max character width, but I don't know where to get that. Note that "1" is thinner, but other digits are about the same width I think.
EDIT: It also takes the possible minus sign into account (if attr.Min >= 0 and Bound = true, you may reduce width a little as there will be no sign).
Then, I added 18f for when the number has decimals, but you can tune that. For instance, you could pass an extra parameter round = attr.Round and if it is false, add even more space to see 4-5 decimals. Unity itself gives a lot of room for floats, so you could copy that.
Finally, change the call to that method to:
FlexibleFloatFieldWidth(attr.Min, attr.Max,
property.propertyType == SerializedPropertyType.Vector2)
so it detects when decimals can be present.
Here is the result:
Nice