Organização do sistema de eventos no Unity - através dos olhos de um designer de jogos

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.

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


All Articles