La manipulation d'entités dans un jeu
Comme un petit exercice que je suis en train d'écrire un très petit, simple moteur de jeu qui vient de poignées entités (déménagement, base de l'IA, etc.)
En tant que tel, je suis en train de penser à la façon d'un jeu gère les mises à jour pour l'ensemble des entités, et je suis un peu confus (Probablement parce que je vais à ce sujet dans le mauvais sens)
J'ai donc décidé de poster cette question ici de vous montrer ma façon actuelle de penser à ce sujet, et à voir si quelqu'un peut me font croire à une meilleure façon de le faire.
Actuellement, j'ai un CEngine classe qui prennent des pointeurs vers d'autres classes dont il a besoin (Par exemple un CWindow classe, CEntityManager classe etc.)
J'ai une boucle de jeu qui en pseudo code irait comme ceci (Dans le CEngine classe)
while(isRunning) {
Window->clear_screen();
EntityManager->draw();
Window->flip_screen();
//Cap FPS
}
Mon CEntityManager classe 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 mon CEntity classe 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, j'aurais créer des classes par exemple, pour un ennemi, et de 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 juste dessiner des sprites à l'écran.
Mais puis je suis venu sur le problème de l'utilisation de fonctions qui existent dans une seule entité, mais pas dans un autre.
Dans le pseudo-code ci-dessus exemple, do_ai_stuff(); et handle_input();
Comme vous pouvez le voir sur ma boucle de jeu, il est un appel à l'EntityManager->draw();
Cette juste itérer le entityVector et a appelé le tirage au sort(); la fonction pour chaque entité - ce Qui a bien fonctionné vu que toutes les entités ont un tirage au sort(); fonction.
Mais ensuite j'ai pensé, si c'est un joueur de l'entité qui doit gérer les signaux d'entrée?
Comment cela fonctionne?
Je n'ai pas essayé mais je suppose que je ne peux pas juste en boucle comme je l'ai fait avec le tirage au sort() de la fonction, parce que des entités telles que les ennemis n'ont pas une handle_input() fonction.
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 sur l'écriture de ce genre de choses donc je ne suis pas sûr de la meilleure façon de le faire.
J'ai écrit beaucoup de choses ici, et je ne me posais pas de questions concrètes, donc je vais préciser ce que je recherche ici:
- Est la façon dont j'ai disposé/conçu mon code ok, et est-il pratique?
- Est-il un meilleur moyen plus efficace pour moi de mettre à jour mes entités et fonctions d'appel que d'autres entités peuvent ne pas avoir?
- Est à l'aide d'un enum pour garder une trace de l'une des entités de type un bon moyen d'identifier les entités?
OriginalL'auteur Jake Lucas | 2010-11-06
Vous devez vous connecter pour publier un commentaire.
Que vous obtenez assez proche de la façon dont la plupart des jeux (même si la performance de l'expert curmudgeon Mike Acton souvent saisines à ce sujet).
En général, vous devriez voir quelque chose comme ceci
et l'entité gestionnaire de passe par et des appels de mise à jour(), handleinput () et draw() de chaque entité dans le monde.
Bien sûr, avoir un tas de ces fonctions, dont la plupart ne rien faire quand vous les appelez, peut obtenir assez de gaspillage, notamment pour les fonctions virtuelles. Donc, j'ai vu quelques autres approches trop.
Est de stocker par exemple les données d'entrée dans un monde (ou en tant que membre d'une interface globale, ou un singleton, etc). Puis remplacer la fonction update() des ennemis, de sorte qu'ils do_ai_stuff(). et la mise à jour() des joueurs afin qu'il ne la gestion des données par interrogation de la mondial de.
Une autre est d'utiliser une variante sur le Auditeur modèle, de sorte que tout ce qui se soucie d'entrée hérite d'un auditeur de la classe, et vous vous inscrivez tous ces auditeurs avec une InputManager. Puis le inputmanager appels chaque auditeur à son tour, chaque image:
Et il existe d'autres, plus complexes, des façons d'aller à ce sujet. Mais tous ces travaux et je les ai vu chacun d'eux dans quelque chose qui effectivement expédiés et vendus.
Peut-être, mais il est aussi pourquoi Ratchet & Clank n'est jamais allé au-dessous de 60fps.
eh bien, il souligne certains problèmes réels, en termes de performances et de design. Il semble également avoir une aversion irrationnelle de C++, et la serra dans certains complètement inutile, coups de gueule (comme celle contre les références). Encore, filtrer les conneries, et la performance des conseils est assez solide.
La Performance des conseils n'est solide avec un profil. Je ne dis pas que le code était le summum de la conception a également été assez terrible. Mais le coup de gueule n'était pas exactement rationnelle et pointant du doigt les vrais problèmes.
le profilage n'est pas un substitut pour la compréhension de votre code. Une fois que vous avez le profil de trouvé les goulets d'étranglement, vous aussi besoin de comprendre pourquoi il est lent. Et avant de profilage, vous avez également besoin d'une certaine compréhension de la façon de structurer votre code et les données, parce que vous ne voulez pas vous retrouver dans une situation où le profiler vous dit "votre code est vissé, recommencer à partir de zéro". Il y a quelques hypothèses fondamentales qui sont assez difficiles à corriger une fois que vous avez profilés entre eux et les a trouvés à être un problème.
OriginalL'auteur Crashworks
Vous devez regarder dans les composants, plutôt que l'héritage de cette. Par exemple, dans mon moteur, j'ai (en simplifié):
J'ai les diverses composantes qui font des choses différentes:
Ces composants peut être ajouté à un objet de jeu pour induire le comportement. Ils peuvent communiquer par le biais d'un système de messagerie, et des choses qui nécessitent une mise à jour au cours de la boucle principale d'enregistrer une image de l'auditeur. Ils peuvent agir de façon indépendante et en toute sécurité ajoutés/supprimés au moment de l'exécution. Je trouve que c'est une très extensible système.
EDIT: toutes mes Excuses, je vais chair ce un peu, mais je suis dans le milieu de quelque chose tout de suite 🙂
OriginalL'auteur Moo-Juice
Vous pourriez réaliser cette fonctionnalité en utilisant une fonction virtuelle:
Je n'aurais jamais le nom d'une fonction/méthode "do_stuff" ou quelque chose de similaire. J'ai juste adopté la dénomination de "do_ai_stuff" pour un nom plus générique sur le jeûne. Donc, je suis d'accord avec vous, trop! Il y a beaucoup plus de potentiel à améliorer encore la conception de toute façon. 😉
OriginalL'auteur Flinsch
1 Une petite chose - pourquoi voulez-vous changer l'ID de l'entité? Normalement, elle est constante et initialisé lors de la construction, et c'est tout:
Pour les autres choses, il y a différentes approches, le choix dépend du nombre de type de fonctions spécifiques sont là (et comment vous pouvez vous repdict).
Ajouter à tous les
La possibilité la plus simple est juste de l'ajouter à toutes les méthodes de l'interface de base, et de les mettre en œuvre en tant que non-op dans les classes qui ne le supportent pas. Cela peut sembler comme mauvais conseiller, mais est un acceptabel dénormalisation, si il y a très peu de méthodes qui ne s'appliquent pas, et vous pouvez supposer que l'ensemble des méthodes de ne pas augmenter considérablement avec les futures exigences.
Vous mayn même de mettre en œuvre une base de "mécanisme de découverte", par exemple
N'en faites pas trop! Il est facile de commencer de cette façon, et puis de s'y tenir, même lorsqu'il crée un énorme gâchis de votre code. Il peut être mièvres "intentionnel de la dénormalisation de la hiérarchie des types" - mais à la fin c'est juste un hack qui permet de résoudre quelques problèmes rapidement, mais il a rapidement fait mal quand la demande augmente.
Vrai découverte du Type de
l'aide et
dynamic_cast
, vous pouvez lancer votre objet deCEntity
àCFastCat
. Si l'entité est en fait unCReallyUnmovableBoulder
, le résultat sera un pointeur null. De cette façon, vous pouvez sonde un objet pour son type réel, et de réagir en conséquence.Que le mécanisme fonctionne bien si ce n'est que peu de logique, liée à type de méthodes spécifiques. C'est pas une bonne solution si vous vous retrouvez avec des chaînes où vous sonder pour de nombreux types, et d'agir en conséquence:
Qui habituellement signifie que vos méthodes virtuelles ne sont pas choisis avec soin.
Interfaces
Ci-dessus peut être étendu à des interfaces, lorsque le type de fonctionnalités spécifiques n'est pas unique, mais des groupes de méthodes. Ils ne sont#t pris en charge très bien en C++, mais c'est supportable. E. g. vos objets ont des caractéristiques différentes:
De vos différents objets héritent de la classe de base et une ou plusieurs interfaces:
Et encore une fois, vous pouvez utiliser dynamic_cast de sonde pour les interfaces sur n'importe quelle entité.
C'est assez extensible, et généralement le moyen le plus sûr d'aller lorsque vous n'êtes pas sûr. C'est un peu une manière plus détaillé que les solutions ci-dessus, mais peut faire face très bien avec inattendus de futurs changements. L'affacturage les fonctionnalités des interfaces est pas facile, il faut une certaine expérience pour obtenir une sensation pour elle.
Modèle visiteur
La modèle visiteur nécessite beaucoup de frappe, mais il vous permet d'ajouter des fonctionnalités à des classes sans modification de ces classes.
Dans votre contexte, cela signifie que vous pouvez construire votre structure d'entité, mais de mettre en place leurs activités séparément. Ceci est habituellement utilisé lorsque vous avez très distinctes des opérations sur des entités, vous ne pouvez pas modifier librement les classes, ou l'ajout de la fonctionnalité pour les classes vivement de violer la seule responsabilité de principe.
Cela peut faire face à pratiquement toutes les exigence de modification (à condition que votre entités elles-mêmes sont bien pris en compte d').
(Je ne suis qu'un lien, parce que la plupart des gens ont un moment pour envelopper leur tête autour de lui, et je recommande de ne pas utiliser, sauf si vous avez connu les limites d'autres méthodes)
Merci de remarquer - mais j'aimerais tester mon compilateur avant de se prononcer pour rapidement. Il ne devrait pas être un problème pour les "vrais découverte du type" ci-dessus (à moins que votre compilateur pessimizes ce cas - dans ce cas, le mécanisme est facile à mettre en œuvre). Pour très complexe de la hiérarchie, à quelques milliers de cycles peut être possible, mais avec un peu de moyens limités d'entités, plus personnalisé de la mise en œuvre est possible. (Ouais, c'est nul....) De toute façon, il y a une raison, il y a différentes méthodes.
Je suis avec Crashworks. Je n'ai jamais rencontré dynamic_cast utilisé dans la console d'exécution car elle nécessite RTTI pour être activé et a généralement trop élevée sur les performances. La plupart de la console devs rouler leur propre C++ personnalisé, à la réflexion et à l'introspection système
OriginalL'auteur peterchen
En général, votre code est plutôt ok, comme d'autres l'ont souligné.
Pour répondre à votre troisième question: Dans le code que tu nous a montré, que vous n'utilisez pas le type enum à l'exception de la création. Là, il semble ok (bien que je me demande si un "createPlayer()", "createEnemy()" méthode et ainsi de suite woudn pas être 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, puis, vous êtes une violation de certains OO principes. Vous devez alors utiliser la puissance des méthodes virtuelles afin d'assurer qu'ils font ce qu'ils ont à faire. Si vous avez à "trouver" un objet d'un certain type, vous pouvez ainsi stocker un pointeur de votre joueur spécial d'objet lorsque vous le créez.
Vous pourriez aussi envisager de remplacer l'Id avec la crue des pointeurs si vous avez juste besoin d'un ID unique.
Veuillez considérer comme des conseils qui POURRAIENT être appropriées en fonction de ce que vous avez réellement besoin.
dans OO, vous essayez d'encapsuler le comportement des objets. Au lieu d'avoir de si et du commutateur, vous plutôt appeler une méthode virtuelle et parce qu'il est à l'intérieur d'un objet dans le type, cette méthode peut faire ce qui doit être fait. C'est plus souple et plus type-safe (envisager l'ajout d'un nouveau type dans la hiérarchie, vous obtenez au moment de la compilation de sécurité contre d'exécution problèmes)
Je pense comprendre l'utilisation de méthodes virtuelles, mais pourquoi ne permettant à d'autres objets pour connaître un autre type de l'objet de violer l'encapsulation? Avoir un magasin d'objets de son type (ou utilisez dynamic_cast) le rend plus facile à stocker un tableau d'objets avec un ancêtre commun, ou est-ce juste une mauvaise conception?
ne sais pas ce que tu veux dire exactement. Je pense juste que le fait d'avoir " si (x est de type A) foo1(); else if (x est de type B) foo2();' ressemble généralement à une mauvaise conception.
Oui, il ne ressemble à une mauvaise conception, mais il peut aussi rendre les choses plus facile à la fois. Je vais regarder de plus tard 🙂
OriginalL'auteur Philipp