En implémentant des modèles de conception de programmation de jeux courants dans votre projet Unity, vous pouvez créer et maintenir efficacement une base de code propre, organisée et lisible. Les modèles de conception réduisent non seulement le refactoring et le temps passé aux tests, mais ils accélèrent également les processus d'intégration et de développement, contribuant ainsi à une base solide pour la croissance de votre jeu, de votre équipe de développement et de votre entreprise.
Considérez les modèles de conception non pas comme des solutions finies que vous pouvez copier et coller dans votre code, mais comme des outils supplémentaires qui peuvent vous aider à créer des applications plus volumineuses et évolutives lorsqu'ils sont utilisés correctement.
Cette page explique le modèle d'observateur et comment il peut aider à soutenir le principe du couplage lâche entre les objets qui interagissent les uns avec les autres.
Le contenu ici est basé sur le livre électronique gratuit, Améliorez votre code avec des modèles de programmation de jeux, qui explique les modèles de conception bien connus et partage des exemples pratiques pour les utiliser dans votre projet Unity.
D'autres articles de la série de modèles de conception de programmation de jeux Unity sont disponibles sur le hub des meilleures pratiques Unity ou cliquez sur les liens suivants :
Au moment de l'exécution, un certain nombre de choses peuvent se produire dans votre jeu. Que se passe-t-il lorsque le joueur détruit un ennemi ? Ou quand ils récupèrent un pouvoir ou un niveau supérieur ? Vous avez souvent besoin d’un mécanisme permettant à certains objets d’en informer d’autres sans les référencer directement. Malheureusement, à mesure que votre base de code augmente, cela ajoute des dépendances inutiles qui peuvent conduire à un manque de flexibilité et à une surcharge excessive dans la maintenance du code.
Le modèle d'observateur est une solution courante à ce problème. Il permet à vos objets de communiquer mais de rester faiblement couplés en utilisant une dépendance « un-à-plusieurs ». Lorsqu'un objet change d'état, tous les objets dépendants sont automatiquement avertis.
Une analogie pour aider à visualiser ceci est une tour de radio qui diffuse à de nombreux auditeurs différents. Il n'est pas nécessaire de savoir qui est à l'écoute, il suffit simplement de s'assurer que l'émission est diffusée en direct sur la bonne fréquence, au bon moment.
L'objet diffusé s'appelle le sujet. Les autres objets qui écoutent sont appelés les observateurs, ou parfois les abonnés (cette page utilise partout le nom d'observateur).
L'avantage de ce modèle est qu'il dissocie le sujet de l'observateur, qui ne connaît pas vraiment les observateurs et ne se soucie pas vraiment de ce qu'ils font une fois qu'ils reçoivent le signal. Même si les observateurs sont dépendants du sujet, ils ne se connaissent pas eux-mêmes.
Les observateurs sont simplement avertis chaque fois que l'état du sujet change, ce qui leur permet de se mettre à jour en conséquence. De cette façon, il devient plus facile de modifier ou d’étendre le code sans affecter les autres parties du système.
Un autre avantage est que le modèle d'observateur encourage le développement de code réutilisable, car les observateurs peuvent être réutilisés dans différents contextes sans avoir besoin d'être modifiés. Enfin, cela améliore souvent la lisibilité du code, puisque les dépendances entre objets sont clairement définies.
Vous pouvez concevoir vos propres classes sujet-observateur, mais cela est généralement inutile puisque C# implémente déjà le modèle à l'aide d'événements. Le modèle d'observateur est si répandu qu'il est intégré au langage C#, et pour cause : Il peut vous aider à créer un code plus modulaire, réutilisable et maintenable.
Qu'est-ce qu'un événement ? Il s'agit d'une notification qui indique que quelque chose s'est produit et qui implique quelques étapes :
L'éditeur (également appelé sujet) crée un événement basé sur undéléguéétablissant une signature de fonction spécifique. L'événement est simplement une action que le sujet effectuera au moment de l'exécution (par exemple, subir des dégâts, cliquer sur un bouton, etc.). L'éditeur tient à jour une liste de ses dépendants (les observateurs) et leur envoie des notifications lorsque son état change, ce qui est représenté par cet événement.
Les observateurs créent ensuite chacun une méthode appelée gestionnaire d'événements,qui doit correspondre à la signature du délégué. Les observateurs sont des objets qui reçoivent des notifications de l'éditeur et se mettent à jour en conséquence.
Le gestionnaire d'événements de chaque observateur s'abonne à l'événement de l'éditeur. Vous pouvez demander à autant d’observateurs de rejoindre l’abonnement que nécessaire. Tous attendront que l’événement se déclenche.
Lorsque l'éditeur signale l'occurrence d'un événement au moment de l'exécution, on parle de déclenchement de l'événement. Ceci, à son tour, appelle les gestionnaires d'événements des observateurs, qui exécutent leur propre logique interne en réponse.
De cette façon, vous faites réagir de nombreux composants à un seul événement du sujet. Si le sujet indique qu'un bouton est cliqué, les observateurs peuvent lire une animation ou un son, déclencher une cinématique ou enregistrer un fichier. Leur réponse peut être n'importe quoi, c'est pourquoi vous trouverez fréquemment le modèle d'observateur utilisé pour envoyer des messages entre objets.
Délégués contre événements
Un délégué est un type qui définit une signature de méthode. Cela vous permet de transmettre des méthodes comme arguments à d’autres méthodes. Considérez-le comme une variable contenant une référence à une méthode, au lieu d'une valeur.
Un événement, en revanche, est essentiellement un type spécial de délégué qui permet aux classes de communiquer entre elles de manière faiblement couplée. Pour des informations générales sur les différences entre les délégués et les événements, consultez Distinguer les délégués et les événements en C#.
Voyons comment vous pouvez définir un sujet/éditeur de base dans le code ci-dessous.
Dans la classe Subject de l'exemple de code ci-dessous, vous héritez de MonoBehaviour afin de pouvoir l'attacher plus facilement à un GameObject, même si ce n'est pas une obligation.
Bien que vous soyez libre de définir votre propre délégué personnalisé, vous pouvez également utiliser System.Action, qui fonctionne dans la plupart des cas d'utilisation. Dans l'exemple de code, il n'est pas nécessaire d'envoyer des paramètres avec l'événement, mais si cela était nécessaire, c'est aussi simple que d'utiliser le délégué Action<T> et de les transmettre sous forme de List<T> entre crochets (jusqu'à 16 paramètres ).
Dans l'extrait de code, ThingHappened est l'événement réel que le sujet appelle dans la méthode DoThing .
Le "?." L'opérateur est un opérateur conditionnel nul, ce qui signifie que l'événement ne sera invoqué que s'il n'est pas nul. La méthode Invoke est utilisée pour déclencher un événement, ce qui signifie qu'elle exécutera tous les gestionnaires d'événements abonnés à l'événement. Dans ce cas, la méthode DoThing déclenchera l'événement ThingHappened s'il n'est pas nul, ce qui exécutera tous les gestionnaires d'événements abonnés à l'événement.
Vous pouvez télécharger un exemple de projet qui illustre l'observateur et d'autres modèles de conception en action. Cet exemple de code est disponible ici.
Pour écouter l'événement, vous pouvez créer un exemple de classe Observer, comme l'exemple de code épuré ci-dessous (également disponible dans le projet Github).
Attachez ce script à un GameObject en tant que composant et référencez le subjectToObserver dans l'inspecteur pour écouter l'événement ThingHappened .
La méthode OnThingHappened peut contenir n'importe quelle logique que l'observateur exécute en réponse à l'événement. Souvent, les développeurs ajoutent le préfixe « On » pour désigner le gestionnaire d'événements (utilisez la convention de dénomination de votre guide de style).
Dans Awake ou Start, vous pouvez vous abonner à l'événement avec l'opérateur += . Cela combine la méthode OnThingHappened de l'observateur avec la méthode ThingHappeneddu sujet.
Si quelque chose exécute la méthode DoThing du sujet, cela déclenche l'événement. Ensuite, le gestionnaire d'événements OnThingHappened de l'observateur appelle automatiquement et imprime l'instruction de débogage.
Note: Si vous supprimez ou supprimez l'observateur au moment de l'exécution alors qu'il est toujours abonné à ThingHappened, l'appel de cet événement peut entraîner une erreur. Ainsi, il est important de se désabonner de l'événement dans la méthode OnDestroy de MonoBehaviour avec un opérateur -= à des moments appropriés du cycle de vie de l'objet.
Si vous téléchargez l' exemple de projet et accédez au dossier nommé 11 Observer, vous trouverez un exemple qui montre un simple bouton (ExampleSubject) et un haut-parleur (AudioObserver), une animation (AnimObserver) et un effet de particules (ParticleSystemObserver).
Lorsque vous cliquez sur le bouton, l'ExempleSubject appelle un événement ThingHappened. AudioObserver, AnimObserver et ParticleSystemObserver appellent leurs méthodes de gestion d'événements en réponse.
Les observateurs peuvent exister sur le même ou sur des GameObjects différents. Notez que l'AnimObserver produit l'animation du bouton sur l'ExampleSubject, tandis que les AudioObservers et ParticleSystemObserver occupent des GameObjects différents.
Le ButtonSubject permet à l'utilisateur d'invoquer un événement Clicked avec le bouton de la souris. Plusieurs autres GameObjects dotés des composants AudioObserver et ParticleSystemObserver peuvent alors répondre à leur manière à l'événement.
Déterminer quel objet est un sujet et lequel est un observateur ne varie que selon l'usage. Tout ce qui suscite l’événement agit comme sujet, et tout ce qui répond à l’événement est l’observateur. Différents composants sur le même GameObject peuvent être des sujets ou des observateurs. Même un même composant peut être un sujet dans un contexte et un observateur dans un autre.
Par exemple, AnimObserver dans l'exemple ajoute un peu de mouvement au bouton lorsque vous cliquez dessus. Il agit comme un observateur même s'il fait partie du ButtonSubject GameObject.
Unity comprend également un système distinct de UnityEvents, qui utilise le délégué UnityAction de l'API UnityEngine.Events . Il peut être configuré dans l'inspecteur (fournissant une interface graphique pour le modèle d'observateur), permettant aux développeurs de spécifier quelles méthodes doivent être invoquées lorsque l'événement est déclenché.
Si vous avez utilisé le système d'interface utilisateur d'Unity (par exemple, en créant un événement OnClick d'un bouton d'interface utilisateur), vous avez déjà une certaine expérience dans ce domaine.
Dans l'image ci-dessus, l'événement OnClick du bouton appelle et déclenche une réponse des méthodes OnThingHappened des deux AudioObservers. Vous pouvez ainsi paramétrer l'événement d'un sujet sans code.
Les UnityEvents sont utiles si vous souhaitez permettre aux concepteurs ou aux non-programmeurs de créer des événements de gameplay. Cependant, sachez qu'ils peuvent être plus lents que leurs événements ou actions équivalents de l'espace de noms Système. Les UnityActions ont également l'avantage supplémentaire d'être utilisées pour appeler des méthodes qui prennent des arguments, alors que les UnityEvents sont limités aux méthodes qui n'ont pas d'arguments.
Pesez les performances par rapport à l'utilisation lorsque vous envisagez UnityEvents et UnityActions. Les UnityEvents sont plus simples et plus faciles à utiliser, mais sont plus limités en termes de types de méthodes pouvant être invoquées. Certains pourraient également affirmer qu’ils peuvent être plus sujets aux erreurs en exposant tous les événements dans l’Inspecteur.
Voir le module Créer un système de messagerie simple avec des événements sur Unity Learn pour un exemple.
La mise en œuvre d'un événement ajoute du travail supplémentaire, mais elle offre des avantages :
Le modèle d'observateur permet de découpler vos objets : L'éditeur de l'événement n'a pas besoin de savoir quoi que ce soit sur les abonnés à l'événement eux-mêmes. Au lieu de créer une dépendance directe entre une classe et une autre, le sujet et l’observateur communiquent tout en maintenant un certain degré de séparation (low-couplage).
Vous n'êtes pas obligé de le construire : C# inclut un système d'événements établi et vous pouvez utiliser le déléguéSystem.Actionau lieu de définir vos propres délégués. Alternativement, Unity inclut égalementUnityEventsetUnityActions.
Chaque observateur implémente sa propre logique de gestion des événements : De cette manière, chaque objet observateur conserve la logique dont il a besoin pour répondre. Cela facilite le débogage et les tests unitaires.
Il est bien adapté à l'interface utilisateur : Votre code de jeu principal peut vivre séparément de la logique de votre interface utilisateur. Les éléments de votre interface utilisateur écoutent ensuite des événements ou des conditions de jeu spécifiques et réagissent de manière appropriée. Les modèles MVP et MVC utilisent le modèle observateur à cette fin.
Mais vous devez également être conscient de ces mises en garde :
Cela ajoute une complexité supplémentaire : Comme d’autres modèles, la création d’une architecture basée sur les événements nécessite davantage de configuration au départ. Faites également attention en supprimant des sujets ou des observateurs. Assurez-vous de désenregistrer les observateurs dans OnDestroy afin que la référence mémoire soit correctement libérée lorsque l'observateur n'est plus nécessaire.
Les observateurs ont besoin d'une référence à la classe qui définit l'événement : Les observateurs dépendent toujours de la classe qui publie l'événement. L'utilisation d'un EventManager statique (voir section suivante) qui gère tous les événements peut aider à démêler les objets les uns des autres.
Les performances peuvent être un problème : L'architecture basée sur les événements ajoute une surcharge supplémentaire. Les grandes scènes et les nombreux GameObjects peuvent nuire aux performances.
Bien que seule une version de base du modèle d'observateur soit présentée ici, vous pouvez l'étendre pour répondre à tous les besoins de votre application de jeu.
Tenez compte de ces suggestions lors de la configuration du modèle d'observateur :
Utilisez la classe ObservableCollection : C# fournit uneObservableCollectiondynamique pour suivre des modifications spécifiques. Il peut avertir vos observateurs lorsque des éléments sont ajoutés, supprimés ou lorsque la liste est actualisée.
Transmettez un ID d'instance unique comme argument : Chaque GameObject de la hiérarchie possède unID d'instanceunique. Si vous déclenchez un événement qui pourrait s'appliquer à plusieurs observateurs, transmettez l'ID unique dans l'événement (utilisez le typeAction<int>). Ensuite, n'exécutez la logique dans le gestionnaire d'événements que si le GameObject correspond à l'ID unique.
Créez un EventManager statique : Étant donné que les événements peuvent piloter une grande partie de votre gameplay, de nombreuses applications Unity utilisent un EventManager statique ou singleton. De cette façon, vos observateurs peuvent référencer une source centrale d'événements de jeu comme sujet pour faciliter la configuration.
Le FPS Microgame a une bonne implémentation d'un EventManager statique qui implémente des GameEvents personnalisés et inclut des méthodes d'assistance statiques pour ajouter ou supprimer des écouteurs.
Le projet Unity Open présente également une architecture de jeu oùles ScriptableObjects relaient les UnityEvents. Il utilise des événements pour lire de l'audio ou charger de nouvelles scènes.
Créez une file d'attente d'événements : Si votre scène contient de nombreux objets, vous ne souhaiterez peut-être pas déclencher tous vos événements en même temps. Imaginez la cacophonie d'un millier d'objets jouant des sons lorsque vous invoquez un seul événement. La combinaison du modèle d'observateur avec le modèle de commande vous permet d'encapsuler vos événements dans une file d'attente d'événements. Ensuite, vous pouvez utiliser un tampon de commandes pour lire les événements un par un ou les ignorer de manière sélective si nécessaire (par exemple, si vous disposez d'un nombre maximum d'objets pouvant émettre des sons à la fois).
Le modèle d'observateur figure en grande partie dans le modèle architectural Model View Presenter (MVP), qui est couvert dans le livre électronique Améliorez votre code avec des modèles de programmation de jeux.
Vous trouverez de nombreux autres conseils sur la façon d'utiliser les modèles de conception dans vos applications Unity, ainsi que les principes SOLID, dans le livre électronique gratuit Améliorez votre code avec des modèles de programmation de jeux.
Tous les livres électroniques et articles techniques avancés sur Unity sont disponibles sur le hub des meilleures pratiques . Les livres électroniques sont également disponibles sur la page des meilleures pratiques avancées de la documentation.