Question

En tant que petit exercice que je suis en train d'écrire un moteur de jeu très faible, mais simples entités poignées (déplacement, AI base etc.)

En tant que tel, je suis en train de réfléchir à la façon dont une poignée de jeu Les mises à jour pour toutes les entités, et je reçois un peu confus (probablement parce que je vais à ce sujet dans le mauvais sens)

Alors j'ai décidé de poster cette question ici pour vous montrer ma façon actuelle de penser à ce sujet, et de voir si quelqu'un peut me proposer une meilleure façon de le faire.

À l'heure actuelle, j'ai une classe CEngine qui prennent des pointeurs vers d'autres classes dont il a besoin (par exemple une classe CWindow, classe CEntityManager etc.)

J'ai une boucle de jeu qui dans le code pseudo irait comme celui-ci (Au sein de la classe CEngine)

while(isRunning) {
    Window->clear_screen();

    EntityManager->draw();

    Window->flip_screen();

    // Cap FPS
}

classe Mon CEntityManager ressemblait à ceci:

enum {
    PLAYER,
    ENEMY,
    ALLY
};

class CEntityManager {
    public:
        void create_entity(int entityType); // PLAYER, ENEMY, ALLY etc.
        void delete_entity(int entityID);

    private:
        std::vector<CEntity*> entityVector;
        std::vector<CEntity*> entityVectorIter;
};

Et ma classe CEntity ressemblait à ceci:

class CEntity() {
    public:
        virtual void draw() = 0;
        void set_id(int nextEntityID);
        int get_id();
        int get_type();

    private:
        static nextEntityID;
        int entityID;
        int entityType;
};

Après cela, je créer des classes par exemple, pour un ennemi, et lui donner une feuille sprite, ses propres fonctions, etc.

Par exemple:

class CEnemy : public CEntity {
    public:
        void draw(); // Implement draw();
        void do_ai_stuff();

};

class CPlayer : public CEntity {
    public:
        void draw(); // Implement draw();
        void handle_input();
};

Tout cela a bien fonctionné pour dessiner juste sprites à l'écran.

Mais alors je suis venu au problème de l'utilisation des fonctions qui existent dans une seule entité, mais pas dans un autre.

Dans l'exemple de pseudo-code ci-dessus, do_ai_stuff (); et handle_input ();

Comme vous pouvez le voir dans ma boucle de jeu, il y a un appel à EntityManager-> draw (); Ce juste itérer à travers le entityVector et a appelé le tirage au sort (); fonction pour chaque entité - qui a bien fonctionné car toutes les entités ont un tirage au sort (); fonction.

Mais alors je pensais, que si elle est une entité joueur qui doit entrer poignée? Comment ça marche?

Je n'ai pas essayé, mais je suppose que je ne peux pas simplement une boucle à travers comme je l'ai fait avec la fonction draw (), parce que des entités comme les ennemis ne seront pas une fonction handle_input ().

Je pourrais utiliser une instruction if pour vérifier la entityType, comme suit:

for(entityVectorIter = entityVector.begin(); entityVectorIter != entityVector.end(); entityVectorIter++) {
    if((*entityVectorIter)->get_type() == PLAYER) {
        (*entityVectorIter)->handle_input();
    }
}

Mais je ne sais pas comment les gens vont normalement à écrire ce genre de choses, donc je ne suis pas sûr de la meilleure façon de le faire.

J'ai écrit beaucoup ici et je ne poser des questions concrètes, donc je vais clarifier ce que je cherche ici:

  • Est-ce la façon dont j'ai exposé / conçu mon ok code, et est-il pratique?
  • est pour moi il un moyen plus efficace pour mettre à jour mes entités et fonctions d'appel que d'autres entités ne peuvent pas avoir?
  • utilise un ENUM pour garder la trace d'une des entités de type une bonne façon d'identifier les entités?
Était-ce utile?

La solution

Vous obtenez à peu près de la façon dont la plupart des jeux font (bien expert de la performance curmudgeon Mike Acton saisines souvent que ).

En général, vous verriez quelque chose comme ceci

class CEntity {
  public:
     virtual void draw() {};  // default implementations do nothing
     virtual void update() {} ;
     virtual void handleinput( const inputdata &input ) {};
}

class CEnemy : public CEntity {
  public:
     virtual void draw(); // implemented...
     virtual void update() { do_ai_stuff(); }
      // use the default null impl of handleinput because enemies don't care...
}

class CPlayer : public CEntity {
  public:
     virtual void draw(); 
     virtual void update();
     virtual void handleinput( const inputdata &input) {}; // handle input here
}

et le gestionnaire d'entités et passe par la mise à jour des appels (), handleinput (), et d'en tirer () sur chaque entité dans le monde.

Bien sûr, avoir beaucoup de ces fonctions, dont la plupart ne font rien quand vous les appelez, peut être assez inutile, en particulier pour les fonctions virtuelles. Donc, je l'ai vu d'autres approches aussi.

La première consiste à stocker ex les données d'entrée dans un global (ou en tant que membre d'une interface globale, ou un singleton, etc.). Ensuite, passer outre la fonction mise à jour () des ennemis afin qu'ils do_ai_stuff (). et la mise à jour () des joueurs afin qu'il ne l'entrée de manipulation par le vote global.

Une autre est d'utiliser une certaine variation sur le motif Listener, de sorte que tout ce qui soucie de l'entrée hérite d'une classe d'écoute commune, et vous enregistrez tous les auditeurs avec un InputManager. Ensuite, le InputManager appelle chaque auditeur à son tour chaque image:

class CInputManager
{
  AddListener( IInputListener *pListener );
  RemoveListener( IInputListener *pListener );

  vector<IInputListener *>m_listeners;
  void PerFrame( inputdata *input ) 
  { 
     for ( i = 0 ; i < m_listeners.count() ; ++i )
     {
         m_listeners[i]->handleinput(input);
     }
  }
};
CInputManager g_InputManager; // or a singleton, etc

class IInputListener
{
   virtual void handleinput( inputdata *input ) = 0;
   IInputListener() { g_InputManager.AddListener(this); }
   ~IInputListener() { g_InputManager.RemoveListener(this); }
}

class CPlayer : public IInputListener
{
   virtual void handleinput( inputdata *input ); // implement this..
}

Et il y a d'autres moyens plus complexes d'aller à ce sujet. Mais tous ceux qui travaillent et je l'ai vu chacun d'eux quelque chose qui en fait vendu et expédié.

Autres conseils

Vous devriez regarder dans les composants, plutôt que l'héritage pour cela. Par exemple, dans mon moteur, je l'ai (simplifié):

class GameObject
{
private:
    std::map<int, GameComponent*> m_Components;
}; // eo class GameObject

J'ai plusieurs composants qui font des choses différentes:

class GameComponent
{
}; // eo class GameComponent

class LightComponent : public GameComponent // represents a light
class CameraComponent : public GameComponent // represents a camera
class SceneNodeComponent : public GameComponent // represents a scene node
class MeshComponent : public GameComponent // represents a mesh and material
class SoundComponent : public GameComponent // can emit sound
class PhysicsComponent : public GameComponent // applies physics
class ScriptComponent : public GameComponent // allows scripting

Ces composants peuvent être ajoutés à un objet de jeu pour induire un comportement. Ils peuvent communiquer grâce à un système de messagerie, et les choses qui nécessitent la mise à jour au cours de la boucle principale enregistrer un écouteur d'image. Ils peuvent agir en toute indépendance et en toute sécurité ajouté / supprimé lors de l'exécution. Je trouve cela un système très extensible.

EDIT: Toutes mes excuses, j'étoffent ce un peu, mais je suis au milieu de quelque chose en ce moment:)

Vous pourriez réaliser cette fonctionnalité en utilisant la fonction virtuelle ainsi:

class CEntity() {
    public:
        virtual void do_stuff() = 0;
        virtual void draw() = 0;
        // ...
};

class CEnemy : public CEntity {
    public:
        void do_stuff() { do_ai_stuff(); }
        void draw(); // Implement draw();
        void do_ai_stuff();

};

class CPlayer : public CEntity {
    public:
        void do_stuff() { handle_input(); }
        void draw(); // Implement draw();
        void handle_input();
};

1 Une petite chose - pourquoi voudriez-vous changer l'ID d'une entité? Normalement, cela est constant et initialisé lors de la construction, et le tour est:

class CEntity
{ 
     const int m_id;
   public:
     CEntity(int id) : m_id(id) {}
}

Pour les autres, il existe différentes approches, le choix dépend du nombre de fonctions spécifiques de type sont là (et comment vous pouvez les repdict).


Ajouter à tous

La méthode la plus simple est d'ajouter que toutes les méthodes à l'interface de base, et les mettre en œuvre sans op dans les classes qui ne supportent pas. Ce son aimerez peut-être mauvais conseils, mais est une dénormalisation de acceptabel, s'il y a très peu de méthodes qui ne sont pas applicables, et vous pouvez assumer l'ensemble des méthodes ne poussent pas de manière significative aux exigences futures.

Vous mayn même mettre en œuvre une sorte de base de « mécanisme de découverte », par exemple.

 class CEntity
 {
   public:
     ...
     virtual bool CanMove() = 0;
     virtual void Move(CPoint target) = 0;
 }

Ne pas exagérer! Il est facile de commencer de cette façon, et puis de s'y tenir, même quand il crée un énorme gâchis de votre code. Il peut être DRAGEE comme « dénormalisation intentionnelle de type hiérarchie » - mais à la fin il est jsut un hack qui permet de résoudre rapidement quelques problèmes, mais rapidement fait mal lorsque l'application se développe

.

True Type découverte

à l'aide et dynamic_cast, vous pouvez en toute sécurité jeter votre objet de CEntity à CFastCat. Si l'entité est en fait un CReallyUnmovableBoulder, le résultat sera un pointeur NULL. De cette façon, vous pouvez sonder un objet pour son type réel et réagir en conséquence.

CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ;
if (fastCat != 0)
   fastCat->Meow();

Ce mécanisme fonctionne bien s'il n'y a que peu de logique liée aux méthodes spécifiques de type. Il de pas une bonne solution si vous vous retrouvez avec des chaînes où vous sonde pour de nombreux types, et agir en conséquence:

// -----BAD BAD BAD BAD Code -----
CFastCat * fastCat = dynamic_cast<CFastCat *>(entity) ;
if (fastCat != 0)
   fastCat->Meow();

CBigDog * bigDog = dynamic_cast<CBigDog *>(entity) ;
if (bigDog != 0)
   bigDog->Bark();

CPebble * pebble = dynamic_cast<CPebble *>(entity) ;
if (pebble != 0)
   pebble->UhmWhatNoiseDoesAPebbleMake();

Cela signifie généralement vos méthodes virtuelles ne sont pas choisis avec soin.


Interfaces

ci-dessus peut être étendu aux interfaces, lorsque la fonctionnalité spécifique de type ne sont pas des méthodes simples, mais les groupes de méthodes. Ils aren # t très bien pris en charge en C ++, mais il est supportable. Par exemple. vos objets ont des caractéristiques différentes:

class IMovable
{
   virtual void SetSpeed() = 0;
   virtual void SetTarget(CPoint target) = 0;
   virtual CPoint GetPosition() = 0;
   virtual ~IMovable() {}
}

class IAttacker
{
   virtual int GetStrength() = 0;
   virtual void Attack(IAttackable * target) = 0;
   virtual void SetAnger(int anger) = 0;
   virtual ~IAttacker() {}
}

Vos différents objets héritent de la classe de base et une ou plusieurs interfaces:

class CHero : public CEntity, public IMovable, public IAttacker 

Et encore, vous pouvez utiliser dynamic_cast à la sonde pour les interfaces sur une entité.

C'est tout à fait extensible, et généralement le plus sûr moyen d'aller quand vous n'êtes pas sûr. Il est un peu mroe prolixe que des solutions ci-dessus, mais il peut très bien faire face à des changements futurs imprévus. fonctionnalité de factoring en interfaces est pas facile, il faut une certaine expérience pour avoir une idée pour elle.


modèle visiteur

Le motif visiteur nécessite beaucoup de frappe, mais il vous permet d'ajouter des fonctionnalités aux classes sans modifier ces classes.

Dans votre contexte, cela signifie que vous pouvez construire votre structure d'entité, mais la mise en œuvre séparément leurs activités. Ceci est généralement utilisé lorsque vous avez des opérations très distinctes sur vos entités, vous ne pouvez pas modifier librement les classes, ou en ajoutant la fonctionnalité aux classes serait fortement violer la seule responsabilité de principe.

Cela peut faire face à pratiquement tous les besoins de changement (à condition que vos entités elles-mêmes sont bien factoré).

(je ne relie à lui, car il faut la plupart des gens un certain temps pour envelopper la tête autour de lui, et je recommande de ne pas l'utiliser à moins que vous avez connu les limites des autres méthodes)

En général, votre code est assez correct, comme d'autres l'ont souligné.

Pour répondre à votre troisième question: Dans le code que vous nous avez montré, vous n'utilisez pas le type ENUM, sauf pour la création. Là, il semble ok (même si je me demande si une méthode « createPlayer () », « createEnemy () » et ainsi de suite woudn't plus facile à lire). Mais dès que vous avez le code qui utilise si ou même passer à faire des choses différentes en fonction du type, vous violez certains principes OO. Vous devez alors utiliser la puissance des méthodes virtuelles pour assurer leur faire ce qu'ils doivent. Si vous devez « trouver » un objet d'un certain type, vous pourriez aussi bien stocker un pointeur vers votre objet joueur spécial droit lors de sa création.

Vous pouvez également envisager de remplacer les ID avec les pointeurs premières si vous avez juste besoin d'un identifiant unique.

S'il vous plaît considérer comme des conseils qui pourraient être appropriées en fonction de ce que vous avez réellement besoin.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top