Manually drawing a list in a ValueDrawer

Issue #103 resolved
Scott Richmond created an issue

We have a specific use-case where the struct that needs to be inspected cannot have the data we want to inspect in the type definition.

#!c#

    [Serializable]
    [StructLayout(LayoutKind.Sequential)]
    public struct EntityHandle : IEquatable<EntityHandle>
    {
        public int Id;
        public int Version;

#if UNITY_EDITOR
        // List drawer settings make sure we can't add or remove items from the list, nor rearrange them by dragging (I presume you have your own preferred way of doing that?)
        [ShowInInspector] [ListDrawerSettings(DraggableItems = false, IsReadOnly = true, Expanded = false)]
        [BlueprintComponentWrapper] public List<object> Components;
#endif
    }
[OdinDrawer]
public class EntityHandleValueDrawer : OdinValueDrawer<EntityHandle> {
    protected override void DrawPropertyLayout(IPropertyValueEntry<EntityHandle> entry, GUIContent label)
    {
        EntityHandle value = entry.SmartValue;

        SirenixEditorGUI.BeginBox();
        SirenixEditorGUI.BeginBoxHeader();
        {
            if (label != null) GUILayout.Label(label);
        }
        SirenixEditorGUI.EndBoxHeader();

        var components = Engine.Instance.Context.Entities
            .GetAllComponentsOfEntity(value.Id).ToList();
        value.Components = components;
        entry.SmartValue = value;
        entry.ApplyChanges();
        entry.Update();

        this.CallNextDrawer(entry, null);

        //SirenixEditorGUI.BeginVerticalList();
        //SirenixEditorGUI.BeginListItem();
        //GUILayout.Label(label);
        //SirenixEditorGUI.EndListItem();
        //SirenixEditorGUI.BeginListItem();
        //GUILayout.Label(label);
        //SirenixEditorGUI.EndListItem();
        //SirenixEditorGUI.BeginListItem();
        //GUILayout.Label(label);
        //SirenixEditorGUI.EndListItem();
        //SirenixEditorGUI.EndVerticalList();


        SirenixEditorGUI.EndBox();
    }
}

Just temporarily I let the components list exist in the EntityHandle struct, hoping to fill it up within the drawer before drawing. However that doesn't appear to work - The list stays null.

As a longer-term solution I wanted to call the drawer behind the [BlueprintComponentWrapper] attribute to draw the list. Is it possible to call this drawer and pass in a List that is stored on the EntityHandleValueDrawer as a local private property?

Example:

#!c#

    [Serializable]
    [StructLayout(LayoutKind.Sequential)]
    public struct EntityHandle : IEquatable<EntityHandle>
    {
        public int Id;
        public int Version;
    }
[OdinDrawer]
public class EntityHandleValueDrawer : OdinValueDrawer<EntityHandle> {
private List<object> _components;

    protected override void DrawPropertyLayout(IPropertyValueEntry<EntityHandle> entry, GUIContent label)
    {
        EntityHandle value = entry.SmartValue;

        _components = Engine.GetComponentsForEntity(value.Id);

        this.CallAttribDrawer(BlueprintComponentWrapper, _components);

        SirenixEditorGUI.EndBox();
    }
}

Comments (6)

  1. Bjarke Elias

    Hi Scott,

    It looks like the struct is being serialized by Unity? In which case the list of components will not be serialized because it's a list of system.object's. That would explain the values disappearing. Try changing the type of the list to List<UnityEngine.Object / UnityEngine.Component> instead of List<System.Object / object>

    As a rule of thumb; ShowInInspector is only meant to show values that are not necessarily being serialized.

  2. Bjarke Elias
    • changed status to open

    If you want to have it serialize a list of system.object's you need to force Odin to serialize the struct instead of Unity.

    There are a couple of ways you can do that:

    1:

    #!c#
    
    public class MyComponent : SerializedMonoBehaviour
    {
        public MyStruct MyStruct;
    }
    
    // Not marking it [Serializable] means that Unity will not be serializing it, however Odin will.
    public struct MyStruct
    {
         public List<System.Object> SomeList;
    }
    

    2:

    #!c#
    
    public class MyComponent : SerializedMonoBehaviour
    {
    
        [NonSerialized] // Force Unity not to serialize it.
        [OdinSerialize] // But tell Odin to serialize it instead.
        public MyStruct MyStruct;
    }
    
    [Serializable] // Now unity will serialize it.
    public struct MyStruct
    {
         public List<System.Object> SomeList;
    }
    
  3. Scott Richmond reporter

    Nope no serialization happening here. Its purely run-time data in the List. The objects are in fact strongly typed value structs. Very similar to what's going on here in this ticket: https://bitbucket.org/sirenix/odin-inspector/issues/87/odindrawers-for-scripableobjects#comment-37225182

    However in this case we can't store the data on the inspected object.

  4. Tor Esa Vestergaard

    There is a fairly advanced/obscure method for drawing arbitrary data using Odin, despite it not being in the actual inspected property tree. We make use of it in a few different drawers ourselves. It involves creating a new property tree as a context value for the property in question, and then drawing that property tree in the drawer. It could be done like this:

    #!c#
    
    [OdinDrawer]
    public class EntityHandleValueDrawer : OdinValueDrawer<EntityHandle>
    {
        protected override void DrawPropertyLayout(IPropertyValueEntry<EntityHandle> entry, GUIContent label)
        {
            var componentPropertyTreeContext = entry.Property.Context.Get(this, "component_tree", (PropertyTree<ComponentContainer>)null);
    
            if (componentPropertyTreeContext.Value == null)
            {
                // Initialize the property tree
    
                var container = new ComponentContainer();
    
                componentPropertyTreeContext.Value = new PropertyTree<ComponentContainer>(new ComponentContainer[] { container });
            }
    
            // Update components every frame
            // If you only need to update it once on initialization, just move this line into the above if statement
            (componentPropertyTreeContext.Value.WeakTargets[0] as ComponentContainer).Components = Engine.GetComponentsForEntity(entry.SmartValue.Id);
    
            // Draw the tree for the component container, which will draw the components list that was fetched from the entity handle
            componentPropertyTreeContext.Value.Draw(false);
        }
    
        private class ComponentContainer // Property trees can't draw lists directly, only normal types, so we need to store the list in a normal type
        {
            [ShowInInspector, BlueprintComponentWrapper]
            public List<object> Components;
        }
    }
    

    Hope that helps.

    Edit: I should note that that's just one approach, and is a little messy, though it should work. It's a little odd that your value just disappears - your example should actually work. I note that you're calling "entry.Update()" after setting it. Does it work better if you call "entry.Property.Update()" instead?

  5. Log in to comment