参照されているシリアル化されたScriptableObjectでc#イベントをサブスクライブするときに、Unityでメモリリークを回避するにはどうすればよいですか?

アルバカーキ

Unityを使用したゲームの実装中に、次の設定に直面しました。

  • 私が持っているScriptableObjectのAC#の持っているもの(資産など)eventデリゲートを。
  • 私が持っているMonoBehaviourにシリアライズ参照持っているScriptableObjectデリゲートを持っていることを。

MonoBehaviourそのScriptableObjectイベントを「サブスクライブ」し、メモリリークを回避するためにイベントを適切に処理したいと思います。当初は、OnEnableコールバックでイベントをサブスクライブし、でイベントのサブスクライブを解除するOnDisableだけで十分だと思っていましたただし、開発者がUnity Inspectorを使用して、シリアル化された参照の値をScriptableObject再生中にスワップすると、メモリリークが発生します。

ScriptableObjectゲームの開発者がプレイ中にインスペクターで値を交換できるようにしたい場合、のシリアル化された参照でc#イベントを安全にサブスクライブおよびサブスクライブ解除する標準的な方法はありますか?


それを説明するために、私はそのシナリオの簡単なコードを書きました。

SubjectSO.csScriptableObjectイベントあり)

using UnityEngine;
using System;

[CreateAssetMenu]
public class SubjectSO : ScriptableObject
{
    public event Action<string> OnTrigger;

    public void Invoke()
    {
        this.OnTrigger?.Invoke(this.name);
    }
}

ObserverMB .cs(でMonoBehaviourイベントをサブスクライブしたいものScriptableObject

using UnityEngine;

public class ObserverMB : MonoBehaviour
{
    public SubjectSO subjectSO;

    public void OnEnable()
    {
        if(this.subjectSO != null)
        {
            this.subjectSO.OnTrigger += this.OnTriggerCallback;
        }
    }

    public void OnDisable()
    {
        if(this.subjectSO != null)
        {
            this.subjectSO.OnTrigger -= this.OnTriggerCallback;
        }
    }

    public void OnTriggerCallback(string value)
    {
        Debug.Log("Callback Received! Value = " + value);
    }
}

InvokesSubjectSOEveryUpdate .cs(補助MonoBehaviour、テスト用)

using UnityEngine;

public class InvokesSubjectSOEveryUpdate : MonoBehaviour
{
    public SubjectSO subjectSO;

    public void Update()
    {
        this.subjectSO?.Invoke();
    }
}

テストのためにSubjectSO、次の名前のタイプの2つのアセットを作成しました。

  • SubjectA
  • SubjectB

次に、GameObjectインシーンを作成し、次のコンポーネントを添付しました。

  • ObserverMBSubjectAを参照
  • InvokesSubjectSOEveryUpdateSubjectAを参照
  • InvokesSubjectSOEveryUpdateSubjectBを参照

再生を押すと、メッセージCallback Received! Value = SubjectAは更新のたびにコンソールに出力されます。

その後、私は参照を変更するには、インスペクタを使用したときObserverMBからSubjectASubjectBゲームはまだメッセージが、再生中に、Callback Received! Value = SubjectAまだ印刷され続けます。

ObserverMBインスペクターで無効にして有効にすると、更新ごとに両方のメッセージCallback Received! Value = SubjectACallback Received! Value = SubjectB印刷が開始されます。

最初のコールバックサブスクリプションはまだ有効ですが、サブスクライバーとして、ObserverMBそのイベントへの参照を失っています。

どうすればその状況を回避できますか?

私は本当にこれはC#のの使用のための一般的な使用シナリオのようだと信じているevent代表者とScriptableObjectsし、それがいることを私のために奇妙なようOnEnableOnDisable適切に検査員を微調整現像液のシリアル化のケースを処理しません。

derHugo

subjectSO変更されているかどうかを確認し、この場合は登録を解除する必要があります。

インスペクターを介して値を切り替えた後、クラスは以前の値からサブスクライブを解除できなくなります。したがって、最初にサブスクライブされたものはすべて、サブスクライブされたままになります。

ランタイムチェック用

私は例えば次のようなプロパティを使用してそれを行います

// Make it private so no other script can directly change this
[SerializedField] private SubjectSO _currentSubjectSO;

// The value can only be changed using this property
// automatically calling HandleSubjectChange
public SubjectSO subjectSO
{
    get { return _currentSubjectSO; }
    set 
    {
        HandleSubjectChange(this._currentSubjectSO, value);
    }
}

private void HandleSubjectChange(SubjectSO oldSubject, SubjectSO newSubject)
{
    if (!this.isActiveAndEnabled) return;

    // If not null unsubscribe from the current subject
    if(oldSubject) oldSubject.OnTrigger -= this.OnTriggerCallback;

    // If not null subscribe to the new subject
    if(newSubject) 
    {
        newSubject.OnTrigger -= this.OnTriggerCallback;
        newSubject.OnTrigger += this.OnTriggerCallback;
    }

     // make the change
    _currentSubjectSO = newSubject;
}

したがって、他のスクリプトがを使用して値を変更するたびに

observerMBReference.subject = XY;

最初に現在のサブジェクトから自動的にサブスクライブを解除し、次に新しいサブジェクトにサブスクライブします。


インスペクターを介して変更を確認する場合

2つのオプションがあります:

Updateメソッドと、次のようなさらに別のバッキングフィールドを経由するか

#if UNITY_EDITOR
    private SubjectSO _previousSubjectSO;

    private void Update()
    {
        if(_previousSubjectSO != _currentSubjectSO)
        {
            HandleSubjectChange(_previousSubjectSO, _currentSubjectSO);
            _previousSubjectSO = _currentSubjectSO;
        }
    }
#endif

または(zambariに感謝)同じことをします OnValidate

#if UNITY_EDITOR
    private SubjectSO _previousSubjectSO;

    // called when the component is created or changed via the Inspector
    private void OnValidate()
    {
        if(!Apllication.isPlaying) return;

        if(_previousSubjectSO != _currentSubjectSO)
        {
            HandleSubjectChange(_previousSubjectSO, _currentSubjectSO);
            _previousSubjectSO = _currentSubjectSO;
        }
    }
#endif

または、これはインスペクターを介してフィールドが変更された場合にのみ発生するため、フィールドが変更された場合にのみ実行するCutsomEditorを実装できますこれはセットアップが少し複雑ですが、ビルドの後半ではUpdateとにかくメソッドが必要ないため、より効率的です。

通常、エディタスクリプトはと呼ばれる別のフォルダに配置しますEditorが、個人的には、対応するクラス自体に実装することをお勧めします

利点は、この方法でprivateメソッドにもアクセスできることです。このようにして、インスペクターにいくつかの追加の動作があることが自動的にわかります。

#if UNITY_EDITOR
    using UnityEditor;
#endif

    ...

    public class ObserverMB : MonoBehaviour
    {
        [SerializeField] private SubjectSO _currentSubjectSO;
        public SubjectSO subjectSO
        {
            get { return _currentSubjectSO; }
            set 
            {
                HandleSubjectChange(_currentSubjectSO, value);
            }
        }

        private void HandleSubjectChange(Subject oldSubject, SubjectSO newSubject)
        {
            // If not null unsubscribe from the current subject
            if(oldSubject) oldSubject.OnTrigger -= this.OnTriggerCallback;

            // If not null subscribe to the new subject
            if(newSubject) newSubject.OnTrigger += this.OnTriggerCallback;

            // make the change
            _currentSubjectSO = newSubject;
        }

        public void OnEnable()
        {
            if(subjectSO) 
            {
                // I recommend to always use -= before using +=
                // This is allowed even if the callback wasn't added before
                // but makes sure it is added only exactly once!
                subjectSO.OnTrigger -= this.OnTriggerCallback;
                subjectSO.OnTrigger += this.OnTriggerCallback;
            }
        }

        public void OnDisable()
        {
            if(this.subjectSO != null)
            {
                this.subjectSO.OnTrigger -= this.OnTriggerCallback;
            }
        }

        public void OnTriggerCallback(string value)
        {
            Debug.Log("Callback Received! Value = " + value);
        }

#if UNITY_EDITOR
        [CustomEditor(typeof(ObserverMB))]
        private class ObserverMBEditor : Editor 
        { 
            private ObserverMB observerMB;
            private SerializedProperty subject;

            private Object currentValue;

            private void OnEnable()
            {
                observerMB = (ObserverMB)target;
                subject = serializedObject.FindProperty("_currentSubjectSO");
            }

            // This is kind of the update method for Inspector scripts
            public override void OnInspectorGUI()
            {
                // fetches the values from the real target class into the serialized one
                serializedObject.Update();

                EditorGUI.BeginChangeCheck();
                {
                    EditorGUILayout.PropertyField(subject);
                }
                if(EditorGUI.EndChangeCheck() && EditorApplication.isPlaying)
                {
                    // compare and eventually call the handle method
                    if(subject.objectReferenceValue != currentValue) observerMB.HandleSubjectChange(currentValue, (SubjectSO)subject.objectReferenceValue);
                }
            }
        }
#endif
    }

この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。

侵害の場合は、連絡してください[email protected]

編集
0

コメントを追加

0

関連記事

Related 関連記事

ホットタグ

アーカイブ