J'ai un jeu de base de tour de défense 2D en C ++.
Chaque carte est une classe distincte qui hérite de GameState. La carte délègue la logique et le code de dessin à chaque objet du jeu et définit des données telles que le chemin de la carte. Dans le pseudo-code, la section logique pourrait ressembler à ceci:
update():
for each creep in creeps:
creep.update()
for each tower in towers:
tower.update()
for each missile in missiles:
missile.update()
Les objets (creeps, tours et missiles) sont stockés dans des vecteurs de pointeurs. Les tours doivent avoir accès au vecteur de fluage et au vecteur de missiles pour créer de nouveaux missiles et identifier les cibles.
La question est: où puis-je déclarer les vecteurs? Devraient-ils être membres de la classe Map et passés en tant qu'arguments à la fonction tower.update ()? Ou déclaré globalement? Ou y a-t-il d'autres solutions qui me manquent entièrement?
Lorsque plusieurs classes doivent accéder aux mêmes données, où doivent-elles être déclarées?
la source
Réponses:
Lorsque vous avez besoin d’une seule instance d’une classe dans l’ensemble de votre programme, nous appelons cette classe un service . Il existe plusieurs méthodes standard d'implémentation de services dans les programmes:
Singletons . Il y a environ 10-15 ans, les singletons étaient le grand motif de conception à connaître. Cependant, de nos jours, ils sont méprisés. Ils sont beaucoup plus faciles à multi-thread, mais vous devez limiter leur utilisation à un thread à la fois, ce qui n'est pas toujours ce que vous voulez. Suivre les durées de vie est tout aussi difficile qu'avec des variables globales.
Une classe singleton typique ressemblera à quelque chose comme ceci:
Injection de dépendance (DI) . Cela signifie simplement que le service doit être passé en tant que paramètre constructeur. Un service doit déjà exister pour pouvoir être passé dans une classe. Il est donc impossible pour deux services de s'appuyer l'un sur l'autre. dans 98% des cas, c'est ce que vous voulez (et pour les 2% restants, vous pouvez toujours créer une
setWhatever()
méthode et transmettre le service plus tard) . De ce fait, DI n’a pas les mêmes problèmes de couplage que les autres options. Il peut être utilisé avec le multithreading, car chaque thread peut simplement avoir sa propre instance de chaque service (et ne partager que ceux dont il a absolument besoin). Cela rend également le code testable par unité, si cela vous intéresse.Le problème avec l'injection de dépendance est que cela prend plus de mémoire; maintenant chaque instance d'une classe a besoin de références à chaque service qu'elle utilisera. En outre, il devient ennuyeux de l'utiliser lorsque vous avez trop de services. il existe des frameworks qui atténuent ce problème dans d'autres langages, mais en raison du manque de réflexion de C ++, les frameworks DI en C ++ ont tendance à être encore plus laborieux que de le faire manuellement.
Voir cette page (de la documentation de Ninject, un framework C # DI) pour un autre exemple.
L’injection de dépendance est la solution habituelle à ce problème, et c’est la réponse que vous verrez être la plus votée vers des questions comme celle-ci sur StackOverflow.com. DI est un type d' inversion de contrôle (IoC).
Localisateur de service . Fondamentalement, juste une classe qui contient une instance de chaque service. Vous pouvez le faire en utilisant la réflexion ou vous pouvez simplement y ajouter une nouvelle instance chaque fois que vous souhaitez créer un nouveau service. Vous avez toujours le même problème qu'auparavant - Comment les classes accèdent-elles à ce localisateur? - ce qui peut être résolu de l’une quelconque des manières ci-dessus, mais maintenant, vous ne devez le faire que pour votre
ServiceLocator
classe, plutôt que pour des dizaines de services. Cette méthode est également testable à l'unité, si vous vous souciez de ce genre de chose.Les localisateurs de services sont une autre forme d'inversion de contrôle (IoC). Généralement, les infrastructures qui effectuent l'injection automatique de dépendances ont également un localisateur de services.
XNA (infrastructure de programmation de jeux C # de Microsoft) comprend un localisateur de services; pour en savoir plus, voir cette réponse .
À propos, les tours à mon humble avis ne devraient pas être au courant de la chair de poule. À moins que vous ne prévoyiez simplement de parcourir la liste des creeps de chaque tour, vous souhaiterez probablement implémenter un partitionnement d'espace non trivial ; et cette sorte de logique n'appartient pas à la classe des tours.
la source
Personnellement, je voudrais utiliser le polymorphisme ici. Pourquoi avoir un
missile
vecteur, untower
vecteur et un vecteur ...creep
quand ils appellent tous la même fonction;update
? Pourquoi ne pas avoir un vecteur de pointeurs sur une classe de baseEntity
ouGameObject
?Je trouve qu'un bon moyen de concevoir est de penser «est-ce que cela a du sens en termes de propriété»? Il est évident qu'une tour possède un moyen de se mettre à jour, mais une carte possède-t-elle tous les objets qu'elle contient? Si vous optez pour le global, dites-vous que rien ne possède les tours et la chair de poule? Global est généralement une mauvaise solution - elle favorise les mauvais modèles de conception, mais il est beaucoup plus facile de travailler avec. Envisagez de peser "est-ce que je veux finir ceci?" et 'est-ce que je veux quelque chose que je puisse réutiliser'?
Une façon de contourner ce problème consiste à utiliser une forme de système de messagerie. Le
tower
peut envoyer un message aumap
(auquel il a accès, peut-être une référence à son propriétaire?) Qu'il a touché uncreep
,map
puis dire aucreep
qu'il a été touché. Ceci est très propre et sépare les données.Une autre méthode consiste simplement à rechercher sur la carte elle-même ce qu'elle veut. Cependant, il peut y avoir des problèmes avec l'ordre de mise à jour ici.
la source
C'est un cas dans lequel la programmation orientée objet stricte (OOP) tombe en panne.
Selon les principes de la programmation orientée objet, vous devez regrouper les données avec un comportement associé à l'aide de classes. Mais vous avez un comportement (ciblage) qui nécessite des données qui ne sont pas liées les unes aux autres (tours et rampants). Dans cette situation, de nombreux programmeurs essaieront d'associer le comportement à une partie des données dont ils ont besoin (par exemple, les tours gèrent le ciblage, mais ne connaissent pas les creeps), mais il existe une autre option: ne groupez pas le comportement avec les données.
Au lieu de faire du comportement de ciblage une méthode de la classe tower, faites-en une fonction libre qui accepte les tours et rampe comme arguments. Cela pourrait nécessiter de rendre plus public les membres qui restent dans la tour et les classes de fluage, et ce n'est pas grave. Cacher des données est utile, mais c’est un moyen, pas une fin en soi, et vous ne devriez pas en être esclave. En outre, les membres privés ne sont pas le seul moyen de contrôler l'accès aux données: si les données ne sont pas transmises à une fonction et si elles ne sont pas globales, elles sont en réalité masquées à cette fonction. Si vous utilisez cette technique pour éviter les données globales, vous pourriez réellement améliorer l' encapsulation.
Un exemple extrême de cette approche est l' architecture de système d'entité .
la source