Je travaille sur un jeu en 2D où vous pouvez monter, descendre, aller à gauche et à droite. J'ai essentiellement deux objets de logique de jeu:
- Joueur: A une position par rapport au monde
- Monde: Dessine la carte et le joueur
Jusqu'à présent, Monde dépend du joueur (c.-à-d. Qu'il y a une référence), il a donc besoin de sa position pour déterminer où dessiner le personnage du joueur et quelle partie de la carte dessiner.
Maintenant, je veux ajouter une détection de collision pour empêcher le joueur de se déplacer à travers les murs.
Le moyen le plus simple auquel je puisse penser est de demander au joueur de demander au monde si le mouvement prévu est possible. Mais cela introduirait une dépendance circulaire entre Player et World (c’est-à-dire que chacun détient une référence à l’autre), ce qui semble mériter d’être évité. La seule façon dont je suis venu à l’esprit est de laisser le Monde déplacer le joueur , mais je trouve cela peu intuitif.
Quelle est ma meilleure option? Ou éviter une dépendance circulaire n'en vaut-il pas la peine?
Réponses:
Le monde ne devrait pas se dessiner; le moteur de rendu devrait dessiner le monde. Le joueur ne doit pas se dessiner tout seul. le moteur de rendu doit dessiner le joueur par rapport au monde.
Le joueur devrait interroger le monde sur la détection des collisions; ou peut-être que les collisions devraient être gérées par une classe séparée qui vérifierait la détection des collisions non seulement contre le monde statique mais aussi contre d'autres acteurs.
Je pense que le monde ne devrait probablement pas être au courant du joueur; ce devrait être un primitif de bas niveau et non un objet divin. Le joueur devra probablement invoquer certaines méthodes de World, peut-être indirectement (détection de collision, recherche d'objets interactifs, etc.).
la source
Renderer
sorte est nécessaire, mais cela ne signifie pas que la logique de la manière dont chaque chose est rendue est gérée par leRenderer
, chaque chose à dessiner devrait probablement hériter d'une interface commune telle queIDrawable
ouIRenderable
(ou une interface équivalente dans la langue que vous utilisez). Le monde pourrait être leRenderer
, je suppose, mais il semblerait qu’il outrepasserait sa responsabilité, surtout s’il était déjà unIRenderable
soi.Voici comment un moteur de rendu typique gère ces choses:
Il existe une distinction fondamentale entre l'emplacement d'un objet dans l'espace et la manière dont l'objet est dessiné.
Dessiner un objet
Vous avez généralement une classe Renderer qui effectue cela. Il prend simplement un objet (Modèle) et dessine à l'écran. Il peut avoir des méthodes telles que drawSprite (Sprite), drawLine (..), drawModel (Model), tout ce dont vous avez besoin. C'est un moteur de rendu, donc il est supposé faire toutes ces choses. Il utilise également toutes les API que vous avez en dessous afin que vous puissiez avoir par exemple un rendu qui utilise OpenGL et un qui utilise DirectX. Si vous souhaitez porter votre jeu sur une autre plate-forme, il vous suffit d'écrire un nouveau moteur de rendu et de l'utiliser. C'est si facile.
Déplacer un objet
Chaque objet est attaché à quelque chose que nous aimons appeler SceneNode . Vous y parvenez grâce à la composition. Un SceneNode contient un objet. C'est ça. Qu'est-ce qu'un SceneNode? C'est une classe simple contenant toutes les transformations (position, rotation, échelle) d'un objet (généralement par rapport à un autre SceneNode) ainsi que l'objet réel.
Gérer les objets
Comment sont gérées SceneNodes? À travers un SceneManager . Cette classe crée et garde une trace de chaque SceneNode dans votre scène. Vous pouvez lui demander un SceneNode spécifique (généralement identifié par un nom de chaîne tel que "Player" ou "Table") ou une liste de tous les nœuds.
Dessiner le monde
Cela devrait être assez évident maintenant. Il suffit de parcourir chaque SceneNode de la scène et de le dessiner au bon endroit. Vous pouvez le dessiner au bon endroit en faisant en sorte que le moteur de rendu stocke les transformations d'un objet avant de le restituer.
Détection de collision
Ce n'est pas toujours trivial. Habituellement, vous pouvez interroger la scène pour savoir quel objet se trouve à un certain point de l'espace ou quels objets un rayon intersectera. De cette façon, vous pouvez créer un rayon de votre joueur dans la direction du mouvement et demander au responsable de la scène quel est le premier objet que le rayon intersecte. Vous pouvez ensuite choisir de déplacer le joueur vers la nouvelle position, de le déplacer légèrement (pour le placer à côté de l'objet en collision) ou de ne pas le déplacer du tout. Assurez-vous que ces requêtes sont gérées par des classes séparées. Ils doivent demander au SceneManager une liste de SceneNodes, mais la tâche suivante consiste à déterminer si ce SceneNode recouvre un point dans l'espace ou se croise avec un rayon. N'oubliez pas que SceneManager crée et stocke uniquement des nœuds.
Alors, quel est le joueur et quel est le monde?
Le lecteur peut être une classe contenant un SceneNode, qui contient à son tour le modèle à restituer. Vous déplacez le lecteur en modifiant la position du nœud de la scène. Le monde est simplement une instance du SceneManager. Il contient tous les objets (via SceneNodes). Vous gérez la détection de collision en effectuant des requêtes sur l'état actuel de la scène.
Ceci est loin d’être une description complète ou précise de ce qui se passe dans la plupart des moteurs, mais cela devrait vous aider à comprendre les principes fondamentaux et à comprendre pourquoi il est important de respecter les principes de la POO soulignés par SOLID . Ne vous résignez pas à l'idée qu'il est trop difficile de restructurer votre code ou que cela ne vous aidera pas vraiment. Vous gagnerez beaucoup plus à l'avenir en concevant avec soin votre code.
la source
Pourquoi voudriez-vous éviter cela? Les dépendances circulaires doivent être évitées si vous souhaitez créer une classe réutilisable. Mais le joueur n'est pas une classe qui doit être réutilisable. Voulez-vous jamais utiliser le Player sans monde? Probablement pas.
N'oubliez pas que les classes ne sont rien d'autre que des ensembles de fonctionnalités. La question est simplement de savoir comment diviser la fonctionnalité. Faites ce que vous devez faire. Si vous avez besoin d’une décadence circulaire, qu’il en soit ainsi. (Il en va de même pour toutes les fonctionnalités de POO, soit dit en passant. Codez les choses de manière à ce qu'elles servent un but, ne suivez pas les paradigmes à l'aveuglette.)
Éditer
Ok, pour répondre à la question: vous pouvez éviter que le joueur ait besoin de connaître le monde pour les contrôles de collision en utilisant des rappels:
Le monde que vous avez décrit dans la question peut être manipulé par le monde entier si vous exposez la vitesse des entités:
Cependant, notez que vous aurez probablement besoin d’une dépendance du monde tôt ou tard, c’est-à-dire chaque fois que vous avez besoin de fonctionnalités du Monde: vous voulez savoir où se trouve l’ennemi le plus proche? Vous voulez savoir à quelle distance se trouve le prochain rebord? C'est la dépendance.
la source
render(World)
. Le débat porte sur le point de savoir si tout le code doit être rangé dans une classe ou si le code doit être divisé en unités logiques et fonctionnelles, qui sont ensuite plus faciles à gérer, à étendre et à gérer. BTW, bonne chance à réutiliser les gestionnaires de composants, les moteurs physiques et les gestionnaires d’intrants, tous intelligemment indifférenciés et complètement couplés.Votre conception actuelle semble aller à l’encontre du premier principe de la conception SOLID .
Ce premier principe, appelé "principe de responsabilité unique", est généralement un bon guide à suivre pour ne pas créer d'objets monolithiques à tout faire qui feront toujours mal à votre conception.
Concrètement, votre
World
objet est responsable à la fois de la mise à jour et du maintien de l’état du jeu, ainsi que de son dessin.Que se passe-t-il si votre code de rendu change / doit changer? Pourquoi devriez-vous avoir à mettre à jour les deux classes qui n'ont en réalité rien à voir avec le rendu? Comme Liosan l’a déjà dit, vous devriez avoir un
Renderer
.Maintenant, pour répondre à votre question actuelle ...
Il y a plusieurs façons de le faire, et ce n'est qu'un moyen de découpler:
Object
s dans laquelle se trouve le joueur, mais cela ne dépend pas de la classe du joueur (utilisez l'héritage pour y parvenir).InputManager
.Renderer
dessine tous les objets.la source
health
que seule cette instancePlayer
a).Le joueur devrait demander au monde des choses comme la détection de collision. Le moyen d'éviter la dépendance circulaire est de ne pas laisser le monde avoir une dépendance sur Player. Le Monde a besoin de savoir où il se dessine lui-même: vous voulez probablement que ce résumé soit plus éloigné, peut-être avec une référence à un objet Camera qui peut à son tour contenir une référence à une entité à suivre.
Ce que vous voulez éviter en termes de références circulaires n’est pas tant de garder des références les unes aux autres, mais plutôt de se référer explicitement les uns aux autres dans le code.
la source
Chaque fois que deux types d'objets différents peuvent se demander. Ils dépendront les uns des autres car ils doivent avoir une référence à l'autre pour appeler ses méthodes.
Vous pouvez éviter les dépendances circulaires en demandant au monde de demander au joueur, mais ce dernier ne peut pas demander au monde, ou inversement. De cette façon, le monde fait référence aux joueurs mais les joueurs n'ont pas besoin de référence au monde. Ou vice versa. Mais cela ne résoudra pas le problème, car le Monde aurait besoin de demander aux joueurs s'ils ont quelque chose à demander et de le leur dire lors du prochain appel ...
Donc, vous ne pouvez pas vraiment contourner ce "problème" et je pense qu'il n'y a pas besoin de s'inquiéter à ce sujet. Gardez la conception simple et bête aussi longtemps que vous le pouvez.
la source
En supprimant les détails sur le joueur et le monde, vous avez un cas simple de ne pas vouloir introduire une dépendance circulaire entre deux objets (ce qui, selon votre langue, n'a peut-être même pas d'importance, voir le lien dans le commentaire de Fuhrmanator). Il existe au moins deux solutions structurelles très simples qui s'appliqueraient à ce problème et à des problèmes similaires:
1) Introduisez le motif singleton dans votre classe mondiale . Cela permettra au joueur (et à tout autre objet) de trouver facilement l'objet du monde sans recherches coûteuses ou liens tenus en permanence. L'essentiel de ce modèle est simplement que la classe a une référence statique à la seule instance de cette classe, qui est définie sur l'instanciation de l'objet et effacée lors de sa suppression.
En fonction de votre langage de développement et de la complexité souhaitée, vous pouvez facilement l'implémenter en tant que super-classe ou interface et le réutiliser pour de nombreuses classes majeures dont vous ne vous attendez pas à avoir plusieurs dans votre projet.
2) Si la langue dans laquelle vous développez le prend en charge (beaucoup le font), utilisez une référence faible . C’est une référence qui n’affecte pas les tâches telles que la récupération de place. Il est utile exactement dans ces cas, veillez simplement à ne pas présumer que l'objet que vous référencez toujours existe toujours.
Dans votre cas particulier, votre (vos) joueur (s) pourrait avoir une faible référence au monde. L'avantage de cela (comme avec le singleton) est que vous n'avez pas besoin de rechercher l'objet monde d'une manière ou d'une autre, ni d'avoir une référence permanente qui gênera les processus affectés par les références circulaires telles que le garbage collection.
la source
Comme les autres l'ont dit, je pense que vous
World
faites une chose de trop: essayer à la fois de contenir le jeuMap
(qui devrait être une entité distincte) et d' êtreRenderer
en même temps.Créez donc un nouvel objet (appelé
GameMap
éventuellement) et stockez-y les données de niveau cartographique. Écrivez-y des fonctions qui interagissent avec la carte actuelle.Ensuite, vous avez également besoin d'un
Renderer
objet. Vous pouvez faire de cetRenderer
objet la chose qui contientGameMap
etPlayer
(ainsi queEnemies
), et les dessine également.la source
Vous pouvez éviter les dépendances circulaires en n’ajoutant pas les variables en tant que membres. Utilisez une fonction statique CurrentWorld () pour le lecteur ou quelque chose comme ça. N'inventez pas déjà une interface différente de celle implémentée dans World, c'est totalement inutile.
Il est également possible de détruire la référence avant / tout en détruisant l’objet joueur pour arrêter efficacement les problèmes causés par les références circulaires.
la source