Enlace al proyectoEn este artículo, quiero mostrar cómo puede usar
SharedEvents para controlar un personaje en tercera persona que ofrece un conjunto estándar de activos. Escribí sobre
SharedEvents en artículos anteriores (
esto y
esto ).
¡Bienvenido a cat!
Lo primero que necesita es tomar un proyecto con SharedState / SharedEvents implementado y agregar un conjunto estándar de activos

Creé una escena pequeña y muy simple a partir de prototipos de prefabricados

Y hornee la navegación de superficie con configuraciones estándar

Después de eso, debe agregar el prefabricado
ThirdPersonCharacter a esta escena

Luego puede comenzar y asegurarse de que todo funcione de manera inmediata. Luego puede proceder a configurar el uso de la
infraestructura SharedState / SharedEvents creada anteriormente. Para hacer esto, elimine el componente
ThirdPersonUserController del objeto de carácter.

ya que el control manual usando el teclado no es necesario. El personaje será controlado por agentes, indicando la posición donde se moverá.
Y para que esto sea posible, debe agregar y configurar el componente
NavMeshAgent al objeto de personaje

Ahora necesitas crear un controlador simple que controle al personaje
con el 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() {
Y agréguelo al objeto del personaje, dele enlaces a la cámara, al controlador del personaje y al agente. Todo está disponible desde el escenario.

Y eso es todo. Esto es suficiente para controlar al personaje diciéndole al agente dónde moverse, usando el mouse (clic izquierdo).
Puedes comenzar y asegurarte de que todo funcione

Integración de eventos compartidos
Ahora que la escena base está lista, puede proceder a integrar el control de personajes a través de
SharedEvents . Para hacer esto, necesitará crear varios componentes. El primero de ellos es el componente que será responsable de recibir la señal del mouse y notificar a todos los componentes que rastrean la posición del clic del mouse en la escena, solo estarán interesados en las coordenadas del clic.
El componente se llamará, por ejemplo,
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 necesita una clase para enviar datos en notificaciones. Para tales clases que contendrán solo datos para notificaciones, puede crear un archivo y nombrarlo
DefinedEventsData
Y agregue una clase para enviar la posición de un clic con el mouse
using UnityEngine; public class PointOnGroundEventData : EventData { public Vector3 Point { get; set; } }
Lo siguiente que debe hacer es agregar un componente que será un contenedor o decorador, como desee, para el componente
NavMeshAgent . Como no cambiaré los componentes existentes (tercero),
usaré decoradores para integrar con
SharedState / SharedEvents .

Este componente recibirá notificaciones sobre clics del mouse en ciertos puntos de la escena y le indicará al agente dónde moverse. Y también supervise la posición de la posición del agente en cada cuadro y cree una notificación sobre su cambio.
Este componente dependerá del 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 datos, este componente necesita una clase que debe agregarse al archivo
DefinedEventsData. public class AgentMoveEventData : EventData { public Vector3 DesiredVelocity { get; set; } }
Esto ya es suficiente para que el personaje se mueva. Pero lo hará sin animación, ya que todavía no estamos usando
ThirdPersonCharater . Y para ello, al igual que para
NavMeshAgent, debe crear un decorador CharacterWrapperComponent

El componente escuchará las notificaciones sobre el cambio de posición del agente y moverá al personaje en la dirección recibida de la notificación (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); } }
Y eso es todo. Queda por agregar estos componentes al objeto del juego del personaje.
Debe crear una copia a partir de la existente, eliminar el antiguo componente
AgentMouseControl
Y agregue nuevos
MouseHandlerComponent ,
AgentWrapperComponent y
CharacterWrapperComponent .
En
MouseHandlerComponent debe transferir la cámara desde la escena a partir de la cual se calculará la posición del clic.


Puede comenzar y asegurarse de que todo funcione.
Sucedió así con la ayuda de
SharedEvents para controlar el personaje sin tener una conexión directa entre los componentes, como en el primer ejemplo. Esto permitirá una configuración más flexible de diferentes composiciones de componentes y personalizará la interacción entre ellos.
Comportamiento asincrónico para eventos compartidos
La forma en que ahora se implementa el mecanismo de notificación se basa en la transmisión síncrona de la señal y su procesamiento. Es decir, cuantos más oyentes haya, más tardará en procesarse. Para alejarse de esto, debe implementar el procesamiento de notificaciones asíncronas. Lo primero que debe hacer es agregar una versión asincrónica del método de
publicación
Ahora debe cambiar el método abstracto
OnUpdate en la clase base
SharedStateComponent a asíncrono para que devuelva las tareas que se iniciaron dentro de la implementación de este método y
renómbrelo a
OnUpdateAsync protected abstract Task[] OnUpdateAsync();
También necesitará un mecanismo que controle la finalización de las tareas del marco anterior, antes del actual
private Task[] _previosFrameTasks = null;
El método de
actualización en la clase base debe marcarse como
asíncrono y verificar previamente la ejecución de tareas anteriores
async void Update() { await CompletePreviousTasks();
Después de estos cambios en la clase base, puede proceder a cambiar la implementación del antiguo método
OnUpdate al nuevo
OnUpdateAsync . El primer componente donde se hará esto es
AgentWrapperComponent . Ahora este método espera el retorno del resultado. Este resultado será una variedad de tareas. Una matriz porque en el método se pueden lanzar varios en paralelo y los procesaremos en un grupo.
protected override Task[] OnUpdateAsync() {
El próximo candidato para los cambios en el método
OnUpdate es
MouseHandlerController . Aquí el principio es el mismo.
protected override Task[] OnUpdateAsync() {
En todas las demás implementaciones donde este método estaba vacío, es suficiente reemplazarlo con
protected override Task[] OnUpdateAsync() { return null; }
Eso es todo. Ahora puede comenzar, y si los componentes que procesan notificaciones de forma asincrónica no acceden a los componentes que deben procesarse en el hilo principal, como Transformar, por ejemplo, todo funcionará. De lo contrario, obtendremos errores en la consola informando que estamos accediendo a estos componentes no desde el hilo principal

Para resolver este problema, debe crear un componente que procese el código en el hilo principal. Cree una carpeta separada para las secuencias de comandos y llámela Sistema, y también agregue la secuencia de comandos
Dispatcher .

Este componente será un singleton y tendrá un método abstracto público que ejecutará código en el hilo principal. El principio del despachador es bastante simple. Le pasaremos los delegados para que sean ejecutados en el hilo principal, él los pondrá en la cola. Y en cada cuadro, si algo está en la cola, ejecute en el hilo principal. Este componente se agregará a la escena en una sola copia, me gusta un enfoque tan simple y efectivo.
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();
Lo siguiente que debe hacer es aplicar el despachador. Hay 2 lugares para hacer esto. Primero es el decorador del personaje, allí le preguntamos la dirección. En el componente
CharacterWrapperComponent private void OnAgentMove(AgentMoveEventData eventData) { Dispatcher.RunOnMainThread(() => character.Move(eventData.DesiredVelocity, false, false)); }
El segundo es el decorador del agente, allí le indicamos la posición del agente. En el componente
AgentWrapperComponent private void OnPointToGroundGot(PointOnGroundEventData eventData) {
Ahora no habrá errores, el código funcionará correctamente. Puedes comenzar y ver esto.
Un poco de refactorización
Después de que todo esté listo y todo funcione, puede peinar un poco el código y hacerlo un poco más conveniente y simple. Esto requerirá algunos cambios.
Para no crear una matriz de tareas y colocar la única manualmente, puede crear un método de extensión. Para todos los métodos de extensión, puede usar el mismo archivo para la transmisión a notificaciones, así como para todas las clases. Se ubicará en la carpeta
Sistema y se denominará
Extensiones.
En el interior, crearemos un método de extensión genérico simple que envolverá cualquier instancia en una matriz
public static class Extensions {
El siguiente cambio está ocultando el uso directo del despachador en los componentes. En su lugar, cree un método en la clase base
SharedStateComponent y use el despachador desde allí.
protected void PerformInMainThread(Action action) { Dispatcher.RunOnMainThread(action); }
Y ahora necesita aplicar estos cambios en varios lugares. Primero, cambie los métodos donde creamos manualmente matrices de tareas y agreguemos una sola instancia
En el componente
AgentWrapperComponent protected override Task[] OnUpdateAsync() {
Y en el componente
MouseHandlerComponent protected override Task[] OnUpdateAsync() {
Ahora nos deshacemos del uso directo del despachador en los componentes y en su lugar llamamos al método
PerformInMainThread en la clase base.
Primero en
AgentWrapperComponent private void OnPointToGroundGot(PointOnGroundEventData eventData) {
y en el componente
CharacterWrapperComponent private void OnAgentMove(AgentMoveEventData eventData) { PerformInMainThread(() => character.Move(eventData.DesiredVelocity, false, false)); }
Eso es todo. Queda por ejecutar el juego y asegurarse de que nada se haya roto durante la refactorización y que todo funcione correctamente.