Unityを使用したゲームの実装中に、次の設定に直面しました。
ScriptableObject
のAC#の持っているもの(資産など)event
デリゲートを。MonoBehaviour
にシリアライズ参照持っているScriptableObject
デリゲートを持っていることを。MonoBehaviour
そのScriptableObject
イベントを「サブスクライブ」し、メモリリークを回避するためにイベントを適切に処理したいと思います。当初は、OnEnable
コールバックでイベントをサブスクライブし、でイベントのサブスクライブを解除するOnDisable
だけで十分だと思っていました。ただし、開発者がUnity Inspectorを使用して、シリアル化された参照の値をScriptableObject
再生中にスワップすると、メモリリークが発生します。
ScriptableObject
ゲームの開発者がプレイ中にインスペクターで値を交換できるようにしたい場合、のシリアル化された参照でc#イベントを安全にサブスクライブおよびサブスクライブ解除する標準的な方法はありますか?
それを説明するために、私はそのシナリオの簡単なコードを書きました。
SubjectSO.cs(ScriptableObject
イベントあり)
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つのアセットを作成しました。
次に、GameObject
インシーンを作成し、次のコンポーネントを添付しました。
ObserverMB
、SubjectAを参照InvokesSubjectSOEveryUpdate
、SubjectAを参照InvokesSubjectSOEveryUpdate
、SubjectBを参照再生を押すと、メッセージCallback Received! Value = SubjectA
は更新のたびにコンソールに出力されます。
その後、私は参照を変更するには、インスペクタを使用したときObserverMB
からSubjectAにSubjectBゲームはまだメッセージが、再生中に、Callback Received! Value = SubjectA
まだ印刷され続けます。
ObserverMB
インスペクターで無効にして有効にすると、更新ごとに両方のメッセージCallback Received! Value = SubjectA
とCallback Received! Value = SubjectB
印刷が開始されます。
最初のコールバックサブスクリプションはまだ有効ですが、サブスクライバーとして、ObserverMB
そのイベントへの参照を失っています。
どうすればその状況を回避できますか?
私は本当にこれはC#のの使用のための一般的な使用シナリオのようだと信じているevent
代表者とScriptableObjects
し、それがいることを私のために奇妙なようOnEnable
とOnDisable
適切に検査員を微調整現像液のシリアル化のケースを処理しません。
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]
コメントを追加