Custom Editor showing a List as a dropdown Menu ?

[TL;DR]

  • Class “A” is a database of objects of Class “B”.
  • Class “A” also holds a list of objects of Class “C”.
  • Class “C” is a type that is used for objects of Class “B”, it defines a name for the type, and a description for the type.
  • Objects of Class “B” have a reference to a database (an instance of Class “A” in a scene) they draw some parameters from. (i.e. an Object of Class “B” needs a database attached, so that it can only have parameters that exist in the instance of the database).
  • I need to use a list of objects of Class “C”, rather than an enum, because I want those types to be editable (add or remove types, change their descriptions and names, etc.)
  • When I edit an instance of an object of Class “B”, I want the Unity Editor to show a dropdown selection holding all the types (instances) of Class “C” that are user-defined in the database (instance of Class “A”), that is referenced by the instance of Class “B” being edited, in order to choose a type (Class “C”) for my instanced object of Class “B”.

Hi everyone, I’m currently working on an need-based AI system, and it seems I have no other choice than to delve into Custom Editors to achieve what I’m looking for. Unfortunately, it’s been a week of struggle to try and achieve a seemingly simple thing : displaying a list<> taken from a master Database in a dropdown menu.

Any help would be GREATLY appreciated :slight_smile:

So, my system is structured as follows:

  • An Agent Class for entities that will have AI
  • An Agent Database containing all the Agents types (human, animal, etc.)
  • A Need Database containing all the different needs an Agent can have (a Need itself holding to what Types of Agent it applies).

I started using enums, and that worked great, but it’s not flexible at all, i.e. I can’t change the Agent types in the editor, add or remove Agent Types, etc.

So I transitioned to using lists.

So an Agent Database Object in a scene will hold, among other things, a list of Agent Types. And when an Agent Object is selected, I’d like to display a dropdown menu to be able to assign a Type to this Agent, and this is where I’m struggling, because lists just display as arrays in the inspector.

Here is my AgentDatabase class below (class “A” in the TL;DR):

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

namespace Storynity.Agents
{
	public class AgentDatabase : MonoBehaviour {

		public static AgentDatabase currentAgentDB;

		void Awake()
		{
			currentAgentDB = this;
		}

		#region Agent Types
		/// <summary>
		/// Agent types (i.e Human, Animal, God, etc.).
		/// </summary>
		[System.Serializable]
		public class AgentType
		{
			/// <summary>
			/// The Agent Type name.
			/// </summary>
			public string agentTypeName;
			/// <summary>
			/// The Agent Type description.
			/// </summary>
			public string agentTypeDescription;
			/// <summary>
			/// Do Agents of this type have subtypes?
			/// </summary>
			public bool agentHasSubtypes;

			#region Constructors
			/// <summary>
			/// Agent type constructor.  
			/// </summary>
			/// <param name="newName">The name of the Agent Type.</param>
			/// <param name="newDescription">The description of the Agent Type.</param>
			/// <param name="hasSubtypes">Can this Agent Type have Subtypes or not.</param>
			public AgentType(string newName, string newDescription, bool hasSubtypes)
			{
				agentTypeName = newName;
				agentTypeDescription = newDescription;
				agentHasSubtypes = hasSubtypes;
			}

			/// <summary>
			/// Agent type constructor.
			/// Defaults hasSubtypes to false.
			/// </summary>
			/// <param name="newName">The name of the Agent Type.</param>
			/// <param name="newDescription">The description of the Agent Type.</param>
			public AgentType(string newName, string newDescription)
			{
				agentTypeName = newName;
				agentTypeDescription = newDescription;
				agentHasSubtypes = false;
			}

			/// <summary>
			/// Agent type constructor.
			/// Defaults Agent type name and description to empty strings.
			/// Defaults hasSubtypes to false.
			/// </summary>
			public AgentType()
			{
				agentTypeName = "";
				agentTypeDescription = "";
				agentHasSubtypes = false;
			}
			#endregion
		}

		/// <summary>
		/// The agent type list for this Agent Database.
		/// </summary>
		public List<AgentType> agentTypeList = new List<AgentType>();

		public int GetAgentTypeIDFromName(string name)
		{
			int agentTypeID = -1;
			for (int i=0; i < agentTypeList.Count; i++)
			{
				if (agentTypeList*.agentTypeName == name)*
  •  		{*
    
  •  			agentTypeID = i;*
    
  •  			return (agentTypeID);*
    
  •  		}*
    
  •  	}*
    
  •  	Debug.LogWarning ("AgentType (" + name + ") doesn't exist in Agent Database (" + currentAgentDB.name + "), returned -1.");*
    
  •  	return (agentTypeID);*
    
  •  }*
    
  •  public void RemoveAgentTypeFromDB(string name)*
    
  •  {*
    
  •  	agentTypeList.RemoveAt (GetAgentTypeIDFromName (name));*
    
  •  }*
    
  •  public void AddAgentTypeToDB(string name)*
    
  •  {*
    
  •  	agentTypeList.Add(new AgentType(name, ""));*
    
  •  }*
    
  •  public void AddAgentTypeToDB(string name, string description)*
    
  •  {*
    
  •  	agentTypeList.Add (new AgentType (name, description));*
    
  •  }*
    
  •  public string[] CopyAgentTypeNamesToStringArray()*
    
  •  {*
    
  •  	string[] agentTypesNames = new string[0];*
    
  •  	for (int i=0; i < agentTypeList.Count; i++)*
    
  •  	{*
    

agentTypesNames = agentTypeList .agentTypeName;
* }*
* return (agentTypesNames);*
* }*

* #endregion*

* #region Agent Subtypes*
* ///

*
* /// Agent Subtypes (i.e Jobs for humans, Diets for Animals, Good or Evil for Gods, etc.).*
* ///
*
* [System.Serializable]*
* public class AgentSubtype*
* {*
* /// *
* /// The Agent Subtype name.*
* ///
*
* public string agentSubtypeName;*
* /// *
* /// The Agent Subtype description.*
* ///
*
* public string agentSubtypeDescription;*

* #region Constructors*
* ///

*
* /// Agent Job constructor.*
* ///
*
* /// The name of the Agent Job.*
* /// The description of the Agent Job.*
* public AgentSubtype(string newName, string newDescription)*
* {*
* agentSubtypeName = newName;*
* agentSubtypeDescription = newDescription;*
* }*

* ///

*
* /// Agent Job constructor.*
* /// Defaults Job description to an empty string.*
* ///
*
* /// The name of the Agent Job.*
* public AgentSubtype(string newName)*
* {*
* agentSubtypeName = newName;*
* agentSubtypeDescription = “”;*
* }*

* ///

*
* /// Agent Job constructor.*
* /// Defaults Job description and name to empty strings.*
* ///
*
* public AgentSubtype()*
* {*
* agentSubtypeName = “”;*
* agentSubtypeDescription = “”;*
* }*
* #endregion*
* }*

* public List agentSubtypeList = new List();*

* public int GetAgentSubtypeIDFromName(string name)*
* {*
* int agentSubtypeID = -1;*
* for (int i=0; i < agentSubtypeList.Count; i++)*
* {*
_ if (agentSubtypeList*.agentSubtypeName == name)
{
agentSubtypeID = i;
return (agentSubtypeID);
}
}
Debug.LogWarning (“AgentJob (” + name + “) doesn’t exist in Agent Database (” + currentAgentDB.name + “), returned -1.”);
return (agentSubtypeID);
}*_

* public void RemoveAgentSubtype(string name)*
* {*
* agentSubtypeList.RemoveAt (GetAgentSubtypeIDFromName (name));*
* }*

* public void AddAgentSubtype(string name)*
* {*
* agentSubtypeList.Add(new AgentSubtype(name, “”));*
* }*

* public void AddAgentSubtype(string name, string description)*
* {*
* agentSubtypeList.Add (new AgentSubtype (name, description));*
* }*
* #endregion*
* }*
}
Here is the Agent Class script (class “B” in the TL;DR):
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Storynity.Needs;
using Storynity.Agents;

[System.Serializable]
public class Agent : MonoBehaviour
{
* public AgentDatabase agentDB;*
* public string agentName;*
* public string agentDescription;*
* public AgentDatabase.AgentType agentType; // this is what I want to be able to display as a drop list to select only one from the available AgentDatabase.Agentypes in the referenced AgentDatabase*
}
Here is what I’ve got so far for the custom Editor:
using UnityEngine;
using UnityEditor;
using Storynity.Agents;

[CustomEditor(typeof(Agent))]
public class AgentEditor : Editor
{
* int index = 0;*
* string[] typeOptions = new string[]{ “test”, “human” }; //Here is where I’m trying to get the string array of the Types Names, but I’m unable to correctly access the database instance referenced by the Agent instance i’m editing (tried something like AgentDatabase.currentAgentDB.CopyAgentTypeNamesToStringArray() to no avail.);*

* public override void OnInspectorGUI()*
* {*
* //base.OnInspectorGUI();*
* serializedObject.Update ();*
* EditorGUILayout.PropertyField (serializedObject.FindProperty (“agentDB”));*
* EditorGUILayout.PropertyField (serializedObject.FindProperty (“agentName”));*
* EditorGUILayout.PropertyField (serializedObject.FindProperty (“agentDescription”));*
* index = EditorGUILayout.Popup(“Agent Type:”,*
* index, typeOptions);*

* serializedObject.ApplyModifiedProperties ();*
* }*
}

Apologies for the long code paste, but I want to make sure everything is accessible to understand what I’m doing.
So in the Scene, I have a GameObject holding an AgentDatabase, and when assign to an Agent, I’d like to be able to display in the editor of that Agent the Agent Type List of the Database in a dropdown form, to assign a Type to that Agent.
Hop I’m clear :slight_smile:
Thanks again if you took the time to read the details of my issue !
Cheers
[edit screenshots]
Here’s what a typical Agent Database would typically look like in inspector:
[60771-screen-2015-12-28-a-085108.jpg|60771]
Now once a GameOject of type Agent is created, and once an AgentDatabase has been assigned to it, here is what I’m trying to achieve:
[60772-screen-2015-12-28-a-085417.jpg*|60772]*
*
*

After googling for sometime, I found out there isn’t a way to use a list directly. So here is my solution,

using System.Linq;

private string[] category[];

private void ReloadList()
        {
            category = null;
            category = list.Select(I => I.name).ToArray();
        }

//And within OnGUI
 categoryInt = EditorGUILayout.Popup("Category", categoryInt, category, EditorStyles.popup);

The ReloadList is called OnEnabled and also a option within the EditorWindow to call it manually.

Though I am not entirely sure if this is a good idea. Cause I could remove a category within the list and add another one, and the script will never know it changed. (Or I could remove a category and that index number will be not found which could cause errors). My best option is no never delete any category once made.