J'ai du mal à trouver un moyen d'organiser les objets du jeu pour qu'ils soient polymorphes mais en même temps pas polymorphes.
Voici un exemple: en supposant que nous voulons tous nos objets pour update()
et draw()
. Pour ce faire, nous devons définir une classe de base GameObject
qui a ces deux méthodes pures virtuelles et laisse le polymorphisme entrer en jeu:
class World {
private:
std::vector<GameObject*> objects;
public:
// ...
update() {
for (auto& o : objects) o->update();
for (auto& o : objects) o->draw(window);
}
};
La méthode de mise à jour est censée prendre soin de tout état que l'objet de classe spécifique doit mettre à jour. Le fait est que chaque objet doit connaître le monde qui les entoure. Par exemple:
- Une mine doit savoir si quelqu'un entre en collision avec elle
- Un soldat doit savoir si le soldat d'une autre équipe est à proximité
- Un zombie devrait savoir où se trouve le cerveau le plus proche, dans un rayon,
Pour les interactions passives (comme la première), je pensais que la détection de collision pouvait déléguer quoi faire dans des cas spécifiques de collisions à l'objet lui-même avec un on_collide(GameObject*)
.
La plupart des autres informations (comme les deux autres exemples) pourraient simplement être interrogées par le monde du jeu transmis à la update
méthode. Maintenant, le monde ne distingue pas les objets en fonction de leur type (il stocke tous les objets dans un seul conteneur polymorphe), donc ce qu'il renverra avec un idéal world.entities_in(center, radius)
est un conteneur de GameObject*
. Mais bien sûr, le soldat ne veut pas attaquer d'autres soldats de son équipe et un zombie ne fait pas cas d'autres zombies. Nous devons donc distinguer le comportement. Une solution pourrait être la suivante:
void TeamASoldier::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<TeamBSoldier*>(e))
// shoot towards enemy
}
void Zombie::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<Human*>(e))
// go and eat brain
}
mais bien sûr, le nombre dynamic_cast<>
par image peut être horriblement élevé, et nous savons tous à quel point la lenteur dynamic_cast
peut être. Le même problème s'applique également au on_collide(GameObject*)
délégué dont nous avons discuté précédemment.
Alors, quel est le moyen idéal d'organiser le code afin que les objets puissent être conscients d'autres objets et pouvoir les ignorer ou entreprendre des actions en fonction de leur type?
la source
dynamic_cast<Human*>
, implémentez quelque chose comme abool GameObject::IsHuman()
, qui retournefalse
par défaut mais est remplacé pour revenirtrue
dans laHuman
classe.IsA
remplacements personnalisés s'est avérée être légèrement meilleure que la diffusion dynamique dans la pratique pour moi. La meilleure chose à faire est que l'utilisateur ait, dans la mesure du possible, des listes de données triées au lieu d'itérer aveuglément sur l'ensemble du pool d'entités.TeamASoldier
etTeamBSoldier
est vraiment identique - tiré sur n'importe qui dans l'autre équipe. Tout ce dont elle a besoin pour d'autres entités est uneGetTeam()
méthode à son plus spécifique et, par l'exemple de congusbongus, qui peut être encore plus abstraite en uneIsEnemyOf(this)
sorte d'interface. Le code n'a pas besoin de se soucier des classifications taxonomiques des soldats, des zombies, des joueurs, etc. Concentrez-vous sur l'interaction, pas sur les types.Réponses:
Au lieu de mettre en œuvre la prise de décision de chaque entité en soi, vous pouvez également opter pour le modèle de contrôleur. Vous auriez des classes de contrôleurs centraux qui connaissent tous les objets (qui leur importent) et contrôlent leur comportement.
Un MovementController gérerait le mouvement de tous les objets qui peuvent se déplacer (faire la recherche d'itinéraire, mettre à jour les positions en fonction des vecteurs de mouvement actuels).
Un MineBehaviorController vérifierait toutes les mines et tous les soldats et ordonnerait à une mine d'exploser lorsqu'un soldat s'approche trop.
Un ZombieBehaviorController vérifierait tous les zombies et les soldats à proximité, choisirait la meilleure cible pour chaque zombie et lui ordonnerait de s'y déplacer et de l'attaquer (le mouvement lui-même est géré par le MovementController).
Un SoldierBehaviorController analyserait toute la situation et proposerait ensuite des instructions tactiques pour tous les soldats (vous vous déplacez là-bas, vous tirez dessus, vous guérissez ce type ...). L'exécution réelle de ces commandes de niveau supérieur serait également gérée par des contrôleurs de niveau inférieur. Lorsque vous y mettez des efforts, vous pouvez rendre l'IA capable de prendre des décisions coopératives assez intelligentes.
la source
std::map
s et les entités ne sont que des ID et nous devons ensuite créer une sorte de système de type (peut-être avec un composant tag, car le moteur de rendu devra savoir quoi dessiner); et si nous ne voulons pas le faire, nous aurons besoin d'un composant de dessin: mais il a besoin du composant de position pour savoir où se dessiner, donc nous créons des dépendances entre les composants que nous résolvons avec un système de messagerie super complexe. Est-ce bien ce que vous proposez?Tout d'abord, essayez d'implémenter des fonctionnalités afin que les objets restent indépendants les uns des autres, dans la mesure du possible. Surtout, vous voulez le faire pour le multi-threading. Dans votre premier exemple de code, l'ensemble de tous les objets pourrait être divisé en ensembles correspondant au nombre de cœurs de processeur et être mis à jour très efficacement.
Mais comme vous l'avez dit, une interaction avec d'autres objets est nécessaire pour certaines fonctionnalités. Cela signifie que l'état de tous les objets doit être synchronisé à certains points. En d'autres termes, votre application doit attendre que toutes les tâches parallèles se terminent en premier, puis appliquer des calculs impliquant une interaction. Il est bon de réduire le nombre de ces points de synchronisation, car ils impliquent toujours que certains threads doivent attendre que d'autres se terminent.
Par conséquent, je suggère de mettre en mémoire tampon ces informations sur les objets qui sont nécessaires à l'intérieur d'autres objets. Avec un tel tampon global, vous pouvez mettre à jour tous vos objets indépendamment les uns des autres, mais uniquement en fonction d'eux-mêmes et du tampon global, qui est à la fois plus rapide et plus facile à entretenir. À un pas de temps fixe, par exemple après chaque image, mettez à jour le tampon avec l'état actuel des objets.
Donc, ce que vous faites une fois par image, 1. mettez en mémoire tampon l’état actuel des objets, 2. mettez à jour tous les objets en fonction d’eux-mêmes et du tampon, 3. dessinez vos objets et recommencez avec le renouvellement du tampon.
la source
Utilisez un système basé sur les composants, dans lequel vous avez un GameObject barebones qui contient 1 ou plusieurs composants qui définissent leur comportement.
Par exemple, si un objet est censé se déplacer à gauche et à droite tout le temps (une plate-forme), vous pouvez créer un tel composant et le joindre à un GameObject.
Supposons maintenant qu'un objet de jeu soit censé tourner lentement tout le temps, vous pouvez créer un composant distinct qui fait exactement cela et le fixer au GameObject.
Et si vous vouliez avoir une plate-forme mobile qui tournait également, dans une hiérarchie de classe traditionnelle qui devient difficile à faire sans dupliquer le code.
La beauté de ce système, c'est qu'au lieu d'avoir une classe Rotatable ou MovingPlatform, vous attachez ces deux composants au GameObject et vous avez maintenant une MovingPlatform qui tourne automatiquement.
Tous les composants ont une propriété, "requiresUpdate" qui, bien que vraie, GameObject appellera la méthode "update" sur ledit composant. Par exemple, supposons que vous ayez un composant déplaçable, ce composant en survolant la souris (s'il se trouvait sur le GameObject) peut définir "requiresUpdate" sur true, puis sur mouse-up le définir sur false. Lui permettant de suivre la souris uniquement lorsque la souris est en panne.
L'un des développeurs de Tony Hawk Pro Skater a écrit de facto dessus, et cela vaut la peine d'être lu: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
la source
Privilégiez la composition à l'héritage.
Mon conseil le plus fort en dehors de cela serait: Ne vous laissez pas entraîner dans la mentalité de "Je veux que cela soit suprêmement flexible". La flexibilité est excellente, mais rappelez-vous qu'à un certain niveau, dans tout système fini tel qu'un jeu, il y a des parties atomiques qui sont utilisées pour construire le tout. D'une manière ou d'une autre, votre traitement repose sur ces types atomiques prédéfinis. En d'autres termes, la prise en charge de "tout" type de données (si cela était possible) ne vous aiderait pas à long terme, si vous n'avez pas de code pour les traiter. Fondamentalement, tout le code doit analyser / traiter les données en fonction de spécifications connues ... ce qui signifie un ensemble prédéfini de types. Quelle est la taille de cet ensemble? Dépend de vous.
Cet article offre un aperçu du principe de la composition sur l'héritage dans le développement de jeux via une architecture à composants d'entité robuste et performante.
En créant des entités à partir de sous-ensembles (différents) de certains sur-ensembles de composants prédéfinis, vous offrez à vos IA des façons concrètes et fragmentaires de comprendre le monde et les acteurs qui les entourent, en lisant les états de ces composants.
la source
Personnellement, je recommande de garder la fonction draw hors de la classe Object elle-même. Je recommande même de garder l'emplacement / les coordonnées des objets hors de l'objet lui-même.
Cette méthode draw () va traiter de l'API de rendu de bas niveau d'OpenGL, OpenGL ES, Direct3D, votre couche d'encapsulation sur ces API ou une API de moteurs. Il se peut que vous deviez échanger entre (Si vous vouliez prendre en charge OpenGL + OpenGL ES + Direct3D par exemple.
Ce GameObject devrait simplement contenir les informations de base sur son apparence visuelle, comme un maillage ou peut-être un ensemble plus grand, y compris des entrées de shader, l'état d'animation, etc.
Vous voudrez également un pipeline graphique flexible. Que se passe-t-il si vous souhaitez commander des objets en fonction de leur distance à la caméra. Ou leur type de matériau. Que se passe-t-il si vous souhaitez dessiner un objet «sélectionné» d'une couleur différente. Que se passe-t-il si au lieu de restituer aussi soo que vous appelez une fonction de dessin sur un objet, il la place plutôt dans une liste de commandes d'actions à effectuer pour le rendu (peut être nécessaire pour le thread). Vous pouvez faire ce genre de chose avec l'autre système mais c'est un PITA.
Ce que je recommande, au lieu de dessiner directement, vous liez tous les objets que vous souhaitez à une autre structure de données. Cette liaison n'a vraiment besoin que d'une référence à l'emplacement des objets et aux informations de rendu.
Vos niveaux / morceaux / zones / cartes / hubs / wholeworld / quoi que ce soit reçoivent un index spatial, cela contient les objets et les renvoie en fonction de requêtes de coordonnées et pourrait être une simple liste ou quelque chose comme un Octree. Cela pourrait également être un wrapper pour quelque chose implémenté par un moteur physique tiers en tant que scène physique. Il vous permet de faire des choses comme "Interroger tous les objets qui sont dans la vue de la caméra avec une zone supplémentaire autour d'eux", ou pour des jeux plus simples où vous pouvez simplement tout rendre pour récupérer la liste entière.
Les index spatiaux ne doivent pas contenir les informations de positionnement réelles. Ils fonctionnent en stockant des objets dans des structures arborescentes par rapport à l'emplacement d'autres objets. Ils peuvent être considérés comme une sorte de cache avec perte permettant une recherche rapide d'un objet en fonction de sa position. Il n'est pas vraiment nécessaire de dupliquer vos coordonnées X, Y, Z réelles. Cela dit, vous pouvez si vous le souhaitez
En fait, vos objets de jeu n'ont même pas besoin de contenir leurs propres informations de localisation. Par exemple, un objet qui n'a pas été placé dans un niveau ne doit pas avoir de coordonnées x, y, z, cela n'a aucun sens. Vous pouvez le contenir dans l'index spécial. Si vous devez rechercher les coordonnées de l'objet en fonction de sa référence réelle, vous souhaiterez avoir une liaison entre l'objet et le graphique de la scène (les graphiques de la scène sont destinés au retour d'objets basés sur les coordonnées mais sont lents à renvoyer les coordonnées basées sur les objets) .
Lorsque vous ajoutez un objet à un niveau. Il fera ce qui suit:
1) Créer une structure de localisation:
Cela pourrait également être une référence à un objet dans un moteur physique tiers. Ou il peut s'agir de coordonnées décalées avec une référence à un autre emplacement (pour une caméra de suivi ou un objet ou un exemple attaché). Avec le polymorphisme, cela peut être selon qu'il s'agit d'un objet statique ou dynamique. En gardant une référence à l'index spatial ici lorsque les coordonnées sont mises à jour, l'index spatial peut aussi l'être.
Si vous êtes préoccupé par l'allocation dynamique de mémoire, utilisez un pool de mémoire.
2) Une liaison / liaison entre votre objet, son emplacement et le graphique de la scène.
3) La liaison est ajoutée à l'index spatial à l'intérieur du niveau au point approprié.
Lorsque vous vous préparez à rendre.
1) Obtenez la caméra (ce ne sera qu'un autre objet, sauf que son emplacement suivra le personnage du joueur et que votre moteur de rendu y aura une référence spéciale, en fait c'est tout ce dont il a vraiment besoin).
2) Obtenez le SpacialBinding de la caméra.
3) Obtenez l'index spatial de la liaison.
4) Recherchez les objets qui sont (éventuellement) visibles par la caméra.
5A) Vous devez faire traiter les informations visuelles. Textures téléchargées sur le GPU et ainsi de suite. Il serait préférable de le faire à l'avance (comme au niveau de la charge), mais peut-être de le faire lors de l'exécution (pour un monde ouvert, vous pouvez charger des choses lorsque vous approchez d'un morceau, mais cela devrait toujours être fait à l'avance).
5B) Créez éventuellement un arbre de rendu mis en cache, si vous souhaitez trier la profondeur / le matériau ou suivre les objets à proximité, ils pourraient être visibles ultérieurement. Sinon, vous pouvez simplement interroger l'index spatial à chaque fois que cela dépendra de vos exigences de jeu / performances.
Votre moteur de rendu aura probablement besoin d'un objet RenderBinding qui fera le lien entre l'objet, les coordonnées
Ensuite, lorsque vous effectuez le rendu, exécutez simplement la liste.
J'ai utilisé des références ci-dessus, mais il peut s'agir de pointeurs intelligents, de pointeurs bruts, de poignées d'objets, etc.
ÉDITER:
Quant à rendre les choses «conscientes» les unes des autres. C'est la détection de collision. Il serait probablement mis en œuvre dans l'Octree. Vous devrez fournir un rappel dans votre objet principal. Ce truc est mieux géré par un moteur physique approprié tel que Bullet. Dans ce cas, remplacez simplement Octree par PhysicsScene et Position par un lien vers quelque chose comme CollisionMesh.getPosition ().
la source