Lista desplegable personalizada en el Editor de Unity

El Editor de Unity, al mostrar un script en el inspector, permite controlar cómo se muestran los campos públicos de este, dependiendo del tipo de datos del campo. Si queremos mostrar una lista de valores de los cuales seleccionar uno, podemos crear un campo con un tipo enum.

public enum Opciones {
    UnaOpcion,
    Otra,
    LaUltima
}

public Opciones opcion = Opciones.Otra;

Esto en el inspector se visualizaría así:

Simple y efectivo. Sin embargo, esta opción puede resultar limitada en ciertas circunstancias. A partir de lo publicado en este artículo https://coderwall.com/p/wfy-fa/show-a-popup-field-to-serialize-string-or-integer-values-from-a-list-of-choices-in-unity3d podemos crear un atributo personalizado con el que controlar mejor que elementos se muestran en el desplegable.

StringInList.cs

using System;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

/// <summary>
/// Crea en el editor un selector con opciones.
/// </summary>
public class StringInList : PropertyAttribute
{
    public delegate string[] GetStringList();

    /// <summary>
    /// Muestra un selector con opciones.
    /// </summary>
    /// <param name="list">Opciones</param>
    public StringInList(params string[] list)
    {
        List = list;
    }

    /// <summary>
    /// Crea un selector a partir de la lista que devuelve el método invocado.
    /// </summary>
    /// <param name="type">Clase generadora</param>
    /// <param name="methodName">Nombre del método generador</param>
    public StringInList(Type type, string methodName)
    {
        var method = type.GetMethod(methodName);
        if (method != null)
        {
            List = method.Invoke(null, null) as string[];
            if (List.Length == 0) List = new[] { "" };
        }
        else
        {
            Debug.LogError("No existe el método " + methodName + " en " + type);
        }
    }

    public string[] List
    {
        get;
        private set;
    }
}

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(StringInList))]
public class StringInListDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var stringInList = attribute as StringInList;
        var list = stringInList.List;
        if (property.propertyType == SerializedPropertyType.String)
        {
            int index = Mathf.Max(0, Array.IndexOf(list, property.stringValue));
            index = EditorGUI.Popup(position, property.displayName, index, list);

            property.stringValue = list[index];
        }
        else if (property.propertyType == SerializedPropertyType.Integer)
        {
            property.intValue = EditorGUI.Popup(position, property.displayName, property.intValue, list);
        }
        else
        {
            base.OnGUI(position, property, label);
        }
    }
}
# endif

PropertyDrawersHelper.cs

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public static class PropertyDrawersHelper
{
#if UNITY_EDITOR

    /// <summary>
    /// Obtiene los nombres de las escenas del proyecto que estén en la lista de Build (File > Build Settings).
    /// </summary>
    /// <returns></returns>
    public static string[] AllSceneNames()
    {
        var temp = new List<string>();
        foreach (EditorBuildSettingsScene S in EditorBuildSettings.scenes)
        {
            if (S.enabled)
            {
                string name = S.path.Substring(S.path.LastIndexOf('/') + 1);
                name = name.Substring(0, name.Length - 6);
                temp.Add(name);
            }
        }
        return temp.ToArray();
    }

    /// <summary>
    /// Obtiene los ejes definidos para la clase Input (Edit > Project Settings > Input).
    /// </summary>
    /// <returns></returns>
    public static string[] AllAxes()
    {
        var temp = new List<string>();

        var inputManager = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/InputManager.asset")[0];
        SerializedObject obj = new SerializedObject(inputManager);
        SerializedProperty axisArray = obj.FindProperty("m_Axes");

        if (axisArray.arraySize == 0)
            Debug.Log("No Axes");

        for (int i = 0; i < axisArray.arraySize; ++i)
        {
            var axis = axisArray.GetArrayElementAtIndex(i);

            var name = axis.FindPropertyRelative("m_Name").stringValue;

            temp.Add(name);
        }

        return temp.ToArray();
    }

#endif
}

Ejemplo de uso

Para usar el atributo sólo hay que crear los dos archivos en nuestro proyecto de Unity, en cualquier carpeta que no se llame «Editor», y ya estará disponible.

using UnityEngine;

public class CustomDrawerSample : MonoBehaviour
{
    // Almacena el string seleccionado
    [StringInList("Gato", "Perro")] 
    public string Animal;

    // Almacena el índice del string seleccionado
    [StringInList("Juan", "José", "Joaquín")] 
    public int PersonID;

    // Muestra una lista con las escenas
    [StringInList(typeof(PropertyDrawersHelper), "AllSceneNames")] 
    public string SceneName;

    // Muestra una lista con los ejes de Input
    [StringInList(typeof(PropertyDrawersHelper), "AllAxes")]
    public string InputAxis;

    void Start()
    {
        Debug.Log(Animal);
        Debug.Log(PersonID);
        Debug.Log(SceneName);
        Debug.Log(InputAxis);
    }
}

Este script en el inspector se muestra así: