How to make an enum-like Unity inspector drop-down menu from a string array with C#?
You can't change the enum itself as it needs to be compiled (well it is not completely impossible but I wouldn't recommend to go ways like actively change a script and force a re-compile)
Without seeing the rest of the types you need it is a bit hard but what you want you would best do in a custom editor script using EditorGUILayout.Popup
. As said I don't know your exact needs and the type Characters
or how exactly you reference them so for now I will assume you reference your DialogueElement
to a certain character via its index in the list Dialogue.CharactersList
. This basically works like an enum
then!
Since these editor scripts can get quite complex I try to comment every step:
using System;
using System.Collections.Generic;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
#endif
using UnityEngine;
[CreateAssetMenu]
public class Dialogue : ScriptableObject
{
public string[] CharactersList;
public List<DialogueElement> DialogueItems;
}
[Serializable] //needed to make ScriptableObject out of this class
public class DialogueElement
{
// You would only store an index to the according character
// Since I don't have your Characters type for now lets reference them via the Dialogue.CharactersList
public int CharacterID;
//public Characters Character;
// By using the attribute [TextArea] this creates a nice multi-line text are field
// You could further configure it with a min and max line size if you want: [TextArea(minLines, maxLines)]
[TextArea] public string DialogueText;
}
// This needs to be either wrapped by #if UNITY_EDITOR
// or placed in a folder called "Editor"
#if UNITY_EDITOR
[CustomEditor(typeof(Dialogue))]
public class DialogueEditor : Editor
{
// This will be the serialized clone property of Dialogue.CharacterList
private SerializedProperty CharactersList;
// This will be the serialized clone property of Dialogue.DialogueItems
private SerializedProperty DialogueItems;
// This is a little bonus from my side!
// These Lists are extremely more powerful then the default presentation of lists!
// you can/have to implement completely custom behavior of how to display and edit
// the list elements
private ReorderableList charactersList;
private ReorderableList dialogItemsList;
// Reference to the actual Dialogue instance this Inspector belongs to
private Dialogue dialogue;
// class field for storing available options
private GuiContent[] availableOptions;
// Called when the Inspector is opened / ScriptableObject is selected
private void OnEnable()
{
// Get the target as the type you are actually using
dialogue = (Dialogue) target;
// Link in serialized fields to their according SerializedProperties
CharactersList = serializedObject.FindProperty(nameof(Dialogue.CharactersList));
DialogueItems = serializedObject.FindProperty(nameof(Dialogue.DialogueItems));
// Setup and configure the charactersList we will use to display the content of the CharactersList
// in a nicer way
charactersList = new ReorderableList(serializedObject, CharactersList)
{
displayAdd = true,
displayRemove = true,
draggable = false, // for now disable reorder feature since we later go by index!
// As the header we simply want to see the usual display name of the CharactersList
drawHeaderCallback = rect => EditorGUI.LabelField(rect, CharactersList.displayName),
// How shall elements be displayed
drawElementCallback = (rect, index, focused, active) =>
{
// get the current element's SerializedProperty
var element = CharactersList.GetArrayElementAtIndex(index);
// Get all characters as string[]
var availableIDs = dialogue.CharactersList;
// store the original GUI.color
var color = GUI.color;
// Tint the field in red for invalid values
// either because it is empty or a duplicate
if(string.IsNullOrWhiteSpace(element.stringValue) || availableIDs.Count(item => string.Equals(item, element.stringValue)) > 1)
{
GUI.color = Color.red;
}
// Draw the property which automatically will select the correct drawer -> a single line text field
EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, EditorGUI.GetPropertyHeight(element)), element);
// reset to the default color
GUI.color = color;
// If the value is invalid draw a HelpBox to explain why it is invalid
if (string.IsNullOrWhiteSpace(element.stringValue))
{
rect.y += EditorGUI.GetPropertyHeight(element);
EditorGUI.HelpBox(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), "ID may not be empty!", MessageType.Error );
}else if (availableIDs.Count(item => string.Equals(item, element.stringValue)) > 1)
{
rect.y += EditorGUI.GetPropertyHeight(element);
EditorGUI.HelpBox(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), "Duplicate! ID has to be unique!", MessageType.Error );
}
},
// Get the correct display height of elements in the list
// according to their values
// in this case e.g. dependent whether a HelpBox is displayed or not
elementHeightCallback = index =>
{
var element = CharactersList.GetArrayElementAtIndex(index);
var availableIDs = dialogue.CharactersList;
var height = EditorGUI.GetPropertyHeight(element);
if (string.IsNullOrWhiteSpace(element.stringValue) || availableIDs.Count(item => string.Equals(item, element.stringValue)) > 1)
{
height += EditorGUIUtility.singleLineHeight;
}
return height;
},
// Overwrite what shall be done when an element is added via the +
// Reset all values to the defaults for new added elements
// By default Unity would clone the values from the last or selected element otherwise
onAddCallback = list =>
{
// This adds the new element but copies all values of the select or last element in the list
list.serializedProperty.arraySize++;
var newElement = list.serializedProperty.GetArrayElementAtIndex(list.serializedProperty.arraySize - 1);
newElement.stringValue = "";
}
};
// Setup and configure the dialogItemsList we will use to display the content of the DialogueItems
// in a nicer way
dialogItemsList = new ReorderableList(serializedObject, DialogueItems)
{
displayAdd = true,
displayRemove = true,
draggable = true, // for the dialogue items we can allow re-ordering
// As the header we simply want to see the usual display name of the DialogueItems
drawHeaderCallback = rect => EditorGUI.LabelField(rect, DialogueItems.displayName),
// How shall elements be displayed
drawElementCallback = (rect, index, focused, active) =>
{
// get the current element's SerializedProperty
var element = DialogueItems.GetArrayElementAtIndex(index);
// Get the nested property fields of the DialogueElement class
var character = element.FindPropertyRelative(nameof(DialogueElement.CharacterID));
var text = element.FindPropertyRelative(nameof(DialogueElement.DialogueText));
var popUpHeight = EditorGUI.GetPropertyHeight(character);
// store the original GUI.color
var color = GUI.color;
// if the value is invalid tint the next field red
if(character.intValue < 0) GUI.color = Color.red;
// Draw the Popup so you can select from the existing character names
character.intValue = EditorGUI.Popup(new Rect(rect.x, rect.y, rect.width, popUpHeight), new GUIContent(character.displayName), character.intValue, availableOptions);
// reset the GUI.color
GUI.color = color;
rect.y += popUpHeight;
// Draw the text field
// since we use a PropertyField it will automatically recognize that this field is tagged [TextArea]
// and will choose the correct drawer accordingly
var textHeight = EditorGUI.GetPropertyHeight(text);
EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width, textHeight), text);
},
// Get the correct display height of elements in the list
// according to their values
// in this case e.g. we add an additional line as a little spacing between elements
elementHeightCallback = index =>
{
var element = DialogueItems.GetArrayElementAtIndex(index);
var character = element.FindPropertyRelative(nameof(DialogueElement.CharacterID));
var text = element.FindPropertyRelative(nameof(DialogueElement.DialogueText));
return EditorGUI.GetPropertyHeight(character) + EditorGUI.GetPropertyHeight(text) + EditorGUIUtility.singleLineHeight;
},
// Overwrite what shall be done when an element is added via the +
// Reset all values to the defaults for new added elements
// By default Unity would clone the values from the last or selected element otherwise
onAddCallback = list =>
{
// This adds the new element but copies all values of the select or last element in the list
list.serializedProperty.arraySize++;
var newElement = list.serializedProperty.GetArrayElementAtIndex(list.serializedProperty.arraySize - 1);
var character = newElement.FindPropertyRelative(nameof(DialogueElement.CharacterID));
var text = newElement.FindPropertyRelative(nameof(DialogueElement.DialogueText));
character.intValue = -1;
text.stringValue = "";
}
};
// Get the existing character names ONCE as GuiContent[]
// Later only update this if the charcterList was changed
availableOptions = dialogue.CharactersList.Select(item => new GUIContent(item)).ToArray();
}
public override void OnInspectorGUI()
{
DrawScriptField();
// load real target values into SerializedProperties
serializedObject.Update();
EditorGUI.BeginChangeCheck();
charactersList.DoLayoutList();
if(EditorGUI.EndChangeCheck())
{
// Write back changed values into the real target
serializedObject.ApplyModifiedProperties();
// Update the existing character names as GuiContent[]
availableOptions = dialogue.CharactersList.Select(item => new GUIContent(item)).ToArray();
}
dialogItemsList.DoLayoutList();
// Write back changed values into the real target
serializedObject.ApplyModifiedProperties();
}
private void DrawScriptField()
{
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.ObjectField("Script", MonoScript.FromScriptableObject((Dialogue)target), typeof(Dialogue), false);
EditorGUI.EndDisabledGroup();
EditorGUILayout.Space();
}
}
#endif
And this is how it would look like now
Related videos on Youtube
Jorge Luque
If you're interested in 3D modeling, animation, and rendering, then checkout this stackexchange proposal http://area51.stackexchange.com/proposals/86368/3d-graphics?referrer=OlMhNSRB3XOKQcyqcR7pKw2 I make custom skins to modify how websites look with Stylish at http://typhlosion24.deviantart.com I also make music! https://soundcloud.com/guitarjorge24 I'm studying C++, C#, and game development. Some programs I use are Unity, Visual Studio, Photoshop, and Maya. I'm fluent in English and Spanish and I'm currently learning Japanese. Some of my hobbies are playing and composing music, learning Japanese, developing games, reading personal development books, and running.
Updated on June 04, 2022Comments
-
Jorge Luque almost 2 years
I'm making a Unity C# script that is meant to be used by other people as a character dialog tool to write out conversations between multiple game characters.
I have a
DialogueElement
class and then I create a list ofDialogueElement
objects. Each object represents a line of dialogue.[System.Serializable] //needed to make ScriptableObject out of this class public class DialogueElement { public enum Characters {CharacterA, CharacterB}; public Characters Character; //Which characters is saying the line of dialog public string DialogueText; //What the character is saying }
public class Dialogue : ScriptableObject { public string[] CharactersList; //defined by the user in the Unity inspector public List<DialogueElement> DialogueItems; //each element represents a line of dialogue }
I want the user to be able to use the dialog tool by only interacting with the Unity inspector (so no editing code). The problem with this setup then is that the user of the dialogue tool cannot specify their own custom names (such as Felix or Wendy) for the characters in the
Characters
enum since they are hardcoded as "CharacterA" and "CharacterB" in theDialogueElement
class.For those not familiar with Unity, it is a game creation program. Unity lets users create physical files (known as scriptable objects) that acts as containers for class objects. The public variables of the scriptable object can be defined through a visual interface called the "inspector" as you can see below:
I want to use an enum to specify which characters is the one saying the line of dialog because using an enum creates a nice drop-down menu in the inspector where the user can easily select the character without having to manually type the name of the character for each line of dialogue.
How can I allow the user to define the elements of the
Characters
enum? In this case I was trying to use a string array variable where the player can type the name of all the possible characters and then use that array to define the enum.I don't know if solving the problem this way is possible. I'm open to ANY ideas that will allow the user to specify a list of names that can then be used to create a drop down menu in the inspector where the user selects one of the names as seen in the image above.
The solution doesn't need to specifically declare a new enum from a string array. I just want to find a way to make this work. One solution I thought of is to write a separate script that would edit the text of the C# script that contains the Character enum. I think this would technically work since Unity automatically recompiles scripts every time it detects they were changed and updates the scriptable objects in the inspector, but I was hoping to find a cleaner way.
Link to repository for reference:
https://github.com/guitarjorge24/DialogueTool-
derHugo about 4 yearsYou can't change the
enum
itself as it needs to be compiled (well it is not completely impossible but I wouldn't recommend to go ways like actively change a script and force a re-compile) ;) What you can do however would be a custom editor and use something likeEditorGUILayout.Popup
which you can feed with astring
array of available options (including User defined ones), or since you already have a Characters List simply use their indices -
derHugo about 4 yearsCould you add the type
Characters
?
-
-
Jorge Luque about 4 yearsThanks, this is great! However, there is a problem where I can't remove characters from the Character List by clicking the minus " - " symbol. Do you know why this happens?
-
derHugo about 4 yearsIs the item you want to remove selected?
-
Jorge Luque about 4 yearsOh I see, I thought that clicking on the textbox to edit the character name counted as selecting the item but I had to click on "element 1" and such. Thanks again! Is there a resource you recommend for learning about Unity Editor scripts? I want to add more properties from the DialogueElement class like a character picture, a GUIStyle property to select the text color, and more.
-
derHugo about 4 yearsglad to help :) Honestly I learned it by trial and error over 3 months and looking into the API and google a lot .. oh and StackOverflow ;) ... unfortunately I don't know a specific source I could recommend here apart from that :)
-
Jorge Luque about 4 yearsoh wow, that's pretty impressive. There has been a lag problem with the custom editor code once I have about 17 or more dialog items in a single Dialogue scriptable object. Like I would finish typing a sentence but the text won't finish appearing in the text box in the inspector until like 4 seconds later. I reverted back to an older project version before I modified the editor script you shared here and it still lags a lot. I then reverted to before I used the custom editor script and there is no lag even with more than 20 dialogue items. Do you have any idea what could be causing this?
-
Jorge Luque about 4 yearsI've been reading on forums that it's best to avoid doing any processing in OnInspectorGUI() but I'm not sure where or when else would I do the processing for the reorderable lists.
-
derHugo about 4 yearsThe most expensive thing is currently probably
var availableOptions = dialogue.CharactersList.Select(item => new GUIContent(item)).ToArray();
What you could do instead is process this only once perOnInspectorGUI
and rather store the result in a field likeprivate GUIContent[] availableOptions;
.. updated it in the code above. This should hopefully improve the performance -
Jorge Luque almost 4 yearsHi derHugo, thanks again for you help. I tried the new code you updated and I also tried tweaking the code myself but couldn't fix the lag issue. Maybe it's not the CharactersList but the dialogItemsList that is making it lag? I know you have already helped me a lot and I'm very grateful. Would you mind taking a look at my tool again if I paid you? github.com/guitarjorge24/DialogueTool I'm a game dev student and I'm trying to fix this before the semester's end so I'd really appreciate any pointers on what else could be causing the extreme lag.