Il y a quelque temps, j'ai commencé à travailler avec Unity et je suis toujours aux prises avec le problème des scripts étroitement couplés. Comment puis-je structurer mon code pour éviter ce problème?
Par exemple:
Je veux avoir des systèmes de santé et de mort dans des scripts séparés. Je veux également avoir différents scripts de marche interchangeables qui me permettent de changer la façon dont le personnage du joueur est déplacé (contrôles inertiels basés sur la physique comme dans Mario par rapport à des contrôles serrés et nerveux comme dans Super Meat Boy). Le script Santé doit contenir une référence au script Mort, afin qu'il puisse déclencher la méthode Die () lorsque la santé des joueurs atteint 0. Le script Mort doit contenir une référence au script de marche utilisé, pour désactiver la marche à mort (I suis fatigué des zombies).
Je créerais normalement des interfaces, comme IWalking
, IHealth
et IDeath
, de sorte que je puisse changer ces éléments à volonté sans casser le reste de mon code. Je voudrais les configurer par un script séparé sur l'objet joueur, par exemple PlayerScriptDependancyInjector
. Peut-être que ce script serait public IWalking
, IHealth
etIDeath
les attributs, de sorte que les dépendances peuvent être définies par le concepteur de niveau de l'inspecteur en faisant glisser et déposer des scripts appropriés.
Cela me permettrait d'ajouter simplement des comportements aux objets de jeu facilement et de ne pas me soucier des dépendances codées en dur.
Le problème dans Unity
Le problème est que dans Unity, je ne peux pas exposer les interfaces dans l'inspecteur, et si j'écris mes propres inspecteurs, les références ne seront pas sérialisées, et c'est beaucoup de travail inutile. C'est pourquoi il me reste à écrire du code étroitement couplé. Mon Death
script expose une référence à un InertiveWalking
script. Mais si je décide que je veux que le personnage du joueur contrôle étroitement, je ne peux pas simplement glisser-déposer le TightWalking
script, je dois changer le Death
script. Ça craint. Je peux y faire face, mais mon âme pleure chaque fois que je fais quelque chose comme ça.
Quelle est l'alternative préférée aux interfaces dans Unity? Comment résoudre ce problème? J'ai trouvé ça , mais ça me dit ce que je sais déjà, et ça ne me dit pas comment faire ça dans Unity! Cela aussi discute de ce qui doit être fait, pas comment, cela ne résout pas le problème du couplage étroit entre les scripts.
Dans l'ensemble, je pense que ceux-ci sont écrits pour les personnes qui sont arrivées à Unity avec une formation en conception de jeux qui apprennent juste à coder, et il y a très peu de ressources sur Unity pour les développeurs réguliers. Existe-t-il un moyen standard de structurer votre code dans Unity ou dois-je trouver ma propre méthode?
The problem is that in Unity I can't expose interfaces in the inspector
Je ne pense pas comprendre ce que vous entendez par "exposer l'interface dans l'inspecteur" parce que ma première pensée a été "pourquoi pas?"Réponses:
Il existe plusieurs méthodes pour éviter un couplage de script étroit. Interne à Unity est une fonction SendMessage qui, lorsqu'elle est ciblée sur GameObject d'un Monobehaviour, envoie ce message à tout ce qui se trouve sur l'objet de jeu. Vous pourriez donc avoir quelque chose comme ça dans votre objet de santé:
C'est vraiment simplifié mais cela devrait montrer précisément où je veux en venir. La propriété lançant le message comme vous le feriez pour un événement. Votre script de mort intercepterait alors ce message (et s'il n'y a rien pour l'intercepter, SendMessageOptions.DontRequireReceiver vous garantira de ne pas recevoir d'exception) et effectuerait des actions basées sur la nouvelle valeur de santé après qu'il a été défini. Ainsi:
Il n'y a cependant aucune garantie d'ordre dans ce cas. D'après mon expérience, cela va toujours du script le plus haut sur un GameObject au script le plus bas, mais je ne miserais pas sur tout ce que vous ne pouvez pas observer par vous-même.
Il existe d'autres solutions que vous pourriez étudier, à savoir la création d'une fonction GameObject personnalisée qui obtient des composants et ne renvoie que les composants qui ont une interface spécifique au lieu de Type, cela prendrait un peu plus de temps mais pourrait bien servir vos objectifs.
En outre, vous pouvez écrire un éditeur personnalisé qui vérifie qu'un objet est affecté à une position dans l'éditeur pour cette interface avant de valider réellement l'affectation (dans ce cas, vous affecteriez le script comme normal, ce qui lui permettrait d'être sérialisé, mais en lançant une erreur s'il n'a pas utilisé la bonne interface et a refusé l'affectation). J'ai déjà fait les deux auparavant et je peux produire ce code, mais il faudra un certain temps pour le trouver en premier.
la source
Personnellement, je n'utilise JAMAIS SendMessage. Il y a toujours une dépendance entre vos composants avec SendMessage, c'est juste très mal affiché et facile à casser. L'utilisation d'interfaces et / ou de délégués supprime vraiment la nécessité d'utiliser SendMessage (ce qui est plus lent, bien que cela ne devrait pas être un problème tant qu'il ne doit pas l'être).
http://forum.unity3d.com/threads/free-vfw-full-set-of-drawers-savesystem-serialize-interfaces-generics-auto-props-delegates.266165/
Utilisez les trucs de ce mec. Il fournit un tas de trucs d'éditeur comme des interfaces exposées et des délégués. Il y a des tas de trucs comme ça, ou vous pouvez même faire les vôtres. Quoi que vous fassiez, NE compromettez PAS votre conception en raison du manque de prise en charge des scripts par Unity. Ces choses devraient être dans Unity par défaut.
Si ce n'est pas une option, faites glisser et déposez les classes monobehaviour à la place et convertissez-les dynamiquement dans l'interface requise. C'est plus de travail et moins élégant, mais cela fait le travail.
la source
Une approche spécifique à Unity qui me vient à l'esprit serait
GetComponents<MonoBehaviour>()
à obtenir une liste de scripts, puis à convertir ces scripts en variables privées pour les interfaces spécifiques. Vous souhaitez le faire dans Start () sur le script d'injecteur de dépendance. Quelque chose comme:En fait, avec cette méthode, vous pourriez même demander aux composants individuels de dire au script d'injection de dépendances quelles sont leurs dépendances, en utilisant le commande typeof () et le type 'Type' . En d'autres termes, les composants donnent à l'injecteur de dépendances une liste de types, puis l'injecteur de dépendances renvoie une liste d'objets de ces types.
Alternativement, vous pouvez simplement utiliser des classes abstraites au lieu d'interfaces. Une classe abstraite peut accomplir à peu près le même objectif qu'une interface, et vous pouvez référencer cela dans l'inspecteur.
la source
GetComponent<IMyInterface>()
etGetComponents<IMyInterface>()
directement, qui retourne le ou les composants qui l'implémentent.D'après ma propre expérience, le développement de jeu implique traditionnellement une approche plus pragmatique que le développement industriel (avec moins de couches d'abstraction). En partie parce que vous voulez privilégier les performances à la maintenabilité, et aussi parce que vous êtes moins susceptible de réutiliser votre code dans d'autres contextes (il y a généralement un fort couplage intrinsèque entre les graphiques et la logique du jeu)
Je comprends parfaitement votre préoccupation, en particulier parce que les performances sont moins critiques avec les appareils récents, mais j'ai le sentiment que vous devrez développer vos propres solutions si vous souhaitez appliquer les meilleures pratiques de POO industrielle au développement de jeux Unity (comme l'injection de dépendances, etc...).
la source
Jetez un œil à IUnified ( http://u3d.as/content/wounded-wolf/iunified/5H1 ), qui vous permet d'exposer des interfaces dans l'éditeur, comme vous le mentionnez. Il y a un peu plus de câblage à faire par rapport à la déclaration de variable normale, c'est tout.
De plus, comme une autre réponse le mentionne, l'utilisation de SendMessage ne supprime pas le couplage. Au lieu de cela, il ne fait pas d'erreur au moment de la compilation. Très mauvais - à mesure que vous développez votre projet, cela peut devenir un cauchemar à entretenir.
Si vous cherchez à découpler votre code sans utiliser d'interface dans l'éditeur, vous pouvez utiliser un système basé sur les événements fortement typé (ou même un système vaguement typé en fait, mais encore une fois, plus difficile à maintenir). J'ai basé le mien sur ce post , qui est super pratique comme point de départ. Soyez conscient de créer un tas d'événements car cela pourrait déclencher le GC. J'ai plutôt contourné le problème avec un pool d'objets, mais c'est principalement parce que je travaille avec un mobile.
la source
Source: http://www.udellgames.com/posts/ultra-useful-unity-snippet-developers-use-interfaces/
la source
J'utilise quelques stratégies différentes pour contourner les limitations que vous mentionnez.
L'un de ceux que je ne vois pas mentionné utilise un
MonoBehavior
qui expose l'accès aux différentes implémentations d'une interface donnée. Par exemple, j'ai une interface qui définit comment les Raycasts sont implémentés nommésIRaycastStrategy
Je l'implémente ensuite dans différentes classes avec des classes comme
LinecastRaycastStrategy
etSphereRaycastStrategy
et implémente un MonoBehaviorRaycastStrategySelector
qui expose une seule instance de chaque type. Quelque chose commeRaycastStrategySelectionBehavior
, qui est implémenté quelque chose comme:Si les implémentations sont susceptibles de référencer les fonctions Unity dans leurs constructions (ou simplement pour être du bon côté, j'ai juste oublié cela lors de la saisie de cet exemple)
Awake
pour éviter les problèmes avec l'appel des constructeurs ou le référencement des fonctions Unity dans l'éditeur ou en dehors du principal filCette stratégie présente certains inconvénients, en particulier lorsque vous devez gérer de nombreuses implémentations différentes qui évoluent rapidement. Les problèmes liés au manque de vérification au moment de la compilation peuvent être résolus en utilisant un éditeur personnalisé, car la valeur d'énumération peut être sérialisée sans aucun travail spécial (bien que j'y renonce généralement, car mes implémentations n'ont pas tendance à être aussi nombreuses).
J'utilise cette stratégie en raison de la flexibilité qu'elle vous offre en étant basée sur MonoBehaviors sans vous forcer à mettre en œuvre les interfaces à l'aide de MonoBehaviors. Cela vous donne "le meilleur des deux mondes", car le sélecteur est accessible à l'aide de
GetComponent
, la sérialisation est entièrement gérée par Unity et le sélecteur peut appliquer une logique au moment de l'exécution pour changer automatiquement d'implémentation en fonction de certains facteurs.la source