- Home /
How do I property Drawer?
So What I'm trying to do is sort of complicated and very specific I'll try to provide an appropriate amount of context. Essentially I have a Sound Collection class that has a big list of sounds and parameters that those sounds should be played with (volume, fade in, pitch, stuff like that).
Entirely separately I have an Interactible Object class which should play one or more sounds when interacted with. that class has a list of serialized classes (Controlled Sound) which define what type of sound the sound is, and what the name of the sound is to play (so it can be looked up in the sound collection by name).
Thats all well and good. Heres the problem I'm trying to solve.
I want to create a custom inspector/property drawer for that Sound Controller class that will let me select the type of sound the controlled sound is, and then a drop down of all the sounds in the sound collection from which to select a sound from (so the user doesnt have to type the exact name of the sound into the field.)
Here is the serialized class as well as my not-functioning attempt at the property drawer. I get the following error when trying to select a sound from the dropdown
ArgumentException: Getting control 0's position in a group with only 0 controls when doing Repaint Aborting
What am I doing wrong here? I've been studying the property drawer from the unity manual but it and the video tutorial seem very vague.
using UnityEngine;
using System.Collections;
namespace Sol
{
[System.Serializable]
public class ControlledSound
{
[HideInInspector]
public SoundType soundType;
[HideInInspector]
public string soundName;
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
namespace Sol
{
[CustomPropertyDrawer(typeof(ControlledSound))]
public class ControlledSoundDrawer : PropertyDrawer
{
private SoundType selectedSoundType = SoundType.None;
private string selectedSoundName = "";
private SoundManager cachedSoundManager;
private SoundManager CachedSoundManager
{
get { return (cachedSoundManager != null) ? cachedSoundManager : cachedSoundManager = GameManager.Get<SoundManager>(); }
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
Rect rect1 = new Rect(position.x, position.y, position.width / 2, position.height);
selectedSoundType = (SoundType)EditorGUI.EnumPopup(rect1, selectedSoundType);
EditorGUILayout.Space();
int selectedIndex = 0;
string[] options = new string[1];
foreach(SoundCollection soundCollection in CachedSoundManager.collections)
{
if(soundCollection.soundType == selectedSoundType)
{
List<string> soundNameList = soundCollection.GetSoundNames();
options = new string[soundNameList.Count];
for(int i = 0; i < soundNameList.Count; i++)
{
options[i] = soundNameList[i];
if (options[i] == selectedSoundName) selectedIndex = i;
}
}
}
Rect rect2 = new Rect(position.x + position.width / 2, position.y, position.width / 2, position.height);
selectedSoundName = options[EditorGUI.Popup(rect2, selectedIndex, options)];
DrawControlledSound(selectedSoundType, selectedSoundName);
}
public ControlledSound DrawControlledSound(SoundType type, string soundName)
{
ControlledSound newControlledSound = new ControlledSound();
newControlledSound.soundType = type;
newControlledSound.soundName = soundName;
return newControlledSound;
}
}
}
Here is a bit more about property drawers: Custom Data, an introduction to serialized classes and property drawers by Catlike Coding.
Why are you using [HideInInspector] attribute? You don't need it in this case.
Also you handle property drawer in a wierd way.
private SoundType selectedSoundType = SoundType.None;
private string selectedSoundName = "";
private Sound$$anonymous$$anager cachedSound$$anonymous$$anager;
Those references in a property drawer class act wierd: they are shared over all instances of a class being drawn. So if you have several ControlledSounds you still will have a single ControlledSoundDrawer.
And what is that for?!
private Sound$$anonymous$$anager CachedSound$$anonymous$$anager
{
get { return (cachedSound$$anonymous$$anager != null) ? cachedSound$$anonymous$$anager : cachedSound$$anonymous$$anager = Game$$anonymous$$anager.Get<Sound$$anonymous$$anager>(); }
}
Thanks, these posts have gotten me most of the way there i think? all that remains is creating a drop-down list of all the sounds in the sound collection that correspond to the selected type. For instance if they have the '$$anonymous$$usic' enum selected, i want the 'sound name' options to only be populated with music clips names from the sound collection (which incidentally is where getting a reference to the sound manager comes in handy. Game$$anonymous$$anager.get() is actually a lot cheaper than GameObject.FindObjectOfType() since it is looking up and instance of the sound manager singleton from a dictionary.)
Problem I'm running into is I can't really seem to compare the serialized property to the actual enum or string value in the serialized class. I'll keep messing with it and see what i can come up with.
Thanks for your help so far
Also have you tried commenting parts of your code to find out were exactly an error occurs?
Btw, u never actually assign selectedSoundType anywhere, so it stays in property drawer class and never assigned to an instance of a property.
I mean that you need to use EditorGUI.PropertyField() to draw GUI element that gets and sets value.
Other approach is something like this:
property.FindPropertyRelative("text").stringValue = EditorGUI.TextArea(contentPosition, property.FindPropertyRelative("text").stringValue, style);
Answer by troien · May 02, 2016 at 10:55 AM
A few things here are weird as @incorrect already mentioned.
The first thing you should do is to get rid of "selectedSoundType " and "selectedSoundName".
You should get these values from the SerializedProperty which is a parameter of the OnGUI method.
like so:
SerializedProperty name = property.FindPropertyRelative("soundName");
// You can display it using this
EditorGUI.PropertyField(position, name);
// Or use something like this
name.stringValue = EditorGUI.TextField(position, name.stringValue);
// Or in your case: (although your current code should break whenever options.Length == 0)
name.stringValue = options[EditorGUI.Popup(rect2, selectedIndex, options)];
Also, the manual has some good examples.
Secondly, what is this "DrawControlledSound()" supposed to do? It clearly doesn't draw the controlled sound... It just creates a new one?
Also, get rid of:
EditorGUILayout.Space();
As EditorGUILayout is not supported when doing PropertyDrawers (probably the reason for the error you have). Besides you are already using Rects to draw your properties anyway, they ignore this Layout as you hardcode the positions...
Depending on your implemetation I think your CachedSoundManager approach should work, as long as it never returns null. However, note that soundName can still be empty, your editor behaviour here assumes that your index is 0 then and you select the first option. To prevent weird errors, you should make sure your editor and playmode code make the same asumptions ;)
Are you sure that SerializedProperty name = property.FindPropertyRelative("soundName");
will work? Is it possible to cast from a generic SerializedProperty to some type?
I'm pretty sure it will work, I'm not sure I understand your question here though.
property is the SerializedProperty of an instance of ControlledSound.
ControlledSound has a field called "soundName".
So I can call "property.FindPropertyRelative("soundName")" to get the SerializedProperty of the field "soundName".
To access the content of the field, which is of type string. I can then use "name.stringValue".
So I can call "property.FindPropertyRelative("soundName")" to get the SerializedProperty of the field "soundName".
To access the content of the field, which is of type string. I can then use "name.stringValue".
Is there a way to retrieve the enum value from property.FindPropertyRelative("soundType") ?
Well summarized.
Yes, PropertyDrawers are a bit nasty as they only work on the serialized data, not on the actual class instance. You can't directly access the actual class instance, only it's serialized data by using the property that the PropertyDrawer gets from the system.
Of course the "FindPropertyRelative" should be done once in OnEnable.
Of course the "FindPropertyRelative" should be done once in OnEnable.
In normal occasions you are absolutely right. But not when talking about CustomPropertyDrawers. The only way to get the 'base' SerializedProperty here is through the parameter of the OnGUI method which you can override.
EDIT: Just checked, OnEnable doesn't even get called on a PropertyDrawer ;) It is a plain old C# class that doesn't inherrit from UnityEngine.Object. And in the constructor, you don't have access to any SerializedProperty as I mentioned :p
Yes, you're right ^^ PropertyDrawers actually don't have an OnEnable callback. So maybe it's possible to cache them anyways, but i never tried that. It won't work if Unity is re-using the PropertyDrawer instance for other properties as well.
Your answer
