Link para o projetoNeste 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() {
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() {
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() {
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
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;
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();
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() {
O próximo candidato a alterações no método
OnUpdate é
MouseHandlerController . Aqui o princípio é o mesmo
protected override Task[] OnUpdateAsync() {
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();
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) {
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 {
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() {
E no componente
MouseHandlerComponent protected override Task[] OnUpdateAsync() {
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) {
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.