UI Document: Binding a List of Structs To Scroll View

Hi there,

I’m currently stuck while trying to create a custom forest generator tool. I want to create a list of tree types that I can add and remove using UI Elements, that I can then bind to and get the data from when I come to generate the forest.

I’m currently trying to bind Scroll View Items that I create on button press to values inside an list of structs, these struct instances are getting created as I’m binding. I’m trying to get the path using:

FindProperty()

&

BindProperty()

Every time I try to find the property it tells me that the property in question is null. to get the list of structs I return the list after I’ve added a list value in the monoBehaviour class (I call this function from the editor as seen below)

I’m adding an empty instance of a scriptable object of the tree type class each time I click “add” before returning the full array. I then create a serialized object from the relevant item in the list, which is what I get each property from.

I have two scripts here. One for the monoBehaviour class and one for the Editor Class. This is how I’m adding the scroll view items, you can see the first time I create a list item, i try to use BindingPath(), this also return null:

Editor Class:

[CustomEditor(typeof(GenerationTool))]
[CanEditMultipleObjects]
public class GenerationToolUI : Editor
{

    [SerializeField] private GenerationTool m_generationScript;
    [SerializeField] private GameObject m_ForestSpawnObject;

    [SerializeField] private int m_ListSize = 1;

    [SerializeField] private List<TreeType> m_TreeTypesEditor;

    private VisualElement root;

    public void OnEnable()
    {
        m_generationScript = GameObject.FindGameObjectWithTag("Generator").GetComponent<GenerationTool>();
        m_ForestSpawnObject = GameObject.FindGameObjectWithTag("GeneratorObject");
    }

    public override VisualElement CreateInspectorGUI()
    {

        VisualElement rootInspector = new VisualElement();

        var addButton = new Button { text = "+"};
        addButton.RegisterCallback<ClickEvent>(AddTreeItem);

        rootInspector.Add(addButton);


        /* ---------------------------------------- */
        /* ADDING SCROLL VIEW AND FIRST SCROLL ITEM */
        /* ---------------------------------------- */

        //m_generationScript.TreeSelectionUpdate(m_ListSize);

        m_TreeTypesEditor = m_generationScript.TreeSelectionUpdate(m_ListSize);
    
        var scrollContainer = new ScrollView();
        scrollContainer.name = "scroll";
        scrollContainer.style.width = 350;
        scrollContainer.style.height = 400;

        var Treename = new TextField();
        Treename.label = "Tree Name:";
        Treename.value = "Test";
        Treename.bindingPath = $"m_TreeTypes.Array.Data[{m_ListSize - 1}].name";
        Debug.Log(Treename.bindingPath);
        Treename.multiline = false;
        Treename.maxLength = 140;

        scrollContainer.Add(Treename);

        var prefab = new ObjectField();
        prefab.label = "Tree Prefab:";
        prefab.objectType = typeof(GameObject);

        scrollContainer.Add(prefab);

        var spawnFrequency = new Slider();
        spawnFrequency.label = "Spawn frequency:";
        spawnFrequency.lowValue = 0.0f;
        spawnFrequency.highValue = 1.0f;
        spawnFrequency.value = 0.5f;

        scrollContainer.Add(spawnFrequency);

        var treeSpacing = new FloatField();
        treeSpacing.label = "Spawn frequency:";
        treeSpacing.maxLength = 1;

        scrollContainer.Add(treeSpacing);

        rootInspector.Add(scrollContainer);

        VisualTreeAsset asset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UI/TreeGenerator.uxml");
        asset.CloneTree(rootInspector);

        root = rootInspector;

        StyleSheet sheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/UI/Stylesheets/ToolStylesheet.uss");
        rootInspector.styleSheets.Add(sheet);

        return rootInspector;
    }

    public void AddTreeItem(ClickEvent eventData)
    {
        if (m_ListSize < 25)
        {
            m_ListSize++;

            m_TreeTypesEditor = m_generationScript.TreeSelectionUpdate(m_ListSize);
            var serializedObject = new SerializedObject(m_TreeTypesEditor[m_ListSize - 1]);
            serializedObject.Update();
            var property = serializedObject.FindProperty($"m_TreeTypes.Array.data[{m_ListSize}].name");
            Debug.Log(property);

            var Treename = new TextField();
            Treename.label = "Tree Name:";
            Treename.multiline = false;
            Treename.maxLength = 140;
            Treename.BindProperty(property);

            root.Q<ScrollView>(name: "scroll").Add(Treename);

            var prefab = new ObjectField();
            prefab.label = "Tree Prefab:";
            prefab.objectType = typeof(GameObject);

            root.Q<ScrollView>(name: "scroll").Add(prefab);

            var spawnFrequency = new Slider();
            spawnFrequency.label = "Spawn frequency:";
            spawnFrequency.lowValue = 0.0f;
            spawnFrequency.highValue = 1.0f;
            spawnFrequency.value = 0.5f;

            root.Q<ScrollView>(name: "scroll").Add(spawnFrequency);
            //scrollContainer.Add(new Label(spawnFrequency.value.ToString()));

            var treeSpacing = new FloatField();
            treeSpacing.label = "Tree Spacing:";
            treeSpacing.maxLength = 1;

            root.Q<ScrollView>(name: "scroll").Add(treeSpacing);
        }
    }

Monobehaviour Script and Scriptable Obj:

[Header("Tree Type Variables")]
    [Space]
    public List<TreeType> m_TreeTypes;
    [SerializeField] private TreeType m_EmptyTreeType;

    public void OnEnable()
    {
        m_EmptyTreeType = ScriptableObject.CreateInstance<TreeType>();
    }


    public List<TreeType> TreeSelectionUpdate(int listSize)
    {
        m_TreeTypes = new List<TreeType>();

        for (int i = 0; i < listSize; i++)
        {
            m_TreeTypes.Add(m_EmptyTreeType);

            /* Can't get the values of the m_TreeTypes array to bind to the scroll list variables */
        }
        Debug.Log(m_TreeTypes[0]);
        return m_TreeTypes;
    }

public class TreeType : ScriptableObject
{
    public string name;

    public GameObject prefab;

    public float spacing;

    public float frequency;
}

I would greatly appreciate any ideas on how to do bind to a list of objects, it is mainly this part of my tool code that I think is wrong:

 m_TreeTypesEditor = m_generationScript.TreeSelectionUpdate(m_ListSize);
            var serializedObject = new SerializedObject(m_TreeTypesEditor[m_ListSize - 1]);
            serializedObject.Update();
            var property = serializedObject.FindProperty($"m_TreeTypes.Array.data[{m_ListSize}].name");
            Debug.Log(property);

I think i’m not pointing to the proper data here. I don’t know if this is because the m_TreeTypes array is on another class and the FindProperty() class can’t find it. can anybody help me?

TreeType is a ScriptableObject, this means its not part of the same serialized data. Its a separate object.
So you cant access its members through the same SerializedObject. You need to create another one to access them.

Something like this:

var treeTypes = serializedObject.FindProperty("m_TreeTypes");
var item = property.GetArrayElementAtIndex(treeTypes.arraySize - 1);

var soItem = new SerializedObject(item.objectReferenceValue);
var name = soItem.FindProperty("name");
1 Like

Hi Karl,

Thanks for your reply,

I tried this and worked exactly as intended for my first list item, however when I tried to add multiple list items, the binding path seems to persist between each of the individual items.

Unity_qoydkO8FJA.gif

I’m unsure why this is because when the “item” is set it checks the index in the array. Surely that should set individual binding path.

Here is the way I’ve set the binding path on button press in the editor script, m_TreeTypesSP is a serialised property global variable that I update on button press. I set it to find the property of the list of TreeTypes on CreateInspectorGUI():

  m_ListSize++;

            // Changing Actual data in Generation tool
            // not returning anything just updating the array size
            m_generationScript.TreeSelectionUpdate(m_ListSize);

            m_TreeTypesSP.serializedObject.Update();
            var item = m_TreeTypesSP.GetArrayElementAtIndex(m_TreeTypesSP.arraySize - 1);

            var soItem = new SerializedObject(item.objectReferenceValue);
            var name = soItem.FindProperty("name");

Is there another way to do this?

Thanks

9797349--1406136--Unity_qoydkO8FJA.gif