Gerenciamento de caracteres com SharedEvents


Link para o projeto

Neste artigo, quero mostrar como você pode usar o SharedEvents para controlar um caractere de terceira pessoa que oferece um conjunto padrão de ativos. Eu escrevi sobre o SharedEvents em artigos anteriores ( isso e isso ).

Bem-vindo ao gato!

A primeira coisa que você precisa é levar um projeto com o SharedState / SharedEvents implementado e adicionar um conjunto padrão de ativos



Eu criei uma cena pequena e muito simples de pré-fabricar protótipos



E faça a navegação na superfície com configurações padrão



Depois disso, você precisa adicionar o ThirdPersonCharacter pré-fabricado a esta cena



Então você pode começar e garantir que tudo funcione imediatamente. Em seguida, você pode continuar a configurar o uso da infraestrutura SharedState / SharedEvents criada anteriormente. Para fazer isso, remova o componente ThirdPersonUserController do objeto de caractere.



pois o controle manual usando o teclado não é necessário. O personagem será controlado pelos agentes, indicando a posição para onde se moverá.

E para tornar isso possível, você precisa adicionar e configurar o componente NavMeshAgent ao objeto de caractere



Agora você precisa criar um controlador simples que controle o personagem
com o mouse AgentMouseController



using UnityEngine; using UnityEngine.AI; using UnityStandardAssets.Characters.ThirdPerson; public class AgentMouseController : MonoBehaviour { public NavMeshAgent agent; public ThirdPersonCharacter character; public Camera cam; void Start() { //      agent.updateRotation = false; } void Update() { //     if (Input.GetMouseButtonDown(0)) { Ray ray = cam.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { agent.SetDestination(hit.point); } } //    ,     if(agent.remainingDistance > agent.stoppingDistance) { character.Move(agent.desiredVelocity, false, false); } else // ,    { character.Move(Vector3.zero, false, false); } } } 

E adicione-o ao objeto do personagem, vincule-o à câmera, ao controlador do personagem e ao agente. Está tudo disponível a partir do palco.



E isso é tudo. Isso é suficiente para controlar o personagem dizendo ao agente para onde se mover, usando o mouse (clique esquerdo).

Você pode começar e garantir que tudo funcione



Integração SharedEvents


Agora que a cena base está pronta, você pode prosseguir para integrar o controle de caracteres através do SharedEvents . Para fazer isso, você precisará criar vários componentes. O primeiro deles é o componente que será responsável por receber o sinal do mouse e notificar todos os componentes que rastreiam a posição do clique do mouse na cena; eles estarão interessados ​​apenas nas coordenadas do clique.

O componente será chamado, por exemplo, MouseHandlerComponent



 using UnityEngine; public class MouseHandlerComponent : SharedStateComponent { public Camera cam; #region MonoBehaviour protected override void OnSharedStateChanged(SharedStateChangedEventData newState) { } protected override void OnStart() { if (cam == null) throw new MissingReferenceException("   "); } protected override void OnUpdate() { //     if (Input.GetMouseButtonDown(0)) { //           var hit = GetMouseHit(); Events.PublishAsync("poittogound", new PointOnGroundEventData { Sender = this, Point = hit.point }); } } #endregion private RaycastHit GetMouseHit() { Ray ray = cam.ScreenPointToRay(Input.mousePosition); RaycastHit hit; Physics.Raycast(ray, out hit); return hit; } } 

Este componente precisa de uma classe para enviar dados nas notificações. Para essas classes que conterão apenas dados para notificações, você pode criar um arquivo e nomeá-lo como DefinedEventsData



E adicione uma classe a ela, para enviar a posição de um clique com o mouse

 using UnityEngine; public class PointOnGroundEventData : EventData { public Vector3 Point { get; set; } } 

A próxima coisa a fazer é adicionar um componente que será um wrapper ou decorador, como você desejar, para o componente NavMeshAgent . Como não alterarei os componentes existentes (de terceiros), usarei decoradores para integrar-se ao SharedState / SharedEvents .



Este componente receberá notificações sobre os cliques do mouse em determinados pontos da cena e informará o agente para onde se mover. E também monitore a posição da posição do agente em cada quadro e crie uma notificação sobre sua alteração.

Este componente dependerá do componente NavMeshAgent.

 using UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(NavMeshAgent))] public class AgentWrapperComponent : SharedStateComponent { private NavMeshAgent agent; #region Monobehaviour protected override void OnSharedStateChanged(SharedStateChangedEventData newState) { } protected override void OnStart() { //  agent = GetComponent<NavMeshAgent>(); //      agent.updateRotation = false; Events.Subscribe<PointOnGroundEventData>("pointtoground", OnPointToGroundGot); } protected override void OnUpdate() { //     if (agent.remainingDistance > agent.stoppingDistance) { Events.Publish("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity }); } else { Events.Publish("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero }); } } #endregion private void OnPointToGroundGot(PointOnGroundEventData eventData) { //    agent.SetDestination(eventData.Point); } } 


Para enviar dados, esse componente precisa de uma classe que precise ser adicionada ao arquivo DefinedEventsData.
 public class AgentMoveEventData : EventData { public Vector3 DesiredVelocity { get; set; } } 

Isso já é suficiente para o personagem se mover. Mas ele fará isso sem animação, pois ainda não estamos usando o ThirdPersonCharater . E, para isso, assim como no NavMeshAgent, você precisa criar um decorador CharacterWrapperComponent



O componente ouvirá notificações sobre a mudança de posição do agente e moverá o personagem na direção recebida da notificação (evento).

 using UnityEngine; using UnityStandardAssets.Characters.ThirdPerson; [RequireComponent(typeof(ThirdPersonCharacter))] public class CharacterWrapperComponent : SharedStateComponent { private ThirdPersonCharacter character; #region Monobehaviour protected override void OnSharedStateChanged(SharedStateChangedEventData newState) { } protected override void OnStart() { character = GetComponent<ThirdPersonCharacter>(); Events.Subscribe<AgentMoveEventData>("agentmoved", OnAgentMove); } protected override void OnUpdate() { } #endregion private void OnAgentMove(AgentMoveEventData eventData) { //       character.Move(eventData.DesiredVelocity, false, false); } } 

E isso é tudo. Resta adicionar esses componentes ao objeto de jogo do personagem. Você precisa criar uma cópia da existente, remover o antigo componente AgentMouseControl



E adicione os novos MouseHandlerComponent , AgentWrapperComponent e CharacterWrapperComponent .

No MouseHandlerComponent, você precisa transferir a câmera da cena a partir da qual a posição do clique será calculada.





Você pode iniciar e garantir que tudo funcione.

Aconteceu com a ajuda do SharedEvents para controlar o personagem sem ter uma conexão direta entre os componentes, como no primeiro exemplo. Isso permitirá uma configuração mais flexível de diferentes composições de componentes e personalizará a interação entre eles.

Comportamento assíncrono para SharedEvents


A maneira como o mecanismo de notificação é implementado agora é baseada na transmissão síncrona do sinal e seu processamento. Ou seja, quanto mais ouvintes houver, mais tempo levará para processar. Para evitar isso, você precisa implementar o processamento de notificações assíncronas. A primeira coisa a fazer é adicionar uma versão assíncrona do método Publish

 //  data    eventName  public async Task PublishAsync<T>(string eventName, T data) where T : EventData { if (_subscribers.ContainsKey(eventName)) { var listOfDelegates = _subscribers[eventName]; var tasks = new List<Task>(); foreach (Action<T> callback in listOfDelegates) { tasks.Add(Task.Run(() => { callback(data); })); } await Task.WhenAll(tasks); } } 

Agora você precisa alterar o método OnUpdate abstrato na classe base SharedStateComponent para assíncrono, para que ele retorne tarefas iniciadas dentro da implementação desse método e renomeie-o para OnUpdateAsync

 protected abstract Task[] OnUpdateAsync(); 

Você também precisará de um mecanismo que controle a conclusão das tarefas do quadro anterior, antes do atual

 private Task[] _previosFrameTasks = null; //   private async Task CompletePreviousTasks() { if (_previosFrameTasks != null && _previosFrameTasks.Length > 0) await Task.WhenAll(_previosFrameTasks); } 

O método Update na classe base precisa ser marcado como assíncrono e verificar previamente a execução de tarefas anteriores

 async void Update() { await CompletePreviousTasks(); //     _previosFrameTasks = OnUpdateAsync(); } 

Após essas alterações na classe base, você pode continuar alterando a implementação do método OnUpdate antigo para o novo OnUpdateAsync . O primeiro componente em que isso será feito é o AgentWrapperComponent . Agora este método espera o retorno do resultado. Este resultado será uma variedade de tarefas. Uma matriz porque no método várias podem ser iniciadas em paralelo e as processaremos em conjunto.

 protected override Task[] OnUpdateAsync() { //     if (agent.remainingDistance > agent.stoppingDistance) { return new Task[] { Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity }) }; } else { return new Task[] { Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero }) }; } } 

O próximo candidato a alterações no método OnUpdate é MouseHandlerController . Aqui o princípio é o mesmo

  protected override Task[] OnUpdateAsync() { //     if (Input.GetMouseButtonDown(0)) { //           var hit = GetMouseHit(); return new Task[] { Events.PublishAsync("pointtoground", new PointOnGroundEventData { Sender = this, Point = hit.point }) }; } return null; } 

Em todas as outras implementações em que esse método estava vazio, basta substituir por

 protected override Task[] OnUpdateAsync() { return null; } 

Só isso. Agora você pode iniciar e, se os componentes que processam notificações de forma assíncrona não acessarem os componentes que devem ser processados ​​no encadeamento principal, como o Transform, por exemplo, tudo funcionará. Caso contrário, obteremos erros no console informando que estamos acessando esses componentes e não do encadeamento principal



Para resolver esse problema, você precisa criar um componente que processará o código no thread principal. Crie uma pasta separada para scripts e chame-o de Sistema, e também adicione o script Dispatcher .



Este componente será um singleton e terá um método abstrato público que executará o código no thread principal. O princípio do expedidor é bastante simples. Passaremos a ele os delegados para serem executados no thread principal, ele os colocará na fila. E em cada quadro, se houver algo na fila, execute no encadeamento principal. Este componente se adiciona à cena em uma única cópia, eu gosto de uma abordagem tão simples e eficaz.

 using System; using System.Collections; using System.Collections.Concurrent; using UnityEngine; public class Dispatcher : MonoBehaviour { private static Dispatcher _instance; private volatile bool _queued = false; private ConcurrentQueue<Action> _queue = new ConcurrentQueue<Action>(); private static readonly object _sync_ = new object(); //     public static void RunOnMainThread(Action action) { _instance._queue.Enqueue(action); lock (_sync_) { _instance._queued = true; } } //       () [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Initialize() { if (_instance == null) { _instance = new GameObject("Dispatcher").AddComponent<Dispatcher>(); DontDestroyOnLoad(_instance.gameObject); } } void Update() { if (_queued) //   { while (!_queue.IsEmpty) { if (_queue.TryDequeue(out Action a)) { StartCoroutine(ActionWrapper(a)); } } lock (_sync_) { _queued = false; } } } //    IEnumerator ActionWrapper(Action a) { a(); yield return null; } } 

A próxima coisa a fazer é aplicar o expedidor. Existem 2 lugares para fazer isso. 1º é o decorador do personagem, lá pedimos a ele a direção. No componente CharacterWrapperComponent

 private void OnAgentMove(AgentMoveEventData eventData) { Dispatcher.RunOnMainThread(() => character.Move(eventData.DesiredVelocity, false, false)); } 

2º é o decorador do agente, lá indicamos a posição do agente. No componente AgentWrapperComponent

 private void OnPointToGroundGot(PointOnGroundEventData eventData) { //    Dispatcher.RunOnMainThread(() => agent.SetDestination(eventData.Point)); } 

Agora não haverá erros, o código funcionará corretamente. Você pode começar e ver isso.

Um pouco de refatoração


Depois que tudo estiver pronto e tudo funcionar, você poderá pentear o código um pouco e torná-lo um pouco mais conveniente e simples. Isso exigirá algumas alterações.

Para não criar uma matriz de tarefas e colocar a única manualmente, você pode criar um método de extensão. Para todos os métodos de extensão, você pode usar o mesmo arquivo para transmitir notificações, bem como para todas as classes. Ele estará localizado na pasta Sistema e chamado Extensões



Dentro, criaremos um método de extensão genérico simples que envolverá qualquer instância em uma matriz

 public static class Extensions { //    public static T[] WrapToArray<T>(this T source) { return new T[] { source }; } } 

A próxima alteração está ocultando o uso direto do despachante nos componentes. Em vez disso, crie um método na classe base SharedStateComponent e use o distribuidor a partir daí.

 protected void PerformInMainThread(Action action) { Dispatcher.RunOnMainThread(action); } 

E agora você precisa aplicar essas alterações em vários lugares. Primeiro, altere os métodos nos quais criamos manualmente matrizes de tarefas e colocamos nelas uma única instância
No componente AgentWrapperComponent

 protected override Task[] OnUpdateAsync() { //     if (agent.remainingDistance > agent.stoppingDistance) { return Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity }) .WrapToArray(); } else { return Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero }) .WrapToArray(); } } 

E no componente MouseHandlerComponent

 protected override Task[] OnUpdateAsync() { //     if (Input.GetMouseButtonDown(0)) { //           var hit = GetMouseHit(); return Events.PublishAsync("pointtoground", new PointOnGroundEventData { Sender = this, Point = hit.point }) .WrapToArray(); } return null; } 

Agora nos livramos do uso direto do despachante nos componentes e, em vez disso, chamamos o método PerformInMainThread na classe base.

Primeiro no AgentWrapperComponent

 private void OnPointToGroundGot(PointOnGroundEventData eventData) { //    PerformInMainThread(() => agent.SetDestination(eventData.Point)); } 

e no componente CharacterWrapperComponent

 private void OnAgentMove(AgentMoveEventData eventData) { PerformInMainThread(() => character.Move(eventData.DesiredVelocity, false, false)); } 

Só isso. Resta executar o jogo e garantir que nada tenha quebrado durante a refatoração e que tudo funcione corretamente.

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


All Articles