2009-09-22 11 views
9

J'ai le code suivant, pensez shooter simple C++:Quelle est la manière la plus élégante et la plus efficace de modéliser une hiérarchie d'objets de jeu? (Conception dérange)

// world.hpp 
//---------- 
class Enemy; 
class Bullet; 
class Player; 

struct World 
{ 
    // has-a collision map 
    // has-a list of Enemies 
    // has-a list of Bullets 
    // has-a pointer to a player 
}; 

// object.hpp 
//----------- 
#include "world.hpp" 

struct Object 
{ 
    virtual ~Object(); 
    virtual void Update() =0; 
    virtual void Render() const =0; 

    Float xPos, yPos, xVel, yVel, radius; // etc. 
}; 

struct Enemy: public Object 
{ 
    virtual ~Enemy(); 
    virtual void Update(); 
    virtual void Render() const; 
}; 

// Bullet and Player are similar (they render and update differently per se, 
/// but the behavior exposed to World is similar) 

// world.cpp 
//---------- 
#include "object.hpp" 

// object.cpp 
//----------- 
#include "object.hpp" 

deux problèmes:

1, les objets du jeu connaissant d'autres objets de jeu.

Il doit y avoir un service qui connaît tous les objets. Il peut ou non devoir exposer TOUS les objets, il doit en exposer certains, en fonction des paramètres du demandeur (position, par exemple). Cette installation est censée être mondiale.

Les objets doivent connaître le monde dans lequel ils se trouvent pour rechercher des informations sur les collisions et d'autres objets. Cela introduit une dépendance où l'implémentation d'Objets et de World doit accéder à l'en-tête de l'objet, ainsi World n'inclura pas son propre en-tête directement plutôt qu'en incluant object.hpp (qui inclut à son tour world.hpp). Cela me met mal à l'aise - je ne pense pas que world.cpp devrait recompiler après avoir modifié object.hpp. Le monde n'a pas l'impression que cela devrait fonctionner avec Object. Cela ressemble à un mauvais design - comment peut-il être réparé? 2, Polymorphisme - peut-il et devrait-il être utilisé pour autre chose que la réutilisation de code et le regroupement logique d'entités de jeu (et comment)? Enemies, Bullets et Player mettront à jour et rendront différemment, sûrement, d'où la fonctionnalité virtuelle Update() et Render() - une interface identique. Ils sont toujours conservés dans des listes séparées et la raison en est que leur interaction dépend des listes de deux objets qui interagissent - deux ennemis se rebondissent, une balle et un ennemi se détruisent, etc.

Ceci est une fonctionnalité c'est au-delà de la mise en œuvre de Enemy and Bullet; ce n'est pas mon problème ici. Je pense qu'au-delà de leurs interfaces identiques, il y a un facteur qui sépare les Ennemis, les Puces et les Joueurs et cela pourrait et devrait être exprimé d'une autre manière, me permettant de créer une liste unique pour chacun d'entre eux.

Le problème est de savoir comment distinguer une catégorie d'objet d'une autre si elle est contenue dans la même liste. Typeid ou autre forme d'identification de type est hors de question - mais alors quelle autre manière de le faire? Ou peut-être qu'il n'y a rien de mal à l'approche?

Répondre

5

C'est probablement le plus gros problème que je rencontre lors de la conception de programmes similaires. L'approche sur laquelle je me suis arrêté consiste à réaliser qu'un objet ne se soucie vraiment pas de l'endroit où il se trouve en termes absolus. Tout ce qui l'intéresse, c'est ce qui l'entoure. En conséquence, l'objet World (et je préfère l'approche par objet à un singleton pour de nombreuses bonnes raisons que vous pouvez rechercher sur Internet) maintient où sont tous les objets, et ils peuvent demander au monde quels objets se trouvent à proximité, où d'autres les objets sont en relation avec lui, etc. World ne devrait pas se préoccuper du contenu de Object; il contiendra à peu près tout, et les Object eux-mêmes définiront comment ils interagissent les uns avec les autres. Les événements sont également un excellent moyen de gérer les objets interagissant, car ils permettent à World d'informer un Object de ce qui se passe sans se soucier de ce qu'est un Object.

informations Hiding d'un Object sur tous les objets est une bonne idée, et ne doit pas être confondue avec la dissimulation d'information au sujet toutObject. Pensez en termes de personnes - il est raisonnable que vous connaissiez et conserviez des informations sur de nombreuses personnes différentes, bien que vous ne puissiez trouver cette information qu'en la rencontrant ou en demandant à quelqu'un d'autre de vous en parler.

ÉDITER DE NOUVEAU:

Très bien. Il est assez clair pour moi ce que vous voulez vraiment, et c'est l'envoi multiple - la capacité de gérer une situation polymorphiquement sur les types de nombreux paramètres de l'appel de la fonction, plutôt que d'un seul. C++ ne supporte malheureusement pas l'envoi multiple en mode natif.

Deux options sont disponibles ici. Vous pouvez tenter de reproduire plusieurs envois en double répartition ou le modèle de visiteur, ou vous pouvez utiliser dynamic_cast. Ce que vous voulez utiliser dépend des circonstances. Si vous avez beaucoup de types différents à utiliser, dynamic_cast est probablement la meilleure approche. Si vous en avez seulement quelques-uns, la répartition double ou le modèle de visiteur est probablement plus approprié.

+0

Nous vous remercions de les deux premiers paragraphes; cela a beaucoup de sens et m'a aidé à me vider la tête. J'ai réécrit ma question pour clarifier ce que je voulais faire avec le polymorphisme. – zyndor

+0

Le motif Visitor est génial, merci de le signaler. Je devine que les méthodes OnHit() sont analogues à la méthode Visit() du modèle et les objets seraient leurs propres visiteurs, non? – zyndor

+1

Il a été difficile de choisir une réponse à accepter comme étant la meilleure, car tous les points étaient bons, mais aucun d'entre eux n'était vraiment complet. Je sentais que les grandes lignes de l'interaction entre l'objet et le monde étaient les plus utiles et les plus pertinentes à ma question. - – zyndor

6

Je pense que vous voudriez donner une référence au Monde à l'objet de jeu. C'est un principe assez simple de Dependency Injection. Pour les collisions, le Monde peut utiliser le Strategy pattern pour déterminer comment les types spécifiques d'objets interagissent.

Ceci conserve la connaissance des différents types d'objets sur les objets primaires et les encapsule dans des objets ayant des connaissances spécifiques à l'interaction.

+0

Est-ce une trop mauvaise solution (OO design et performance-sage) de, au lieu de passer la référence à World à chaque fois, il suffit de stocker un pointeur membre statique sur World, accessible à tous les objets? (Pas dans la forme publique brute, il a été présenté, peut-être - pourrait être un pointeur protégé et une méthode statique publique pour le définir.) – zyndor

+0

Comment vous stockez la référence à l'objet monde est à vous. Puisque ces objets ne seront probablement pas instanciés simultanément dans plusieurs mondes, le stockage de la référence sur la classe est probablement correct. L'idée principale est que le monde est toujours fourni aux objets individuels au moment de l'instanciation, donc si vous voulez avoir deux objets du même type dans des mondes différents, l'API d'instance ne change pas et le seul refactoring qui doit avoir lieu est dans la mise en œuvre de l'objet lui-même. –

3

objets doivent savoir sur le monde ils sont, pour demander des informations collision et d'autres objets.

En bref - non, ils ne le font pas. Des classes comme le Monde peuvent gérer la plupart de cela et tout ce que l'Objet a à faire est de se comporter de manière appropriée quand le Monde lui dit qu'il y a eu une collision.

Parfois, vous pourriez avoir besoin de l'objet pour avoir une sorte de contexte afin de prendre un autre type de décision. Vous pouvez passer l'objet World à ce point en cas de besoin. Cependant, au lieu de passer le Monde, il est préférable de transmettre un objet plus petit et plus pertinent en fonction du type de requête en cours. Il est probable que votre objet World exécute plusieurs rôles différents et que les objets n'ont besoin que d'un accès transitoire à un ou deux de ces rôles. Dans ce cas, il est bon de diviser l'objet World en sous-objets, ou si cela n'est pas pratique, d'implémenter plusieurs interfaces distinctes.

0

Après avoir beaucoup réfléchi, je compris que, bien que les objets DO besoin d'avoir accès à un monde, il est pas la responsabilité du monde pour servir les objets à proximité objets.

C'est ce que j'ai eu le non-objet Arena (jamais instancié + tous les membres statiques, contient les listes des catégories d'objets requises - puces, ennemis, visuels, etc.) Pour, mais il introduit une structure de dépendance similaire ayant les listes de catégorie dans le cadre du monde (ce qui est la raison pour laquelle il ne se sentait pas comme une bonne solution):

// object.hpp 
//----------- 
#include "world.hpp" 

// NOTE: the obvious virtual and pure virtual methods are omitted in the following code 
class Object 
{...}; 

class Enemy: public Object 
{...}; 

class Bullet: public Object 
{...}; 

class Player: public Object 
{...}; 

// arena.hpp 
//----------- 
#include "object.hpp" 

struct Arena 
{ 
    // has-a lists of object categories and a (pointer to a) player 
} 

// object.cpp 
//----------- 
#include "arena.hpp" // for the damn dependencies 

// arena.cpp 
//----------- 
#include "arena.hpp" 

Ainsi, la solution, ou ce qu'il semble être à ce stade, est d'avoir l'intégralité de l'objet (categorie) s pas un niveau de compilation ci-dessus ou ci-dessous la déclaration des objets, mais sur le même niveau.

// object.hpp 
//----------- 
#include "world.hpp" 

class Object 
{ 
    static World *pWorld; 
    ... 
}; 

class Enemy: public Object 
{ 
    typedef std::list<Enemy*> InstList; 
    static InstList insts; 
    ... 
}; 

class Bullet: public Object 
{ 
    typedef std::list<Bullet*> InstList; 
    static InstList insts; 
    ... 
}; 

class Player: public Object 
{ 
    static Player *pThePlayer; 
    ... 
}; 

// object.cpp 
//----------- 
#include "object.hpp" 

Même s'il y a d'autres en-têtes pour les ennemis spécialisés, des balles, etc. leurs (et d'autres) les listes de catégories de se consacrer pleinement à leur en incluant object.hpp qu'ils ont évidemment de toute façon. A propos du bit de polymorphisme et pourquoi les différentes catégories sont conservées dans des listes séparées: les classes de base des catégories d'objets (Bullet, Enemy, Player) peuvent fournir des "gestionnaires d'événements" pour frapper des objets de certains types; ils ne sont cependant pas déclarés au niveau de l'objet plutôt qu'au niveau de la catégorie. (Par exemple, nous ne nous soucions pas de collisions balle contre-balles, ils ne sont pas vérifiés et non traitées.)

// object.hpp 
//----------- 
#include "world.hpp" 

class Object 
{ 
    static World *pWorld; 
    ... 
}; 

class Bullet: public Object 
{ 
    typedef std::list<Bullet*> InstList; 
    static InstList insts; 
    ... 
}; 

class Player: public Object 
{ 
    static Player *pThePlayer; 

    void OnHitBullet(const Bullet *b); 
    ... 
}; 

class Enemy: public Object 
{ 
    typedef std::list<Enemy*> InstList; 
    static InstList insts; 

    virtual void OnHitBullet(const Bullet *b); 
    virtual void OnHitPlayer(const Player *p); 
    ... 
}; 

EDIT, pour être complet saké:

// game.hpp 
//----------- 
#include "object.hpp" 

struct Game 
{ 
    static World world; 

    static void Update(); // update world and objects, check for collisions and 
         /// handle them. 
    static void Render(); 
}; 
Questions connexes