Hey,
I’m trying to make a custom inspector for a script. One of the important elements inside the inspector is a list of commands. This list gets updated based on another list and so it’s okay to just list out the commands without the need of a reorderable list. The problem occurs inside each command, as each one needs to have a reorderable list of actions. Each action has a pretty complicated structure as I need to setup the conditions, the unity event results etc. Because the structure is so complicated, I decided to do it myself and to do a custom ReorderableList. Here’s how it looks like. There is one command called default and one action I added:
My solution works except for one thing: when I actually try to reorder the elements, the entire Unity software crashes:
This window shows up and after about a minute and a half Unity closes itself and a Unity Bug Reporter window shows up.
By detecting when I let go of the left mouse button, I was able to debug the code when I let go of the element that I’m trying to reorder and I found out where exactly the program crashes but I’m not able to find a reason for it. Here’s the code of the Unity editor as well as the runtime script itself and afterwards I describe where it fails:
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using System;
[CustomEditor(typeof(Interactable))]
public class InteractableEditor : Editor
{
Interactable m_Target;
SerializedProperty itemProperty;
SerializedProperty onCursorEnterProperty;
SerializedProperty onCursorExitProperty;
SerializedProperty commandsProperty;
readonly Dictionary<int, ReorderableList> actionLists = new();
readonly List<bool> foldouts = new(); // Stores open/close state for each Name
bool foldCommands = true; // Toggle visibility of the entire "Commands" section
bool foldAll = true; // Toggle all Commands at once
protected virtual void OnEnable()
{
m_Target = (Interactable)target;
if (m_Target == null)
return;
itemProperty = serializedObject.FindProperty("Item");
onCursorEnterProperty = serializedObject.FindProperty("OnCursorEnter");
onCursorExitProperty = serializedObject.FindProperty("OnCursorExit");
commandsProperty = serializedObject.FindProperty("Commands");
// Initialize foldouts list to match the number of Commands
for (int i = 0; i < commandsProperty.arraySize; i++)
{
foldouts.Add(false); // Default: not expanded
}
UpdateCommands();
//SetCommands();
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// Get the target object
m_Target = (Interactable)target;
if (m_Target == null)
return;
if (Event.current.type == EventType.MouseUp)
{
if (Event.current.button == 0)
Debug.Log("Left mouse released."); // for detecting crash
}
// Draw other property fields in the Inspector
EditorGUILayout.PropertyField(itemProperty);
UpdateCommands();
SetCommands();
EditorGUILayout.PropertyField(onCursorEnterProperty);
EditorGUILayout.PropertyField(onCursorExitProperty);
serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// Sets up the Commands property objectField.
/// </summary>
protected void SetCommands()
{
foldCommands = EditorGUILayout.Foldout(foldCommands, "Commands", true);
EditorGUILayout.BeginVertical("box");
if (foldCommands)
{
if (commandsProperty.arraySize > 0)
{
GUILayout.Space(2);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button(foldAll ? "Collapse All" : "Expand All", GUILayout.Width(EditorGUIUtility.currentViewWidth - 50)))
{
foldAll = !foldAll;
for (int i = 0; i < foldouts.Count; i++) foldouts[i] = foldAll;
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
GUILayout.Space(2);
}
if (commandsProperty.arraySize == 0)
EditorGUILayout.HelpBox("No commands available.", MessageType.Info);
for (int i = 0; i < commandsProperty.arraySize; i++)
{
SerializedProperty commandElementProp = commandsProperty.GetArrayElementAtIndex(i);
SerializedProperty itemProp = commandElementProp.FindPropertyRelative("data");
SerializedProperty actionsProp = commandElementProp.FindPropertyRelative("actions");
EditorGUI.indentLevel++;
foldouts[i] = EditorGUILayout.Foldout(foldouts[i], itemProp.FindPropertyRelative("name").stringValue, true);
if (foldouts[i])
{
EditorGUILayout.BeginVertical("box");
if (!actionLists.ContainsKey(i))
{
actionLists[i] = CreateReorderableActionList(actionsProp);
}
actionLists[i].DoLayoutList();
EditorGUILayout.EndVertical();
}
EditorGUI.indentLevel--;
}
}
EditorGUILayout.EndVertical();
}
private ReorderableList CreateReorderableActionList(SerializedProperty actionsProp)
{
var list = new ReorderableList(actionsProp.serializedObject, actionsProp, true, true, true, true)
{
drawHeaderCallback = (Rect rect) =>
{
EditorGUI.LabelField(rect, "Actions");
},
drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
{
SerializedProperty actionProp = actionsProp.GetArrayElementAtIndex(index);
SerializedProperty moveCloserProp = actionProp.FindPropertyRelative("moveCloser");
SerializedProperty conditionsProp = actionProp.FindPropertyRelative("conditions");
SerializedProperty previousInteractablesProp = actionProp.FindPropertyRelative("previousInteractables");
SerializedProperty leftActionProp = actionProp.FindPropertyRelative("leftMouseButtonActions");
SerializedProperty rightActionProp = actionProp.FindPropertyRelative("rightMouseButtonActions");
EditorGUI.BeginProperty(rect, GUIContent.none, actionProp);
Rect r = rect;
r.y += EditorGUIUtility.singleLineHeight + 4f; // Move content down
r.height = EditorGUIUtility.singleLineHeight;
float indent = 15f; // Optional horizontal shift to indent a bit
r.x += indent;
r.width -= indent;
EditorGUI.PropertyField(r, previousInteractablesProp, new GUIContent("Previous Interactables"), true);
r.y += EditorGUI.GetPropertyHeight(previousInteractablesProp, true) + 2f;
EditorGUI.PropertyField(r, conditionsProp, new GUIContent("Conditions"), true);
r.y += EditorGUI.GetPropertyHeight(conditionsProp, true) + 2f;
EditorGUI.LabelField(r, "Add condition");
r.y += 5f;
// Add buttons to add conditions (inside each action's list)
Rect buttonRect = r;
buttonRect.y += r.height;
buttonRect.width /= 4f; // Adjust width for buttons
if (GUI.Button(buttonRect, "Bool"))
{
AddConditionToAction(conditionsProp, typeof(BoolCondition));
}
buttonRect.x += buttonRect.width;
if (GUI.Button(buttonRect, "String"))
{
AddConditionToAction(conditionsProp, typeof(StringCondition));
}
buttonRect.x += buttonRect.width;
if (GUI.Button(buttonRect, "Int"))
{
AddConditionToAction(conditionsProp, typeof(IntCondition));
}
buttonRect.x += buttonRect.width;
if (GUI.Button(buttonRect, "Float"))
{
AddConditionToAction(conditionsProp, typeof(FloatCondition));
}
r.y += buttonRect.height * 3 + 4f;
EditorGUI.PropertyField(r, moveCloserProp, new GUIContent("Move closer?"));
r.y += EditorGUIUtility.singleLineHeight + 4f;
// Left Mouse Button Actions
EditorGUI.PropertyField(r, leftActionProp, new GUIContent("Left Mouse Actions"), true);
r.y += EditorGUI.GetPropertyHeight(leftActionProp, true) + 2f;
// Right Mouse Button Actions
EditorGUI.PropertyField(r, rightActionProp, new GUIContent("Right Mouse Actions"), true);
r.y += EditorGUI.GetPropertyHeight(rightActionProp, true) + 2f;
EditorGUI.EndProperty();
},
elementHeightCallback = (int index) =>
{
SerializedProperty actionProp = actionsProp.GetArrayElementAtIndex(index);
float height = EditorGUIUtility.singleLineHeight + 2;
height += EditorGUI.GetPropertyHeight(actionProp.FindPropertyRelative("leftMouseButtonActions"));
height += EditorGUI.GetPropertyHeight(actionProp.FindPropertyRelative("rightMouseButtonActions"));
height += EditorGUI.GetPropertyHeight(actionProp.FindPropertyRelative("conditions"));
height += EditorGUI.GetPropertyHeight(actionProp.FindPropertyRelative("previousInteractables"));
height += EditorGUI.GetPropertyHeight(actionProp.FindPropertyRelative("moveCloser"));
return height + 80; // add some padding
}
};
return list;
}
private void AddConditionToAction(SerializedProperty conditionsProp, Type conditionType)
{
var newCondition = Activator.CreateInstance(conditionType);
conditionsProp.arraySize++;
conditionsProp.GetArrayElementAtIndex(conditionsProp.arraySize - 1).managedReferenceValue = newCondition;
}
/// <summary>
/// Updates the Commands property according to GameManager.Instance.Commands.
/// </summary>
protected void UpdateCommands()
{
// Retrieve Name list from the GameManager
var gmCommands = new List<CommandData>();
if (GameManager.Instance == null)
gmCommands = FindObjectOfType<GameManager>().Commands;
else
gmCommands = GameManager.Instance.Commands;
var updatedCommands = new List<CommandAction>();
var updatedFoldouts = new List<bool>();
while (foldouts.Count < m_Target.Commands.Count)
{
foldouts.Add(true);
}
// Handles new Commands in GM
foreach (var gm_c in gmCommands)
{
// Preserve existing Commands if they match a Name
var existingCommand = m_Target.Commands.Find(c => c.data.guid == gm_c.guid);
if (existingCommand != null)
{
existingCommand.data.name = gm_c.name;
updatedCommands.Add(existingCommand); // Keep existing Name
updatedFoldouts.Add(foldouts[m_Target.Commands.IndexOf(existingCommand)]);
}
else
{
updatedCommands.Add(new CommandAction(gm_c)); // Create new Name
updatedFoldouts.Add(false);
}
}
// Update EnvironmentObject Commands and mark object as modified
m_Target.Commands = updatedCommands;
}
}
and now the script for Interactable:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class Interactable : MonoBehaviour
{
public Item Item;
public UnityEvent OnCursorEnter;
public UnityEvent OnCursorExit;
public List<CommandAction> Commands = new();
}
/// <summary>
/// Defines the Name and the action of a Name.
/// </summary>
[System.Serializable]
public class CommandAction
{
[HideInInspector] public CommandData data;
public List<Action> actions;
public CommandAction(CommandData item)
{
data = item;
}
}
[System.Serializable]
public class Action
{
public List<Interactable> previousInteractables;
[SerializeReference] public List<Condition> conditions;
public bool moveCloser;
public UnityEvent leftMouseButtonActions;
public UnityEvent rightMouseButtonActions;
public UnityEvent CallLeftOrRightEvent(bool left)
{
return left ? leftMouseButtonActions : rightMouseButtonActions;
}
}
So the place where it crashes is the line in SetCommands() method:
actionLists[i].DoLayoutList();
At first, it runs normally: the code in drawElementCallback in CreateReorderableActionList(...) gets called as many times as there are elements in the ReorderableList. Finally after the last element runs the line EditorGUI.EndProperty();, Unity crashes as described above. There is no exception as far as I can see, no warning, just a crash.
I also noticed that when I unfold and show all the reorderable lists (meaning actions) in editor, the inspector gets slower the more actions there are. The scrolling becomes pretty laggy and it might have something to do with the crash.
I feel like I’m doing something wrong with the ReorderableList but I don’t know what. I tried to follow tutorials but my custom list is pretty complex and I might’ve done something very wrong. I haven’t had much experience with ReorderableList before trying this thing.
I’m looking for help with either:
- Showing me how to fix this mess while keeping the ReorderableList.
- Showing me how to set it up in another way so that I can achieve a similar effect as right now.
If there’s anything I failed to explain properly, let me know. Any help would be appreciated!

