1

Since there is no support for serializing ObservableCollection of C# in Unity as of yet, I am using a script which extends List<> and creates a Serializable class called ObservableList as mentioned in this unity forum answer ObservableList. Following is the same code:

[Serializable]
 public class ObservedList<T> : List<T>
 {
     public event Action<int> Changed = delegate { };
     public event Action Updated = delegate { };
     public new void Add(T item)
     {
         base.Add(item);
         Updated();
     }
     public new void Remove(T item)
     {
         base.Remove(item);
         Updated();
     }
     public new void AddRange(IEnumerable<T> collection)
     {
         base.AddRange(collection);
         Updated();
     }
     public new void RemoveRange(int index, int count)
     {
         base.RemoveRange(index, count);
         Updated();
     }
     public new void Clear()
     {
         base.Clear();
         Updated();
     }
     public new void Insert(int index, T item)
     {
         base.Insert(index, item);
         Updated();
     }
     public new void InsertRange(int index, IEnumerable<T> collection)
     {
         base.InsertRange(index, collection);
         Updated();
     }
     public new void RemoveAll(Predicate<T> match)
     {
         base.RemoveAll(match);
         Updated();
     }


     public new T this[int index]
     {
         get
         {
             return base[index];
         }
         set
         {
             base[index] = value;
             Changed(index);
         }
     }
 }

Still it is not being serialized and I cannot see it in Unity editor. Any help would be greatly appreciated!

EDIT #1

Intended Use case:

public class Initialize : MonoBehaviour
{
    public ObservedList<int> percentageSlider = new ObservedList<int>();
    
    void Start()
    {
        percentageSlider.Changed += ValueUpdateHandler;
    }
    
    void Update()
    {
    }
    
    private void ValueUpdateHandler(int index)
    {
        // Some index specific action
        Debug.Log($"{index} was updated");
    }
}

I am attaching this script as a component to a GameObject so that I can input the size of this list, play around with the values (just like I can do with List) and perform some action which only gets fired when some value inside the ObservableList is updated.

What I want to see

Inspector

What I am seeing

Inspector2

hulkinBrain
  • 746
  • 8
  • 28
  • Could you show us how exactly you are using it? I mean the class where you'd expect that lost to show up in the inspector – derHugo May 18 '21 at 06:54
  • @derHugo I have edited the question to include a code snippet. Maybe this helps in explaining the use case – hulkinBrain May 18 '21 at 08:21
  • You mean the screenshot you added is what is supposed to happen right? You currently see only `Percentage Slider` but not the child properties, correct? – derHugo May 18 '21 at 08:30
  • @derHugo The screenshot is what supposed to happen yes. But I'm not seeing anything. I have added another snapshot of how it looks like – hulkinBrain May 18 '21 at 08:37

3 Answers3

1
  1. If you want the actions to be serialized then you'll have to use UnityEvents (which hook into Unity's serialized event system).

  2. Make sure you're using the [SerializedField] attribute before all the types you want serialized.

  3. The main problem is likely stemming from inheritance, which does not play well with Unity's serialization system. In general, I would never inherit from List anyways (here are some reasons why) Instead I would make your observable type a wrapper for List with some added features (List would be a member within Observable).

Edit 1

Added a tested code example. Instead of inheriting, an ObservedList<T> acts as wrapper for List<T>. You'll be able to subscribe to the Changed and Updated events, but they're not serialized. If you want to access any other list functionality, you'll just have to add a public method in the ObservedList<t> class which acts as its wrapper. Let me know if you have any other issues.

[Serializable]
public class ObservedList<T>
{
    public event Action<int> Changed;
    public event Action Updated;
    [SerializeField] private List<T> _value;
    
    public T this[int index]
    {
        get
        {
            return _value[index];
        }
        set
        {
            //you might want to add a check here to only call changed
            //if _value[index] != value
            _value[index] = value;
            Changed?.Invoke(index);
        }
    }
    
    public void Add(T item)
    {
        _value.Add(item);
        Updated?.Invoke();
    }
    public void Remove(T item)
    {
        _value.Remove(item);
        Updated?.Invoke();
    }
    public void AddRange(IEnumerable<T> collection)
    {
        _value.AddRange(collection);
        Updated?.Invoke();
    }
    public void RemoveRange(int index, int count)
    {
        _value.RemoveRange(index, count);
        Updated?.Invoke();
    }
    public void Clear()
    {
        _value.Clear();
        Updated?.Invoke();
    }
    public void Insert(int index, T item)
    {
        _value.Insert(index, item);
        Updated?.Invoke();
    }
    public void InsertRange(int index, IEnumerable<T> collection)
    {
        _value.InsertRange(index, collection);
        Updated?.Invoke();
    }
    public void RemoveAll(Predicate<T> match)
    {
        _value.RemoveAll(match);
        Updated?.Invoke();
    }
}
Charly
  • 450
  • 2
  • 13
  • I have updated the question with a sample use case scenario. Maybe it helps to highlight my problem. 1. I am trying to achieve it without using UnityEvents since I don't want to expose the events in editor. If there is no other way then I shall use it. 2. I tried to do it with [SerializeField] but with no effect. 3. The class is only acting kinda like a wrapper and not changing any of the core functionalities. Only addition are the events which are firing, could this still be a problem? Even if I want to make it a member withing Observable, I would still have to detect changes. – hulkinBrain May 18 '21 at 08:17
  • I hope it is just casual that your edit came right after I posted the exact thing as an answer ;) – derHugo May 18 '21 at 09:00
  • It seems you did more work then I! I just reworked hulkinBrain's code, but you seem to have added more complete functionality and best practices. I would leave your post up, your version is definitely more complete. – Charly May 18 '21 at 09:02
  • I did exactly as you instructed. But it still doesn't get serialized. I even tried changing the API compatibility to .Net 4x in ```Player settings```, but to no use. Here is the gist https://gist.github.com/hulkinBrain/cecd5821f18e9a9889938abeb095d099 and the editor screenshot https://i.imgur.com/nyBQQCY.png – hulkinBrain May 18 '21 at 10:44
  • Hmm weird. What version of Unity do you have? you might have an older version that can't serialize lists by default? Try replacing the List with an array and see if that works. If it does, you might want to upgrade your unity version. Or install Odin Inspector / Serializer (a Unity asset) – Charly May 18 '21 at 10:52
  • It works on Unity 2020.3.20f1 (so i guess 2020 versions) but not on 2019 *weird*. Could you confirm your unity version? – hulkinBrain May 18 '21 at 11:58
  • im using 2020.3 as well, glad you got it fixed! – Charly May 18 '21 at 17:49
1

So here is what I did

Instead of inheriting from the List<T> and trying to fix the Inspector somehow with editor scripting you could simply use a "backend" List<T> for which Unity already provides a serialization and let your class implement IList<T> like e.g.

[Serializable]
public class ObservedList<T> : IList<T>
{
    public delegate void ChangedDelegate(int index, T oldValue, T newValue);

    [SerializeField] private List<T> _list = new List<T>();

    // NOTE: I changed the signature to provide a bit more information
    // now it returns index, oldValue, newValue
    public event ChangedDelegate Changed;

    public event Action Updated;

    public IEnumerator<T> GetEnumerator()
    {
        return _list.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public void Add(T item)
    {
        _list.Add(item);
        Updated?.Invoke();
    }

    public void Clear()
    {
        _list.Clear();
        Updated?.Invoke();
    }

    public bool Contains(T item)
    {
        return _list.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _list.CopyTo(array, arrayIndex);
    }

    public bool Remove(T item)
    {
        var output = _list.Remove(item);
        Updated?.Invoke();

        return output;
    }

    public int Count => _list.Count;
    public bool IsReadOnly => false;

    public int IndexOf(T item)
    {
        return _list.IndexOf(item);
    }

    public void Insert(int index, T item)
    {
        _list.Insert(index, item);
        Updated?.Invoke();
    }

    public void RemoveAt(int index)
    {
        _list.RemoveAt(index);
        Updated?.Invoke();
    }

    public void AddRange(IEnumerable<T> collection)
    {
        _list.AddRange( collection);
        Updated?.Invoke();
    }

    public void RemoveAll(Predicate<T> predicate)
    {
        _list.RemoveAll(predicate);
        Updated?.Invoke();
    }

    public void InsertRange(int index, IEnumerable<T> collection)
    {
        _list.InsertRange(index, collection);
        Updated?.Invoke();
    }

    public void RemoveRange(int index, int count)
    {
        _list.RemoveRange(index, count);
        Updated?.Invoke();
    }

    public T this[int index]
    {
        get { return _list[index]; }
        set
        {
            var oldValue = _list[index];
            _list[index] = value;
            Changed?.Invoke(index, oldValue, value);
            // I would also call the generic one here
            Updated?.Invoke();
        }
    }
}

In the usage from code absolutely nothing changes (except as mentioned the one signature of the event):

public class Example : MonoBehaviour
{
    public ObservedList<int> percentageSlider = new ObservedList<int>();

    private void Start()
    {
        percentageSlider.Changed += ValueUpdateHandler;

        // Just as an example
        percentageSlider[3] = 42;
    }

    private void ValueUpdateHandler(int index, int oldValue, int newValue)
    {
        // Some index specific action
        Debug.Log($"Element at index {index} was updated from {oldValue} to {newValue}");
    }
}

But the Inspector now looks like this

enter image description here

enter image description here


Then if you really need to because you hate how it looks like in the editor ^^ You could use a bit of a dirty hack and overwrite it like

Have a non-generic base class because custom editors don't work with generics

public abstract class ObservedList { }

Then inherit from that

[Serializable]
public class ObservedList<T> : ObservedList, IList<T>
{
   ...
}

Then we can implement a custom drawer actually providing the base type but it will be applied to the inheritor

[CustomPropertyDrawer(typeof(ObservedList), true)]
public class ObservedListDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // NOTE: Now here is the dirty hack
        // even though the ObservedList itself doesn't have that property
        // we can still look for the field called "_list" which only the 
        // ObservedList<T> has
        var list = property.FindPropertyRelative("_list");
        EditorGUI.PropertyField(position, list, label, true);
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        var list = property.FindPropertyRelative("_list");

        return EditorGUI.GetPropertyHeight(list, label, true);
    }
}

Now it looks like this

enter image description here


Unity 2019 and older

In Unity 2019 and older the serialization of generics wasn't supported at all.

In Script Serialization 2020 this was added:

Generic field types can also be directly serialized, without the need to declare a non-generic subclass.

this is not the case in Script Serialization 2019 and older.

So here you would need to have explicit implementations like

[Serializable]
public class ObservedListInt : ObservedList<int>{ }

public class Example : MonoBehaviour
{
    public ObservedListInt percentageSlider = new ObservedListInt();

    private void Start()
    {
        percentageSlider.Changed += ValueUpdateHandler;

        // Just as an example
        percentageSlider[3] = 42;
    }

    private void ValueUpdateHandler(int index, int oldValue, int newValue)
    {
        // Some index specific action
        Debug.Log($"Element at index {index} was updated from {oldValue} to {newValue}");
    }
}

Without the custom drawer

enter image description here

With the custom drawer

enter image description here

derHugo
  • 83,094
  • 9
  • 75
  • 115
  • Thanks for the detailed explanation. I did all that you mentioned shown in this gist https://gist.github.com/hulkinBrain/ae89f06fbdaa301cd12b240949d08f91. I even tried changin gthe API compatibility in ```Player Settings``` but to no avail. I attached the `Test.cs` to a gameobject but still I can't see it in the editor https://i.imgur.com/nyBQQCY.png – hulkinBrain May 18 '21 at 10:40
  • Alright it works on Unity 2020 version but not in 2019. Could you please confirm your Unity version? – hulkinBrain May 18 '21 at 11:56
  • 1
    @hulkinBrain 2020 ;) The reason: Since 2020 generics are supported by the serializer. In 2019 and older you will have to make an explicit implementation otherwise there is no way to get it serialized at all, see update at the bottom – derHugo May 18 '21 at 12:36
  • Ahaa I see. Thankyou so much, it works perfectly now! Also, thankyou for linking the documentation, learnt something new about Script serialization in 2019 and 2020 – hulkinBrain May 18 '21 at 12:40
0

I slightly modified @derHugo answer. I wanted to implement INotifyCollectionChanged interface like ObservableCollection does.

This code is not tested yet, but I'll try to edit this answer ASAP if any bug would be found.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using UnityEngine;

public abstract class ObservableList
{
}

[Serializable]
public class ObservableList<T> : ObservableList, IList<T>, INotifyCollectionChanged
{
    public event NotifyCollectionChangedEventHandler CollectionChanged;

    [SerializeField] private List<T> _list = new List<T>();

    public IEnumerator<T> GetEnumerator()
    {
        return _list.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public void Add(T item)
    {
        _list.Add(item);

        OnCollectionChanged(NotifyCollectionChangedAction.Add, item, _list.Count - 1);
    }

    public void Clear()
    {
        _list.Clear();

        OnCollectionReset();
    }

    public bool Contains(T item)
    {
        return _list.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _list.CopyTo(array, arrayIndex);
    }

    public bool Remove(T item)
    {
        var itemIndex = _list.IndexOf(item);
        var doExist = itemIndex != -1;
        var didRemove = _list.Remove(item);
        if (doExist && didRemove)
        {
            OnCollectionChanged(NotifyCollectionChangedAction.Remove, item, itemIndex);
        }

        return didRemove;
    }

    public int Count => _list.Count;
    public bool IsReadOnly => false;

    public int IndexOf(T item)
    {
        return _list.IndexOf(item);
    }

    public void Insert(int index, T item)
    {
        _list.Insert(index, item);

        OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
    }

    public void RemoveAt(int index)
    {
        _list.RemoveAt(index);

        var item = _list[index];

        OnCollectionChanged(NotifyCollectionChangedAction.Remove, item, index);
    }

    public void AddRange(IEnumerable<T> collection)
    {
        // _list.AddRange(collection);
        InsertRange(_list.Count, collection);
    }

    public void RemoveAll(Predicate<T> predicate)
    {
        // _list.RemoveAll(predicate);
        int index = 0;
        while (index < _list.Count)
        {
            if (predicate(_list[index]))
            {
                RemoveAt(index);
            }
            else
            {
                index++;
            }
        }
    }

    public void InsertRange(int index, IEnumerable<T> collection)
    {
        // _list.InsertRange(index, collection);
        foreach (var item in collection)
        {
            Insert(index, item);
            index++;
        }
    }

    public void RemoveRange(int index, int count)
    {
        // _list.RemoveRange(index, count);
        if (index < 0 || _list.Count < index + count - 1)
        {
            throw new ArgumentOutOfRangeException();
        }

        for (var i = 0; i < count; i++)
        {
            RemoveAt(index);
        }
    }

    public T this[int index]
    {
        get => _list[index];
        set
        {
            var oldValue = _list[index];
            _list[index] = value;

            OnCollectionChanged(NotifyCollectionChangedAction.Replace, oldValue, value, index);
        }
    }

    protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        CollectionChanged?.Invoke(this, e);
    }

    private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index) =>
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index));

    private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index, int oldIndex)
    {
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index, oldIndex));
    }

    private void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index)
    {
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
    }

    private void OnCollectionReset() =>
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

    /// <summary>
    /// Not-LINQ lambda foreach
    /// </summary>
    public void ForEach(Action<T> action)
    {
        foreach (var cur in _list)
        {
            action(cur);
        }
    }
}
NullOne
  • 25
  • 6
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jul 25 '22 at 05:44