Hi, I want to make a custom control for the UI Builder that has a dropdown field in the inspector which is filled by a list of strings. This list of strings is coming from a scriptable object. Is it currently possible to have such a field and if so, how do you make this?
I do something like this below.
you create your standard field for the class. Then create a custom popupfield.
You can also bind the popupfield to the same property as the original field. In my case i have a string property called actionKey, and i wanted a popup next to it that contained values from another object. i.e. your serialised object.
The thing to watch out for is that a PopupField will complain if you enter a value that doesn’t exist. So i also display an error message, rather than ignoring the existing value thats in the field.
using System.Collections.Generic;
using MissionTaskManager.Scripts.missions.core;
using MissionTaskManager.Scripts.missions.helpers;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace MissionTaskManager.Scripts.missions.Editor
{
[CustomPropertyDrawer(typeof(MissionActionEvent), true)]
public class MissionActionEventDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var container = new VisualElement() {name = "root-action"};
var warning = new TextElement()
{
visible = false,
text =
"It doesn't appear that this key is used in the scene. For an action to do something in the scene, you'll need to add the MissionActionDataStore.cs script to an object.",
};
var popupFieldValues = new List<string>();
// Styles
container.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(
"Assets/MissionTaskManager/Scripts/missions/Editor/Resources/mission-action-styles.uss"));
// Props
var actionKeyProp = property.FindPropertyRelative("actionKey");
// Task fields
var actionTypeField = new PropertyField(property.FindPropertyRelative("actionType"));
var actionKeyField = new PropertyField(actionKeyProp);
actionKeyField.style.flexGrow = 1;
actionKeyField.RegisterCallback<ChangeEvent<string>>(e =>
{
var quickTestExistence = popupFieldValues.Contains(actionKeyProp.stringValue);
if (!quickTestExistence)
{
warning.visible = true;
}
else
{
warning.visible = false;
}
buildExternalisedList(property, popupFieldValues);
property.serializedObject.UpdateIfRequiredOrScript();
});
buildExternalisedList(property, popupFieldValues);
var popupField = new PopupField<string>(popupFieldValues, popupFieldValues[0]);
popupField.BindProperty(property.FindPropertyRelative("actionKey"));
var actionRow = new VisualElement()
{
style =
{
flexDirection = FlexDirection.Row
}
};
// Construct
container.Add(actionTypeField);
container.Add(actionRow);
actionRow.Add(actionKeyField);
actionRow.Add(popupField);
container.Add(warning);
return container;
}
private void buildExternalisedList(SerializedProperty property, IList<string> popupFieldValues)
{
popupFieldValues.Clear();
SerializedProperty actionKeyProp = property.FindPropertyRelative("actionKey");
MissionActionDataStore[] mappers = GameObject.FindObjectsOfType<MissionActionDataStore>();
foreach (var missionEventMapper in mappers)
{
popupFieldValues.Add("");
foreach (var missionEventItem in missionEventMapper.missionActions)
{
popupFieldValues.Add(missionEventItem.actionKey);
}
}
/// if my external list does't contain the current field value, add it. and display an error message.
var eventKeyIsValid = popupFieldValues.Contains(actionKeyProp.stringValue);
if (!eventKeyIsValid)
{
popupFieldValues.Insert(0, actionKeyProp.stringValue);
}
}
void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
evt.menu.AppendAction("SomeAction", OnMenuAction, DropdownMenuAction.AlwaysEnabled);
}
void OnMenuAction(DropdownMenuAction action)
{
Debug.Log(action.name);
}
}
}
I also have an instance where the fields are a part of the actual object.
So in buildExternalisedList() in other custom drawers i do this instead of the GameObject.FindObjectsOfType()
SerializedProperty missionPresets = property.serializedObject.FindProperty("missionPresets");
foreach (SerializedProperty missionPreset in missionPresets)
{
SerializedProperty presetIDProperty = missionPreset.FindPropertyRelative("presetName");
popupFieldValues.Add(presetIDProperty.stringValue);
}
What is this MissionActionEvent and how is it connected to the UI Builder and visual element?
Ah, sorry, Thats just my class for which i’m building a custom inspector. I’m assuming your looking to create a custom property draw for a class you ahve, and not a custom drop down for an ingame UI.
So below is code for customising the inspector for a class that might be a monobehaviour you ad to a in game object.
so you might have
public class MyClass : Monobehaviour{
public string myProp;
}
and then have for a typical setup…
[CustomPropertyDrawer((MyClass), true)]
public class MyClassDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var container = VisualElement() {};
// Grab the variable you want to display. and use the default Field for displaying it.
var myPropField =new PropertyField(property.FindPropertyRelative("myProp"));
container.Add(myPropField);
return container;
}
}
and then have for a custom popup
[CustomPropertyDrawertypeo(MyClass), true)]
public class MyClassDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var container =new VisualElement() {};
// grab the SerialisedProperty as well use it in two places. 1. for the default input field, in this case a text input. and 2. for binding to the dropdown.
var myPropSerialisedProperty = property.FindPropertyRelative("myProp")
// your default text input field.
var myPropField = new PropertyField(myPropSerialisedProperty);
// a list to store the strings in for the dropdown.
var popupFieldValues =new List<string>();
// the drop down field itself.
var popupField =new PopupField<string>(popupFieldValues, popupFieldValues[0]);
// when you change the popup field the bind below will apply your selection to the myProp.value.
popupField.BindProperty(myPropSerialisedProperty);
// its nice i think to have the drop down to the right of the text input field for myProp.
var rowLayout= new VisualElement()
{
style =
{
flexDirection = FlexDirection.Row
}
};
rowLayout.Add(myPropField)
rowLayout.Add(popupField)
container.Add(rowLayout);
// See previous code for this.
buildExternalisedList(property, popupFieldValues);
return container;
}
}
For brevity i haven’t added the warning message and the actual buildExternalisedList which is the previous comment.
I hope thats somewhat clearer.
Thanks for the quick answer! That did clear up the custom things you do, but I’m still not certain how this is called in the UI Builder. I thought UI Toolkit elements couldn’t have MonoBehaviour on them? I’m not sure if we are still thinking about the same thing here.
Missed your alert, sorry, True,
public class MyClass : Monobehaviour{
public string myProp;
}
could just as well be
public class MyClass
{
public string myProp;
}
Typically you’ll have a SerialisedObject or a Monobehaviour defining some properties. I.e. root objects that exist in your scene on an object or in your library. Basically things you can click in your Unity Project that will display information in the Inspector panel.
So you might have for example a scene Object with a MyFunctionality monobehaviour on it.
[code=CSharp]public class MyFunctionality : Monobehaviour{
public string myProp;
}
[/code]
The inspector will use its default code for display the string myProp, i.e. label and value.
You might then add a custom prop with a Type of another class i.e.
[code=CSharp]public class MyFunctionality : Monobehaviour{
public string myProp;
public MyClass anotherProp;
}
[/code]
Again the Inspector will display the information using its default style. But that may not be to you liking, so you can then use a custom Drawer class i.e. MyClassDrawer
[CustomPropertyDrawer(typeof(MyClass),true)]
public class MyClassDrawer: PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
//layout your properties as you wish.
}
}
So when you click on the object in your scene, that has the MyFunctionality class attached as a monobehaviour it will use the default inspector layout for the string prop, but will use the custom drawer for the myclass prop.
of course you could then also define custom Drawer class for the MyFunctionality class too, and choose to order the properties how you want them.
Something i haven’t really looked into yet is what the deal is with Custom Editor. i should probably look that up.
Oh, Also, you can use UIToolkit for creating in game guis (i.e. Canvas). Which might be what your looking at doing, i’m not sure. The in game gui side isn’t something i’ve looked at yet.