Lien vers le projetDans cet article, je veux montrer comment vous pouvez utiliser
SharedEvents pour contrôler un personnage à la troisième personne qui propose un ensemble standard d'actifs. J'ai écrit sur
SharedEvents dans des articles précédents (
ceci et
cela ).
Bienvenue au chat!
La première chose dont vous avez besoin est de prendre un projet avec SharedState / SharedEvents implémentés et d'ajouter un ensemble standard d'actifs

J'ai créé une petite scène très simple à partir de préfabriqués de prototypage

Et cuire la navigation de surface avec des paramètres standard

Après cela, vous devez ajouter le préfabriqué
ThirdPersonCharacter à cette scène

Ensuite, vous pouvez commencer et vous assurer que tout fonctionne hors de la boîte. Ensuite, vous pouvez procéder à la configuration de l'utilisation de l'
infrastructure SharedState / SharedEvents précédemment créée. Pour ce faire, supprimez le composant
ThirdPersonUserController de l'objet caractère.

car un contrôle manuel à l'aide du clavier n'est pas nécessaire. Le personnage sera contrôlé par des agents, indiquant la position où il se déplacera.
Et pour rendre cela possible, vous devez ajouter et configurer le composant
NavMeshAgent à l'objet personnage

Maintenant, vous devez créer un contrôleur simple qui contrôlera le personnage
avec la souris
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() {
Et ajoutez-le à l'objet du personnage, donnez-lui des liens vers la caméra, le contrôleur du personnage et l'agent. Tout est disponible sur scène.

Et c'est tout. Cela suffit pour contrôler le personnage en disant à l'agent où se déplacer, à l'aide de la souris (clic gauche).
Vous pouvez commencer et vous assurer que tout fonctionne

Intégration SharedEvents
Maintenant que la scène de base est prête, vous pouvez procéder à l'intégration du contrôle des personnages via
SharedEvents . Pour ce faire, vous devrez créer plusieurs composants. Le premier d'entre eux est le composant qui sera chargé de recevoir le signal de la souris et de notifier tous les composants qui suivent la position du clic de souris sur la scène, ils ne seront intéressés que par les coordonnées du clic.
Le composant sera appelé, par exemple,
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() {
Ce composant a besoin d'une classe pour envoyer des données dans les notifications. Pour ces classes qui ne contiendront que des données pour les notifications, vous pouvez créer un fichier et le nommer
DefinedEventsData
Et ajoutez-y une classe, pour envoyer la position d'un clic avec la souris
using UnityEngine; public class PointOnGroundEventData : EventData { public Vector3 Point { get; set; } }
La prochaine chose à faire est d'ajouter un composant qui sera un wrapper ou un décorateur, comme vous le souhaitez, pour le composant
NavMeshAgent . Étant donné que je ne changerai pas les composants existants (tiers), j'utiliserai des décorateurs pour intégrer avec
SharedState / SharedEvents .

Ce composant recevra des notifications sur les clics de souris à certains points de la scène et indiquera à l'agent où se déplacer. Et surveillez également la position de la position de l'agent dans chaque trame et créez une notification concernant sa modification.
Ce composant dépendra du composant
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() {
Pour envoyer des données, ce composant a besoin d'une classe qui doit être ajoutée au fichier
DefinedEventsData. public class AgentMoveEventData : EventData { public Vector3 DesiredVelocity { get; set; } }
C'est déjà suffisant pour que le personnage bouge. Mais il le fera sans animation, car nous n'utilisons pas encore
ThirdPersonCharater . Et pour cela, tout comme pour
NavMeshAgent, vous devez créer un décorateur CharacterWrapperComponent

Le composant écoutera les notifications concernant le changement de position de l'agent et déplacera le personnage dans la direction reçue de la notification (événement).
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); } }
Et c'est tout. Il reste à ajouter ces composants à l'objet de jeu du personnage. Vous devez créer une copie de l'existant, supprimer l'ancien composant
AgentMouseControl
Et ajoutez de nouveaux
MouseHandlerComponent ,
AgentWrapperComponent et
CharacterWrapperComponent .
Dans
MouseHandlerComponent, vous devez transférer la caméra depuis la scène à partir de laquelle la position du clic sera calculée.


Vous pouvez commencer et vous assurer que tout fonctionne.
C'est arrivé avec l'aide de
SharedEvents pour contrôler le personnage sans avoir une connexion directe entre les composants, comme dans le premier exemple. Cela permettra une configuration plus flexible des différentes compositions de composants et personnalisera l'interaction entre eux.
Comportement asynchrone pour SharedEvents
Le mode de mise en œuvre du mécanisme de notification est basé sur la transmission synchrone du signal et son traitement. Autrement dit, plus il y a d'auditeurs, plus il faudra de temps pour traiter. Pour éviter cela, vous devez implémenter un traitement de notification asynchrone. La première chose à faire est d'ajouter une version asynchrone de la méthode
Publish
Vous devez maintenant modifier la méthode
OnUpdate abstraite de la classe de base
SharedStateComponent en asynchrone afin qu'elle renvoie les tâches qui ont été lancées dans l'implémentation de cette méthode et la renommer en
OnUpdateAsync protected abstract Task[] OnUpdateAsync();
Vous aurez également besoin d'un mécanisme qui contrôlera l'achèvement des tâches du cadre précédent, avant le courant
private Task[] _previosFrameTasks = null;
La méthode
Update de la classe de base doit être marquée comme
asynchrone et pré-vérifier l'exécution des tâches précédentes
async void Update() { await CompletePreviousTasks();
Après ces modifications dans la classe de base, vous pouvez passer de l'implémentation de l'ancienne méthode
OnUpdate à la nouvelle
OnUpdateAsync . Le premier composant où cela sera fait est
AgentWrapperComponent . Maintenant, cette méthode attend le retour du résultat. Ce résultat sera un tableau de tâches. Un tableau, car dans la méthode, plusieurs peuvent être lancés en parallèle et nous les traiterons en groupe.
protected override Task[] OnUpdateAsync() {
Le candidat suivant pour les modifications de la méthode
OnUpdate est
MouseHandlerController . Ici, le principe est le même
protected override Task[] OnUpdateAsync() {
Dans toutes les autres implémentations où cette méthode était vide, il suffit de la remplacer par
protected override Task[] OnUpdateAsync() { return null; }
C’est tout. Vous pouvez maintenant démarrer et si les composants qui traitent les notifications de manière asynchrone n'accèdent pas aux composants qui doivent être traités dans le thread principal, tels que Transformer, par exemple, tout fonctionnera. Sinon, nous obtiendrons des erreurs dans la console vous informant que nous accédons à ces composants pas à partir du thread principal

Pour résoudre ce problème, vous devez créer un composant qui traitera le code dans le thread principal. Créez un dossier séparé pour les scripts et appelez-le System, et ajoutez-y également le script
Dispatcher .

Ce composant sera un singleton et aura une méthode abstraite publique qui exécutera le code dans le thread principal. Le principe du répartiteur est assez simple. Nous lui transmettrons les délégués à exécuter dans le fil principal, il les mettra dans la file d'attente. Et dans chaque trame, si quelque chose est dans la file d'attente, exécutez dans le thread principal. Ce composant s'ajoutera à la scène en un seul exemplaire, j'aime une approche aussi simple et efficace.
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();
La prochaine chose à faire est d'appliquer le répartiteur. Il y a 2 endroits pour le faire. Le premier est le décorateur du personnage, là on lui demande la direction. Dans le composant
CharacterWrapperComponent private void OnAgentMove(AgentMoveEventData eventData) { Dispatcher.RunOnMainThread(() => character.Move(eventData.DesiredVelocity, false, false)); }
2ème est le décorateur de l'agent, là nous indiquons la position de l'agent. Dans le composant
AgentWrapperComponent private void OnPointToGroundGot(PointOnGroundEventData eventData) {
Maintenant, il n'y aura plus d'erreur, le code fonctionnera correctement. Vous pouvez commencer et voir cela.
Un peu de refactoring
Une fois que tout est prêt et que tout fonctionne, vous pouvez peigner un peu le code et le rendre un peu plus pratique et simple. Cela nécessitera quelques modifications.
Afin de ne pas créer un tableau de tâches et de mettre la seule manuellement, vous pouvez créer une méthode d'extension. Pour toutes les méthodes d'extension, vous pouvez également utiliser le même fichier pour envoyer des notifications à toutes les classes. Il sera situé dans le dossier
System et appelé
Extensions
À l'intérieur, nous allons créer une méthode d'extension générique simple qui encapsulera n'importe quelle instance dans un tableau
public static class Extensions {
La prochaine modification consiste à masquer l'utilisation directe du répartiteur dans les composants. À la place, créez une méthode dans la classe de base
SharedStateComponent et utilisez le répartiteur à partir de là.
protected void PerformInMainThread(Action action) { Dispatcher.RunOnMainThread(action); }
Et maintenant, vous devez appliquer ces modifications à plusieurs endroits. Tout d'abord, changez les méthodes dans lesquelles nous créons manuellement des tableaux de tâches et y mettons une seule instance
Dans le composant
AgentWrapperComponent protected override Task[] OnUpdateAsync() {
Et dans le composant
MouseHandlerComponent protected override Task[] OnUpdateAsync() {
Maintenant, nous nous débarrassons de l'utilisation directe du répartiteur dans les composants et à la place nous appelons la méthode
PerformInMainThread dans la classe de base.
Premier dans
AgentWrapperComponent private void OnPointToGroundGot(PointOnGroundEventData eventData) {
et dans le composant
CharacterWrapperComponent private void OnAgentMove(AgentMoveEventData eventData) { PerformInMainThread(() => character.Move(eventData.DesiredVelocity, false, false)); }
C’est tout. Reste à lancer le jeu et à s'assurer que rien ne s'est cassé lors du refactoring et que tout fonctionne correctement.