
Utiliser les ScriptableObjects comme canaux d'événements dans le code du jeu
Cette page a été traduite automatiquement pour faciliter votre expérience. Nous ne pouvons pas garantir l'exactitude ou la fiabilité du contenu traduit. Si vous avez des doutes quant à la qualité de cette traduction, reportez-vous à la version anglaise de la page web.
Comment faire fonctionner ensemble des systèmes disparates dans votre application ? Une solution courante consiste à utiliser un événement pour envoyer des messages entre les objets. Continuez à lire pour apprendre comment utiliser les ScriptableObjects comme canaux d'événements dans votre projet Unity.
Il s'agit du cinquième d'une série de six mini-guides créés pour aider les développeurs Unity à utiliser la démo qui accompagne le livre électronique, Créer une architecture de jeu modulaire dans Unity avec ScriptableObjects.
La démo s'inspire des mécanismes classiques des jeux d'arcade, et montre comment ScriptableObjects peut vous aider à créer des composants testables, évolutifs et conviviaux pour les concepteurs.
Ensemble, l'e-book, le projet de démonstration et ces mini-guides fournissent les meilleures pratiques pour utiliser les modèles de conception de la programmation avec la classe ScriptableObject dans votre projet Unity. Ces conseils peuvent vous aider à simplifier votre code, à réduire l'utilisation de la mémoire et à favoriser la réutilisation du code.
Cette série comprend les articles suivants :
- Démarrer avec la démo Unity ScriptableObjects
- Séparer les données et la logique du jeu avec les objets scriptables (ScriptableObjects)
- Utiliser les enums basés sur les ScriptableObjects dans votre projet Unity
- Utiliser les ScriptableObjects comme objets délégués
- Comment utiliser un ensemble d'exécution basé sur des objets scriptables ?
Note importante avant de commencer
Avant de vous plonger dans le projet de démonstration ScriptableObject et dans cette série de mini-guides, n'oubliez pas qu'à la base, les modèles de conception ne sont que des idées. Elles ne s'appliquent pas à toutes les situations. Ces techniques peuvent vous aider à apprendre de nouvelles façons de travailler avec Unity et ScriptableObjects.
Chaque modèle présente des avantages et des inconvénients. Ne choisissez que ceux qui bénéficient de manière significative à votre projet spécifique. Vos concepteurs utilisent-ils beaucoup l'éditeur Unity ? Un modèle basé sur ScriptableObject pourrait être un bon choix pour les aider à collaborer avec vos développeurs.
En fin de compte, la meilleure architecture de code est celle qui s'adapte à votre projet et à votre équipe.
Couplage souple, forte cohésion
Lorsque l'on crée différents modules ou systèmes dans une application, il est souvent utile de les considérer comme des "îlots de code". Chaque module peut comporter plusieurs composants ou objets de jeu qui fonctionnent ensemble dans un but commun.
Par exemple, la palette du joueur peut comprendre un script qui interprète les entrées du joueur, un script qui gère les mouvements ou les collisions, etc. Si ces éléments sont interdépendants, vous pouvez utiliser l'inspecteur pour établir ces liens étroits.
Cependant, n'oubliez pas que chaque fois que vous ajoutez une dépendance à un autre objet, cela comporte un petit risque. Dans la mesure du possible, vous voudrez minimiser ces dépendances avec des objets externes. La communication avec les éléments extérieurs à votre module ou à votre système ne sera pas aussi directe.
Vous pouvez faire en sorte que le script de la pagaie fasse référence à la balle dans votre jeu, mais cela signifie qu'il y a un lien entre les deux. Une fois qu'ils sont liés par une dépendance, toute modification apportée à l'un d'entre eux peut avoir une incidence sur l'autre.
Idéalement, vous voulez pouvoir modifier une partie de l'application sans casser quoi que ce soit d'autre. L'objectif est d'assurer la cohésion interne de vos modules tout en les découplant de l'extérieur.
Vous pouvez utiliser la classe NullRefChecker du projet pour émettre un avertissement poli lorsque des références requises dans l'inspecteur sont manquantes. Il suffit d'appeler la méthode statique Validate quelque part (par exemple, dans Awake) après la mise en place ou l'initialisation de chaque composant.
Ajoutez l'attribut facultatif personnalisé pour ignorer la vérification si le champ peut être laissé vide.
Utilisation d'événements
Alors, comment faire fonctionner ensemble ces systèmes disparates dans votre application ?
Une solution consiste à utiliser un événement pour envoyer des messages entre les objets. Les événements adhèrent au modèle diffuseur-auditeur, représenté dans l'image ci-dessus.
Ici, l'objet d'écoute s'abonne à l'événement sur le diffuseur, plutôt que d'appeler une méthode ou de faire référence à une propriété directement.
Les modifications apportées à un composant ont moins d'impact sur les autres. Les choses peuvent encore se casser lorsque vous modifiez le code, mais les objets ne seront pas aussi imbriqués les uns dans les autres. L'événement au milieu fonctionne comme un tampon entre les deux.
Nous décrivons souvent les objets dans cette relation diffuseur-auditeur comme étant faiblement couplés.
Vous pouvez en savoir plus sur les événements et le modèle d'observateur dans notre e-book technique, Level up your code with game programming patterns.

UN SYSTÈME CENTRALISÉ D'ÉVÉNEMENTS
Événements centralisés
Dans le scénario ci-dessus, le radiodiffuseur n'est responsable que de l'envoi d'un signal. Il ne se préoccupe pas des objets qui écoutent.
Cependant, l'auditeur doit toujours avoir une certaine connaissance du diffuseur afin de s'abonner et de se désabonner du délégué à l'aide des méthodes OnEnable et OnDisable.
Vous pouvez découpler davantage le diffuseur et l'auditeur en déplaçant les événements dans une classe statique. Une classe générale "événements de jeu" peut aider à insérer une couche supplémentaire d'abstraction entre les deux. Cela permet de relier le diffuseur et l'auditeur sans qu'ils aient une connaissance directe l'un de l'autre.
Dans cet exemple, nous utiliserons une classe GameEvents statique pour des raisons de simplicité. Cependant, dans un scénario de production réel, il est préférable de le diviser en classes spécialisées plus petites par fonction, telles que UIEvents, GameStateEvents, HealthEvents, InventoryEvents, etc.
Par exemple, vous pouvez créer des événements statiques pour quitter l'application, afficher un écran d'interface utilisateur ou charger une scène. En rendant ces événements statiques, ils sont accessibles à partir de n'importe quelle partie de votre application.
Par exemple, vous pouvez créer des GameEvents comme dans l'exemple ci-dessous.
Le GameEvent statique sert d'intermédiaire entre le diffuseur et l'auditeur d'origine. Les modifications apportées au récepteur ou à l'expéditeur ont moins de chances d'avoir un impact sur l'autre.
Par conséquent, la mise à jour du code a moins d'effets secondaires inattendus. Le fait de stocker les définitions de vos événements dans un seul endroit facilite également leur gestion.
Bien que les GameEvents statiques soient efficaces, ils ne sont pas toujours accessibles aux concepteurs de jeux. Parce qu'ils sont statiques, ils doivent être définis dans le code et ne sont pas sérialisables dans l'éditeur.
Pour un système plus convivial pour l'éditeur, envisagez d'implémenter des événements basés sur des objets scriptables (ScriptableObjects).
using UnityEngine;
using System;
public static class GameEvents
{
public static Action ExitApplication;
public static Action HomeScreenShown;
public static Action<float> LoadProgressUpdated;
}
LES CANAUX D'ÉVÉNEMENTS RELAIENT LES SIGNAUX ENTRE LES DIFFUSEURS ET LES AUDITEURS.
Configuration des canaux d'événements
Les événements basés sur des objets scriptables offrent une alternative graphique aux événements statiques. Bien qu'ils remplissent tous deux des fonctions similaires, les objets scriptables sont plus faciles à concevoir car ils apparaissent dans l'inspecteur.
Étant donné qu'ils relaient un signal d'un radiodiffuseur à un auditeur, on peut les considérer comme des "canaux d'événements", qui sont analogues à une transmission à partir d'une tour de radiodiffusion.
Tout objet scriptable doté des caractéristiques suivantes peut servir de canal d'événements :
- Un délégué (comme UnityAction ou System.Action) : Il notifie les abonnés et transmet les données en tant que paramètres.
- Une méthode de collecte d'événements : Cette méthode publique invoque le délégué.
Vous pouvez mettre en place un nombre illimité de canaux d'événements pour déterminer divers aspects du jeu.
UnityAction et System.Action sont tous deux des délégués. Vous pouvez utiliser l'un ou l'autre de ces types dans votre projet.
L'UnityAction crée une expérience plus conviviale pour les artistes. Sinon, utilisez le délégué System.Action.
Vous trouverez ci-dessous un exemple de VoidEventChannelSO issu du projet. Il s'agit d'un événement basé sur un objet scriptable qui ne transmet aucun paramètre.
Ici, nous utilisons une UnityAction nommée OnEventRaised et exposons une méthodepublique RaiseEventmethod.
using UnityEngine;
using UnityEngine.Events;
[CreateAssetMenu(menuName = "Events/Void Event Channel",
fileName = "VoidEventChannel")]
public class VoidEventChannelSO : DescriptionSO
{
[Tooltip("The action to perform")]
public UnityAction OnEventRaised;
public void RaiseEvent()
{
if (OnEventRaised != null)
OnEventRaised.Invoke();
}
}

CRÉER DES CANAUX D'ÉVÉNEMENTS DANS LE PROJET.
Création des ressources du canal d'événement
Créez la ressource de canal d'événement dans le projet pour l'utiliser. Vous pouvez utiliser le menu Créer ou dupliquer une ressource existante.
Renommez chaque ressource et utilisez le champ de description pour identifier chaque ressource ScriptableObject. N'oubliez pas que chaque canal d'événement existe en tant qu'atout au niveau du projet. Vous ferez référence à ces actifs dans vos MonoBehaviours.
Bien que cela soit facultatif, vous pouvez marquer les canaux d'événements basés sur des objets scriptables avec le suffixe _SO pour les différencier des autres objets scriptables qui transportent des données (qui ont le suffixe _Data).
Les dossiers et les conventions d'appellation peuvent aider votre projet à rester organisé. Vous devrez les adapter aux besoins de votre projet. Pour plus d'informations, lisez Créer un guide de style C#.

ASSIGNER LE CANAL D'ÉVÉNEMENT DANS L'INSPECTEUR.
Manifestations de collecte de fonds
Tout objet de votre scène peut maintenant référencer le canal d'événement et appeler l'événement à l'aide de la méthode RaiseEvent. Par exemple, regardez l'exemple de MonoBehaviour avec une méthode TriggerEvent ci-dessous.
Dans l'inspecteur, la ressource ScriptableObject doit être affectée au champ m_EventChannel. Lorsque quelque chose invoque TriggerEvent, l'événement s'exécute. Tout ce qui écoute reçoit alors une notification.
Ce mécanisme ajoute une grande partie de l'interactivité à votre application de jeu. Chaque module ou système déclenche un événement (par exemple, le système de saisie enregistre une pression sur une touche, une balle entre en collision avec un mur, etc.) En réponse, quelque chose d'autre réagit.
public class EventRaiser: MonoBehaviour
{
[SerializeField]
private VoidEventChannelSO m_EventChannel;
public void TriggerEvent()
{
m_EventChannel.RaiseEvent();
}
}

LE GAMEMANAGER ÉCOUTE CERTAINS CANAUX D'ÉVÉNEMENTS ET DIFFUSE SUR D'AUTRES.
À l'écoute des événements
Pour mettre en place un écouteur, un MonoBehaviour ou un autre composant devra s'abonner à l'événement OnEventRaised du canal d'événement . Cela se produit généralement dansOnEnable, comme dans l'exemple ci-dessous.
Lorsque le canal d'événements déclenche un événement, la méthode HandleEvent est exécutée en réponse. Ce mécanisme peut être utilisé à diverses fins, comme la diffusion de sons ou d'effets, la modification de paramètres, etc. en fonction du contexte de l'événement.
Dans le projet PaddleBallSO, voici comment nous avons configuré la boucle principale du jeu. Le GameManager écoute un ensemble de canaux d'événements, puis diffuse sur un autre. Cela permet à différents systèmes d'envoyer des messages les uns aux autres sans nécessairement avoir de dépendances directes.
Enfin, désabonnez-vous de l'événement OnEventRaised dans la méthode OnDisable pour éviter les erreurs ou les fuites de mémoire.
public class EventListener: MonoBehaviour
{
[SerializeField]
private VoidEventChannelSO m_EventChannel;
private void OnEnable()
{
m_EventChannel.OnEventRaised += HandleEvent;
}
private void OnDisable()
{
m_EventChannel.OnEventRaised -= HandleEvent;
}
private void HandleEvent()
{
Debug.Log("Event received");
}
}

L'INTERACTIVITÉ SANS CODE MISE EN PLACE DANS L'INSPECTEUR
Ajout d'un écouteur sans code
Si vous travaillez avec des concepteurs, vous pouvez leur fournir un script général préconfiguré capable d'écouter un événement. Cela leur permettra de créer des interactions de jeu sans l'aide d'un programmeur.
Le VoidEventChannelListener en est un exemple. Ce composant déclenche un UnityEvent lorsqu'il reçoit un signal d'un canal d'événement. Il suffit d'ajouter le VoidEventChannelListener à un GameObject, puis de définir le canal d'événement et la logique UnityEvent.
Un concepteur peut alors prototyper une logique événementielle en procédant à quelques réglages dans l'inspecteur.
Par exemple, le préfabriqué GameOverSounds écoute le canal d'événement GameOver_SO. Une fois qu'il a reçu cet événement, il joue un son sur la source audio donnée via l'événement UnityEvent m_Response.
La classe VoidEventChannelListener comprend également un délai utile pour ajuster le temps de chaque réponse.
Avec un peu de pratique, il s'agit d'un moyen simple de créer des interactions entre vos différents systèmes et modules.

CANAUX D'ÉVÉNEMENTS MARQUÉS POUR L'ENVOI ET LA RÉCEPTION
Comment les canaux de communication événementielle peuvent-ils être utiles ?
Étant donné qu'ils existent au niveau du projet, les canaux d'événements sont accessibles à l'échelle mondiale. Cela leur permet de connecter n'importe quel objet de la hiérarchie de la scène et de persister à travers les chargements de la scène.
Tout objet peut jouer le rôle de diffuseur ou d'auditeur - il s'agit simplement de savoir comment il s'interface avec le canal d'événements. Vous disposez ainsi d'une grande souplesse dans l'envoi des messages.
Remarque: Il est bon d'indiquer dans l'inspecteur si le canal est destiné à l'envoi ou à la réception. Pour ce faire, utilisez l'attribut HeaderAttribute.
L'utilisation d'événements au niveau du projet présente l'avantage de pouvoir souvent remplacer un singleton. Les canaux d'événements sont disponibles à l'échelle mondiale et peuvent donc relier n'importe quoi à n'importe quoi. Laissez-les piloter les systèmes du jeu tels que les caméras, les quêtes, la santé et les succès, sans créer de dépendances inutiles.
En outre, comme une architecture basée sur les événements n'est exécutée qu'en cas de besoin, elle est plus optimisée que les méthodes de mise à jour de MonoBehaviour.
Signature de la fonction d'un événement de base
Cette classe VoidEventChannelSO ne fonctionne que pour les événements qui ne nécessitent aucun paramètre. Souvent, l'événement soulevé a besoin d'une charge de données supplémentaire pour être significatif.
Par exemple, si vous envoyez un événement qui applique des dégâts dans un système de santé, vous pouvez vouloir transmettre une valeur pour la cible, la quantité de dégâts à envoyer, le type de dégâts, etc.
Vous pouvez modifier la signature de la fonction de votre événement de base pour que le canal d'événement soit mieux adapté. Le projet définit un GenericEventChannelSO à cette fin. Regardez l'exemple ci-dessous.
Il s'agit d'une classe abstraite avec un seul paramètre générique. Vous en tirerez d'autres canaux d'événements. Ceux-ci peuvent alors passer un seul paramètre, tel qu'un float, un int ou un bool.
Comme le VoidEventChannelSO, le GenericEventChannelSO comporte une UnityAction appelée OnEventRaised. Cependant, cette fois-ci, l'action porte un paramètre de type T.
Les objets externes invoqueront la méthode publique RaiseEvent correspondante. Si l'événement a des auditeurs, il s'exécute en passant un paramètre donné.
public abstract class GenericEventChannelSO<T>: DescriptionSO
{
public UnityAction<T> OnEventRaised;
public void RaiseEvent(T parameter)
{
if (OnEventRaised == null)
return;
OnEventRaised.Invoke(parameter);
}
}
Créer des canaux d'événements concrets
Il ne vous reste plus qu'à dériver des canaux d'événements concrets à partir de GenericEventChannelSO et à renseigner la valeur de T.
Hormis l'attribut habituel CreateAssetMenu, il n'est pas nécessaire d'expliciter les détails de la mise en œuvre.
La création d'un canal d'événement transportant un flotteur, FloatEventChannelSO, est simple. Jetez un coup d'œil à l'exemple de code ci-dessous.
C'est aussi simple que cela ! Utilisez ce flux de travail pour en créer d'autres pour BoolEventChannelSO, IntEventChannelSO, etc.
Si vous avez besoin de plus d'un paramètre comme charge utile, définissez des classes génériques supplémentaires (par exemple, GenericEventChannelSO<T,U>, GenericEventChannelSO<T,U,V>, etc.
[CreateAssetMenu(menuName = "Events/Float EventChannel", fileName = "FloatEventChannel")]
public class FloatEventChannelSO : GenericEventChannelSO<float> {}

SÉQUENCE D'ÉVÉNEMENTS LORSQUE LE BALLON TOUCHE UN BUT
La mise en place de l'ensemble
L'idée est de diviser l'application en parties plus petites et plus modulaires. En établissant des limites claires autour de ces éléments, on évite qu'ils ne s'entremêlent avec des dépendances et on évite le code spaghetti.
Les composants qui n'ont pas de connaissance directe des objets extérieurs ne peuvent pas manipuler quelque chose qu'ils ne sont pas censés manipuler. Au lieu de cela, ils sont obligés d'envoyer et de recevoir des messages par le biais de canaux événementiels.
Vous pouvez voir comment cela fonctionne si vous retracez une petite séquence du jeu de la balle au pied. Par exemple, imaginons ce qui se passe lorsqu'une balle entre en collision avec un but :
Le composant ScoreGoal enregistre une collision. Après avoir détecté le ballon, il déclenche un événement sur le canal GoalHit_SO. Il transmet l'ID du joueur qui a marqué le point.
Le canal d'événement informe le GameManager qui, en réponse, crée un autre canal d'événement appelé PointsScored_SO. Il transmet également l'identifiant du joueur.
Ce canal notifie le ScoreManager, qui incrémente le score (stocké dans un objet séparé) et met à jour les composants de l'interface utilisateur. Ensuite, il transmet les scores des deux joueurs via le canal d'événement ScoreManagerUpdated_SO.
En réponse, l'objectif ScoreObjective_SO vérifie si un joueur a atteint le score cible.
Si une condition de victoire est atteinte, le jeu se termine. Dans le cas contraire, le GameManager réinitialise le tour et la balle est remise en jeu.
À première vue, l'incrémentation d'un point de la valeur d'un score peut sembler un travail supplémentaire considérable. Toutefois, l'objectif est d'isoler tous les éléments concernés : Le Ballon, le ScoreManager, le GameManager, l'ObjectiveManager, etc.
Chaque partie de l'application dispose d'une certaine autonomie, ce qui la rend plus facile à tester. L'ajout de nouveaux systèmes ne doit pas perturber la logique existante. En fait, le mode de jeu original peut les ignorer complètement.
Imaginez que vous souhaitiez ajouter des effets secondaires tels que des sons et des animations pour accompagner le processus de notation. Vous pouvez créer de nouveaux composants qui écoutent les bons événements et réagissent de manière appropriée. La logique sous-jacente et le déroulement du jeu peuvent rester inchangés, même lorsque vous ajoutez de nouveaux systèmes.
N'oubliez pas que le mantra de la programmation SOLID est "ouvert à l'extension, fermé à la modification". Vous souhaitez pouvoir ajouter de nouvelles fonctionnalités à votre logiciel sans avoir à modifier le code existant. L'utilisation de canaux d'événements de ce type permet une certaine évolutivité.

LES SCRIPTS DE L'ÉDITEUR PEUVENT AIDER À DÉBOGUER LES ÉVÉNEMENTS.
Événements de débogage
L'architecture événementielle facilite le débogage et la maintenance. Les petites parties sont plus faciles à tester, que vous écriviez des tests unitaires automatisés avec le Unity Test Framework ou que vous fassiez du dépannage informel. Cela vous permet de vous concentrer sur un problème spécifique et de le tester de manière isolée.
Les scripts personnalisés de l'éditeur peuvent être utiles à cet égard. PaddleBallSO fait la démonstration de quelques outils qui aident à tracer le flux de votre application lors de l'utilisation de canaux d'événements :
- La plupart des canaux d'événements du projet PaddleBallSO affichent une liste d'auditeurs dans l'inspecteur. Cliquez sur le nom de chaque auditeur pour le mettre en évidence dans la hiérarchie.
- Un bouton RaiseEvent personnalisé peut invoquer un événement fictif à volonté (en utilisant la valeur par défaut de T s'il contient une charge utile). Lorsque l'application est en cours d'exécution, il suffit de la déclencher manuellement d'un simple clic.
Lors du dépannage des canaux d'événements, sélectionnez la ressource ScriptableObject. Testez l'événement manuellement si nécessaire. L'inspecteur peut vous guider à travers les objets susceptibles d'être écoutés. Sélectionnez les auditeurs que vous souhaitez inspecter plus en détail.
Si vous avez étiqueté les canaux d'événements avec le HeaderAttritute, vous pouvez retracer plusieurs événements pour comprendre le flux logique.

Plus de ressources ScriptableObject
Nous espérons que les canaux d'événements et l'architecture pilotée par les événements pourront profiter à vos nouveaux projets et à ceux à venir.
Pour en savoir plus sur les modèles de conception avec ScriptableObjects, consultez notre e-book technique, Créer une architecture de jeu modulaire dans Unity avec ScriptableObjects. Vous pouvez également en savoir plus sur les modèles de conception courants du développement Unity dans la section Améliorez votre code avec des modèles de programmation de jeux.