- Home /
PropertyDrawer, local members behave like static members
I've come across something that seems pretty odd to me, but I'm no expert at this.
I have a property (Object), for which I want to create a dropdown selector "on the fly", to set one of its properties.
Everything works, except for members declared in the PropertyDrawer. They behave like they all share the same value, always.
This is my custom Object and the component using them :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
namespace Test
{
[Serializable]
public class TestProperty : System.Object
{
public string testString = "None";
}
public class TestComponent : MonoBehaviour
{
public List<TestProperty> properties = new List<TestProperty>();
}
}
And this is the PropertyDrawer :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
namespace Test
{
[CustomPropertyDrawer(typeof(TestProperty))]
public class TestPropertyDrawer : PropertyDrawer
{
SerializedProperty stringProp;
void RefreshProperties ()
{
pNames = new string[5];
for (int i = 0; i < 5; i++)
pNames[i] = Random.Range(0, 100).ToString();
}
public string [] pNames = new string[3] {"Test 1", "Test 2", "Test 3"}; // this behaves like static (common amongst all properties)
private int _pSelection; // this behaves like static (common amongst all properties)
public int pSelection
{
get {return _pSelection;}
set
{
if (_pSelection != value)
{
_pSelection = value;
stringProp.stringValue = pNames[value];
}
}
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
stringProp = property.FindPropertyRelative("testString");
var cRect = new Rect (position.x, position.y, position.width*0.20f, position.height);
var pRect = new Rect (position.x+position.width*0.20f, position.y, position.width*0.40f, position.height);
var sRect = new Rect (position.x+position.width*0.60f, position.y, position.width*0.40f, position.height);
EditorGUI.BeginProperty (position, label, property);
if (GUI.Button(cRect, "refresh"))
RefreshProperties();
pSelection = EditorGUI.Popup (pRect, pSelection, pNames);
EditorGUI.LabelField (sRect, stringProp.stringValue);
EditorGUI.EndProperty();
}
}
}
As a result, the list, and the selection appears to always be the same, although the mechanic works.
Answer by Adam-Mechtley · Feb 13, 2018 at 07:51 PM
That said, it is worth acknowledging that you sometimes need per-property view state data (i.e. data that you don't actually want to store in your model). In these cases, the typical approach that we use internally is to key such data with the propertyPath for the property being drawn. Something like this:
class MyDrawer : PropertyDrawer {
class ViewData {
public int someInteger;
public string someString = string.Empty;
}
Dictionary<string, ViewData> m_PerPropertyViewData = new Dictionary<string, ViewData>();
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
ViewData viewData;
if (!m_PerPropertyViewData.TryGetValue(property.propertyPath, out viewData) {
viewData = new ViewData();
m_PerPropertyViewData[property.propertyPath] = viewData;
}
// do stuff with viewData as needed
}
}
Thanks @Adam-$$anonymous$$echtley, this is exactly what I was hoping for. Let me try this.
Answer by Bunny83 · Feb 13, 2018 at 04:36 PM
A property drawer should never be used to store any state that should be preserved for each instance. Unity will of course reuse the property drawer instance for all properties of the same type. It would be horrible inefficient to create a new property drawer instance for each element. The OnGUI method receives all parameters necessary to draw the property.
If you want to store information for each property seperately you would need to add that information to your "TestProperty" class
Thanks @Bunny83, let me try that. I'll post the (hopefully) working solution and close the question.
Your answer, while partially accurate from a performance standpoint misses from a usability standpoint. If one is to never store property drawer display configuration data, then how would one go about using EditorGUI.Foldout in the drawer for children of a collection?
In order for the foldout to remain folded for more than one frame you'd need to store the value of the foldout's state while the drawer is visible. I really don't like having editor-only display data being stored in and cluttering up my runtime data. (even when using conditional compliation to strip it from the build)
As a gray, i'm partial to absolutes, but not everything is as cut and dry as never.
Every serializedProperty has an isExpanded property which is exactly for that usecase and is used exactly for this by the default drawer.
As for any other IMGUI related state information, there are the methods GUIUtility.GetStateObject and GUIUtility.QueryStateObject which are exactly for storing state of a certain IMGUI control ID. Those methods allows you to specify any pure data class which is created and remembered internally by the IMGUI system.
In my custom property drawer, EditorGUI.Foldout isn't bound to a runtime serialized property and as such isExpanded isn't available to me.
The below is a fragment from one of my property drawers (better?) articulating the use I'm describing. The drawer is for a serializable class that is used in a List.
//A struct containing data returned from the dictionary for this 'view-state'
//as per OP's selected answer.
public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label){
//FindProperties, EditorGUI.BeginProperty, DrawRectBorder... layout boilerplate stuff.
//Draw Object reference field
//Foldout Group for Rotational Data
//Define if has rotational data
EditorGUI.PropertyField(rect, prop_hasRot);
EditorGUI.BeginDisabledGroup(!prop_hasRot.boolValue);
foldoutState.showRot = EditorGUI.Foldout(rect, prop_hasRot.boolValue && foldoutState.showRot, new GUIContent("Rotational data"));
if(if (foldoutState.showRot) {
//A bunch of labels, fields, and buttons for setting and manipulating the data.
}
EditorGUI.EndDisabledGroup();
//Foldout Group for Translation Data (Similar to above)
//More stuff...
}
In the above if I didn't store the view-state data for the foldout, then opening one would open all foldouts in the list (static bool), or would open for 1 frame then close again (private bool)
I'm open to learning new things and the QueryStateObject sounds like it's an alternate to the answer given by Adam, but IsExpanded only works if I'm targeting an entire serialized class/struct as opposed to showing/hiding sections of that class/struct to reduce clutter.
p.s. The reply editor here really needs a preview to ensure formatting is working before posting lest there be 10,000 edits...
I didn't even realize this. Thanks for clearing that up.
Your answer
Follow this Question
Related Questions
Get SerializedProperty from outside class for use in popup (PropertyDrawer) 1 Answer
Using DuplicateCommand and DeleteCommand with Properties 0 Answers
Do PropertyDrawers support inheritance? 1 Answer
Texture2D on Property Drawers. 0 Answers
Property Drawers In Serializable Classes - GetPropertyHeight 2 Answers