PrefabOnly or ChildOnly attribute like item?

Issue #457 resolved
V created an issue

I am currently imagining it as an attribute, but let me sketch the situation for this enhancement.

I often work with artists in my unity project, and most often our project structure works in a way where the root of a prefab acts like the script container, and basically references everything it needs to build up or set up that prefab and do it's things. And often i limit my assignable variables to something limiting so nothing weird might be assigned to it.

More the rule than the exception, we need these variables to be Asset/Prefab references. Any child of this prefab will do. Only sometimes an [AssetsOnly], rarely we make a scene reference or use [SceneObjectsOnly]. I am looking for something like [ChildOfThisOnly].

My original idea was that where the Unity Asset Picker shows "Scene" "Assets" tab, to add a "Prefab/Children" tab to just quickly find "particles" under this specific GameObject, because 9/10 that is the one item i am looking for (the picker is not helping me if i have to look through all eligible items) I often resort to just dragging the child object into the parent instead of using the picker because of this.

An additional benefit might be, that you can select a child object of a prefab, without actually pulling said prefab into the scene to make said change/assign (as it might be more than 2 layers deep, and we all know unity does not allow you to open up a prefab like that unless it is in the scene)

So all in all, this might be both a request for an attribute, and a request for a new type of asset picker, but i imagine the later will be quite hard to add.

Let me know what you think, or ask away if you need more info on it.

Comments (4)

  1. Bjarke Elias

    It's an excellent suggestion, so I just went ahead and added it. It'll be include in the 2.1, but I've pasted all code below if you want to start using it, and perhaps share some feedback should you have any :)

    We've name it ChildGameObjectsOnly for now, to make it a bit more clear where you can use the attribute.

    The AssetsOnly attribute should btw already do the same as a PrefabsOnly attribute would do. Do you have a case where that doesn't work?

    using Sirenix.OdinInspector.Editor;
    using Sirenix.Utilities;
    using Sirenix.Utilities.Editor;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    
    
    public class ChildGameObjectsOnlyAttribute : Attribute
    {
        public bool IncludeSelf = true;
    }
    
    public class ChildrenOnlyAttributeDrawer<T> : OdinAttributeDrawer<ChildGameObjectsOnlyAttribute, T>
    {
        private bool isValidValues;
        private bool rootIsComponent;
        private int rootCount;
    
        protected override void Initialize()
        {
            var entry = this.ValueEntry;
            var root = this.GetRoot(0);
            this.rootIsComponent = root is Component;
            this.rootCount = this.Property.SerializationRoot.BaseValueEntry.WeakValues.Count;
            this.Property.ValueEntry.OnValueChanged += x => this.ValidateCurrentValue();
    
            if (this.rootIsComponent)
            {
                this.ValidateCurrentValue();
            }
        }
    
        private Transform GetRoot(int index)
        {
            // If the user should have the ability to provide an alternative 
            // root component via the attribute, then support for that can be added here.
            var parentValues = this.Property.SerializationRoot.BaseValueEntry.WeakValues;
            var root = parentValues[index] as Component;
            if (root)
            {
                return root.transform;
            }
            return null;
        }
    
        private void ValidateCurrentValue()
        {
            var entry = this.ValueEntry;
    
            this.isValidValues = true;
            if (entry.SmartValue as UnityEngine.Object)
            {
                for (int i = 0; i < this.rootCount; i++)
                {
                    var root = this.GetRoot(i);
                    var uObj = this.ValueEntry.Values[i] as UnityEngine.Object;
                    if (!uObj)
                    {
                        continue;
                    }
    
                    var component = uObj as Component;
                    var go = uObj as GameObject;
                    if (go) component = go.transform;
                    if (!component)
                    {
                        this.isValidValues = false;
                        return;
                    }
    
                    var transform = component.transform;
                    if (!this.Attribute.IncludeSelf && transform == root)
                    {
                        this.isValidValues = false;
                        return;
                    }
    
                    if (IsRootOf(root, transform) == false)
                    {
                        this.isValidValues = false;
                        return;
                    }
                }
            }
        }
    
        private string GetGameObjectPath(Transform root, Transform child)
        {
            if (root == child)
            {
                return root.name;
            }
    
            var path = "";
            var curr = child;
    
            while (curr)
            {
                if (!this.Attribute.IncludeSelf && curr == root)
                {
                    return path.Trim('/');
                }
    
                path = curr.name + "/" + path;
    
                if (this.Attribute.IncludeSelf && curr == root)
                {
                    return path.Trim('/');
                }
    
    
                curr = curr.parent;
            }
    
            return null;
        }
    
        private static bool IsRootOf(Transform root, Transform child)
        {
            var curr = child;
            while (curr)
            {
                if (curr == root)
                {
                    return true;
                }
                curr = curr.parent;
            }
    
            return false;
        }
    
        protected override void DrawPropertyLayout(GUIContent label)
        {
            if (!this.rootIsComponent)
            {
                // Serialization root is not a Component, so it has no children.
                // Should we display a warning or an error here?
                this.CallNextDrawer(label);
                return;
            }
    
            var entry = this.ValueEntry;
    
            // If we want it to validate the value each frame.
            // if (Event.current.type == EventType.Layout)
            // {
            //     this.ValidateCurrentValue();
            // }
    
            if (!this.isValidValues)
            {
                SirenixEditorGUI.ErrorMessageBox("The object must be child of the selected GameObject.");
            }
    
            if (this.rootCount > 1)
            {
                // TODO: Add support for multi-selection to the child-selector dropdown.
                this.CallNextDrawer(label);
                return;
            }
    
            GUILayout.BeginHorizontal();
            {
                var width = 15f;
                if (label != null)
                {
                    width += GUIHelper.BetterLabelWidth;
                }
    
                var newResult = GenericSelector<T>.DrawSelectorDropdown(label, GUIContent.none, this.ShowSelector, GUIStyle.none, GUILayoutOptions.Width(width));
                if (newResult != null && newResult.Any())
                {
                    this.ValueEntry.SmartValue = newResult.FirstOrDefault();
                }
    
                if (Event.current.type == EventType.Repaint)
                {
                    var btnRect = GUILayoutUtility.GetLastRect().AlignRight(15);
                    btnRect.y += 4;
                    SirenixGUIStyles.PaneOptions.Draw(btnRect, GUIContent.none, 0);
                }
    
                GUILayout.BeginVertical();
                this.CallNextDrawer(null);
                GUILayout.EndVertical();
            }
            GUILayout.EndHorizontal();
        }
    
        private OdinSelector<T> ShowSelector(Rect rect)
        {
            var selector = this.CreateSelector();
            rect.x = (int)rect.x;
            rect.y = (int)rect.y;
            rect.width = (int)rect.width;
            rect.height = (int)rect.height;
            rect.xMax = GUIHelper.GetCurrentLayoutRect().xMax;
            selector.ShowInPopup(rect);
            return selector;
        }
    
        private GenericSelector<T> CreateSelector()
        {
            var isGo = typeof(T) == typeof(GameObject);
            var root = this.GetRoot(0);
            var t = typeof(T);
            if (isGo)
            {
                t = typeof(Transform);
            }
    
            IEnumerable<UnityEngine.Object> children = this.GetRoot(0).GetComponentsInChildren(t)
                .Where(x => this.Attribute.IncludeSelf || x.transform != root)
                .OfType<UnityEngine.Object>();
    
            if (isGo)
            {
                children = children.OfType<Component>().Select(x => x.gameObject).OfType<UnityEngine.Object>();
            }
    
            Func<T, string> getName = x =>
            {
                var c = x as Component;
                var o = x as GameObject;
                var transform = c ? c.transform : o.transform;
                return this.GetGameObjectPath(root, transform);
            };
    
            GenericSelector<T> selector = new GenericSelector<T>(null, false, getName, children.OfType<T>());
            selector.SelectionTree.Config.DrawSearchToolbar = true;
            selector.EnableSingleClickToSelect();
            selector.SetSelection(this.ValueEntry.SmartValue);
            selector.SelectionTree.EnumerateTree().AddThumbnailIcons(true);
            // Requires 2.1: selector.SelectionTree.EnumerateTree().Where(x => x.Icon == null).ForEach(x => x.Icon = EditorIcons.UnityGameObjectIcon);
            selector.SelectionTree.EnumerateTree().ForEach(x => x.Toggled = true);
    
            return selector;
        }
    }
    
  2. V reporter

    @Bjarkeck

    Hey, sorry for the slow response! This account went to an email address i do not use very often.

    This seems to be exactly what i was looking for, and i will explain to you as to why i would want [PrefabOnly] (but what you provided is basically the same thing? Depending on how it works, i did not have time to test yet.)

    Say i have a MenuController, it is a prefab of a menu, with a link to all it's button and text components linked for easy/fast access. I want those components to ONLY be children of this menu/prefab thing, not any asset, not any other gameobject in the scene either.

    Problem would have been: AssetOnly would still allow me to drag any other asset in there from the project. And SceneOnly would allow any old scene link to happen. Both are not what you would want in this case. The 1st makes no sense to do. And the 2nd would break/not be linked to the prefab in your project folder, as it would be a scene link. Asset only would also not allow you to link anything in your prefab that is lower than 2 depth due to how unity does prefabs inside the project folder.

    This newly added [ChildrenOnly] basically allows a script that is on the root of a gameobject, to only be filled with components from itself or lower. As a result it effectively isolates selections to just this specific prefab/root/children and nothing else. Capture.PNG

    This would obviously work with any component, not just gameobjects (not sure if your limits it to that or not, it seems to imply that it does limit to GO)

    I hope this explains it a bit better.

    Let me know if you have any more questions and thanks for all your effort so far!

  3. Log in to comment