Unity Editor Data Entry 2.0

posted in Binary Cats
Published April 02, 2016
Advertisement

Unity Data Entry


So, unity has a problem. Well, ok lots of problems, but I want to talk about a specific one. Data entry and how things can quickly get out of hand. For those who are unaware, unity allows you to edit serialize data through the editor using an 'inspector window'.

eaG1ZGl.png

My script has 3 variables that can be edited with types enum, int and float. (unity offers many other data types which can be edited through the inspector).

Entering data through the inspector is fine for simple objects/scripts. However, once a object/scripts becomes more complicated the default data entry becomes a burden. Specifically when you have a variable which will determine if another variable is used or not. A good example of this is you may have a projectile, this projectile could behave in different ways. It could be a homing projectile, it could be a bullet, it could be anything. If the projectile is homing, it will use additional parameters to a standard projectile, if it is a bullet it would use different parameters again.

0PJr66Y.png

The solution to this problem, as stated a lot of places on the internet is to create a custom drawer for the (projectile) script. However, in practice this is impractical. Scripts where I needed the above behaviour required writing 200 lines, just so I could show/hide some variables in the inspector! furthermore this 'solution' just isn't scalable, as you would have to write custom draw code for each script!.

My solution to this involves using attributes to define when a variable is shown or not (and to handle the drawing) (in real time!) Which changes the above image to:
LQWHyZ0.pngkrYgVFo.pngzkppj68.png

So, What do we need to do?


  • Create a generic attribute which any variable can use
  • Use reflection to get another variable
  • Check to see if that variable is of a particular value


The most basic condition (to decide if one variable should be shown or not) is a Boolean.if Varible A is True Show Varible Belse Don't show Varible B
YnG9IKp.pngSVjbKSt.png


First we have to declare our fields, this is pretty standard, however on variable B we will have to declare it with our attribute (conditional)public bool a;[Conditional("a", true)]public int b;
as you can see, the attribute takes in two parameters; "a" and True. The first parameter is the name of the parameter we want to check. The second parameter is the value the first parameter needs to be to make variable b show. These are both stored with in the Attribute.


We need to create a drawer, which will draw the variable in editor, for our attribute. We do this by doing:[CustomPropertyDrawer(typeof(ConditionalAttribute))]public class ConditionalDrawer : PropertyDrawer{ public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { .... }}
The OnGUI method will handle drawing the variable in editor. As you can see, it has a SerializedProperty parameter, this is our variable "b". We need to do some reflection magic on that parameter to get variable "a" and check it's value. public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { ConditionalAttribute conAtr = attribute as ConditionalAttribute; string path = GetPath(property); string conditionname = path + conAtr.ConditionVaribleName; ConditionalProperty = property.serializedObject.FindProperty(conditionname);} private string GetPath(SerializedProperty property) { string path = property.propertyPath; int index = path.LastIndexOf("."); return path.Substring(0, index + 1); }
This code gets the condition variable ("bool a") and stores it in ConditionalProperty. The GetPath Method is needed as the variables could be nested in an object.


?
?
Next we need to check the value of ConditionalProperty, against the value we passed (and stored) into the attribute. if (ConditionalProperty.propertyType == SerializedPropertyType.Boolean) { conAtr.ShouldShow = ConditionalProperty.boolValue == conAtr.CheckValue; }
as the same instance of the drawer is used to draw all the variables with our attribute, we can not store anything with in the drawer. we have to store everything in the attribute.


Next, still within the OnGUI method we need to draw (or not draw) the variable ("int b").if (conAtr.ShouldBeShown) EditorGUI.PropertyField(position, property, label, false);
We also need to alter the height of the variable being drawn otherwise there will be a blank space. Luckily we can just do: public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { ConditionalAttribute conAtr= attribute as ConditionalAttribute; if (!conAtr.ShouldBeShown) return 0.0f; return EditorGUI.GetPropertyHeight(property, label, true); }
within the drawer class.


Great!! we now have a variable which is shown/not shown depending on another Boolean variable. But what if we don't want to use a Boolean, what if we want to make it dependant on another type, what if it has to be greater than X or less than X. What if we want it conditional to multiple things.
Don't fear, I have thought of that!

I have another constructor in the attribute:public enum OperatorEnum { Equals, NotEqualTo, GreaterThan, LessThan, EqualsOrGreaterThan, EqualsOrLessThan } public enum LogicEnum { AND, OR } //LHS, is the varible name to check which are got through reflection //Opperator, is the operator to use for the check //RHS, is the value to check against //BetweenElements, and/or to use between the last element and this element public ConditionalAttribute(string[] LHS, int[] Operator, object[] RHS, int[] BetweenElements) { int maxsize = LHS.Length; m_LHSVaribleNames = new string[maxsize]; m_Operator = new OperatorEnum[maxsize]; m_RHSValues = new object[maxsize]; m_BetweenElementsLogic = new LogicEnum[maxsize - 1]; m_LHSVaribleNames = LHS; for (int i = 0; i < maxsize; i++) { if (i < Operator.Length) { m_Operator = (OperatorEnum)Operator; } else m_Operator = (int)OperatorEnum.Equals;//Set it to == if no value was given } for (int i = 0; i < maxsize; i++) { if (i < RHS.Length) { m_RHSValues = RHS; } else m_RHSValues = RHS[RHS.Length - 1];//Set it to the last value if no value was given } for (int i = 0; i < maxsize - 1; i++) { if (i < BetweenElements.Length) { m_BetweenElementsLogic = (LogicEnum)BetweenElements; } else m_BetweenElementsLogic = LogicEnum.AND;//Set it to && if no value was given } }
unfortunately, there are only certain types which can be passed into attributes. I would have liked to do all this using a lambda to decide all this, but alas that was not possible so I had to parse the values to make the correct statement. Also, enum values can not be passed through so they need to be cast to an int.


So now if we want a new variable ("c") to be shown when:if a == true && b > 5 showelse don't show
the declaration would look like: public bool a; [Conditional("a", true)] public int b; [Conditional(new string[] { "a","b"}, new int[] { (int)ConditionalAttribute.OperatorEnum.Equals, (int)ConditionalAttribute.OperatorEnum.GreaterThan }, new object[] { true, 5}, new int[] { (int)ConditionalAttribute.LogicEnum.AND} )] public float c;
yikes that's a big constructor :(


One last thing was that because I don't know the type of the variable who's name is passed in (the condition variable) - I only know the type of the variable being drawn, I had to use(IComparable)LHS).CompareTo(RHS) == 0
for the check to know if the variable should be shown or not.


Using this attribute allows the data entry for unity to be much easier, as now you can edit fields that are relative to the options you have already chosen!

You can now write, simple/complex statements and control the visibility of variables in the editor without the need of creating custom drawer's for each script. I am really happy with how this has turned out, it is really flexible and really generic.

Hopefully someone else will find it as useful as I do :)

Until next time, dsm

full code:using UnityEngine;using System.Collections;using UnityEditor;using System;public class ConditionalAttribute : PropertyAttribute{ public enum OperatorEnum { Equals, NotEqualTo, GreaterThan, LessThan, EqualsOrGreaterThan, EqualsOrLessThan } public enum LogicEnum { AND, OR } protected string[] m_LHSVaribleNames; protected OperatorEnum[] m_Operator; protected object[] m_RHSValues; protected LogicEnum[] m_BetweenElementsLogic; protected bool m_ShouldBeShown = true; public bool ShouldBeShown { get { return m_ShouldBeShown; } set { m_ShouldBeShown = value; } } public string[] LHSVaribleNames { get { return m_LHSVaribleNames; } } public OperatorEnum[] Operator { get { return m_Operator; } } public object[] RHSValues { get { return m_RHSValues; } } public LogicEnum[] BetweenElementsLogic { get { return m_BetweenElementsLogic; } } //LHS, is the varible name to check which are got through reflection //Opperator, is the operator to use for the check //RHS, is the value to check against //BetweenElements, and/or to use between the last element and this element public ConditionalAttribute(string[] LHS, int[] Operator, object[] RHS, int[] BetweenElements) { int maxsize = LHS.Length; m_LHSVaribleNames = new string[maxsize]; m_Operator = new OperatorEnum[maxsize]; m_RHSValues = new object[maxsize]; m_BetweenElementsLogic = new LogicEnum[maxsize - 1]; m_LHSVaribleNames = LHS; for (int i = 0; i < maxsize; i++) { if (i < Operator.Length) { m_Operator = (OperatorEnum)Operator; } else m_Operator = (int)OperatorEnum.Equals;//Set it to == if no value was given } for (int i = 0; i < maxsize; i++) { if (i < RHS.Length) { m_RHSValues = RHS; } else m_RHSValues = RHS[RHS.Length - 1];//Set it to the last value if no value was given } for (int i = 0; i < maxsize - 1; i++) { if (i < BetweenElements.Length) { m_BetweenElementsLogic = (LogicEnum)BetweenElements; } else m_BetweenElementsLogic = LogicEnum.AND;//Set it to && if no value was given } } public ConditionalAttribute(string LHSVaribleName, object RHSValue) { m_LHSVaribleNames = new string[] { LHSVaribleName }; m_RHSValues = new object[] { RHSValue }; m_Operator = new OperatorEnum[] { OperatorEnum.Equals }; } public ConditionalAttribute() { }}///////Conditional//Decides if a varible should be shown in the inspector depending on another (boolean) varible[CustomPropertyDrawer(typeof(ConditionalAttribute))]public class ConditionalDrawer : PropertyDrawer{ public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { if (!bute.ShouldBeShown) return 0.0f; return EditorGUI.GetPropertyHeight(property, label, true); } SerializedProperty ConditionalProperty; public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { bute.ShouldBeShown = ShouldShow(property); if (bute.ShouldBeShown) EditorGUI.PropertyField(position, property, label, false); } private string GetPath(SerializedProperty property) { string path = property.propertyPath; int index = path.LastIndexOf("."); return path.Substring(0, index + 1); } protected virtual ConditionalAttribute bute { get { return (ConditionalAttribute)attribute; } } protected bool ShouldShow(SerializedProperty property) { string path = GetPath(property); bool previous = false; for (int i = 0; i < bute.LHSVaribleNames.Length; i++) { string conditionname = path + bute.LHSVaribleNames; ConditionalProperty = property.serializedObject.FindProperty(conditionname); bool test = false; object lhsValue; if (ConditionalProperty.propertyType == SerializedPropertyType.Integer) { lhsValue = ConditionalProperty.intValue; } else if (ConditionalProperty.propertyType == SerializedPropertyType.Float) { lhsValue = ConditionalProperty.floatValue; } else if (ConditionalProperty.propertyType == SerializedPropertyType.Boolean) { lhsValue = ConditionalProperty.boolValue; } else if (ConditionalProperty.propertyType == SerializedPropertyType.Enum) { lhsValue = ConditionalProperty.enumNames[ConditionalProperty.enumValueIndex]; } else throw new Exception("Type needs implementing"); test = Check(lhsValue, bute.Operator, bute.RHSValues); if (bute.BetweenElementsLogic == null) { return test; } if (i != 0 && test && bute.BetweenElementsLogic[i - 1] == (int)ConditionalAttribute.LogicEnum.AND) { if (!previous) { test = false; } } if (test && i < bute.BetweenElementsLogic.Length && bute.BetweenElementsLogic == ConditionalAttribute.LogicEnum.OR) { return true; } previous = test; } return previous; } protected bool Check(object LHS, ConditionalAttribute.OperatorEnum op, object RHS) { if (!(LHS is IComparable) || !(RHS is IComparable)) throw new Exception("Check using non basic type"); switch (op) { case ConditionalAttribute.OperatorEnum.Equals: return ((IComparable)LHS).CompareTo(RHS) == 0; case ConditionalAttribute.OperatorEnum.NotEqualTo: return ((IComparable)LHS).CompareTo(RHS) != 0; case ConditionalAttribute.OperatorEnum.EqualsOrGreaterThan: return ((IComparable)LHS).CompareTo(RHS) >= 0; case ConditionalAttribute.OperatorEnum.EqualsOrLessThan: return ((IComparable)LHS).CompareTo(RHS) <= 0; case ConditionalAttribute.OperatorEnum.GreaterThan: return ((IComparable)LHS).CompareTo(RHS) > 0; case ConditionalAttribute.OperatorEnum.LessThan: return ((IComparable)LHS).CompareTo(RHS) < 0; default: break; } return false; }}

5 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement