イベントUnity3dのイベントアグリゲーター

Unity3d向けに独自の拡張イベントアグリゲーターを作成するというアイデアは、長らく待ち望まれていました。 このトピックに関するいくつかの記事を読んだ後、私は必要な「正しい」(Unity3d内)およびアグリゲーターが不足していることに気付きました。すべてのソリューションは削除され、必要な機能がありません。



必要な機能:



  1. 任意のクラスが任意のイベントをサブスクライブできます(多くの場合、ユニット内のアグリゲーターが特定のGameobjectサブスクライバーを作成します)
  2. 特定のインスタンスを特定のイベントに二重サブスクライブする可能性は除外する必要があります(標準ツールでは、これを自分で追跡する必要があります)
  3. インスタンスを削除する/ monobekhを無効にする場合は、手動で登録解除と自動の両方の機能を使用する必要があります(登録して、加入者が突然ひづめを返すスチームバスを利用したくない)
  4. イベントは複雑なデータ/リンクを転送できる必要があります(1行でサブスクライブし、問題なくデータセット全体を取得したい)


適用場所



  1. これは、接続せずにオブジェクトからデータを転送する必要がある場合のUIに最適です。
  2. データ変更に関するメッセージ、リアクティブコードの類似物。
  3. 依存性注入の場合
  4. グローバルコールバック


弱点



  1. デッドサブスクライバーとテイクのチェックのため(後で明らかにします)、コードは同様のソリューションよりも遅いです
  2. クラス/構造体はイベントのコアとして使用されるため、メモリ+一番上の問題を割り当てないように、更新時にイベントをスパムすることは推奨されません)


一般的なイデオロギー



一般的なイデオロギーでは、イベントは特定の関連するデータパッケージです。 インターフェース/ジョイスティックのボタンを押したとしましょう。 そして、さらに処理するために特定のボタンを押す兆候のあるイベントを送信したいと思います。 クリックの処理の結果は、インターフェイスの視覚的な変更とロジックの何らかのアクションです。 したがって、2つの異なる場所で処理/サブスクライブすることができます。



私の場合、イベント本体/データパケットは次のようになります。



イベント本文の例
public struct ClickOnButtonEvent   {     public int ButtonID; //     enum    }
      
      







イベントサブスクリプションは次のようになります。



 public static void AddListener<T>(object listener, Action<T> action)
      
      





購読するには、以下を指定する必要があります。

サブスクライバーであるオブジェクト(通常はサブスクリプションのクラス自体ですが、これは必須ではありませんが、サブスクライバーはクラスフィールドからクラスインスタンスの1つを指定できます。

購読しているタイプ/イベント。 これがこのアグリゲーターの重要なエッセンスです。特定の種類のクラスは、リッスンして処理するイベントです。

AwakeおよびOnEnableで最適なサブスクリプション。







 public class Example : MonoBehaviour { private void Awake() { EventAggregator.AddListener<ClickOnButtonEvent>(this, ClickButtonListener); } private void ClickButtonListener(ClickOnButtonEvent obj) { Debug.Log("  " + obj.ButtonID); } }
      
      





チップが何であるかを明確にするために、より複雑なケースを検討してください



次のキャラクターアイコンがあります。

  1. どのキャラクターにアタッチされているかを知ってください。
  2. マナ、馬力、経験値、ステータス(スタン、失明、恐怖、狂気)の量を反映する


そして、ここでいくつかのイベントを行うことができます



インジケーターを変更するには:



 public struct CharacterStateChanges { public Character Character; public float Hp; public float Mp; public float Xp; }
      
      





否定的なステータスを変更するには:



 public struct CharacterNegativeStatusEvent { public Character Character; public Statuses Statuses; //enum  }
      
      





どちらの場合でも、文字クラスを渡すのはなぜですか? イベントサブスクライバーとそのハンドラーを次に示します。



 private void Awake() { EventAggregator.AddListener<CharacterNegativeStatusEvent> (this, CharacterNegativeStatusListener); } private void CharacterNegativeStatusListener(CharacterNegativeStatusEvent obj) { if (obj.Character != _character) return; _currentStatus = obj.Statuses; }
      
      





これは、イベントを処理し、必要なものを正確に把握するためのマーカーです。

Characterクラスを直接サブスクライブしないのはなぜですか? そしてそれらをスパムしますか?

デビューするのは難しいでしょう。クラス/イベントのグループが独自の個別のイベントを作成することをお勧めします。



繰り返しになりますが、なぜイベント内で、Characterを配置せず、すべてを取得しないのですか?

ところで、それは可能ですが、多くの場合、クラスには可視性の制限があり、イベントに必要なデータは外部からは見えない場合があります。



クラスが重すぎてマーカーとして使用できない場合

実際、ほとんどの場合、マーカーは必要ありません;更新されたクラスのグループはかなりまれです。 通常、1つの特定のエンティティにイベントが必要です-通常は1番目のキャラクターの状態を表示するコントローラー/ビューモデル。 そのため、いつもの解決策があります-さまざまな種類のID(inamから複雑なハッシュなど)。



フードの下には何があり、どのように機能しますか?



直接集約コード
 namespace GlobalEventAggregator public delegate void EventHandler<T>(T e); { public class EventContainer<T> : IDebugable { private event EventHandler<T> _eventKeeper; private readonly Dictionary<WeakReference, EventHandler<T>> _activeListenersOfThisType = new Dictionary<WeakReference, EventHandler<T>>(); private const string Error = "null"; public bool HasDuplicates(object listener) { return _activeListenersOfThisType.Keys.Any(k => k.Target == listener); } public void AddToEvent(object listener, EventHandler<T> action) { var newAction = new WeakReference(listener); _activeListenersOfThisType.Add(newAction, action); _eventKeeper += _activeListenersOfThisType[newAction]; } public void RemoveFromEvent(object listener) { var currentEvent = _activeListenersOfThisType.Keys.FirstOrDefault(k => k.Target == listener); if (currentEvent != null) { _eventKeeper -= _activeListenersOfThisType[currentEvent]; _activeListenersOfThisType.Remove(currentEvent); } } public EventContainer(object listener, EventHandler<T> action) { _eventKeeper += action; _activeListenersOfThisType.Add(new WeakReference(listener), action); } public void Invoke(T t) { if (_activeListenersOfThisType.Keys.Any(k => k.Target.ToString() == Error)) { var failObjList = _activeListenersOfThisType.Keys.Where(k => k.Target.ToString() == Error).ToList(); foreach (var fail in failObjList) { _eventKeeper -= _activeListenersOfThisType[fail]; _activeListenersOfThisType.Remove(fail); } } if (_eventKeeper != null) _eventKeeper(t); return; } public string DebugInfo() { string info = string.Empty; foreach (var c in _activeListenersOfThisType.Keys) { info += c.Target.ToString() + "\n"; } return info; } } public static class EventAggregator { private static Dictionary<Type, object> GlobalListeners = new Dictionary<Type, object>(); static EventAggregator() { SceneManager.sceneUnloaded += ClearGlobalListeners; } private static void ClearGlobalListeners(Scene scene) { GlobalListeners.Clear(); } public static void AddListener<T>(object listener, Action<T> action) { var key = typeof(T); EventHandler<T> handler = new EventHandler<T>(action); if (GlobalListeners.ContainsKey(key)) { var lr = (EventContainer<T>)GlobalListeners[key]; if (lr.HasDuplicates(listener)) return; lr.AddToEvent(listener, handler); return; } GlobalListeners.Add(key, new EventContainer<T>(listener, handler)); } public static void Invoke<T>(T data) { var key = typeof(T); if (!GlobalListeners.ContainsKey(key)) return; var eventContainer = (EventContainer<T>)GlobalListeners[key]; eventContainer.Invoke(data); } public static void RemoveListener<T>(object listener) { var key = typeof(T); if (GlobalListeners.ContainsKey(key)) { var eventContainer = (EventContainer<T>)GlobalListeners[key]; eventContainer.RemoveFromEvent(listener); } } public static string DebugInfo() { string info = string.Empty; foreach (var listener in GlobalListeners) { info += "     " + listener.Key.ToString() + "\n"; var t = (IDebugable)listener.Value; info += t.DebugInfo() + "\n"; } return info; } } public interface IDebugable { string DebugInfo(); } }
      
      







メインから始めましょう



これは、キーがタイプで値がコンテナである辞書です



 public class EventContainer<T> : IDebugable
      
      





 private static Dictionary<Type, object> GlobalListeners = new Dictionary<Type, object>();
      
      





なぜコンテナをオブジェクトとして保存するのですか? 辞書はジェネリックを保存する方法を知りません。 しかし、キーのおかげで、オブジェクトを必要な型にすばやく持ち込むことができます。



コンテナには何が含まれていますか?



 private event EventHandler<T> _eventKeeper; private readonly Dictionary<WeakReference, EventHandler<T>> _activeListenersOfThisType = new Dictionary<WeakReference, EventHandler<T>>();
      
      





これには、汎用マルチデリゲートと、キーがサブスクライバーであるオブジェクトであり、値が同じメソッドハンドラーであるコレクションが含まれます。 実際、この辞書には、このタイプに属するすべてのオブジェクトとメソッドが含まれています。 結果として、マルチデリゲートを呼び出し、彼はすべてのサブスクライバーを呼び出します。これはサブスクライバーに制限のない「正直な」イベントシステムですが、他のほとんどのアグリゲーターでは、フードの下で、特別なインターフェイスによって一般化されるか、システムを実装するクラスから継承されるクラスのコレクションが繰り返されますメッセージ。



マルチデリゲートが呼び出されると、デッドキーがあるかどうかが確認され、コレクションから死体が削除され、関連するサブスクライバーを持つマルチデリゲートが取得されます。 これには時間がかかりますが、実際には、イベントの機能が分離されている場合、1つのイベントには3〜5人のサブスクライバーがいるので、チェックはそれほど怖くなく、快適さの利点はより明白です。 1000人以上のサブスクライバーが存在する可能性があるネットワークストーリーの場合、このアグリゲーターは使用しないことをお勧めします。 質問は未解決のままですが、死体のチェックを削除すると、高速になりますが、1kのサブスクライバーの配列を反復処理したり、1kサブスクライバーからマルチデリゲートを呼び出したりします。



使用の特徴



サブスクリプションはAwakeにプッシュするのが最適です。



オブジェクトがアクティブにオン/オフになっている場合は、AwakeとOnEnableをサブスクライブすることをお勧めします。2回署名することはありませんが、非アクティブなGameObjectがデッドと見なされる可能性は除外されます。



すべてのサブスクライバーが作成および登録されるとき、開始前にイベントを請求する方が適切です。



アグリゲーターは、シーンのアンロード時にリストを消去します。 一部のアグリゲーターでは、シーンの読み込みをクリーンアップすることをお勧めします-これはファイルです。シーンの読み込みイベントはAwake / OnEnableの後に発生し、追加されたサブスクライバーは削除されます。



アグリゲーターには-public static string DebugInfo()があり、どのクラスがどのイベントにサブスクライブされているかを確認できます。



GitHubリポジトリ



All Articles