Hallo allerseits!
Ich entschuldige mich im Voraus für Amateurismus, aber ich habe einen Artikel darüber gelesen, wie eine Person versucht hat, mit übermäßiger Konnektivität von Entitäten in Unity umzugehen, und dachte, es wäre interessant, über mein Fahrrad zu sprechen, das ich zusammengestellt habe, um als Spieledesigner Spielprototypen zu erstellen.
Meine Aufgabe war es, ein System von Ereignissen und Nachrichten verschiedener Entitäten zu erstellen, wobei die Kohärenz vermieden wurde, wenn jedes Objekt eine große Anzahl von Verknüpfungen zu anderen Objekten aufweist.
Infolgedessen erlaubt mir mein System, solche Links überhaupt nicht herzustellen. Es löst das Hauptproblem: Es ist praktisch für mich, damit zu arbeiten, es verschmutzt den Code nicht mit unnötigem Müll und es scheint, dass die Leistung nicht so schlecht ist wie die ständigen Aufrufe von GetComponent ().
Ich freue mich über jede Kritik zum Thema, warum dies nicht notwendig ist und wie es trotzdem geht.
Zunächst habe ich die Standardfunktionalität von Unity-Ereignissen neu definiert, um zwei GameObject als Parameter zu übergeben: das Subjekt und das Ereignisobjekt:
[System.Serializable] public class Event : UnityEvent<GameObject, GameObject> {}
Ich speichere Ereignistypen in einer statischen Klasse mit allen Arten von Konstanten:
public enum EventTypes { TargetLock, TargetLost, TargetInRange, TargetOutOfRange, Attack, }
Die Handlerklasse dieser Ereignisse ist trivial. 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); } } }
Dann habe ich die Ereigniskomponente erstellt, die an jedes Objekt im Spiel angehängt ist.
Darin erstelle ich Event-Handler-Paare für alle Arten von Events im Spiel.
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 }, }; } }
Infolgedessen ist die Datei umständlich, aber für mich ist es praktisch, dass sie eine ist - für alle Objekte gleichzeitig.
Ich füge dem Listener für alle Ereignisse im Wörterbuch Ein- und Ausschalter hinzu, sodass alle Spielobjekte alle Spielereignisse abhören, was nicht optimal ist, aber es ist auch für das Prototyping praktisch, wenn ich das Verhalten bestimmter Entitäten im laufenden Betrieb ändere:
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); }
Jetzt muss ich verstehen, an welches Objekt diese Ereignisinstanz angehängt ist.
Dazu suche ich nach Links zu Komponenten von gameObject: Wenn unser Objekt beispielsweise Character ist, wird das entsprechende Feld zu! = Null:
Monster _mob; Character _char; void ComponentsInit() { _mob = GetComponent<Monster>(); _char = GetComponent<Character>(); }
Dies ist eine teure Operation, aber ich mache sie nur einmal in Awake ().
Nun müssen die Handler für alle Arten von Ereignissen beschrieben werden:
void TargetLock(GameObject g1, GameObject g2) { if (_char) _char.TargetLock(g1, g2); if (_mob) _mob.TargetLock(g1, g2); }
Das Ergebnis ist eine große Liste von Methoden, eine für jeden Ereignistyp, in denen der entsprechende Handler innerhalb der Komponente aufgerufen wird, je nachdem, an welchen Ereignistyp diese Instanz angehängt ist.
Dementsprechend schreibe ich in den Charakter- oder Monsterkomponenten bereits so etwas:
public virtual void TargetLock(GameObject g1, GameObject g2) { if (g1 == gameObject) target = g2; if (g2 == gameObject) TargetedBy(g1); }
Gleichzeitig muss ich keine Querverweise zwischen Objekten pflegen, ich behalte alle neuen Ereignisse und ihre „primären“ Handler an einem Ort und die endgültigen Objekte erhalten alle Informationen, die sie zusammen mit dem Ereignis benötigen.
Bisher sind keine nennenswerten Leistungsprobleme aufgetreten: Das System arbeitet "unmerklich" mit mehr als 100 Arten von Ereignissen und Dutzenden von Objekten auf dem Bildschirm und verarbeitet sogar zeitkritische Ereignisse wie das Erleiden von Schäden durch eine Kollision mit einem Pfeil auf einen Charakter.