Link zum ProjektIn 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() {
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() {
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() {
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
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;
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();
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() {
Der nächste Kandidat für Änderungen an der
OnUpdate- Methode ist
MouseHandlerController . Hier ist das Prinzip dasselbe
protected override Task[] OnUpdateAsync() {
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();
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) {
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 {
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() {
Und in der Komponente
MouseHandlerComponent protected override Task[] OnUpdateAsync() {
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) {
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.