Comment puis-je avoir des objets interagissant et communiquant entre eux sans forcer une hiérarchie?

9

J'espère que ces divagations clarifieront ma question - je comprendrais totalement si elles ne le font pas, alors faites-le moi savoir si c'est le cas, et j'essaierai de me clarifier.

Rencontrez BoxPong , un jeu très simple je fait pour se familiariser avec le développement de jeux orienté objet. Faites glisser la boîte pour contrôler le ballon et collecter des objets jaunes.
Faire BoxPong m'a aidé à formuler, entre autres, une question fondamentale: comment puis-je avoir des objets qui interagissent entre eux sans avoir à «appartenir» les uns aux autres? En d'autres termes, existe-t-il un moyen pour les objets de ne pas être hiérarchisés, mais plutôt de coexister? (Je vais entrer dans les détails ci-dessous.)

Je soupçonne que le problème des objets coexistant est un problème courant, alors j'espère qu'il existe un moyen établi de le résoudre. Je ne veux pas réinventer la roue carrée, donc je suppose que la réponse idéale que je recherche est "voici un modèle de conception qui est couramment utilisé pour résoudre votre type de problème."

Surtout dans des jeux simples comme BoxPong, il est clair qu'il y a, ou devrait y avoir, une poignée d'objets coexistant au même niveau. Il y a une boîte, il y a une balle, il y a un objet de collection. Tout ce que je peux exprimer dans des langages orientés objet, bien que - ou du moins il semble - ce sont des relations HAS-A strictes . Cela se fait via des variables membres. Je ne peux pas simplement commencer ballet le laisser faire, j'ai besoin qu'il appartienne en permanence à un autre objet. Je l' ai mis en place afin que le principal objet de jeu a une boîte, et la boîte à son tour , a une balle, et a un compteur de score. Chaque objet possède également unupdate(), qui calcule la position, la direction, etc., et j'y vais de la même manière: j'appelle la méthode de mise à jour de l'objet de jeu principal, qui appelle les méthodes de mise à jour de tous ses enfants, et ils appellent à leur tour les méthodes de mise à jour de tous leurs enfants. C'est la seule façon dont je peux voir pour créer un jeu orienté objet, mais je pense que ce n'est pas la manière idéale. Après tout, je ne penserais pas exactement à la balle comme appartenant à la boîte, mais plutôt comme étant au même niveau et interagissant avec elle. Je suppose que cela peut être réalisé en transformant tous les objets de jeu en variables membres de l'objet de jeu principal, mais je ne vois pas cela résoudre quoi que ce soit. Je veux dire ... en laissant de côté le désordre évident, comment y aurait-il un moyen pour le ballon et la boîte de se connaître , c'est-à-dire d'interagir?

Il y a aussi le problème des objets qui ont besoin de passer des informations entre eux. J'ai un peu d'expérience dans l'écriture de code pour la SNES, où vous avez accès à pratiquement toute la RAM tout le temps. Supposons que vous vous faites un ennemi personnalisé pour Super Mario World , et que vous souhaitez qu'il supprime toutes les pièces de Mario, puis stockez simplement zéro pour adresser 0DBF $, pas de problème. Il n'y a pas de limitations empêchant les ennemis d'accéder au statut du joueur. Je suppose que j'ai été gâté par cette liberté, car avec C ++ et autres, je me demande souvent comment rendre une valeur accessible à d'autres objets (ou même globaux).
En utilisant l'exemple de BoxPong, que faire si je voulais que la balle rebondisse sur les bords de l'écran? widthet heightsont des propriétés de la Gameclasse,balld'y avoir accès. Je pourrais transmettre ces types de valeurs (soit par le biais de constructeurs ou des méthodes où elles sont nécessaires), mais cela me fait juste penser à de mauvaises pratiques.

Je suppose que mon principal problème est que j'ai besoin d'objets pour se connaître, mais la seule façon dont je peux voir cela est une hiérarchie stricte, ce qui est laid et peu pratique.

J'ai entendu parler de "classes d'amis" sur C ++ et je sais un peu comment elles fonctionnent, mais si elles sont la solution finale, alors comment se fait-il que je ne vois pas de friendmots - clés répandus partout dans chaque projet C ++, et comment le concept n'existe pas dans toutes les langues POO? (Il en va de même pour les pointeurs de fonction, dont je viens d'apprendre récemment.)

Merci d'avance pour les réponses de toute nature - et encore une fois, s'il y a une partie qui n'a pas de sens pour vous, faites-le moi savoir.

vvye
la source
2
Une grande partie de l'industrie du jeu s'est orientée vers l'architecture Entity-Component-System et ses variantes. C'est un état d'esprit différent des approches OO traditionnelles, mais cela fonctionne bien et n'a de sens que lorsque le concept s'enfonce. Unity l'utilise. En fait, Unity utilise uniquement la partie Entity-Component mais est basé sur ECS.
Dunk
Le problème de permettre aux classes de collaborer entre elles sans se connaître est résolu par le modèle de conception Mediator. L'avez-vous regardé?
Fuhrmanator

Réponses:

13

En général, cela se passe très mal si des objets de même niveau se connaissent. Une fois que les objets se connaissent, ils sont liés ou couplés les uns aux autres. Cela les rend difficiles à changer, difficiles à tester, difficiles à entretenir.

Cela fonctionne beaucoup mieux s'il existe un objet "au-dessus" qui connaît les deux et peut définir les interactions entre eux. L'objet qui connaît les deux pairs peut les lier ensemble via l'injection de dépendances ou via des événements ou via le passage de messages (ou tout autre mécanisme de découplage). Oui, cela mène à une sorte de hiérarchie artificielle, mais c'est bien mieux que le désordre de spaghetti que vous obtenez lorsque les choses interagissent simplement bon gré mal gré. Cela n'est que plus important en C ++, car vous avez également besoin de quelque chose pour posséder la durée de vie des objets.

Donc, en bref, vous pouvez le faire en ayant simplement des objets côte à côte partout liés par un accès ad hoc, mais c'est une mauvaise idée. La hiérarchie fournit l'ordre et la propriété claire. La principale chose à retenir est que les objets dans le code ne sont pas nécessairement des objets de la vie réelle (ou même du jeu). Si les objets du jeu ne font pas une bonne hiérarchie, une abstraction différente peut être meilleure.

Telastyn
la source
2

En utilisant l'exemple de BoxPong, que faire si je voulais que la balle rebondisse sur les bords de l'écran? largeur et hauteur sont des propriétés de la classe Game, et j'aurais besoin de la balle pour y avoir accès.

Non!

Je pense que le principal problème que vous rencontrez est que vous prenez la "programmation orientée objet" un peu trop littéralement. En POO, un objet ne représente pas une "chose" mais une "idée" qui signifie qu'une "balle", un "jeu", une "physique", une "math", une "date", etc. Tous sont des objets valides. Il n'y a pas non plus d'obligation pour les objets de "savoir" quoi que ce soit. Par exemple, Date.Now().getTommorrow()demanderait à l'ordinateur quel jour est aujourd'hui, appliquer des règles de date floues pour déterminer la date de demain et la renvoyer à l'appelant. L' Dateobjet ne sait rien d'autre, il a seulement besoin de demander les informations nécessaires au système. De plus, Math.SquareRoot(number)n'a besoin de rien savoir en plus de la logique de calcul d'une racine carrée.

Donc, dans votre exemple que j'ai cité, la "Boule" ne devrait rien savoir de la "Boîte". Les boîtes et les balles sont des idées complètement différentes et n'ont pas le droit de se parler. Mais un moteur physique sait ce qu'est une boîte et une balle (ou du moins, ThreeDShape), et il sait où ils se trouvent et ce qui devrait leur arriver. Donc, si la balle rétrécit parce qu'il fait froid, le moteur physique dirait à cette instance de balle qu'elle est plus petite maintenant.

C'est un peu comme construire une voiture. Une puce informatique ne sait rien d'un moteur de voiture, mais une voiture peut utiliser une puce informatique pour contrôler un moteur. L'idée simple d'utiliser de petites choses simples ensemble pour créer une chose légèrement plus grande et plus complexe, qui est elle-même réutilisable en tant que composant d'autres parties plus complexes.

Et dans votre exemple Mario, que se passe-t-il si vous êtes dans une salle de défi où toucher un ennemi ne vide pas les pièces de Marios, mais l'éjecte simplement de cette pièce? C'est en dehors de l'espace des idées de Mario ou de l'ennemi que Mario devrait perdre des pièces lorsqu'il touche un ennemi (en fait, si Mario a une étoile d'invulnérabilité, il tue l'ennemi à la place). Donc, quel que soit l'objet (domaine / idée) responsable de ce qui se passe lorsque mario touche un ennemi, c'est le seul qui doit être au courant de l'un ou de l'autre et devrait faire tout ce qu'il veut pour l'un ou l'autre (dans la mesure où cet objet permet des changements externes) ).

En outre, avec vos déclarations sur les objets appelant des enfants Update(), cela est extrêmement sujet aux bogues, comme si Updateon l'appelait plusieurs fois par image de différents parents? (même si vous attrapez cela, c'est du temps CPU perdu qui peut ralentir votre jeu) Tout le monde ne devrait toucher que ce dont il a besoin, quand il le faut. Si vous utilisez Update (), vous devez utiliser une forme de modèle d'abonnement pour vous assurer que toutes les mises à jour sont appelées une fois par trame (si cela n'est pas géré pour vous comme dans Unity)

Apprendre à définir vos idées de domaine en blocs clairs, isolés, bien définis et faciles à utiliser sera le plus grand facteur dans la façon dont vous pouvez utiliser la POO.

Tezra
la source
1

Découvrez BoxPong, un jeu très simple que j'ai créé pour vous familiariser avec le développement de jeux orientés objet.

Faire BoxPong m'a aidé à formuler, entre autres, une question fondamentale: comment puis-je avoir des objets qui interagissent entre eux sans avoir à «appartenir» les uns aux autres?

J'ai un peu d'expérience dans l'écriture de code pour la SNES, où vous avez accès à pratiquement toute la RAM tout le temps. Supposons que vous vous faites un ennemi personnalisé pour Super Mario World, et que vous souhaitez qu'il supprime toutes les pièces de Mario, puis stockez simplement zéro pour adresser 0DBF $, pas de problème.

Vous semblez manquer le point de la programmation orientée objet.

La programmation orientée objet consiste à gérer les dépendances en inversant de manière sélective certaines dépendances clés de votre architecture afin de prévenir la rigidité, la fragilité et la non-réutilisation.

Qu'est-ce que la dépendance? La dépendance est la dépendance à autre chose. Lorsque vous stockez zéro pour adresser $ 0DBF, vous comptez sur le fait que cette adresse est l'endroit où se trouvent les pièces de Mario et que les pièces sont représentées sous forme d'entier. Votre code ennemi personnalisé dépend du code implémentant Mario et ses pièces. Si vous modifiez l'emplacement où Mario stocke ses pièces en mémoire, vous devez mettre à jour manuellement tout le code faisant référence à l'emplacement de la mémoire.

Le code orienté objet consiste à faire dépendre votre code des abstractions et non des détails. Donc au lieu de

class Mario
{
    public:
        int coins;
}

tu écrirais

class Mario
{
    public:
        void LoseCoins();

    private:
        int coins;
}

Maintenant, si vous voulez changer la façon dont Mario stocke ses pièces d'un entier à un long ou un double ou le stocker sur le réseau ou le stocker dans une base de données ou lancer un autre long processus, vous effectuez le changement en un seul endroit: le La classe Mario et tous vos autres codes continuent de fonctionner sans aucun changement.

Par conséquent, lorsque vous demandez

comment puis-je avoir des objets qui interagissent entre eux sans avoir à «appartenir» les uns aux autres?

vous demandez vraiment:

comment puis-je avoir du code qui dépend directement les uns des autres sans aucune abstraction?

qui n'est pas une programmation orientée objet.

Je vous suggère de commencer par tout lire ici: http://objectmentor.com/omSolutions/oops_what.html puis de rechercher tout sur YouTube par Robert Martin et de tout regarder.

Mes réponses viennent de lui et certaines d'entre elles sont directement citées de lui.

Mark Murfin
la source
Merci pour la réponse (et la page à laquelle vous avez lié; semble intéressante). En fait, je connais l'abstraction et la réutilisabilité, mais je suppose que je n'ai pas très bien expliqué cela dans ma réponse. Cependant, à partir de l'exemple de code que vous avez fourni, je peux mieux illustrer mon point maintenant! Vous dites essentiellement que l'objet ennemi ne devrait pas faire mario.coins = 0;, mais mario.loseCoins();ce qui est bien et vrai - mais mon point est, comment l'ennemi peut-il avoir accès à l' marioobjet de toute façon? marioêtre une variable membre de enemyne me semble pas juste.
vvye
Eh bien, la réponse simple est de passer Mario comme argument à une fonction dans Enemy. Vous pourriez avoir une fonction comme marioNearby () ou attackMario () qui prendrait un Mario comme argument. Donc, chaque fois que la logique derrière laquelle un ennemi et un Mario doivent interagir est déclenchée, vous appellerez ennemi.marioNearby (mario) qui appellera mario.loseCoins (); Plus tard sur la route, vous pouvez décider qu'il existe une classe d'ennemis qui fait que Mario ne perd qu'une seule pièce ou même en gagne. Vous avez maintenant un endroit pour effectuer ce changement qui n'entraîne pas de changements d'effets secondaires dans un autre code.
Mark Murfin
En passant Mario à un ennemi, vous venez de les coupler. Mario et l'ennemi ne devraient pas savoir que l'autre est même une chose. C'est pourquoi nous créons des objets d'ordre supérieur pour savoir comment coupler les objets simples ensemble.
Tezra
@Tezra Mais alors, ces objets d'ordre supérieur ne sont-ils pas du tout réutilisables? On dirait que ces objets agissent comme des fonctions, ils n'existent que pour être la procédure qu'ils présentent.
Steve Chamaillard
@SteveChamaillard Chaque programme aura au moins un peu de logique spécifique qui n'a de sens dans aucun autre programme, mais l'idée est de garder cette logique isolée à quelques classes de haut niveau. Si vous avez un mario, un ennemi et une classe de niveau, vous pouvez réutiliser mario et l'ennemi dans d'autres jeux. Si vous attachez l'ennemi et mario directement l'un à l'autre, alors tout jeu qui en a besoin doit aussi tirer l'autre.
Tezra
0

Vous pouvez activer le couplage lâche en appliquant le modèle de médiateur. Son implémentation nécessite que vous ayez une référence à un composant médiateur qui connaît tous les composants récepteurs.

Il réalise le genre de pensée "maître du jeu, s'il vous plaît laissez ceci et cela arriver".

Un modèle généralisé est le modèle de publication-abonnement. Il convient que le médiateur ne contienne pas beaucoup de logique. Sinon, utilisez un médiateur fabriqué à la main qui sait où acheminer tous les appels et peut-être même les modifier.

Il existe des variantes synchrones et asynchrones généralement appelées bus d'événements, bus de messages ou file d'attente de messages. Recherchez-les pour déterminer si elles sont appropriées dans votre cas particulier.

Hero Wanders
la source