Olá pessoal!
Peço desculpas antecipadamente pelo amadorismo, mas li um artigo sobre como uma pessoa tentou lidar com a excessiva conectividade de entidades no Unity e pensei que seria interessante falar sobre minha bicicleta, que eu montei para criar protótipos de jogos como designer de jogos.
Minha tarefa era criar um sistema de eventos e mensagens de várias entidades, evitando a própria coerência quando cada objeto possui um grande número de links para outros objetos.
Como resultado, meu sistema me permite não fazer esses links. Ele resolve o problema principal: é conveniente para mim trabalhar com ele, não enche o código de lixo desnecessário e, ao que parece, não é tão terrível no desempenho quanto as constantes chamadas para GetComponent ().
Ficarei feliz em receber qualquer crítica sobre o assunto sobre por que isso não é necessário e sobre como fazer o mesmo.
Para começar, redefini a funcionalidade padrão dos eventos do Unity para passar dois GameObject como parâmetros: o assunto e o objeto do evento:
[System.Serializable] public class Event : UnityEvent<GameObject, GameObject> {}
Eu armazeno tipos de eventos em uma classe estática com todos os tipos de constantes:
public enum EventTypes { TargetLock, TargetLost, TargetInRange, TargetOutOfRange, Attack, }
A classe de manipulador desses eventos é 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); } } }
Então eu criei o componente Events, que é anexado a todos os objetos no jogo.
Nele, crio pares Event-Handler para todos os tipos de eventos no jogo.
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, o arquivo é complicado, mas é conveniente para mim que seja um - para todos os objetos ao mesmo tempo.
Eu adiciono e desativei os ouvintes para todos os eventos no dicionário, para que todos os objetos do jogo escutem todos os eventos do jogo, o que não é o ideal, mas, novamente, é conveniente para a criação de protótipos quando altero o comportamento de determinadas entidades em tempo real:
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); }
Agora eu preciso entender a qual objeto esta instância de Eventos está anexada.
Para fazer isso, procuro links para componentes de gameObject: por exemplo, se nosso objeto for Character, o campo correspondente se tornará! = Null:
Monster _mob; Character _char; void ComponentsInit() { _mob = GetComponent<Monster>(); _char = GetComponent<Character>(); }
Esta é uma operação cara, mas eu a faço apenas uma vez em Awake ().
Agora resta descrever os manipuladores para todos os tipos de eventos:
void TargetLock(GameObject g1, GameObject g2) { if (_char) _char.TargetLock(g1, g2); if (_mob) _mob.TargetLock(g1, g2); }
O resultado é uma grande lista de métodos, um para cada tipo de evento, dentro de cada um dos quais o manipulador correspondente é chamado dentro do componente, dependendo do tipo de evento ao qual esta instância está anexada.
Assim, dentro dos componentes Personagem ou Monstro, já estou escrevendo algo assim:
public virtual void TargetLock(GameObject g1, GameObject g2) { if (g1 == gameObject) target = g2; if (g2 == gameObject) TargetedBy(g1); }
Ao mesmo tempo, não preciso manter nenhuma referência cruzada entre objetos, mantenho todos os novos eventos e seus manipuladores "primários" em um único local, e os objetos finais recebem todas as informações necessárias junto com o evento.
Até agora, não encontrei nenhum problema perceptível de desempenho: o sistema "imperceptivelmente" trabalha com mais de 100 tipos de eventos e dezenas de objetos na tela, processando até eventos sensíveis ao tempo, como sofrer dano de uma colisão com uma flecha em um personagem.