Skip to content

Instantly share code, notes, and snippets.

@deebrol
Last active May 31, 2018 18:18
Show Gist options
  • Save deebrol/2067237647b2f228becf1f871864d526 to your computer and use it in GitHub Desktop.
Save deebrol/2067237647b2f228becf1f871864d526 to your computer and use it in GitHub Desktop.
Range with custom intervals – PropertyDrawer
using System;
namespace Helpers
{
public static class FloatHelper
{
/// <summary>
/// Convert the number to the nearest value of a given multiple.
/// Function from StackOverflow response <see href="https://stackoverflow.com/a/34444056">HERE</see>
/// </summary>
/// <param name="number">Number to convert</param>
/// <param name="multiple">Multiple for intervals</param>
/// <param name="min">Min number to avoid wrong numbers</param>
/// <param name="max">Max number to avoid wrong numbers</param>
public static float NearestRound(this float number, float multiple, float? min = null, float? max = null)
{
float val = number;
if (multiple == 0)
return val;
if (multiple < 1)
{
float i = (float) Math.Floor(number);
float x2 = i;
while ((x2 += multiple) < number){}
float x1 = x2 - multiple;
val = (Math.Abs(number - x1) < Math.Abs(number - x2)) ? x1 : x2;
}
else
{
val = (float) Math.Round(number / multiple, MidpointRounding.AwayFromZero) * multiple;
}
if (min != null && val < min)
val += multiple;
else if (max != null && val > max)
val -= multiple;
return val;
}
/// <summary>
/// Convert the number to the nearest value of a given multiple adjusting the return precision
/// </summary>
/// <param name="number">Number to convert</param>
/// <param name="min">Min number to avoid wrong numbers</param>
/// <param name="max">Max number to avoid wrong numbers</param>
/// <param name="multiple">Multiple for intervals</param>
/// <param name="floatPrecision">Number of decimals</param>
/// <param name="precisionMethod">Method to round the float</param>
public static float AdjustIntervalAndPrecision(this float number, float multiple, float? min = null, float? max = null, float floatPrecision = 2,
PrecisionMethod precisionMethod = PrecisionMethod.Round)
{
float val = number.NearestRound(multiple, min, max);
double mult = Math.Pow(10, floatPrecision);
if (precisionMethod == PrecisionMethod.Round)
val = (float) (Math.Round(mult * val) / mult);
else if (precisionMethod == PrecisionMethod.Truncate)
val = (float) (Math.Truncate(mult * val) / mult);
return val;
}
}
}
// DONT PUT IN EDITOR FOLDER
using System;
using UnityEngine;
/// <summary>
/// <para>Attribute used to make a float or int variable in a script be restricted to a specific range with a interval change</para>
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class IntervalRangeAttribute : PropertyAttribute
{
public readonly float min;
public readonly string minString;
public readonly float max;
public readonly string maxString;
public readonly float interval;
public readonly string intervalString;
public readonly float floatPrecision;
public readonly PrecisionMethod precisionMethod;
/// <summary>
/// <para>Attribute used to make a float or int variable in a script be restricted to a specific range.</para>
/// </summary>
/// <param name="min">The minimum allowed value.</param>
/// <param name="max">The maximum allowed value.</param>
/// <param name="interval">The interval between two successive values.</param>
/// <param name="floatPrecision">Number of decimals.</param>
/// <param name="precisionMethod">Choose between round or truncate.</param>
public IntervalRangeAttribute(float min, float max, float interval = 0, int floatPrecision = 2, PrecisionMethod precisionMethod = PrecisionMethod.Round)
{
this.min = min;
this.max = max;
this.interval = interval;
this.floatPrecision = floatPrecision;
this.precisionMethod = precisionMethod;
}
/// <summary>
/// <para>Attribute used to make a float or int variable in a script be restricted to a specific range.</para>
/// </summary>
/// <param name="minString">The minimum allowed value.</param>
/// <param name="max">The maximum allowed value.</param>
/// <param name="interval">The interval between two successive values.</param>
/// <param name="floatPrecision">Number of decimals.</param>
/// <param name="precisionMethod">Choose between round or truncate.</param>
public IntervalRangeAttribute(string minString, float max, float interval = 0, int floatPrecision = 2, PrecisionMethod precisionMethod = PrecisionMethod.Round)
: this (0, max, interval, floatPrecision, precisionMethod)
{
this.minString = minString;
}
/// <summary>
/// <para>Attribute used to make a float or int variable in a script be restricted to a specific range.</para>
/// </summary>
/// <param name="min">The minimum allowed value.</param>
/// <param name="maxString">The maximum allowed value.</param>
/// <param name="interval">The interval between two successive values.</param>
/// <param name="floatPrecision">Number of decimals.</param>
/// <param name="precisionMethod">Choose between round or truncate.</param>
public IntervalRangeAttribute(float min, string maxString, float interval = 0, int floatPrecision = 2, PrecisionMethod precisionMethod = PrecisionMethod.Round)
: this (min, 0, interval, floatPrecision, precisionMethod)
{
this.maxString = maxString;
}
/// <summary>
/// <para>Attribute used to make a float or int variable in a script be restricted to a specific range.</para>
/// </summary>
/// <param name="min">The minimum allowed value.</param>
/// <param name="max">The maximum allowed value.</param>
/// <param name="intervalString">The interval between two successive values.</param>
/// <param name="floatPrecision">Number of decimals.</param>
/// <param name="precisionMethod">Choose between round or truncate.</param>
public IntervalRangeAttribute(float min, float max, string intervalString, int floatPrecision = 2, PrecisionMethod precisionMethod = PrecisionMethod.Round)
: this (min, max, 0, floatPrecision, precisionMethod)
{
this.intervalString = intervalString;
}
/// <summary>
/// <para>Attribute used to make a float or int variable in a script be restricted to a specific range.</para>
/// </summary>
/// <param name="minString">The minimum allowed value.</param>
/// <param name="maxString">The maximum allowed value.</param>
/// <param name="interval">The interval between two successive values.</param>
/// <param name="floatPrecision">Number of decimals.</param>
/// <param name="precisionMethod">Choose between round or truncate.</param>
public IntervalRangeAttribute(string minString, string maxString, float interval = 0, int floatPrecision = 2, PrecisionMethod precisionMethod = PrecisionMethod.Round)
: this (0, 0, interval, floatPrecision, precisionMethod)
{
this.minString = minString;
this.maxString = maxString;
}
/// <summary>
/// <para>Attribute used to make a float or int variable in a script be restricted to a specific range.</para>
/// </summary>
/// <param name="min">The minimum allowed value.</param>
/// <param name="maxString">The maximum allowed value.</param>
/// <param name="intervalString">The interval between two successive values.</param>
/// <param name="floatPrecision">Number of decimals.</param>
/// <param name="precisionMethod">Choose between round or truncate.</param>
public IntervalRangeAttribute(float min, string maxString, string intervalString, int floatPrecision = 2, PrecisionMethod precisionMethod = PrecisionMethod.Round)
: this (min, 0, 0, floatPrecision, precisionMethod)
{
this.maxString = maxString;
this.intervalString = intervalString;
}
/// <summary>
/// <para>Attribute used to make a float or int variable in a script be restricted to a specific range.</para>
/// </summary>
/// <param name="minString">The minimum allowed value.</param>
/// <param name="max">The maximum allowed value.</param>
/// <param name="intervalString">The interval between two successive values.</param>
/// <param name="floatPrecision">Number of decimals.</param>
/// <param name="precisionMethod">Choose between round or truncate.</param>
public IntervalRangeAttribute(string minString, float max, string intervalString, int floatPrecision = 2, PrecisionMethod precisionMethod = PrecisionMethod.Round)
: this (0, max, 0, floatPrecision, precisionMethod)
{
this.minString = minString;
this.intervalString = intervalString;
}
/// <summary>
/// <para>Attribute used to make a float or int variable in a script be restricted to a specific range.</para>
/// </summary>
/// <param name="minString">The minimum allowed value.</param>
/// <param name="maxString">The maximum allowed value.</param>
/// <param name="intervalString">The interval between two successive values.</param>
/// <param name="floatPrecision">Number of decimals.</param>
/// <param name="precisionMethod">Choose between round or truncate.</param>
public IntervalRangeAttribute(string minString, string maxString, string intervalString, int floatPrecision = 2, PrecisionMethod precisionMethod = PrecisionMethod.Round)
: this (0, 0, 0, floatPrecision, precisionMethod)
{
this.minString = minString;
this.maxString = maxString;
this.intervalString = intervalString;
}
}
// PUT IN EDITOR FOLDER
using UnityEditor;
using UnityEngine;
using Helpers;
[CustomPropertyDrawer(typeof(IntervalRangeAttribute))]
internal sealed class IntervalRangeDrawer : PropertyDrawer
{
private bool error;
private string errorMessage;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
IntervalRangeAttribute attribute = (IntervalRangeAttribute) this.attribute;
error = false;
errorMessage = "";
// Check the Field type. Range only works with Float and Integer.
if (property.propertyType == SerializedPropertyType.Float)
{
// We get the values passed to the attribute
float minValue = attribute.min;
float maxValue = attribute.max;
float intervalValue = attribute.interval;
// Or check and assign if the values are linked to other fields
GetValue(ref minValue, property, attribute.minString);
GetValue(ref maxValue, property, attribute.maxString);
GetValue(ref intervalValue, property, attribute.intervalString);
if (!error)
{
// Block to paint the slider
label = EditorGUI.BeginProperty(position, label, property);
EditorGUI.BeginChangeCheck();
float num = EditorGUI.Slider(position, label, property.floatValue, minValue, maxValue);
// VERY IMPORTANT LINE: We need to adjust the output to our interval
num = num.AdjustIntervalAndPrecision(intervalValue, minValue, maxValue, attribute.floatPrecision, attribute.precisionMethod);
if (EditorGUI.EndChangeCheck())
property.floatValue = num;
EditorGUI.EndProperty();
}
}
else if (property.propertyType == SerializedPropertyType.Integer)
{
// We get the values passed to the attribute
int minValue = (int)attribute.min;
int maxValue = (int)attribute.max;
float intervalValue = attribute.interval;
// Or check and assign if the values are linked to other fields
GetValue(ref minValue, property, attribute.minString);
GetValue(ref maxValue, property, attribute.maxString);
GetValue(ref intervalValue, property, attribute.intervalString);
if (!error)
{
// Block to paint the slider
label = EditorGUI.BeginProperty(position, label, property);
EditorGUI.BeginChangeCheck();
int num = EditorGUI.IntSlider(position, label, property.intValue, minValue, maxValue);
// VERY IMPORTANT LINE: We need to adjust the output to our interval
num = num.NearestRound(intervalValue, minValue, maxValue);
if (EditorGUI.EndChangeCheck())
property.intValue = num;
EditorGUI.EndProperty();
}
}
else
{
error = true;
errorMessage = "Use Range with float or int.";
}
if (error)
EditorGUI.LabelField(position, label.text, errorMessage);
}
/// <summary>
/// Assign to val the value of the field named with propertyName if is valid
/// </summary>
private void GetValue(ref int val, SerializedProperty property, string propertyName)
{
if (propertyName == null)
return;
SerializedProperty p = property.serializedObject.FindProperty(propertyName);
if (p != null)
{
switch (p.propertyType)
{
case SerializedPropertyType.Float:
error = true;
errorMessage = "Float property params not valid for int properties.";
break;
case SerializedPropertyType.Integer:
val = p.intValue;
break;
default:
error = true;
errorMessage = "Use Range with float or int param values.";
break;
}
}
else
{
error = true;
errorMessage = "Invalid param name or privated property.";
}
}
/// <summary>
/// Assign to val the value of the field named with propertyName if is valid
/// </summary>
private void GetValue(ref float val, SerializedProperty property, string propertyName)
{
if (propertyName == null)
return;
SerializedProperty p = property.serializedObject.FindProperty(propertyName);
if (p != null)
{
switch (p.propertyType)
{
case SerializedPropertyType.Float:
val = p.floatValue;
break;
case SerializedPropertyType.Integer:
val = p.intValue;
break;
default:
error = true;
errorMessage = "Use Range with float or int param values.";
break;
}
}
else
{
error = true;
errorMessage = "Invalid param name or privated property.";
}
}
}
using Helpers;
using UnityEngine;
public class IntervalRangeExample : MonoBehaviour
{
[Header("Unity Range")]
[SerializeField]
[Range(0, 10)]
private float valueFloat0To10;
[SerializeField]
[Range(10, 0)]
private float valueFloat10To0;
[SerializeField]
[Range(0, 0)]
private float valueFloat0To0;
[SerializeField]
[Range(0, 10)]
private int valueInt0To10;
[SerializeField]
[Range(0, 10)]
private string valueString0To10;
[Header("Custom Interval Range")]
[SerializeField]
[IntervalRangeAttribute(0, 10)]
private float valueIntervalFloat0To10;
[SerializeField]
[IntervalRangeAttribute(10, 0)]
private float valueIntervalFloat10To0;
[SerializeField]
[IntervalRangeAttribute(0, 0)]
private float valueIntervalFloat0To0;
[SerializeField]
[IntervalRangeAttribute(0, 10, 3)]
private float valueIntervalFloat0To10Interval3;
[SerializeField]
[IntervalRangeAttribute(0, 10, 0.5f)]
private float valueIntervalFloat0To10IntervalHalf;
[SerializeField]
[IntervalRangeAttribute(10, 0, 1f/3f)]
private float valueIntervalFloat10To0Interval1Third;
[SerializeField]
[IntervalRangeAttribute(10, 0, 1f/3f, precisionMethod:PrecisionMethod.Truncate)]
private float valueIntervalFloat10To0Interval1ThirdTruncate;
[SerializeField]
[IntervalRangeAttribute(10, 0, 1f/3f, 10)]
private float valueIntervalFloat10To0Interval1Third10Precision;
[SerializeField]
[IntervalRangeAttribute(0, 10, 0.1f)]
private float valueIntervalFloat0To10Interval1Tenth;
[SerializeField]
[IntervalRangeAttribute(0, 10, 0.1f, 10)]
private float valueIntervalFloat0To10Interval1Tenth10Precision;
[SerializeField]
[IntervalRangeAttribute(0, 10)]
private int valueIntervalInt0To10;
[SerializeField]
[IntervalRangeAttribute(0, 10, 3)]
private int valueIntervalInt0To10Interval3;
[SerializeField]
[IntervalRangeAttribute(0, 10, 0.5f)]
private int valueIntervalInt0To10IntervalHalf;
[SerializeField]
[IntervalRangeAttribute(0, 10)]
private string valueIntervalString0To10;
[Header("Custom Interval Range With Variables References")]
[SerializeField]
private float min;
[SerializeField]
private int max;
[SerializeField]
private float interval;
private string varForError;
[SerializeField]
[IntervalRangeAttribute("min", "max", "interval")]
private float valueWithVariableReferences;
[SerializeField]
[IntervalRangeAttribute(5, "max", 3)]
private int valueWithMaxVariableReferencesMin5Interval3;
[SerializeField]
[IntervalRangeAttribute("varForError", "max", "interval")]
private float valueWithVariableReferencesErrorType;
[SerializeField]
[IntervalRangeAttribute(5, "min", "interval")]
private int valueWithVariableReferencesErrorTypeForInt;
private void Start()
{
Debug.Log("<color=red>Note: Only for Editor changes.</color>");
Debug.Log("If we modify the values internally, they don't respect the interval property");
Debug.Log("Example ------>");
Debug.Log("What we expect:");
Debug.Log("5.9 , 5.899999 , 3");
Debug.Log("The real values:");
valueIntervalFloat0To10Interval1Tenth = 5.94f;
valueIntervalFloat0To10Interval1Tenth10Precision = 5.94f;
valueIntervalInt0To10Interval3 = 2;
Debug.Log(valueIntervalFloat0To10Interval1Tenth + " , " + valueIntervalFloat0To10Interval1Tenth10Precision+ " , " + valueIntervalInt0To10Interval3);
Debug.Log("You can use the extension methods to archive de same behaviour:");
valueIntervalFloat0To10Interval1Tenth = 5.94f.AdjustIntervalAndPrecision(0.1f);
valueIntervalFloat0To10Interval1Tenth10Precision = 5.94f.AdjustIntervalAndPrecision(0.1f, floatPrecision:10);
valueIntervalInt0To10Interval3 = 2.NearestRound(3);
Debug.Log(valueIntervalFloat0To10Interval1Tenth + " , " + valueIntervalFloat0To10Interval1Tenth10Precision+ " , " + valueIntervalInt0To10Interval3);
}
}
namespace Helpers
{
public static class IntHelper
{
/// <summary>
/// Convert the number to the nearest value of a given multiple.
/// Function from StackOverflow response <see href="https://stackoverflow.com/a/34444056">HERE</see>
/// </summary>
/// <param name="number">Number to convert</param>
/// <param name="multiple">Multiple</param>
/// <param name="min">Min number to avoid wrong numbers</param>
/// <param name="max">Max number to avoid wrong numbers</param>
public static int NearestRound(this int number, float multiple, float? min = null, float? max = null)
{
return (int)((float) number).NearestRound(multiple, min, max);
}
}
}
public enum PrecisionMethod
{
Round,
Truncate,
None
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment