【问题标题】:Serialize ObservableCollection-like class ObservableList in Unity C#在 Unity C# 中序列化 ObservableCollection 类 ObservableList
【发布时间】:2021-05-18 00:36:40
【问题描述】:

由于目前尚不支持在 Unity 中序列化 C# 的 ObservableCollection,因此我正在使用扩展 List<> 的脚本并创建一个名为 ObservableList 的可序列化类,如本统一论坛答案 ObservableList 中所述.以下是相同的代码:

[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);
         }
     }
 }

它仍然没有被序列化,我在 Unity 编辑器中看不到它。任何帮助将不胜感激!

编辑#1

预期用例:

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");
    }
}

我将此脚本作为组件附加到游戏对象,以便我可以输入此列表的 size,使用这些值(就像我可以使用 List 一样)并执行一些只能获取的操作当ObservableList 中的某些值更新时触发。

我想看什么

我所看到的

【问题讨论】:

  • 你能告诉我们你是如何使用它的吗?我的意思是你期望丢失的类会出现在检查器中
  • @derHugo 我已编辑问题以包含代码 sn-p。也许这有助于解释用例
  • 您的意思是您添加的屏幕截图是应该发生的事情吧?您目前只看到 Percentage Slider 而看不到子属性,对吗?
  • @derHugo 截图是应该发生的。但我什么也没看到。我添加了另一个外观快照

标签: c# unity3d


【解决方案1】:

So here is what I did

与其继承 List&lt;T&gt; 并尝试使用编辑器脚本以某种方式修复 Inspector,您可以简单地使用“后端”List&lt;T&gt;,Unity 已经为其提供了序列化并让您的类实现IList&lt;T&gt;,例如

[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();
        }
    }
}

在代码的使用中绝对没有任何变化(除了提到的事件的一个签名):

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}");
    }
}

但 Inspector 现在看起来像这样


如果你真的需要,因为你讨厌它在编辑器中的样子^^你可以使用一些肮脏的黑客并像这样覆盖它

有一个非泛型基类,因为自定义编辑器不适用于泛型

public abstract class ObservedList { }

然后从那个继承

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

然后我们可以实现一个自定义抽屉,实际上提供基本类型,但它将应用于继承者

[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);
    }
}

现在是这个样子


Unity 2019 及更早版本

在 Unity 2019 及更早版本中,根本不支持泛型的序列化。

Script Serialization 2020 中添加了这个:

泛型字段类型也可以直接序列化,无需声明非泛型子类。

Script Serialization 2019 和更早版本的情况并非如此。

所以在这里你需要有明确的实现,比如

[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}");
    }
}

没有自定义抽屉

带有自定义抽屉

【讨论】:

  • 感谢您的详细解释。我做了你在这个要点gist.github.com/hulkinBrain/ae89f06fbdaa301cd12b240949d08f91 中提到的所有内容。我什至尝试在Player Settings 中更改 API 兼容性,但无济于事。我将Test.cs 附加到一个游戏对象,但我仍然无法在编辑器中看到它i.imgur.com/nyBQQCY.png
  • 好的,它适用于 Unity 2020 版本,但不适用于 2019。请确认您的 Unity 版本吗?
  • @hulkinBrain 2020 ;) 原因:自 2020 年以来,序列化程序支持泛型。在 2019 年及更早的版本中,您必须进行显式实现,否则根本无法对其进行序列化,请参阅底部的更新
  • 啊哈,我明白了。非常感谢,现在完美运行!另外,感谢您链接文档,在 2019 年和 2020 年了解了有关脚本序列化的新知识
【解决方案2】:
  1. 如果您希望对操作进行序列化,则必须使用 UnityEvents(它与 Unity 的序列化事件系统挂钩)。

  2. 确保在所有要序列化的类型之前使用 [SerializedField] 属性。

  3. 主要问题可能源于继承,它不能很好地与 Unity 的序列化系统配合使用。一般来说,我永远不会从 List 继承 (here are some reasons why) 相反,我会让你的 observable 类型成为 List 的包装器,并带有一些附加功能(List 将是 Observable 中的成员)。

编辑 1

添加了一个经过测试的代码示例。 ObservedList 不是继承,而是充当 List 的包装器。 您将能够订阅 Changed 和 Updated 事件,但它们没有被序列化。如果您想访问任何其他列表功能,您只需在 ObservedList 类中添加一个公共方法,该方法充当其 包装器。如果您有任何其他问题,请告诉我。

[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();
    }
}

【讨论】:

  • 我已经用一个示例用例场景更新了这个问题。也许它有助于突出我的问题。 1. 我试图在不使用 UnityEvents 的情况下实现它,因为我不想在编辑器中公开事件。如果没有其他方法,那么我将使用它。 2. 我尝试用 [SerializeField] 来做,但没有效果。 3. 这个类只是有点像一个包装器,并没有改变任何核心功能。唯一添加的是正在触发的事件,这仍然是一个问题吗?即使我想让它成为 Observable 的成员,我仍然需要检测变化。
  • 我希望您的编辑是在我发布确切的答案后立即进行的;)
  • 看来你做的比我多!我刚刚重新编写了 hulkinBrain 的代码,但您似乎添加了更完整的功能和最佳实践。我会留下你的帖子,你的版本肯定更完整。
  • 我完全按照你的指示做了。但它仍然没有被序列化。我什至尝试在Player settings 中将 API 兼容性更改为 .Net 4x,但没有用。这是要点gist.github.com/hulkinBrain/cecd5821f18e9a9889938abeb095d099和编辑器截图i.imgur.com/nyBQQCY.png
  • 嗯,很奇怪。你有什么版本的 Unity?您可能有一个旧版本默认无法序列化列表?尝试用数组替换列表,看看是否有效。如果是这样,您可能需要升级您的统一版本。或者安装 Odin Inspector / Serializer(一个 Unity 资产)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-04-20
  • 2022-08-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-11
相关资源
最近更新 更多