تنظيم نظام الأحداث في الوحدة - من خلال عيون مصمم اللعبة

مرحبا بالجميع!

أعتذر مقدمًا عن الهواة ، لكنني قرأت مقالًا عن كيفية محاولة شخص التعامل مع اتصال الكيان المفرط في Unity ، واعتقدت أنه سيكون من المثير للاهتمام التحدث عن دراجتي ، التي جمعتها لإنشاء نماذج أولية للعبة كمصمم للألعاب.

كانت مهمتي هي إنشاء نظام للأحداث ورسائل الكيانات المختلفة ، وتجنب التماسك ذاته عندما يكون لكل كائن عدد كبير من الروابط لكائنات أخرى.

نتيجة لذلك ، يسمح لي نظامي بعدم إنشاء مثل هذه الروابط على الإطلاق. يحل المشكلة الرئيسية: من المريح بالنسبة لي أن أعمل معها ، وهو لا يتناثر في الكود مع القمامة غير الضرورية ، ويبدو أنه ليس فظيعًا في الأداء مثل المكالمات المستمرة إلى GetComponent ().

سأكون مسرورًا لأي انتقادات حول سبب عدم ضرورة ذلك ، وكيفية القيام بذلك بنفس الطريقة.

للبدء ، قمت بإعادة تعريف الوظيفة القياسية لأحداث Unity لتمرير GameObject اثنين كمعلمات: الموضوع وكائن الحدث:

[System.Serializable] public class Event : UnityEvent<GameObject, GameObject> {} 

أقوم بتخزين أنواع الأحداث في فئة ثابتة مع جميع أنواع الثوابت:

 public enum EventTypes { TargetLock, TargetLost, TargetInRange, TargetOutOfRange, Attack, } 

فئة معالج هذه الأحداث تافهة.
 public class EventManager : MonoBehaviour { Dictionary<EventTypes, Event> events; static EventManager eventManager; public static EventManager Instance { get { if (!eventManager) { eventManager = FindObjectOfType(typeof(EventManager)) as EventManager; if (!eventManager) { print("no event manager"); } else { eventManager.Init(); } } return eventManager; } } void Init() { if (events == null) { events = new Dictionary<EventTypes, Event>(); } } public static void StartListening(EventTypes eventType, UnityAction<GameObject, GameObject> listener) { if (Instance.events.TryGetValue(eventType, out Event thisEvent)) { thisEvent.AddListener(listener); } else { thisEvent = new Event(); thisEvent.AddListener(listener); Instance.events.Add(eventType, thisEvent); } } public static void StopListening(EventTypes eventType, UnityAction<GameObject, GameObject> listener) { if (eventManager == null) return; if (Instance.events.TryGetValue(eventType, out Event thisEvent)) { thisEvent.RemoveListener(listener); } } public static void TriggerEvent(EventTypes eventType, GameObject obj1, GameObject obj2) { if (Instance.events.TryGetValue(eventType, out Event thisEvent)) { thisEvent.Invoke(obj1, obj2); } } } 


ثم قمت بإنشاء مكون الأحداث ، والذي يرتبط بكل كائن في اللعبة.
في ذلك ، أقوم بإنشاء أزواج Event-Handler لجميع أنواع الأحداث في اللعبة.

 public class Events : MonoBehaviour { Dictionary<EventTypes, UnityAction<GameObject, GameObject>> eventHandlers; void HandlersInit() { eventHandlers = new Dictionary<EventTypes, UnityAction<GameObject, GameObject>> { { EventTypes.TargetLock, TargetLock }, { EventTypes.TargetLost, TargetLost }, { EventTypes.TargetInRange, TargetInRange }, { EventTypes.TargetOutOfRange, TargetOutOfRange }, { EventTypes.Attack, Attack }, }; } } 

ونتيجة لذلك ، يكون الملف مرهقًا ، لكن من المناسب بالنسبة لي أنه واحد - لكل الكائنات في آن واحد.

أقوم بإضافة وإيقاف تشغيل المستمعين لجميع الأحداث في القاموس ، وبالتالي فإن جميع كائنات اللعبة تستمع إلى جميع أحداث اللعبة ، وهو أمر غير مثالي ، ولكن ، مرة أخرى ، يكون مناسبًا للنماذج الأولية عندما أقوم بتغيير سلوك بعض الكيانات أثناء التنقل:

  void OnEnable() { foreach (KeyValuePair<EventTypes, UnityAction<GameObject, GameObject>> pair in eventHandlers) StartListening(pair.Key, pair.Value); } void OnDisable() { foreach (KeyValuePair<EventTypes, UnityAction<GameObject, GameObject>> pair in eventHandlers) StopListening(pair.Key, pair.Value); } 

أحتاج الآن إلى فهم أي كائن مرفق به مثيل الأحداث هذا.

للقيام بذلك ، أبحث عن روابط لمكونات من gameObject: على سبيل المثال ، إذا كان كائننا هو الحرف ، فسيصبح الحقل المقابل! = لاغ:

  Monster _mob; Character _char; void ComponentsInit() { _mob = GetComponent<Monster>(); _char = GetComponent<Character>(); } 

هذه عملية باهظة الثمن ، لكنني أفعلها مرة واحدة فقط في Awake ().

يبقى الآن لوصف معالجات لجميع أنواع الأحداث:

  void TargetLock(GameObject g1, GameObject g2) { if (_char) _char.TargetLock(g1, g2); if (_mob) _mob.TargetLock(g1, g2); } 

والنتيجة هي قائمة كبيرة من الطرق ، واحدة لكل نوع من الأحداث ، داخل كل منها يسمى المعالج المقابل داخل المكون ، وهذا يتوقف على نوع الحدث الذي يتم إرفاق هذا المثيل به.

وفقًا لذلك ، داخل مكونات الحرف أو Monster ، أكتب بالفعل شيء من هذا القبيل:

  public virtual void TargetLock(GameObject g1, GameObject g2) { if (g1 == gameObject) target = g2; if (g2 == gameObject) TargetedBy(g1); } 

في الوقت نفسه ، لا أحتاج إلى الاحتفاظ بأي مراجع تبادلية بين الكائنات ، وأحتفظ بجميع الأحداث الجديدة ومعالجاتها "الأولية" في مكان واحد ، وتتلقى الكائنات النهائية جميع المعلومات التي تحتاجها مع الحدث.

حتى الآن ، لم أواجه أي مشاكل ملحوظة في الأداء: يعمل النظام "بشكل غير محسوس" مع أكثر من 100 نوع من الأحداث وعشرات الكائنات على الشاشة ، ويعالج حتى الأحداث الحساسة للوقت مثل أخذ الضرر من تصادم بسهم لشخص ما.

Source: https://habr.com/ru/post/ar445358/


All Articles