J'avais une question sur l'architecture du jeu: quelle est la meilleure façon de faire communiquer les différents composants entre eux?
Je m'excuse vraiment si cette question a déjà été posée un million de fois, mais je ne trouve rien avec exactement le type d'informations que je recherche.
J'ai essayé de créer un jeu à partir de zéro (C ++ si cela importe) et j'ai observé certains logiciels de jeu open source pour l'inspiration (Super Maryo Chronicles, OpenTTD et autres). Je remarque que beaucoup de ces conceptions de jeux utilisent des instances globales et / ou des singletons partout (pour des choses comme les files d'attente de rendu, les gestionnaires d'entités, les gestionnaires vidéo, etc.). J'essaie d'éviter les instances globales et les singletons et de construire un moteur aussi peu couplé que possible, mais je rencontre certains obstacles dus à mon inexpérience dans la conception efficace. (Une partie de la motivation de ce projet est de résoudre ce problème :))
J'ai construit un design où j'ai un GameCore
objet principal qui a des membres qui sont analogues aux instances globales que je vois dans d'autres projets (c'est-à-dire qu'il a un gestionnaire d'entrée, un gestionnaire vidéo, un GameStage
objet qui contrôle toutes les entités et le jeu) quelle que soit l'étape actuellement chargée, etc.). Le problème est que, puisque tout est centralisé dans l' GameCore
objet, je n'ai pas de moyen facile pour différents composants de communiquer entre eux.
En regardant Super Maryo Chronicles, par exemple, chaque fois qu'un composant du jeu doit communiquer avec un autre composant (c'est-à-dire qu'un objet ennemi veut s'ajouter à la file d'attente de rendu à dessiner dans la phase de rendu), il parle simplement au instance globale.
Pour moi, je dois demander à mes objets de jeu de transmettre les informations pertinentes à l' GameCore
objet, afin que l' GameCore
objet puisse transmettre ces informations aux autres composants du système qui en ont besoin (par exemple: pour la situation ci-dessus, chaque objet ennemi transmettraient leurs informations de rendu à l' GameStage
objet, qui les collecterait toutes et les retransmettrait GameCore
, qui à leur tour les transmettraient au gestionnaire vidéo pour le rendu). Cela ressemble à un design vraiment horrible tel quel, et j'essayais de penser à une résolution. Mes réflexions sur les designs possibles:
- Instances globales (conception de Super Maryo Chronicles, OpenTTD, etc.)
- Faire en sorte que l'
GameCore
objet agisse comme un intermédiaire par lequel tous les objets communiquent (conception actuelle décrite ci-dessus) - Donnez des pointeurs de composants à tous les autres composants avec lesquels ils devront parler (c.-à-d., Dans l'exemple Maryo ci-dessus, la classe ennemie aurait un pointeur sur l'objet vidéo avec lequel elle doit parler)
- Diviser le jeu en sous-systèmes - Par exemple, avoir des objets de gestionnaire dans l'
GameCore
objet qui gèrent la communication entre les objets de leur sous-système - (Autres options? ....)
J'imagine que l'option 4 ci-dessus est la meilleure solution, mais j'ai du mal à la concevoir ... peut-être parce que j'ai pensé en termes de conceptions que j'ai vues utiliser des globales. J'ai l'impression de prendre le même problème qui existe dans ma conception actuelle et de le reproduire dans chaque sous-système, juste à une échelle plus petite. Par exemple, l' GameStage
objet décrit ci-dessus est en quelque sorte une tentative, mais l' GameCore
objet est toujours impliqué dans le processus.
Quelqu'un peut-il offrir des conseils de conception ici?
Merci!
la source
Réponses:
Quelque chose que nous utilisons dans nos jeux pour organiser nos données globales est le modèle de conception ServiceLocator . L'avantage de ce modèle par rapport au modèle Singleton est que l'implémentation de vos données globales peut changer pendant l'exécution de l'application. De plus, vos objets globaux peuvent également être modifiés pendant l'exécution. Un autre avantage est qu'il est plus facile de gérer l'ordre d'initialisation de vos objets globaux, ce qui est très important notamment en C ++.
par exemple (code C # qui peut être facilement traduit en C ++ ou Java)
Disons que vous avez une interface de rendu qui a des opérations communes pour le rendu des trucs.
Et que vous avez l'implémentation du backend de rendu par défaut
Dans certaines conceptions, il semble légitime de pouvoir accéder au backend de rendu globalement. Dans le modèle Singleton , cela signifie que chaque implémentation IRenderBackend doit être implémentée en tant qu'instance globale unique. Mais l' utilisation du modèle ServiceLocator ne nécessite pas cela.
Voici comment:
Pour pouvoir accéder à votre objet global, vous devez d'abord l'initialiser.
Juste pour montrer comment les implémentations peuvent varier pendant l'exécution, disons que votre jeu a un mini-jeu où le rendu est isométrique et vous implémentez un IsometricRenderBackend .
Lorsque vous passez de l'état actuel à l'état de mini-jeu, il vous suffit de modifier le backend de rendu global fourni par le localisateur de services.
Un autre avantage est que vous pouvez également utiliser des services nuls. Par exemple, si nous avions un service ISoundManager et que l'utilisateur voulait désactiver le son, nous pourrions simplement implémenter un NullSoundManager qui ne fait rien lorsque ses méthodes sont appelées, donc en définissant l' objet de service ServiceLocator sur un objet NullSoundManager que nous pourrions atteindre ce résultat avec peu de travail.
Pour résumer, il peut parfois être impossible d'éliminer les données globales, mais cela ne signifie pas que vous ne pouvez pas les organiser correctement et de manière orientée objet.
la source
std::unique_ptr<ISomeService>
.Il existe de nombreuses façons de concevoir un moteur de jeu et tout se résume vraiment à vos préférences.
Pour éliminer les bases, certains développeurs préfèrent la concevoir comme une pyramide où il existe une classe de noyau supérieure souvent appelée noyau, classe de noyau ou classe de structure qui crée, possède et initialise une série de sous-systèmes tels que comme l'audio, les graphiques, le réseau, la physique, l'IA et la gestion des tâches, des entités et des ressources. Généralement, ces sous-systèmes vous sont exposés par cette classe d'infrastructure et vous transmettez généralement cette classe d'infrastructure à vos propres classes en tant qu'argument constructeur, le cas échéant.
Je pense que vous êtes sur la bonne voie avec votre réflexion sur l'option # 4.
Gardez à l'esprit quand il s'agit de la communication elle-même, cela ne doit pas toujours impliquer un appel direct à une fonction. Il existe de nombreuses façons indirectes de communiquer, que ce soit par une méthode indirecte utilisant
Signal and Slots
ou en utilisantMessages
.Parfois, dans les jeux, il est important de permettre aux actions de se produire de manière asynchrone pour que notre boucle de jeu se déplace aussi rapidement que possible afin que les fréquences d'images soient fluides à l'œil nu. Les joueurs n'aiment pas les scènes lentes et saccadées et nous devons donc trouver des moyens de faire bouger les choses pour eux, mais garder la logique qui coule mais en échec et ordonné aussi. Bien que les opérations asynchrones aient leur place, elles ne sont pas non plus la réponse pour chaque opération.
Sachez simplement que vous aurez un mélange de communications synchrones et asynchrones. Choisissez ce qui est approprié, mais sachez que vous devrez prendre en charge les deux styles parmi vos sous-systèmes. Concevoir un support pour les deux vous sera utile dans le futur.
la source
Vous devez juste vous assurer qu'il n'y a pas de dépendances inverses ou cycliques. Par exemple, si vous avez une classe
Core
, et celle-ciCore
a unLevel
, etLevel
a une liste deEntity
, alors l'arborescence des dépendances devrait ressembler à:Donc, étant donné cet arbre de dépendance initial, vous ne devriez jamais
Entity
dépendre deLevel
ouCore
etLevel
ne devriez jamais dépendre deCore
. Si l'unLevel
ou l' autreEntity
doit avoir accès à des données plus haut dans l'arborescence des dépendances, il doit être transmis en tant que paramètre par référence.Considérez le code suivant (C ++):
En utilisant cette technique, vous pouvez voir que chacun
Entity
a accès auLevel
, et leLevel
a accès auCore
. Notez que chacunEntity
stocke une référence à la mêmeLevel
, ce qui gaspille de la mémoire. Après avoir remarqué cela, vous devriez vous demander si chacun aEntity
vraiment besoin d'accéder auLevel
.D'après mon expérience, il existe soit A) une solution vraiment évidente pour éviter les dépendances inverses, soit B) il n'y a aucun moyen possible d'éviter les instances globales et les singletons.
la source
Donc, fondamentalement, vous voulez éviter un état mutable global ? Vous pouvez en faire un état local, immuable ou pas du tout. Ce dernier est le plus efficace et le plus flexible, imo. Il est connu sous le nom de masquage par mise en œuvre.
la source
La question semble en fait être de savoir comment réduire le couplage sans sacrifier les performances. Tous les objets globaux (services) forment généralement une sorte de contexte qui est modifiable pendant l'exécution du jeu. En ce sens, le modèle de localisateur de service disperse différentes parties du contexte dans différentes parties de l'application, qui peuvent ou non être ce que vous voulez. Une autre approche du monde réel serait de déclarer une structure comme celle-ci:
Et passez-le comme un pointeur brut non propriétaire
sEnvironment*
. Ici, les pointeurs pointent vers des interfaces, de sorte que le couplage est réduit de manière similaire par rapport au localisateur de services. Cependant, tous les services sont au même endroit (ce qui pourrait ou non être bon). Ceci est juste une autre approche.la source