Hola a todos!
Me disculpo de antemano por el amateurismo, pero leí un artículo sobre cómo una persona trató de lidiar con una conectividad de entidad excesiva en Unity, y pensé que sería interesante hablar sobre mi bicicleta, que reuní para crear prototipos de juegos como diseñador de juegos.
Mi tarea era crear un sistema de eventos y mensajes de varias entidades, evitando la coherencia cuando cada objeto tiene una gran cantidad de enlaces a otros objetos.
Como resultado, mi sistema me permite no hacer tales enlaces en absoluto. Resuelve el problema principal: es conveniente para mí trabajar con él, no ensucia el código con basura innecesaria y, al parecer, no tiene un rendimiento tan terrible como las constantes llamadas a GetComponent ().
Estaré encantado de cualquier crítica sobre el tema de por qué esto no es necesario y cómo hacerlo de todos modos.
Para comenzar, redefiní la funcionalidad estándar de los eventos de Unity para pasar dos GameObject como parámetros: el sujeto y el objeto del evento:
[System.Serializable] public class Event : UnityEvent<GameObject, GameObject> {}
Almacene tipos de eventos en una clase estática con todo tipo de constantes:
public enum EventTypes { TargetLock, TargetLost, TargetInRange, TargetOutOfRange, Attack, }
La clase de controlador de estos eventos es 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); } } }
Luego creé el componente Eventos, que se adjunta a cada objeto en el juego.
En él, creo pares de Event-Handler para todo tipo de eventos en el juego.
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 }, }; } }
Como resultado, el archivo es engorroso, pero es conveniente para mí que sea uno, para todos los objetos a la vez.
Le agrego oyentes para todos los eventos del diccionario, por lo que todos los objetos del juego escuchan todos los eventos del juego, lo que no es óptimo, pero, de nuevo, es conveniente para la creación de prototipos cuando cambio el comportamiento de ciertas entidades sobre la marcha:
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); }
Ahora necesito entender a qué objeto está conectada esta instancia de Eventos.
Para hacer esto, busco enlaces a componentes de gameObject: por ejemplo, si nuestro objeto es Character, el campo correspondiente se convertirá en! = Nulo:
Monster _mob; Character _char; void ComponentsInit() { _mob = GetComponent<Monster>(); _char = GetComponent<Character>(); }
Esta es una operación costosa, pero solo lo hago una vez en Awake ().
Ahora queda por describir los controladores para todo tipo de eventos:
void TargetLock(GameObject g1, GameObject g2) { if (_char) _char.TargetLock(g1, g2); if (_mob) _mob.TargetLock(g1, g2); }
El resultado es una gran lista de métodos, uno para cada tipo de evento, dentro de cada uno de los cuales se llama al controlador correspondiente dentro del componente, dependiendo del tipo de evento al que se adjunte esta instancia.
En consecuencia, dentro de los componentes de Personaje o Monstruo, ya estoy escribiendo algo así:
public virtual void TargetLock(GameObject g1, GameObject g2) { if (g1 == gameObject) target = g2; if (g2 == gameObject) TargetedBy(g1); }
Al mismo tiempo, no necesito mantener referencias cruzadas entre objetos, mantengo todos los eventos nuevos y sus controladores "primarios" en un solo lugar, y los objetos finales reciben toda la información que necesitan junto con el evento.
Hasta ahora, no he encontrado ningún problema de rendimiento notable: el sistema "imperceptiblemente" funciona con más de 100 tipos de eventos y docenas de objetos en la pantalla, procesando incluso eventos sensibles al tiempo como recibir daños de una colisión con una flecha a un personaje.