- Home /
PropertyDrawer dynamic subdrawer
In this project, I wanted to create a drawer within a drawer that changed based on what kind of object was attached. For this example, I'll say that I want 'MyCollider : MonoBehaviour' to contain 'public BuffEffect Debuff;' which requests a MonoScript from the user that only allows classes that implement an desired interface. This interface will be called 'Effect', and contain methods desired by the game engine.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class MyCollider : MonoBehaviour {
[SerializeField]
public BuffEffect Debuff;
}
using UnityEngine;
using UnityEditor;
using System.Collections;
[System.Serializable]
public class BuffEffect : System.Object {
[SerializeField]
public Texture Icon;
[SerializeField]
public Object Buff;
}
using UnityEngine;
using System.Collections;
using UnityEditor;
[CanEditMultipleObjects]
[CustomPropertyDrawer(typeof(BuffEffect))]
public class BuffDrawer : PropertyDrawer {
public Object script;
private SerializedProperty _script;
public Texture Icon = EditorGUIUtility.whiteTexture;
private SerializedProperty _Icon;
//private float width;
private string lasterror = "";
// Here you must define the height of your property drawer. Called by Unity.
public override float GetPropertyHeight (SerializedProperty prop,
GUIContent label) {
int height = 0;
if (lasterror != "")
height += 16;
if (script != null) {
//height += 16;
//height += 64;
}
return base.GetPropertyHeight (prop, label) + height;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (_script == null) {
if ((_script = property.FindPropertyRelative("Buff")) != null)
script = _script.objectReferenceValue as Object;
}
if (_Icon == null) {
if ((_Icon = property.FindPropertyRelative("Icon")) != null)
Icon = _Icon.objectReferenceValue as Texture;
}
//script = property.objectReferenceValue as MonoScript;
//Icon = property.FindPropertyRelative("Icon").objectReferenceValue as Texture;
Rect pos = position;
pos.height = 16;
RequestScript(pos, property);
DisplayError(pos, property);
if (script != null) {
//property.objectReferenceValue = script as Object;
//RequestIcon(pos, property);
}
}
public void RequestScript(Rect position, SerializedProperty prop) {
EditorGUI.LabelField(position, "Effect: ");
EditorGUI.BeginChangeCheck ();
//position.x += 100;
//position.width -= 100;
EditorGUI.indentLevel = 8;
Object value = EditorGUI.ObjectField(position, script as Object, typeof(MonoScript), false) as MonoScript;
if (EditorGUI.EndChangeCheck()) {
try {
if (typeof(Effect).IsAssignableFrom((value as MonoScript).GetClass())) {
script = value;
_script.objectReferenceValue = script as Object;
} else
lasterror = value.name+" does not implement <Effect>";
lasterror = "";
} catch (System.Exception e) {
Debug.LogError("Script Load Error: "+e.ToString());
lasterror = e.ToString();
script = null;
}
}
}
public void RequestIcon(Rect position, SerializedProperty prop) {
EditorGUI.indentLevel = 0;
position.y += 16;
//EditorGUI.DrawPreviewTexture(new Rect(position.x,position.y, 64, 64), Icon);
GUIStyle style = new GUIStyle();
if (Icon != null)
style.normal.background = Icon as Texture2D;
else
style.normal.background = EditorGUIUtility.whiteTexture;
EditorGUI.LabelField(new Rect(position.x,position.y, 64, 64), GUIContent.none, style);
position.y += 64;
EditorGUI.BeginChangeCheck ();
Texture value = EditorGUI.ObjectField(new Rect(position.x,position.y, 64, 16), Icon as Object, typeof(Texture), false) as Texture;
if (EditorGUI.EndChangeCheck()) {
try {
Icon = value;
_Icon.objectReferenceValue = Icon as Object;
} catch (System.Exception e) {
Debug.LogError("Texture Load Error: "+e.ToString());
lasterror = e.ToString();
Icon = null;
}
}
}
public void DisplayError(Rect position, SerializedProperty prop) {
if (lasterror != "") {
EditorGUI.indentLevel = 2;
position.y += 16;
EditorGUI.LabelField(position, "ERR: "+lasterror);
}
}
}
Got it? Great. Now, the code has the functionality to display the disired Icon, but we want to let our new subdrawer take care of that. So next on the list is to create us a buff. Let's say we want a Poison buff to take effect, and by buff, I mean debuff.
using UnityEngine;
using UnityEditor;
using System.Collections;
public interface Effect {
string Name();
void SetTarget(Transform transform);
void StartEffect();
void Tick(float Elapsed);
void EndEffect();
}
using UnityEngine;
using System.Collections;
[System.Serializable]
public class Poison : Object, Effect {
[SerializeField]
public Texture Icon;
[SerializeField]
public float Damage = 1f;
[SerializeField]
public float Interval = 1f;
[SerializeField]
public float Duration = 10f;
private float elapsed = 0;
private float totalelapsed = 0;
public Transform target;
#region Effect implementation
public string Name() {
return "Poison";
}
public void SetTarget(Transform transform) {
target = transform;
}
public void StartEffect ()
{
Icon = Resources.Load("effect/poison", typeof(Texture)) as Texture;
}
public void Tick (float Elapsed)
{
elapsed += Elapsed;
// loop all ticks, but no more than allowed
while (elapsed >= Interval && (totalelapsed+elapsed < Duration)) {
elapsed -= Interval;
totalelapsed += Interval;
target.GetComponent<SurvivalManager>().Blood -= Damage;
}
totalelapsed += elapsed;
elapsed = 0;
if (totalelapsed >= Duration) {
// signal buff for deletion
PlayerController plyr = target.GetComponent<PlayerController>();
if (plyr != null)
plyr.ClearBuff((Effect)this);
}
}
public void EndEffect ()
{
}
#endregion
}
Keep in mind that all of these scripts need to be separated in order for them to be accessible via all other scripts. A quick read through should inform you that this is a rather basic buff process. We can script different buffs that all call a start, tick, and end effects. Next we need to generate our drawer to print the preferred editable variables into the component editor.
This code needs assistance
using UnityEngine;
using System.Collections;
using UnityEditor;
[CanEditMultipleObjects]
[CustomPropertyDrawer(typeof(Poison))]
public class PoisonDrawer : PropertyDrawer {
public Texture Icon = null;
public float Damage = 0;
public float Interval = 0;
public float Duration = 0;
private SerializedProperty _Icon;
private SerializedProperty _Damage;
private SerializedProperty _Interval;
private SerializedProperty _Duration;
// Here you must define the height of your property drawer. Called by Unity.
public override float GetPropertyHeight (SerializedProperty prop,
GUIContent label) {
int height = 80;
return base.GetPropertyHeight (prop, label) + height;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (_Icon == null) {
if ((_Icon = property.FindPropertyRelative("Icon")) != null)
Icon = _Icon.objectReferenceValue as Texture;
}
if (_Damage == null) {
if ((_Damage = property.FindPropertyRelative("Damage")) != null)
Damage = _Damage.floatValue;
}
//script = property.objectReferenceValue as MonoScript;
//Icon = property.FindPropertyRelative("Icon").objectReferenceValue as Texture;
Rect pos = position;
pos.height = 16;
//property.objectReferenceValue = script as Object;
RequestIcon(pos, property);
RequestFloat(ref pos, "Damage", ref Damage, ref _Damage, property);
RequestFloat(ref pos, "Interval", ref Interval, ref _Interval, property);
RequestFloat(ref pos, "Duration", ref Duration, ref _Duration, property);
}
public void RequestFloat(ref Rect position,string Variable, ref float Value, ref SerializedProperty _Value, SerializedProperty prop) {
EditorGUI.indentLevel = 5;
position.y += 16;
//position.x = 70;
EditorGUI.LabelField(position, Variable+": ");
position.x += 70;
position.width -= 70;
EditorGUI.BeginChangeCheck ();
string value = EditorGUI.TextField(position, Value.ToString());
if (EditorGUI.EndChangeCheck()) {
try {
Value = float.Parse(value);
_Value.floatValue = Value;
} catch (System.Exception e) {
Debug.LogError("Float Set Error: "+e.ToString());
Value = 0;
}
}
position.x -= 70;
position.width += 70;
}
public void RequestIcon(Rect position, SerializedProperty prop) {
EditorGUI.indentLevel = 0;
position.y += 16;
//EditorGUI.DrawPreviewTexture(new Rect(position.x,position.y, 64, 64), Icon);
GUIStyle style = new GUIStyle();
if (Icon == null)
style.normal.background = EditorGUIUtility.whiteTexture;
else
style.normal.background = Icon as Texture2D;
EditorGUI.LabelField(new Rect(position.x,position.y, 64, 64), GUIContent.none, style);
position.y += 64;
EditorGUI.BeginChangeCheck ();
Texture value = EditorGUI.ObjectField(new Rect(position.x,position.y, 64, 16), Icon as Object, typeof(Texture), false) as Texture;
if (EditorGUI.EndChangeCheck()) {
try {
Icon = value as Texture;
_Icon.objectReferenceValue = Icon as Object;
} catch (System.Exception e) {
Debug.LogError("Texture Load Error: "+e.ToString());
Icon = null;
}
}
}
}
This code currently relates to a Poison : Effect, but the serialized object relates to an Effect type. Not sure how I can subdrawer it, and I keep getting System.NullReferenceException on the SerializedProperty's in ALL cases in Poison. I'm wondering if this has anything to do with it implementing an interface, and how to get around it, while still having access to the methods within.
Your answer
Follow this Question
Related Questions
[JS] [Editor] SerializedObject from a custom class. 0 Answers
SerializedObject.FindProperty returning null 2 Answers
How to apply script saved in variable 2 Answers
Draw custom Property Field in Custom Editor 1 Answer
AttributeDrawer: Draw parent property? (the one [AttributeName] is attached to) 0 Answers