我正在嘗試做的事情:
一個 ScriptableObject 類,用于保存可以在觀察者模式中訂閱的單個變數,以便在值更改時接收通知。
我的目的是讓 UI 顯示更新之類的內容在顯示更改時更新,而不必在每次更改時手動觸發事件。
此外,我希望我的課程具有三個特點:
- 使用 try/catch 來真正解耦事物,而不是讓所有監聽器僅僅因為一個監聽器失敗而失敗
- 可以選擇記錄用于除錯的內容
- 在檢查器中顯示當前活動的觀察者串列
我認為這是 Delegates 的幾行代碼,但事實證明不是,這根本行不通。
我的第一個天真的迭代是這樣的:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
[CreateAssetMenu(fileName = "New Observable Float", menuName = "Observables/Float")]
public class ObservableFloat : ScriptableObject {
public event Action<float> get;
[SerializeField] private float m_Value;
public float Value {
get {
return m_Value;
}
set {
m_Value = value;
get?.Invoke(value);
}
}
}
我的第二次迭代,在功能上有效,但沒有向我顯示檢查器中的觀察者串列,是這樣的:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
[CreateAssetMenu(fileName = "New Observable Float", menuName = "Observables/Float")]
public class ObservableFloat : ScriptableObject {
[SerializeField] List<UnityAction<float>> listeners = new List<UnityAction<float>>();
[SerializeField] private float m_Value;
public float Value {
get {
return m_Value;
}
set {
m_Value = value;
foreach (UnityAction<float> action in listeners) {
action.Invoke(value);
}
}
}
public void AddListener(UnityAction<float> func) => listeners.Add(func);
public void RemoveListener(UnityAction<float> func) => listeners.Remove(func);
}
我的第三次迭代,用 UnityEvents 替換 UnityAction,乍一看似乎可以作業(串列顯示在檢查器中),但它從不更新串列并且它總是顯示為空,即使它再次在功能上作業:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using Sirenix.OdinInspector;
[CreateAssetMenu(fileName = "New Observable Float", menuName = "Observables/Float")]
public class ObservableFloat : ScriptableObject {
public UnityEvent<float> listeners = new UnityEvent<float>();
[SerializeField] private float m_Value;
public float Value {
get {
return m_Value;
}
set {
m_Value = value;
listeners?.Invoke(value);
}
}
}
uj5u.com熱心網友回復:
一般來說,我認為你正在尋找的是 UnityEvent
[SerializeField] private UnityEvent<float> listeners;
public void AddListener(Action<float> action) => listeners.AddListener(action);
public void RemoveListener(Action<float> action) => listeners.RemoveListener(action);
[SerializeField] private float m_Value;
public float Value {
get {
return m_Value;
}
set {
m_Value = value;
listeners?.Invoke(value);
}
}
不幸的是,這些將始終僅顯示Inspector 中的持久偵聽器。沒有簡單的內置方法來顯示運行時回呼,如果你想這樣做,我想沒有辦法繞過反射和/或非常復雜的特殊檢查器實作。
你可以例如存盤類似的東西
using System.Reflection;
using System.Linq;
...
[Serializable]
public class ListenerInfo
{
public Action<float> action;
public string MethodName;
public string TypeName;
}
[SerializeField] private List<string> listenerInfos;
public void AddListener(Action<float> action)
{
listeners.AddListener(action);
var info = action.GetMethodInfo();
listenerInfos.Add(new ListenerInfo { action = action, MethodName = info.Name, TypeName = info.DeclaringType.Name });
}
public void RemoveListener (Action<float> action)
{
listeners.RemoveListener(action);
var info = var info = action.GetMethodInfo();
listenerInfos.RemoveAll(l => l.action == action);
}
另見例如動作委托。如何獲取呼叫該方法的實體
我想這將是你能得到的最接近的結果,而無需真正深入研究 Unity Editor 腳本甚至更多的反思 ^^
uj5u.com熱心網友回復:
我想出了一個可行的解決方案,盡管我不太確定,所以我將它發布在 CodeReview - https://codereview.stackexchange.com/questions/272241/unity3d-observable-variable
這是代碼(但請查看上面的鏈接以獲取可能的修復/改進)。非常感謝@derHugo,他為我指明了正確的方向:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEditor;
using Sirenix.OdinInspector;
[CreateAssetMenu(fileName = "New Observable Float", menuName = "Observables/Float")]
public class ObservableFloat : ScriptableObject {
[System.Serializable]
public class Listener {
[DisplayAsString, HideInInspector] public int ID;
[DisplayAsString, HideLabel, HorizontalGroup] public string Observer;
[DisplayAsString, HideLabel, HorizontalGroup] public string Method;
[HideInInspector] public UnityAction<float> Callback;
public Listener(UnityAction<float> cb) {
ID = cb.GetHashCode();
Observer = cb.Target.ToString();
Method = cb.Method.ToString();
Callback = cb;
}
}
[Delayed]
[OnValueChanged("NotifyListeners")]
[SerializeField] private float m_Value;
public float Value {
get {
return m_Value;
}
set {
m_Value = value;
NotifyListeners();
}
}
[Tooltip("Log Invoke() calls")]
[SerializeField] bool Trace;
[Tooltip("Use try/catch around Invoke() calls so events continue to other listeners even if one fails")]
[SerializeField] bool CatchExceptions;
[ListDrawerSettings(DraggableItems = false, Expanded = true, ShowIndexLabels = false, ShowPaging = false, ShowItemCount = true)]
[SerializeField] List<Listener> listeners = new List<Listener>();
void Awake() {
// clear out whenever we start - just in case some observer doesn't properly remove himself
// maybe later I'll also add persistent listeners, but for now I don't see the use case
listeners = new List<Listener>();
}
void NotifyListeners() {
foreach (Listener listener in listeners) {
if (Trace) {
Debug.Log("invoking " listener.Observer " / " listener.Method " / value = " m_Value);
}
if (CatchExceptions) {
try {
listener.Callback.Invoke(m_Value);
} catch (System.Exception exception) {
Debug.LogException(exception, this);
}
} else {
listener.Callback.Invoke(m_Value);
}
}
}
public void AddListener(UnityAction<float> func) { listeners.Add(new Listener(func)); }
public void RemoveListener(UnityAction<float> func) { listeners.RemoveAll(l => l.ID == func.GetHashCode()); }
}
這有效并為我提供了我想要的功能,但不確定它是否是一個很好的解決方案,所以我將問題懸而未決以獲得更好的答案。
uj5u.com熱心網友回復:
恐怕您想在檢查器中顯示屬性時不能使用 {get;set;} 方法。Inspector 只會顯示公共屬性,并且帶有 {get;set;} 方法的屬性將被識別為唯一的方法。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/389754.html
