still no search field in the inspector...

when coming back from unreal to unity, you realize that your work goes to a crawl and see that there is no search in the inspector

9690746--1382690--upload_2024-3-9_20-23-10.png

vital because in production, a gamobject easily contains 10 components totaling 50 variables
when making a decision of changing a variable during tuning, one has to switch context from decision to MANUAL search mode, this context switching kicks you out of flow, which is extremely costly

because unreal has search inspector, it doesn’t require context switch to manual search so it is far better at keeping you in the state of flow, which is simply astonishing because unity is smoother

as a result, instead of much preferring working in unity, which should be the case when you look at the list of pros (for example editor state = game state), preference goes towards unreal.

example of typical gameobject

ECS team, acknowledging this problem already implemented inspector search

4 Likes

So true, I don’t understand how hard this is, when plugins like Odin have done it. Its key and vital.

odin can search only individual components, not entire hierarchy of them for entire gameobject

Hi. Interesting suggestion. We have had limited success doing these type of search fields in parts for example we have something in the property window. It gets complicated when we have to deal with IMGUI and custom property drawers. I’ll bring it up with the team, it would certainly be useful.

GPT-4 was able to do it. Here’s the initial version it came up with.

Code

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

[CustomEditor(typeof(GameObject))]
public class CustomGameObjectInspector : Editor
{
    private string searchString = "";
    private List<string> matchingFieldNames = new List<string>();

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI(); // Draws the default inspector

        GUILayout.Space(10);
        GUILayout.Label("Field Search in Inspector", EditorStyles.boldLabel);

        EditorGUI.BeginChangeCheck();
        searchString = EditorGUILayout.TextField("Search String", searchString);
        if (EditorGUI.EndChangeCheck() && !string.IsNullOrWhiteSpace(searchString))
        {
            SearchFieldsByDisplayName(target);
        }

        if (matchingFieldNames.Any())
        {
            GUILayout.Label("Results:", EditorStyles.boldLabel);
            foreach (var fieldName in matchingFieldNames)
            {
                EditorGUILayout.TextField(fieldName);
            }
        }
    }

    private void SearchFieldsByDisplayName(Object searchTarget)
    {
        matchingFieldNames.Clear();
        if (searchTarget == null) return;

        Component[] components = ((GameObject)searchTarget).GetComponents<Component>();
        foreach (Component component in components)
        {
            SerializedObject serializedObject = new SerializedObject(component);
            SerializedProperty serializedProperty = serializedObject.GetIterator();

            while (serializedProperty.NextVisible(true))
            {
                if (serializedProperty.displayName.ToLowerInvariant().Contains(searchString.ToLowerInvariant()))
                {
                    matchingFieldNames.Add($"{component.GetType().Name}: {serializedProperty.displayName}");
                }
            }
        }

        matchingFieldNames = matchingFieldNames.Distinct().ToList(); // Remove duplicates
    }
}

Screenshot

9696923--1384010--upload_2024-3-12_14-11-35.png

And a version that allows you to directly modify the property fields.

Code

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

[CustomEditor(typeof(GameObject))]
public class CustomGameObjectInspectorWithPropertyModification : Editor
{
    private string searchString = "";
    private List<SerializedProperty> matchingProperties = new List<SerializedProperty>();
    private Dictionary<string, SerializedObject> serializedObjectsCache = new Dictionary<string, SerializedObject>();

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI(); // Draws the default inspector

        GUILayout.Space(10);
        GUILayout.Label("Field Search in Inspector", EditorStyles.boldLabel);

        EditorGUI.BeginChangeCheck();
        searchString = EditorGUILayout.TextField("Search String", searchString);
        if (EditorGUI.EndChangeCheck())
        {
            if (!string.IsNullOrWhiteSpace(searchString))
            {
                SearchFieldsByDisplayName(target);
            }
            else
            {
                matchingProperties.Clear(); // Clear results if search string is cleared
            }
        }

        if (matchingProperties.Any())
        {
            GUILayout.Label("Results:", EditorStyles.boldLabel);
            foreach (var property in matchingProperties)
            {
                SerializedObject serializedObject = serializedObjectsCache[property.propertyPath];
                serializedObject.Update();
                EditorGUILayout.PropertyField(property, true);
                serializedObject.ApplyModifiedProperties();
            }
        }
    }

    private void SearchFieldsByDisplayName(Object searchTarget)
    {
        matchingProperties.Clear();
        serializedObjectsCache.Clear();
        if (searchTarget == null) return;

        Component[] components = ((GameObject)searchTarget).GetComponents<Component>();
        foreach (Component component in components)
        {
            SerializedObject serializedObject = new SerializedObject(component);
            SerializedProperty serializedProperty = serializedObject.GetIterator();
            bool enterChildren = true;

            while (serializedProperty.NextVisible(enterChildren))
            {
                if (serializedProperty.displayName.ToLowerInvariant().Contains(searchString.ToLowerInvariant()))
                {
                    matchingProperties.Add(serializedProperty.Copy());
                    serializedObjectsCache[serializedProperty.propertyPath] = serializedObject;
                }
                enterChildren = false;
            }
        }
    }
}

Screenshot

9696923--1384007--upload_2024-3-12_14-11-11.png

There are limitations with both of these scripts but it’s a good enough proof of concept.

6 Likes

They can’t make a sad context menu work in a year, why do we think they can implement a search?

2 Likes

Yeah, I don’t like to rant too much, but it is quite baffling that the company making game engine (which is really complex and demanding task) fails to perform in such not-groundbreaking features.

“We have had limited success doing these type of search fields” that’s one way to put it :slight_smile:

3 Likes

Yeah, I’m confused by this too, I’ve seen them make similar comments on other things too. Like the title bar that they wanted to change the behavior of and seemingly not knowing basic features of Win32. It seems like obvious things that I’m curious if it’s just management making dumb choices which is admittedly normal for this company.

1 Like

Not as great as inspector search, but you can use

[Header("Properties section")]

to organize all of that in collapsible sections.

https://docs.unity3d.com/ScriptReference/HeaderAttribute.html

1 Like

not useful, vital!

did you see that inspector image I showed you? this was in a 2 people project.

now imagine a project with 10x more people, plus contractors that need to get a hang of it quickly, and realize that time is money.

everything that cuts down onboarding time and trends search down to 0 second is money saved

2 Likes

then

sigh

that’s all we need.

it’s not like you’re releasing an API so there is no need to over engineer things. no need for indexing either, if users complain about perfs on large arrays then yeah you can add that

so, just release what you can but do it next month, then work your way up to handle customization

I’ve had so many cases where I wanted to build an editor tool and didn’t know how to start but GPT-4 was able to generate an example with just a few prompts. The first example code took two prompts and the second example came from the third prompt after I had already started posting and was curious.

1 Like

That’s how we get accused of releasing unfinished features. We already have our roadmap and plans, we don’t have space to add things on a whim. They have to be planned for.

There’s a lot more to it than chat gpt would have you believe.

5 Likes

Not to push Unity Devs even further from communicating with us with rants/complaints…but it is abundantly clear there’s way too much bureaucracy involved in the workflow.

Yet still…major issues go released unnoticed, it takes eons for small adjustments and important fixes, QoL features constantly either work poorly or are tossed under the rug. Priorities are always leaving massive question marks for the majority of us.

Constantly having to work around editor/service issues, given the usual shpeal of how complicated it is or that it’s in the works only to see the thread updated by a random pass-byer years later, “Any updates??” followed by apologies and excuses.

On a personal level I’m finding all joy of game development sucked out by working within the Unity editor, and my only hope is improvements on these future versions. Something as simple as a parameter filter as suggested above would be a tiny but solid improvement, but it’ll have to be yet another custom tool working around the editor.

2 Likes

Same for me too. I requested Editor scene objects pipette from 2022.

2 Likes

Don’t you guys already have search in project settings which does support custom menus? Or at least highlighting

Yes and no. What I generated with ChatGPT is a very minimal proof of concept, but what the team is developing is far more than needed because it has to handle all of the common use cases. If I had proper access to the editor’s interface source code I could make the editor behave in the exact fashion that I wanted it to.

I’m tempted to try modifying the UnityEngine assembly because the source code (albeit in a reverse engineered format rather than the original) is right there. I wouldn’t even need to make extensive changes. In some cases it’s literally just switching access permissions from internal or private to public to enable a feature in a custom editor.

Yeah, but that’s broken too… as always.

1 Like

great script, made some tweaks to it, mainly due to how it is presented(didnt care for the text only representation of the original inspector) - search for unity's inspector · GitHub


honestly its so simple, kinda wondering what pitfalls there are for it. One thing I did notice, it wont find fields inside of a serialized struct within a monobehaviour

1 Like

Approximately one dozen prompts later… :stuck_out_tongue:

Screenshot

9699647--1384778--upload_2024-3-13_19-50-31.png

Code

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

namespace Junk.Utilities
{
    public static class SearchInspector
    {
        private static string searchString = "Search...";
        private static Dictionary<string, List<SerializedProperty>> matchingProperties = new Dictionary<string, List<SerializedProperty>>();
        private static Dictionary<string, SerializedObject> serializedObjectsCache = new Dictionary<string, SerializedObject>();

        [InitializeOnLoadMethod]
        static void Init()
        {
            UnityEditor.Editor.finishedDefaultHeaderGUI += OnDisplaySearch;
        }

        static void OnDisplaySearch(UnityEditor.Editor editor)
        {
            if (!(editor.target is GameObject))
                return;

            EditorGUI.BeginChangeCheck();
            searchString = EditorGUILayout.TextField(searchString);
            if (EditorGUI.EndChangeCheck())
            {
                if (!string.IsNullOrWhiteSpace(searchString))
                {
                    SearchFieldsAndProperties(editor.target);
                }
                else
                {
                    matchingProperties.Clear();
                    serializedObjectsCache.Clear();
                }
            }

            if (matchingProperties.Any())
            {
                GUILayout.Label("Results:", EditorStyles.boldLabel);
                foreach (var kvp in matchingProperties)
                {
                    // Get display name for the parent path.
                    string parentDisplayName = GetDisplayNameFromPath(kvp.Key);
                    SerializedObject serializedObject = serializedObjectsCache[kvp.Key];
                    serializedObject.Update();
                    EditorGUI.indentLevel++;

                    // Track if we have shown the struct/class label.
                    bool shownParentLabel = false;

                    foreach (SerializedProperty property in kvp.Value)
                    {
                        // Check if we should skip the parent label.
                        if (!shownParentLabel && parentDisplayName != property.displayName)
                        {
                            GUILayout.Label(parentDisplayName, EditorStyles.boldLabel);
                            shownParentLabel = true;
                        }

                        EditorGUILayout.PropertyField(property, true);
                    }
                    EditorGUI.indentLevel--;
                    serializedObject.ApplyModifiedProperties();
                }
            }
        }

        private static void SearchFieldsAndProperties(Object searchTarget)
        {
            matchingProperties.Clear();
            serializedObjectsCache.Clear();
            if (searchTarget == null) return;

            Component[] components = ((GameObject)searchTarget).GetComponents<Component>();
            foreach (Component component in components)
            {
                SerializedObject serializedObject = new SerializedObject(component);
                SerializedProperty serializedProperty = serializedObject.GetIterator();
                bool enterChildren = true;

                while (serializedProperty.NextVisible(enterChildren))
                {
                    if (PropertyMatchesSearch(serializedProperty))
                    {
                        AddPropertyIfNotAdded(serializedProperty, serializedObject);
                    }
                    else if (serializedProperty.hasChildren)
                    {
                        SearchWithinSerializedProperty(serializedProperty, serializedObject);
                    }
                    enterChildren = false;
                }
            }
        }

        private static bool PropertyMatchesSearch(SerializedProperty property)
        {
            return property.displayName.ToLowerInvariant().Contains(searchString.ToLowerInvariant());
        }

        private static void AddPropertyIfNotAdded(SerializedProperty property, SerializedObject serializedObject)
        {
            string parentPath = GetPropertyParentPath(property);
            if (!matchingProperties.ContainsKey(parentPath))
            {
                matchingProperties[parentPath] = new List<SerializedProperty>();
            }
            if (!matchingProperties[parentPath].Any(p => p.propertyPath == property.propertyPath))
            {
                matchingProperties[parentPath].Add(property.Copy());
                if (!serializedObjectsCache.ContainsKey(parentPath))
                {
                    serializedObjectsCache[parentPath] = serializedObject;
                }
            }
        }

        private static void SearchWithinSerializedProperty(SerializedProperty parentProperty, SerializedObject parentSerializedObject)
        {
            var childSerializedProperty = parentProperty.Copy();
            bool enterChildren = true;
            while (childSerializedProperty.NextVisible(enterChildren))
            {
                if (PropertyMatchesSearch(childSerializedProperty))
                {
                    AddPropertyIfNotAdded(childSerializedProperty, parentSerializedObject);
                }
                enterChildren = false;
            }
        }

        private static string GetPropertyParentPath(SerializedProperty property)
        {
            var path = property.propertyPath;
            int dotIndex = path.LastIndexOf('.');
            if (dotIndex != -1)
            {
                return path.Substring(0, dotIndex);
            }
            return property.propertyPath; // Return the full path if no parent is identified
        }

        private static string GetDisplayNameFromPath(string path)
        {
            // Convert a path into a user-friendly display name.
            var parts = path.Split('.');
            if (parts.Length > 0)
            {
                // Attempt to infer a user-friendly name from the last segment of the path
                return ObjectNames.NicifyVariableName(parts.Last());
            }
            return path; // Fallback to the path if unable to process
        }
    }
}

Sample class

using System;
using UnityEngine;

public class Foo : MonoBehaviour
{
    [Serializable]
    public struct MyStruct
    {
        public int x;
        public float someFloat;
        public Color color;
    }

    public MyStruct myStruct;
}
5 Likes