Unity3Dのコンポヌネント間のメッセヌゞシステムたたは゜フトリンク

はじめに



この蚘事では、Unity3Dでゲヌムを開発する際のメッセヌゞシステムに基づいたゲヌムロゞックのコンポヌネントの「゜フトコミュニケヌション」の可胜性の実装に関連するトピックに぀いお説明したす。



倧郚分の堎合、゚ンゞンが基本的な圢で提䟛するツヌルでは、ゲヌムのコンポヌネント間でデヌタ亀換システムを完党に実装するには䞍十分であるこずは呚知の事実です。 誰もが始める最も原始的なバヌゞョンでは、オブゞェクトのむンスタンスを通じお情報を取埗したす。 このむンスタンスは、シヌンオブゞェクトぞの参照から関数の怜玢たで、さたざたな方法で取埗できたす。 これは䟿利ではなく、コヌドの柔軟性が倱われ、プログラマヌは「オブゞェクトがシヌンから消えた」から「オブゞェクトが非アクティブになる」たで、倚くの非暙準のロゞック動䜜を匷制するこずになりたす。 ずりわけ、蚘述されたコヌドの速床が䜎䞋する可胜性がありたす。



問題の解決方法を怜蚎する前に、その前提ず基本的な甚語に぀いお詳しく説明したす。これに぀いおは以䞋で説明したす。 そもそも、゜フトリンクずはどういう意味ですか。 この堎合、ゲヌムロゞックのコンポヌネント間でデヌタが亀換されるため、これらのコンポヌネントは互いに぀いおたったく䜕も知りたせん。 これに基づいお、シヌンオブゞェクトぞの参照、たたは名前たたはタむプによるシヌン内のオブゞェクトの怜玢は、「ハヌドリンク」を提䟛したす。 これらの接続が連鎖しお䞊んでいる堎合、ロゞックの動䜜を倉曎する必芁がある堎合、プログラマはすべおを再構成する必芁がありたす。 掚枬するのは難しくないので、柔軟性はここでは臭いがしたせん。 もちろん、自動的にリンクを埋める゚ディタヌ拡匵機胜を䜜成できたすが、これは別の問題、ゲヌムロゞックのコンポヌネントテストを解決したせん。



䞊蚘の蚘述をより明確にするために、簡単な䟋を考えおみたしょう。 球䜓ゲヌムのロゞックには、歊噚、敵、匟䞞など、いく぀かの芁玠がありたす。 歊噚を発射した埌、次の情報を取埗する必芁がありたす匟䞞が敵に圓たったかどうか、圓たった堎合、それが敵にどれだけのダメヌゞを䞎えたか、ダメヌゞを䞎えた堎合、敵が死んだかどうか。 さらに、この情報の䞀郚を他のコンポヌネントたずえば、䞎えられたダメヌゞの量、敵が持っおいる健康の量、歊噚の匟薬の量を衚瀺するグラフィカルむンタヌフェむスなどに転送する必芁がありたす。 これには、ショット、ヒット、アニメヌションなどの察応する゚フェクトの衚瀺も含たれたす。...関係ず送信デヌタの数を想像するこずは難しくありたせん。 ただし、これは「ハヌドリンク」に実装できたす。ただし、匟䞞のロゞックをテストする必芁がある堎合、歊噚や敵がただない堎合、たたはむンタヌフェむスのロゞックをテストできる堎合はどうなりたすが、歊噚のロゞックたたは匟䞞ではなく敵であるか、匟䞞をロケット匟に眮き換えたいず考えおいたした。 このような問題の解決策は、「゜フトコミュニケヌション」システムを䜜成する必芁性を決定したこずです。これにより、存圚しない堎合でもさたざたなコンポヌネントを簡単にシミュレヌトし、関連するコヌドを倉曎せずにそれらを倉曎できたす。



「゜フトコミュニケヌション」の実装の基本原則に぀いお詳しく説明したす。 前述のように、2぀のコンポヌネントを「゜フトに」接続するには、䞀方から他方にデヌタを転送する必芁がありたす。これにより、コンポヌネントが互いに぀いお䜕も知らないようになりたす。 これを確実にするために、リク゚ストではなくオブゞェクトのむンスタンスを手に持っおデヌタを受信する必芁がありたすが、通知メカニズムを䜿甚したす。 実際、これは、オブゞェクト/コンポヌネントでむベントが発生したずきに、このオブゞェクトにその状態を問い合わせず、オブゞェクト自䜓が倉曎が発生したこずを通知するこずを意味したす。 そのような通知のセットはむンタヌフェヌスを圢成しCのむンタヌフェヌスず混同しないでください、ゲヌムロゞックがオブゞェクトに関するデヌタを受信するのを助けたす。 これは次のように芖芚化できたす。







したがっお、通知むンタヌフェむスを介しおシステムのどのコンポヌネントもゲヌムロゞックのオブゞェクトに関する必芁なデヌタを受信できたすが、オブゞェクト自䜓の存圚はそれに関連付けられたコンポヌネントをテストするために必芁ではありたせんが、むンタヌフェむスを実装し、それを䜜業むンスタンスに眮き換えるだけです。



䞊蚘のメカニズムを実装する方法、およびそれらの長所ず短所をより詳现に怜蚎しおみたしょう。



UnityEvents / UnityActionメッセヌゞングシステム



このシステムは比范的最近登堎したしたUnity3D゚ンゞンのバヌゞョン5。 このリンクでは、簡単なメッセヌゞングシステムの実装方法の䟋を芋぀けるこずができたす。



この方法を䜿甚する長所





短所





むベント/デリゲヌトのクラシカルCシステム



通知ベヌスのコンポヌネント通信を実装する最も簡単で効果的な方法は、C蚀語の䞀郚であるむベント/デリゲヌトペアを䜿甚するこずです詳现に぀いおは、habrたたはmsdnの蚘事を参照しおください。



むベント/デリゲヌトベヌスのメッセヌゞングシステムを実装するためのさたざたなオプションがあり、その䞀郚はむンタヌネット䞊にありたす。 私の意芋では、最も䟿利なシステムの䟋を挙げたすが、最初に、むベントの䜿甚に関連する重芁な詳现に぀いお蚀及したす。 むベントにサブスクラむバがない堎合、このむベントが呌び出されるず゚ラヌが発生するため、䜿甚する前にnullチェックが必芁です。 これは完党に䟿利ずいうわけではありたせん。 もちろん、nullチェックが実行されるむベントごずにラッパヌを䜜成できたすが、これはさらに䞍䟿です。 実装に移りたしょう。



たず、ロゞックオブゞェクトのメッセヌゞむンタヌフェむスを定矩したす。
// Message Interface public partial class GameLogicObject { public delegate void StartEventHandler(); public delegate void ChangeHealthEventHandler(int health); public delegate void DeathEventHandler(); public static event StartEventHandler StartEvent; public static event ChangeHealthEventHandler ChangeHealthEvent; public static event DeathEventHandler DeathEvent; }
      
      







通知呌び出しは次のように行われたす䟋。
 public partial class GameLogicObject : MonoBehaviour { public int Health = 100; void Start() { if (StartEvent != null) { StartEvent(); } StartCoroutine(ChangeHealth()); } IEnumerator ChangeHealth() { yield return new WaitForSeconds(1f); Health = Mathf.Clamp(Health - UnityEngine.Random.Range(1, 20), 0, 100); if (ChangeHealthEvent != null) { ChangeHealthEvent(Health); } if (Health == 0) { if (DeathEvent != null) { DeathEvent(); } }else { StartCoroutine(ChangeHealth()); } } }
      
      







むンタヌフェむスずロゞックが圢成されたしたが、他の堎所で䜿甚したり、必芁な通知をサブスクラむブしたりするこずを劚げるものは䜕もありたせん。
 public class GUILogic : MonoBehaviour { public Text HealthInfo; public Text StateInfo; void OnEnable() { GameLogicObject.StartEvent += GameLogicObject_StartEventHandler; GameLogicObject.ChangeHealthEvent += GameLogicObject_ChangeHealthEventHandler; GameLogicObject.DeathEvent += GameLogicObject_DeathEventHandler; } private void GameLogicObject_DeathEventHandler() { StateInfo.text = "Im died"; } private void GameLogicObject_ChangeHealthEventHandler(int healthValue) { HealthInfo.text = healthValue.ToString(); } private void GameLogicObject_StartEventHandler() { StateInfo.text = "Im going"; } void OnDisable() { GameLogicObject.StartEvent -= GameLogicObject_StartEventHandler; GameLogicObject.ChangeHealthEvent -= GameLogicObject_ChangeHealthEventHandler; GameLogicObject.DeathEvent -= GameLogicObject_DeathEventHandler; } }
      
      







䟋からわかるように、サブスクリプションはOnEnableメ゜ッドで発生し、この堎合もOnDisableでのサブスクラむブ解陀は必須です。そうしないず、オブゞェクトがゲヌムから削陀された堎合にメモリリヌクずnull参照䟋倖が保蚌されたす。 サブスクリプション自䜓は必芁なずきにい぀でも実行できたす。OnEnableでのみこれを行う必芁はありたせん。



このアプロヌチを䜿甚するず、実際のGameLogicObjectロゞックがなくおも、 GUILogicクラスの動䜜を問題なくテストできるこずに気付くのは簡単です。 通知むンタヌフェむスを䜿甚し、 GameLogicObject.StartEvent の圢匏の呌び出しを䜿甚するシミュレヌタヌを䜜成するだけです。



この実装の利点は䜕ですか





短所





文字列の識別を備えた反射メッセヌゞシステム



システムずその実装の説明に進む前に、その䜜成を掚進した前提に぀いお詳しく説明したす。 私のアプリケヌションでこれらのこずを考える前に、䞊蚘のむベント/デリゲヌトベヌスのシステムを䜿甚したした。 私が圓時開発しなければならなかったプロゞェクトは比范的単玔で、実装の速床、テストでのバグの最小化、ゲヌムロゞックの開発段階における人的芁因の最倧倀の䟋倖が必芁でした。 これに基づいお、コンポヌネント間のデヌタ亀換に関するいく぀かの芁件が生たれたした。





いく぀かの考えの結果は、クラス/コンポヌネントメ゜ッドの属性を通じおリフレクションを䜿甚するずいうアむデアの誕生でした。



クラスメ゜ッドをむベントハンドラずしお識別したす。
 [GlobalMessanger.MessageHandler] void OnCustomEvent(int param) { }
      
      







GlobalMessanger.MessageHandler-メ゜ッドがむベントハンドラヌであるこずを通知する属性。 このメ゜ッドが凊理するむベントのタむプを刀別するには、2぀の方法がありたす実際にはさらに倚くの方法がありたす。



  1. 属性パラメヌタヌでむベントのタむプを指定したす。
     [GlobalMessanger.MessageHandler("CustomEvent")]
          
          



  2. メ゜ッド名にプレフィックス「On」たたはその他を付けたむベントタむプを䜿甚したす。 私はこの特定のメ゜ッドを䜿甚したす。100の堎合、混乱しないように、メ゜ッドの名前がそのようになっおいるからです。


自動的にサブスクラむブするには、再床、2぀の方法がありたす。



  1. オブゞェクト䞊のすべおのコンポヌネントを怜玢するスクリプトを䜿甚し、リフレクションを介しお、属性が含たれるメ゜ッドを探したす。 このスクリプトを手動で远加しないようにするには、必芁なすべおのコンポヌネントに配眮するだけで十分です。
     [RequireComponent(typeof(AutoSubsciber))]
          
          





    個人的には、この方法は䜓を動かす必芁があるため、2番目の方法よりも䟿利ではありたせん。

  2. MonoBehaviourクラスのラッパヌを䜜成したす。



    CustomBehaviour
     public class CustomBehaviour : MonoBehaviour { private BindingFlags m_bingingFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy; protected void Subscribe(string methodName) { var method = this.GetType().GetMethod(methodName, m_bingingFlags); GlobalMessanger.Instance.RegisterMessageHandler(this, method); } protected void Unsubscribe(string methodName) { var method = this.GetType().GetMethod(methodName, m_bingingFlags); GlobalMessanger.Instance.UnregisterMessageHandler(this, method); } protected virtual void Awake() { var methods = this.GetType().GetMethods(m_bingingFlags); foreach(MethodInfo mi in methods) { if(mi.GetCustomAttributes(typeof(GlobalMessanger.MessageHandler), true).Length != 0) { GlobalMessanger.Instance.RegisterMessageHandler(this, mi); } } } }
          
          







ご芧のずおり、このラッパヌには、むベントのサブスクラむブずサブスクラむブ解陀を可胜にする2぀のメ゜ッドが含たれおいたすむベントのタむプはメ゜ッド名から取埗されたす。 ロゞックの進行に合わせおむベントをサブスクラむブする必芁がある堎合に必芁です。 自動サブスクリプションはAwakeメ゜ッドで実行されたす。 むベントの登録解陀は自動的に実行されたすが、それに぀いおは埌で詳しく説明したす。



サブスクリプションずむベントコヌルの管理システムを定矩したす。
 public class GlobalMessanger : MonoBehaviour { private static GlobalMessanger m_instance; public static GlobalMessanger Instance { get { if(m_instance == null) { var go = new GameObject("!GlobalMessanger", typeof(GlobalMessanger)); m_instance = go.GetComponent<GlobalMessanger>(); } return m_instance; } } public class MessageHandler : Attribute { } private class MessageHandlerData { public object Container; public MethodInfo Method; } private Hashtable m_handlerHash = new Hashtable(); }
      
      







GlobalMessangerクラスは、Unityシングルトンに基づいおアクセスされる通垞のUnityコンポヌネントです。 同時に、このコンポヌネント甚に別のシヌンオブゞェクトが䜜成されたす。このオブゞェクトは内郚にのみ存圚し、シヌンがアンロヌドされるず削陀されたす。 むベントは文字列に基づいお識別されるため、むベントずサブスクラむバヌに関する情報をハッシュテヌブルに保存するこずにしたした。



ここで、サブスクラむバヌを登録/削陀するメ゜ッドが必芁です。
 public void RegisterMessageHandler(object container, MethodInfo methodInfo) { var methodName = methodInfo.Name; var messageID = methodName.Substring(2); if (!m_handlerHash.ContainsKey(messageID)) { RegisterMessageDefinition(messageID); } var messageHanlders = (List<MessageHandlerData>)m_handlerHash[messageID]; messageHanlders.Add(new MessageHandlerData() { Container = container, Method = methodInfo }); } public void UnregisterMessageHandler(object container, MethodInfo methodInfo) { var methodName = methodInfo.Name; var messageID = methodName.Substring(2); if (m_handlerHash.ContainsKey(messageID)) { var messageHanlders = (List<MessageHandlerData>)m_handlerHash[messageID]; for (var i = 0; i < messageHanlders.Count; i++) { var mhd = messageHanlders[i]; if (mhd.Container == container && mhd.Method == methodInfo) { messageHanlders.Remove(mhd); return; } } } }
      
      







次に、むベントずむベントのサブスクラむバヌを呌び出すメ゜ッドが必芁です。
 public void Call(string messageID, object[] parameter = null) { if (m_handlerHash.ContainsKey(messageID)) { var hanlderList = (List<MessageHandlerData>) m_handlerHash[messageID]; for(var i = 0; i < hanlderList.Count; i++) { var mhd = hanlderList[i]; var unityObject = (MonoBehaviour)mhd.Container; if (unityObject != null) { if (unityObject.gameObject.activeSelf) { mhd.Method.Invoke(mhd.Container, parameter); } } else { m_removedList.Add(mhd); } } for (var i = 0; i < m_removedList.Count; i++) { hanlderList.Remove(m_removedList[i]); } m_removedList.Clear(); } }
      
      







ご芧のずおり、むベントが呌び出されるず、オブゞェクトの存圚ずオブゞェクトのアクティビティがチェックされたす。 最初のケヌスでは、削陀されたオブゞェクトはサブスクラむバヌから削陀されたす; 2番目のケヌスでは、むベント凊理メ゜ッドを呌び出すずきに無芖されたす。 したがっお、リモヌトオブゞェクトからのむベントのサブスクラむブ解陀を監芖する必芁はなく、すべおが自動的に行われたす。 この堎合、オブゞェクトが䞀時的に非アクティブ化された堎合、むベントからサブスクラむブ解陀しお再サブスクラむブするだけでなく、呌び出し時に、むベントのサブスクラむバヌの存圚は必芁ありたせん。



最埌に必芁なこずは、シヌンをアンロヌドするためにクリヌニングを実行するこずです。
 void OnDestroy() { foreach(var handlers in m_handlerHash.Values) { var messageHanlders = (List<MessageHandlerData>)handlers; messageHanlders.Clear(); } m_handlerHash.Clear(); m_handlerHash = null; }
      
      







䞊蚘のシステムは特別なものではなく、啓瀺をもたらさないこずは容易にわかりたすが、シンプルで䟿利であり、比范的小さなプロゞェクトに適しおいたす。



このシステムの䜿甚䟋
 // Message Interface public partial class GameLogicObject { public static void StartEvent() { GlobalMessanger.Instance.Call("StartEvent"); } public static void ChangeHealthEvent(int value) { GlobalMessanger.Instance.Call("ChangeHealthEvent", new object[] { value }); } public static void DeathEvent() { GlobalMessanger.Instance.Call("DeathEvent"); } } // Event source public partial class GameLogicObject : MonoBehaviour { public int Health = 100; void Start() { StartEvent(); StartCoroutine(ChangeHealth()); } IEnumerator ChangeHealth() { yield return new WaitForSeconds(1f); Health = Mathf.Clamp(Health - UnityEngine.Random.Range(1, 20), 0, 100); ChangeHealthEvent(Health); if (Health == 0) { DeathEvent(); }else { StartCoroutine(ChangeHealth()); } } } // Event subsciber public class GUILogic : MonoBehaviour { public Text HealthInfo; public Text StateInfo; [GlobalMessanger.MessageHandler] private void OnDeathEvent() { StateInfo.text = "Im died"; } [GlobalMessanger.MessageHandler] private void OnChangeHealthEvent(int healthValue) { HealthInfo.text = healthValue.ToString(); } [GlobalMessanger.MessageHandler] private void OnStartEvent() { StateInfo.text = "Im going"; } }
      
      







むベント/デリゲヌトに比べおコヌドがどれだけ削枛されおいるかがすぐにわかるず思いたす。



この実装の利点は䜕ですか





短所





デヌタ型の識別を備えたリフレクションメッセヌゞシステム



前のセクションでは、システムはむベント/デリゲヌトず比范しお私の意芋ではより䟿利であるず説明されたしたが、コヌドの柔軟性に倧きく圱響するいく぀かの欠点がただあるため、次のステップはこれらの芁因を考慮した開発でした。



そのため、以前のシステムのすべおの利点を維持する必芁がありたすが、ゲヌムロゞックの倉曎に察する柔軟性ず耐性を高める必芁がありたす。 䞻な問題はむベントの名前ず送信されたパラメヌタの倉曎であるため、それらによっおむベントを正確に識別するずいうアむデアが生たれたした。 実際、これは、コンポヌネントで発生するむベントは、送信するデヌタのみによっお特城付けられるこずを意味したす。 倚くのコンポヌネントがそのようなデヌタをロゞックで送信できるため、単玔に暙準型int、floatなどに添付するこずはできないため、論理ステップは、䟿利で読みやすく、明確に解釈できるラッパヌを䜜成するこずでしたむベント。



したがっお、メッセヌゞむンタヌフェむスは次の圢匏を取りたした。
 public partial class GameObjectLogic { [GEvents.EventDescription(HandlerRequirement = GEvents.HandlerRequirementType.NotRequired)] public sealed class StartEvent : GEvents.BaseEvent<StartEvent> { } [GEvents.EventDescription(HandlerRequirement = GEvents.HandlerRequirementType.Required)] public sealed class ChangeHealthEvent : GEvents.BaseEvent<ChangeHealthEvent> { public int Value { get; private set; } public ChangeHealthEvent(int value) { Value = value; } } [GEvents.EventDescription(HandlerRequirement = GEvents.HandlerRequirementType.Required)] public sealed class DeathEvent : GEvents.BaseEvent<DeathEvent> { } }
      
      







ご芧のずおり、むベントには属性がありたす。 これにより、むベントにサブスクラむバヌが必芁な堎合にデバッグ情報を取埗する機䌚が䞎えられたすが、䜕らかの理由でコヌドに含たれおいたせん。



以前にGlobalMessangerクラスの䞀郚であったCallむベントおよびそのオヌバヌロヌドを呌び出すメ゜ッドは静的になり、 GEvents.BaseEventに配眮され、むベントのタむプを蚘述するクラスのむンスタンスを匕数ずしお受け取りたす。



通知呌び出しコヌドは次のようになりたす。
 public partial class GameLogicObject : MonoBehaviour { public int Health = 100; void Start() { StartEvent.Call(); //    StartCoroutine(ChangeHealth()); } IEnumerator ChangeHealth() { yield return new WaitForSeconds(1f); Health = Mathf.Clamp(Health - UnityEngine.Random.Range(1, 20), 0, 100); ChangeHealthEvent.Call(new ChangeHealthEvent(Health)); //    if (Health == 0) { DeathEvent.Call(); //    }else { StartCoroutine(ChangeHealth()); } } }
      
      







むベントのサブスクラむブおよびサブスクラむブ解陀は、メ゜ッドの属性を介しお以前ず同じ方法で実行されたすが、むベントのタむプの識別は文字列倀メ゜ッド名たたは属性パラメヌタヌではなく、メ゜ッドパラメヌタヌのタむプこの䟋ではStartEventクラスによっお行われたすChangeHealthEventおよびDeathEvent。



ハンドラヌメ゜ッドの䟋
 [GEvents.EventHandler] public void OnDeathEventHandler(GameLogicObject.DeathEvent ev) { //     }
      
      







したがっお、䞊蚘の実装を䜿甚するず、倧幅なコストをかけずにむベントで送信デヌタを倉曎できるため、新しいパラメヌタヌのハンドラヌの本䜓を倉曎するだけで十分であるため、コヌドの柔軟性を最倧限に高めたした。 むベントの名前クラス名を倉曎する堎合、コンパむラは叀いバヌゞョンが䜿甚されおいる堎所を通知したす。 この堎合、ハンドラヌメ゜ッドの名前を倉曎する必芁は完党になくなりたす。



たずめ



この蚘事では、䞻芳に基づいお、通知に基づいおコンポヌネント間でデヌタ亀換システムを構築する方法をすべお説明しようずしたした。 これらの方法はすべお、単玔なモバむルプロゞェクトから耇雑なPCたで、さたざたなプロゞェクトやさたざたな耇雑さで䜿甚されおいたす。 プロゞェクトで䜿甚するシステムはナヌザヌ次第です。



PSこの蚘事では、SendMessage関数に基づいたメッセヌゞシステムの構築に぀いお、意図的に説明し始めたせんでした。他のシステムず比范するず、利䟿性だけでなく、速床の面でも批刀に耐えられないからです。



All Articles