Skip to content

Instantly share code, notes, and snippets.

@DCFApixels
Last active January 4, 2023 13:29
Show Gist options
  • Save DCFApixels/068386bce3a96618244b9dd9da16c086 to your computer and use it in GitHub Desktop.
Save DCFApixels/068386bce3a96618244b9dd9da16c086 to your computer and use it in GitHub Desktop.
Range value type for Unity. It has a custom property drawer.
using System;
using UnityEngine;
using Random = UnityEngine.Random;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace DCFApixels
{
static class RangeValueUtils
{
public static float NormalizeT(float t)
{
if (t > 1f)
return t % 1f;
if (t < 0)
return 1 + (t % 1f);
return t;
}
}
[Serializable]
public struct RangeFloat
{
public static readonly RangeFloat one = new RangeFloat(0f, 1f);
public float x;
public float length;
public float AbsLength => Mathf.Abs(length);
#region Positive/Negative
public bool IsPositive => length >= 0;
public bool IsNegative => length < 0;
#endregion
#region Constructors
public RangeFloat(float x, float length)
{
this.x = x;
this.length = length;
}
public static RangeFloat MinMax(float min, float max)
{
return new RangeFloat(min, max - min);
}
#endregion
#region Min/Max
public float Min
{
get => Mathf.Min(x, XLength);
set
{
length -= (value - x);
x = value;
}
}
public float Max
{
get => Mathf.Max(x, XLength);
set
{
float a = (XLength - value);
length -= a;
}
}
#endregion
public float XLength
{
get => x + length;
set
{
float a = (XLength - value);
length -= a;
}
}
public float Average => length / 2f + x;
public float GetRandom()
{
return Random.Range(Min, Max);
}
#region Evaluate
public float Evaluate(float t)
{
return x + length * RangeValueUtils.NormalizeT(t);
}
public float EvaluateClamp(float t)
{
return x + length * Mathf.Clamp01(t);
}
#endregion
#region MathPercent
public float MathPercent(float value)
{
return (value - Min) / Mathf.Abs(length);
}
public float MathPercentClamp(float value)
{
return Mathf.Clamp01(MathPercent(value));
}
#endregion
public float Clamp(float value)
{
return Mathf.Clamp(value, Min, Max);
}
public bool IsInRange(float value)
{
return value >= Min && value < Max;
}
public override string ToString()
{
return $"x:{x} length:{length} min:{Min} max:{Max}";
}
}
[Serializable]
public struct RangeInt
{
public static readonly RangeFloat one = new RangeFloat(0f, 1f);
public int x;
public int length;
public int AbsLength => Mathf.Abs(length);
#region Positive/Negative
public bool IsPositive => length >= 0;
public bool IsNegative => length < 0;
#endregion
#region Constructors
public RangeInt(int x, int length)
{
this.x = x;
this.length = length;
}
public static RangeInt MinMax(int min, int max)
{
return new RangeInt(min, max - min);
}
#endregion
#region Min/Max
public int Min
{
get => Mathf.Min(x, XLength);
set
{
length -= (value - x);
x = value;
}
}
public int Max
{
get => Mathf.Max(x, XLength);
set
{
int a = (XLength - value);
length -= a;
}
}
#endregion
public int XLength
{
get => x + length;
set
{
int a = (XLength - value);
length -= a;
}
}
public float Average => length / 2f + x;
public int GetRandom()
{
return Random.Range(Min, Max);
}
#region Evaluate
public float Evaluate(float t)
{
return x + length * RangeValueUtils.NormalizeT(t);
}
public float EvaluateClamp(float t)
{
return x + length * Mathf.Clamp01(t);
}
#endregion
#region MathPercent
public float MathPercent(float value)
{
return (value - Min) / (float)Mathf.Abs(length);
}
public float MathPercentClamp(float value)
{
return Mathf.Clamp01(MathPercent(value));
}
#endregion
public float Clamp(float value)
{
return Mathf.Clamp(value, Min, Max);
}
public bool IsInRange(float value)
{
return value >= Min && value < Max;
}
public override string ToString()
{
return $"x:{x} length:{length} min:{Min} max:{Max}";
}
}
public class ClampedRangeAttribute: PropertyAttribute
{
public float min;
public float max;
public ClampedRangeAttribute(float min, float max)
{
this.min = min;
this.max = max;
}
public ClampedRangeAttribute() : this(0f, 1f) { }
}
public class MinMaxRangeAttribute : PropertyAttribute
{
}
}
#if UNITY_EDITOR
namespace DCFApixels.Editors
{
[CustomPropertyDrawer(typeof(ClampedRangeAttribute), true)]
[CustomPropertyDrawer(typeof(MinMaxRangeAttribute), true)]
[CustomPropertyDrawer(typeof(RangeFloat), true)]
[CustomPropertyDrawer(typeof(RangeInt), true)]
public class RangedValueDrawer : PropertyDrawer
{
private const float SPACING = 4f;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
int indentLevel = EditorGUI.indentLevel;
float defaultlabelWidth = EditorGUIUtility.labelWidth;
SerializedProperty xProp = property.FindPropertyRelative("x");
SerializedProperty lengthProp = property.FindPropertyRelative("length");
Rect labelRect = position;
labelRect.width = defaultlabelWidth;
Rect fieldRect = position;
fieldRect.xMin = labelRect.xMax;
string minmaxTooltip;
switch (xProp.propertyType)
{
case SerializedPropertyType.Integer:
minmaxTooltip = new RangeInt(xProp.intValue, lengthProp.intValue).ToString();
break;
case SerializedPropertyType.Float:
minmaxTooltip = new RangeFloat(xProp.floatValue, lengthProp.floatValue).ToString();
break;
default:
minmaxTooltip = "error";
break;
}
label.tooltip = minmaxTooltip;
EditorGUI.LabelField(labelRect, label);
if (attribute is MinMaxRangeAttribute minMaxRangeAttribute)
{
DrawMinMaxField(fieldRect, property, minMaxRangeAttribute, xProp, lengthProp);
goto exit;
}
if (attribute is ClampedRangeAttribute clampedRangeAttribute)
{
DrawClampedRangeField(fieldRect, property, clampedRangeAttribute, xProp, lengthProp);
goto exit;
}
DrawDefaultField(fieldRect, xProp, lengthProp);
exit:;
EditorGUIUtility.labelWidth = defaultlabelWidth;
}
private void DrawDefaultField(Rect fieldRect, SerializedProperty xProp, SerializedProperty lengthProp)
{
float width = fieldRect.width / 2f - SPACING / 2f;
Rect minRect = fieldRect;
minRect.width = width;
minRect.x = fieldRect.xMin;
Rect maxRect = fieldRect;
maxRect.width = width;
maxRect.x = minRect.xMax + SPACING;
EditorGUI.indentLevel = 0;
EditorGUIUtility.labelWidth = 12f;
EditorGUI.PropertyField(minRect, xProp);
EditorGUIUtility.labelWidth = 42f;
EditorGUI.PropertyField(maxRect, lengthProp);
}
private void DrawClampedRangeField(Rect fieldRect, SerializedProperty property, ClampedRangeAttribute attribute, SerializedProperty xProp, SerializedProperty lengthProp)
{
EditorGUI.indentLevel = 0;
float rightFieldWidth = 24;
float verticalSpacing = EditorGUIUtility.standardVerticalSpacing;
if(fieldRect.width <= rightFieldWidth * 4.5f)
{
rightFieldWidth = 0;
verticalSpacing = 0;
}
Rect leftFieldRect = fieldRect;
leftFieldRect.xMax -= rightFieldWidth * 2f + verticalSpacing * 2f;
Rect rightFieldRect1 = fieldRect;
rightFieldRect1.x = leftFieldRect.xMax + verticalSpacing;
rightFieldRect1.width = rightFieldWidth;
Rect rightFieldRect2 = rightFieldRect1;
rightFieldRect2.x = rightFieldRect1.xMax + verticalSpacing;
switch (xProp.propertyType)
{
case SerializedPropertyType.Integer:
{
int min = xProp.intValue;
int max = lengthProp.intValue + min;
MinMaxSlider(leftFieldRect, ref min, ref max, (int)attribute.min, (int)attribute.max);
min = EditorGUI.IntField(rightFieldRect1, min);
max = EditorGUI.IntField(rightFieldRect2, max);
max = Mathf.Max(min, max);
xProp.intValue = min;
lengthProp.intValue = Mathf.Abs(max) - min;
property.serializedObject.ApplyModifiedProperties();
}
break;
case SerializedPropertyType.Float:
{
float min = xProp.floatValue;
float max = lengthProp.floatValue + min;
EditorGUI.MinMaxSlider(leftFieldRect, ref min, ref max, attribute.min, attribute.max);
min = EditorGUI.FloatField(rightFieldRect1, min);
max = EditorGUI.FloatField(rightFieldRect2, max);
max = Mathf.Max(min, max);
xProp.floatValue = min;
lengthProp.floatValue = Mathf.Abs(max) - min;
property.serializedObject.ApplyModifiedProperties();
}
break;
default:
GUI.Label(fieldRect, "error");
break;
};
}
private void DrawMinMaxField(Rect fieldRect, SerializedProperty property, MinMaxRangeAttribute attribute, SerializedProperty xProp, SerializedProperty lengthProp)
{
float width = fieldRect.width / 2f - SPACING / 2f;
Rect minRect = fieldRect;
minRect.width = width;
minRect.x = fieldRect.xMin;
Rect maxRect = fieldRect;
maxRect.width = width;
maxRect.x = minRect.xMax + SPACING;
EditorGUIUtility.labelWidth = 24f;
EditorGUI.indentLevel = 0;
switch (xProp.propertyType)
{
case SerializedPropertyType.Integer:
{
int min = xProp.intValue;
int max = lengthProp.intValue + min;
min = EditorGUI.IntField(minRect, min < max ? "Min" : "Max", min);
max = EditorGUI.IntField(maxRect, min < max ? "Max" : "Min", max);
xProp.intValue = min;
lengthProp.intValue = Mathf.Abs(max) - min;
property.serializedObject.ApplyModifiedProperties();
}
break;
case SerializedPropertyType.Float:
{
float min = xProp.floatValue;
float max = lengthProp.floatValue + min;
min = EditorGUI.FloatField(minRect, min < max ? "min" : "max", min);
max = EditorGUI.FloatField(maxRect, min < max ? "max" : "min", max);
xProp.floatValue = min;
lengthProp.floatValue = Mathf.Abs(max) - min;
property.serializedObject.ApplyModifiedProperties();
}
break;
default:
GUI.Label(fieldRect, "error");
break;
}
}
private void MinMaxSlider(Rect position, ref int minValue, ref int maxValue, float minLimit, float maxLimit)
{
float minValueFloat = minValue;
float maxValueFloat = maxValue;
EditorGUI.MinMaxSlider(position, ref minValueFloat, ref maxValueFloat, minLimit, maxLimit);
minValue = Mathf.RoundToInt(minValueFloat);
maxValue = Mathf.RoundToInt(maxValueFloat);
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment