Charakterverwaltung mit SharedEvents


Link zum Projekt

In diesem Artikel möchte ich zeigen, wie Sie mit SharedEvents einen Charakter einer dritten Person steuern können, der einen Standardsatz von Assets anbietet. Ich habe in früheren Artikeln ( dies und das ) über SharedEvents geschrieben .

Willkommen bei Katze!

Als erstes müssen Sie ein Projekt mit implementierten SharedState / SharedEvents erstellen und einen Standardsatz von Assets hinzufügen



Ich habe eine kleine und sehr einfache Szene aus Prototypen von Fertighäusern erstellt



Und Backflächennavigation mit Standardeinstellungen



Danach müssen Sie dieser Szene das vorgefertigte ThirdPersonCharacter hinzufügen



Dann können Sie beginnen und sicherstellen, dass alles sofort funktioniert. Anschließend können Sie die Verwendung der zuvor erstellten SharedState / SharedEvents-Infrastruktur konfigurieren . Entfernen Sie dazu die ThirdPersonUserController- Komponente aus dem Zeichenobjekt .



da eine manuelle Steuerung über die Tastatur nicht erforderlich ist. Der Charakter wird von Agenten gesteuert und gibt die Position an, an der er sich bewegen wird.

Um dies zu ermöglichen, müssen Sie die NavMeshAgent- Komponente zum Zeichenobjekt hinzufügen und konfigurieren



Jetzt müssen Sie einen einfachen Controller erstellen, der den Charakter steuert
mit der Maus 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); } } } 

Fügen Sie es dem Objekt des Charakters hinzu und verknüpfen Sie es mit der Kamera, dem Controller des Charakters und dem Agenten. Es ist alles von der Bühne erhältlich.



Und alle. Dies reicht aus, um den Charakter zu steuern, indem Sie dem Agenten mit der Maus mitteilen, wohin er sich bewegen soll (Linksklick).

Sie können beginnen und sicherstellen, dass alles funktioniert



SharedEvents-Integration


Nachdem die Basisszene fertig ist, können Sie die Zeichensteuerung über SharedEvents integrieren . Dazu müssen Sie mehrere Komponenten erstellen. Die erste davon ist die Komponente, die für den Empfang des Signals von der Maus und die Benachrichtigung aller Komponenten verantwortlich ist, die die Position des Mausklicks auf die Szene verfolgen. Sie sind nur an den Koordinaten des Klicks interessiert.

Die Komponente wird beispielsweise MouseHandlerComponent genannt



 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; } } 

Diese Komponente benötigt eine Klasse, um Daten in Benachrichtigungen zu senden. Für solche Klassen, die nur Daten für Benachrichtigungen enthalten, können Sie eine Datei erstellen und sie DefinedEventsData nennen



Fügen Sie eine Klasse hinzu, um die Position eines Klicks mit der Maus zu senden

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

Als Nächstes fügen Sie eine Komponente hinzu, die nach Belieben ein Wrapper oder Dekorator für die NavMeshAgent- Komponente ist. Da ich die vorhandenen Komponenten (Drittanbieter) nicht ändern werde, verwende ich Dekoratoren für die Integration in SharedState / SharedEvents .



Diese Komponente erhält Benachrichtigungen über Mausklicks an bestimmten Stellen in der Szene und teilt dem Agenten mit, wohin er sich bewegen soll. Überwachen Sie außerdem die Position der Agentenposition in jedem Frame und erstellen Sie eine Benachrichtigung über deren Änderung.

Diese Komponente hängt von der NavMeshAgent- Komponente ab .

 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); } } 


Zum Senden von Daten benötigt diese Komponente eine Klasse, die der DefinedEventsData- Datei hinzugefügt werden muss.
 public class AgentMoveEventData : EventData { public Vector3 DesiredVelocity { get; set; } } 

Dies reicht bereits aus, damit sich der Charakter bewegen kann. Aber er wird es ohne Animation tun, da wir ThirdPersonCharater noch nicht verwenden. Und dafür müssen Sie genau wie für NavMeshAgent einen CharacterWrapperComponent-Dekorator erstellen



Die Komponente hört Benachrichtigungen über die Positionsänderung des Agenten ab und bewegt das Zeichen in die Richtung, die von der Benachrichtigung (Ereignis) empfangen wurde.

 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); } } 

Und das ist alles. Diese Komponenten müssen noch dem Spielobjekt des Charakters hinzugefügt werden. Sie müssen eine Kopie von der vorhandenen erstellen und die alte AgentMouseControl- Komponente entfernen



Fügen Sie neue MouseHandlerComponent , AgentWrapperComponent und CharacterWrapperComponent hinzu .

In MouseHandlerComponent müssen Sie die Kamera von der Szene übertragen, aus der die Klickposition berechnet wird.





Sie können beginnen und sicherstellen, dass alles funktioniert.

So geschah es mit Hilfe von SharedEvents , den Charakter zu steuern, ohne eine direkte Verbindung zwischen den Komponenten zu haben, wie im ersten Beispiel. Dies ermöglicht eine flexiblere Konfiguration verschiedener Komponentenzusammensetzungen und die Anpassung der Interaktion zwischen ihnen.

Asynchrones Verhalten für SharedEvents


Die Art und Weise, wie der Benachrichtigungsmechanismus jetzt implementiert wird, basiert auf der synchronen Übertragung des Signals und seiner Verarbeitung. Das heißt, je mehr Zuhörer vorhanden sind, desto länger dauert die Verarbeitung. Um dies zu vermeiden, müssen Sie die asynchrone Benachrichtigungsverarbeitung implementieren. Als erstes müssen Sie eine asynchrone Version der Publish- Methode hinzufügen

 //  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); } } 

Jetzt müssen wir die abstrakte OnUpdate- Methode in der SharedStateComponent- Basisklasse in asynchron ändern, damit Aufgaben zurückgegeben werden, die innerhalb der Implementierung dieser Methode initiiert wurden, und in OnUpdateAsync umbenannt werden

 protected abstract Task[] OnUpdateAsync(); 

Sie benötigen außerdem einen Mechanismus, der die Ausführung von Aufgaben aus dem vorherigen Frame vor dem aktuellen Frame steuert

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

Die Update- Methode in der Basisklasse muss als asynchron markiert sein und die Ausführung vorheriger Aufgaben vorab überprüfen

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

Nach diesen Änderungen in der Basisklasse können Sie die Implementierung der alten OnUpdate- Methode in die neue OnUpdateAsync ändern . Die erste Komponente, in der dies durchgeführt wird, ist AgentWrapperComponent . Diese Methode erwartet nun die Rückgabe des Ergebnisses. Dieses Ergebnis ist eine Reihe von Aufgaben. Ein Array, da in der Methode mehrere parallel gestartet werden können und wir sie in einem Bündel verarbeiten.

 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 }) }; } } 

Der nächste Kandidat für Änderungen an der OnUpdate- Methode ist MouseHandlerController . Hier ist das Prinzip dasselbe

  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; } 

In allen anderen Implementierungen, in denen diese Methode leer war, reicht es aus, sie durch zu ersetzen

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

Das ist alles. Jetzt können Sie beginnen. Wenn die Komponenten, die Benachrichtigungen asynchron verarbeiten, nicht auf die Komponenten zugreifen, die im Hauptthread verarbeitet werden sollen, z. B. Transformieren, funktioniert alles. Andernfalls erhalten wir Fehler in der Konsole, die darauf hinweisen, dass wir nicht über den Hauptthread auf diese Komponenten zugreifen



Um dieses Problem zu lösen, müssen Sie eine Komponente erstellen, die den Code im Hauptthread verarbeitet. Erstellen Sie einen separaten Ordner für Skripte, nennen Sie ihn System und fügen Sie das Dispatcher- Skript hinzu.



Diese Komponente ist ein Singleton und verfügt über eine öffentliche abstrakte Methode, die Code im Hauptthread ausführt. Das Prinzip des Dispatchers ist recht einfach. Wir werden ihm die Delegierten weitergeben, die im Haupt-Thread ausgeführt werden sollen, er wird sie in die Warteschlange stellen. Und wenn sich in jedem Frame etwas in der Warteschlange befindet, führen Sie es im Hauptthread aus. Diese Komponente wird sich in einer einzigen Kopie zur Szene hinzufügen. Ich mag einen so einfachen und effektiven Ansatz.

 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; } } 

Als nächstes wenden Sie den Dispatcher an. Es gibt 2 Orte, um dies zu tun. 1. ist der Dekorateur des Charakters, dort fragen wir ihn nach der Richtung. In der CharacterWrapperComponent- Komponente

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

2. ist der Dekorateur des Agenten, dort geben wir die Position für den Agenten an. In der AgentWrapperComponent- Komponente

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

Jetzt gibt es keine Fehler mehr, der Code funktioniert korrekt. Sie können dies starten und sehen.

Ein bisschen Refactoring


Nachdem alles fertig ist und alles funktioniert, können Sie den Code ein wenig kämmen und ihn ein wenig bequemer und einfacher machen. Dies erfordert einige Änderungen.

Um kein Array von Aufgaben zu erstellen und die einzige manuell darin abzulegen, können Sie eine Erweiterungsmethode erstellen. Für alle Erweiterungsmethoden können Sie dieselbe Datei für die Übertragung an Benachrichtigungen sowie für alle Klassen verwenden. Es befindet sich im Ordner System und heißt Erweiterungen



Im Inneren erstellen wir eine einfache generische Erweiterungsmethode, die jede Instanz in ein Array einschließt

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

Die nächste Änderung besteht darin, die direkte Verwendung des Dispatchers in Komponenten zu verbergen. Erstellen Sie stattdessen eine Methode in der SharedStateComponent- Basisklasse und verwenden Sie von dort aus den Dispatcher.

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

Und jetzt müssen Sie diese Änderungen an mehreren Stellen anwenden. Ändern Sie zunächst die Methoden, mit denen wir manuell Arrays von Aufgaben erstellen, und fügen Sie ihnen eine einzelne Instanz hinzu
In der AgentWrapperComponent- Komponente

 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(); } } 

Und in der Komponente 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; } 

Jetzt werden wir die direkte Verwendung des Dispatchers in den Komponenten los und rufen stattdessen die PerformInMainThread- Methode in der Basisklasse auf.

Zuerst in AgentWrapperComponent

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

und in der CharacterWrapperComponent- Komponente

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

Das ist alles. Es bleibt das Spiel zu starten und sicherzustellen, dass während des Refactorings nichts kaputt gegangen ist und alles richtig funktioniert.

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


All Articles