Comment utiliser correctement les singletons dans la programmation du moteur C ++?


16

Je sais que les singletons sont mauvais, mon ancien moteur de jeu utilisait un objet 'Game' singleton qui gère tout, de la conservation de toutes les données à la boucle de jeu réelle. Maintenant j'en fais un nouveau.

Le problème est que, pour dessiner quelque chose en SFML, vous utilisez window.draw(sprite)où window est un sf::RenderWindow. Il y a 2 options que je vois ici:

  1. Créer un objet Game singleton que chaque entité du jeu récupère (ce que j'ai utilisé auparavant)
  2. Faites-en le constructeur des entités: Entity(x, y, window, view, ...etc)(c'est juste ridicule et ennuyeux)

Quelle serait la bonne façon de le faire tout en gardant le constructeur d'une entité à seulement x et y?

Je pourrais essayer de garder une trace de tout ce que je fais dans la boucle de jeu principale et dessiner manuellement leur sprite dans la boucle de jeu, mais cela semble aussi compliqué, et je veux également un contrôle total absolu sur une fonction de dessin entière pour l'entité.


1
Vous pouvez passer la fenêtre comme argument de la fonction 'render'.
dari

25
Les singletons ne sont pas mauvais! ils peuvent être utiles et parfois nécessaires (bien sûr, c'est discutable).
ExOfDe

3
N'hésitez pas à remplacer les singletons par des globales simples. Inutile de créer des ressources globalement nécessaires "à la demande", inutile de les faire circuler. Pour les entités, vous pouvez cependant utiliser une classe "niveau" pour contenir certaines choses qui sont pertinentes pour toutes.
snake5

Je déclare ma fenêtre et d'autres dépendances dans mon principal, puis j'ai des pointeurs dans mes autres classes.
KaareZ

1
@JAB Facilement corrigé avec l'initialisation manuelle de main (). L'initialisation paresseuse le fait à un moment inconnu, ce qui n'est jamais une bonne idée pour les systèmes centraux.
snake5

Réponses:


3

Stockez uniquement les données nécessaires au rendu de l'image-objet à l'intérieur de chaque entité, puis récupérez-les dans l'entité et passez-les à la fenêtre pour le rendu. Pas besoin de stocker une fenêtre ou d'afficher des données à l'intérieur d'entités.

Vous pourriez avoir une classe de jeu ou de moteur de niveau supérieur qui détient un niveau classe (contient toutes les entités actuellement utilisées) et une classe Renderer (contient la fenêtre, la vue et tout autre élément pour le rendu).

Ainsi, la boucle de mise à jour du jeu dans votre classe de niveau supérieur pourrait ressembler à:

EntityList entities = mCurrentLevel.getEntities();
for(auto& i : entities){
  // Run game logic...
  i->update(...);
}
// Render all the entities
for(auto& i : entities){
  mRenderer->draw(i->getSprite());
}

3
Il n'y a rien d'idéal dans un singleton. Pourquoi rendre publiques les implémentations alors que vous n'y êtes pas obligé? Pourquoi écrire Logger::getInstance().Log(...)au lieu de juste Log(...)? Pourquoi initialiser la classe au hasard lorsqu'on lui a demandé si vous pouvez le faire manuellement une seule fois? Une fonction globale référençant des globaux statiques est beaucoup plus simple à créer et à utiliser.
snake5

@ snake5 Justifier des singletons sur Stack Exchange, c'est comme sympathiser avec Hitler.
Willy Goat

30

L'approche simple consiste simplement à faire de ce qui était autrefois Singleton<T>unT place. Les globaux ont également des problèmes, mais ils ne représentent pas un tas de travail supplémentaire et du code standard pour appliquer une contrainte triviale. C'est fondamentalement la seule solution qui n'implique pas (potentiellement) de toucher le constructeur de l'entité.

L'approche la plus difficile, mais peut-être meilleure, consiste à transférer vos dépendances là où vous en avez besoin . Oui, cela pourrait impliquer de passer unWindow * à un tas d'objets (comme votre entité) d'une manière qui a l'air grossière. Le fait qu'il ait l'air dégoûtant devrait vous dire quelque chose: votre design peut être dégoûtant.

La raison pour laquelle cela est plus difficile (au-delà d'impliquer davantage de frappe) est que cela conduit souvent à refactoriser vos interfaces de sorte que la chose dont vous "avez besoin" pour passer est nécessaire par moins de classes de niveau feuille. Cela rend beaucoup la laideur inhérente au passage de votre moteur de rendu à tout, et améliore également la maintenabilité générale de votre code en réduisant la quantité de dépendances et de couplage, dont vous avez rendu très évidente la prise en compte des dépendances comme paramètres. . Lorsque les dépendances étaient des singletons ou des globales, il était moins évident à quel point vos systèmes étaient interconnectés.

Mais c'est potentiellement une entreprise majeure . Le faire à un système après coup peut être carrément douloureux. Il peut être beaucoup plus pragmatique pour vous de simplement laisser votre système seul, avec le singleton, pour l'instant (surtout si vous essayez réellement de livrer un jeu qui sinon fonctionne très bien; les joueurs ne s'en soucieront généralement pas si vous avez un singleton ou quatre là-dedans).

Si vous voulez essayer de le faire avec votre conception existante, vous devrez peut-être publier beaucoup plus de détails sur votre implémentation actuelle car il n'y a pas vraiment de liste de contrôle générale pour effectuer ces modifications. Ou venez en discuter dans le chat .

D'après ce que vous avez publié, je pense qu'un grand pas dans la direction "pas de singleton" consisterait à éviter que vos entités aient accès à la fenêtre ou à la vue. Cela suggère qu'ils se dessinent eux-mêmes, et vous n'avez pas besoin que les entités les dessinent elles-mêmes . Vous pouvez adopter une méthodologie où les entités contiennent simplement les informations qui permettraient pour être dessinées par un système externe (qui a les références de fenêtre et de vue). L'entité expose simplement sa position et le sprite qu'elle doit utiliser (ou une sorte de référence audit sprite, si vous souhaitez mettre en cache les sprites réels dans le moteur de rendu lui-même pour éviter d'avoir des instances en double). Il est simplement demandé au moteur de rendu de dessiner une liste particulière d'entités, qu'il parcourt, lit les données et utilise son objet fenêtre en interne pour appeler drawavec l'image-objet recherchée pour l'entité.


3
Je ne suis pas familier avec C ++, mais n'y a-t-il pas de cadres d'injection de dépendance confortables pour ce langage?
bgusach

1
Je ne décrirais aucun d'entre eux comme "confortable" et je ne les trouve pas particulièrement utiles en général, mais d'autres peuvent avoir une expérience différente avec eux, c'est donc un bon point pour les aborder.

1
La méthode qu'il décrit comme faisant en sorte que les entités ne les dessinent pas elles-mêmes mais conservent les informations et qu'un seul système gère le dessin de toutes les entités est beaucoup utilisée dans les moteurs de jeu les plus populaires aujourd'hui.
Patrick W. McMahon

1
+1 pour "Le fait qu'il ait l'air dégoûtant devrait vous dire quelque chose: votre design pourrait être dégoûtant."
Shadow503

+1 pour donner à la fois le cas idéal et la réponse pragmatique.

6

Hériter de sf :: RenderWindow

SFML vous encourage en fait à hériter de ses classes.

class GameWindow: public sf::RenderWindow{};

À partir de là, vous créez des fonctions de dessin de membre pour les entités de dessin.

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity);
};

Vous pouvez maintenant le faire:

GameWindow window;
Entity entity;

window.draw(entity);

Vous pouvez même aller plus loin si vos Entités vont tenir leurs propres sprites uniques en faisant hériter Entity de sf :: Sprite.

class Entity: public sf::Sprite{};

Maintenant, sf::RenderWindowil suffit de dessiner des entités, et les entités ont maintenant des fonctions comme setTexture()etsetColor() . L'entité pourrait même utiliser la position de l'image-objet comme sa propre position, vous permettant d'utiliser la setPosition()fonction pour déplacer l'entité ET son image-objet.


Au final , c'est plutôt sympa si vous avez juste:

window.draw(game);

Voici quelques exemples d'implémentations rapides

class GameWindow: public sf::RenderWindow{
 sf::Sprite entitySprite; //assuming your Entities don't need unique sprites.
public:
 void draw(const Entity& entity){
  entitySprite.setPosition(entity.getPosition());
  sf::RenderWindow::draw(entitySprite);
 }
};

OU

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity){
  sf::RenderWindow::draw(entity.getSprite()); //assuming Entities hold their own sprite.
 }
};

3

Vous évitez les singletons dans le développement de jeux de la même manière que vous les évitez dans tous les autres types de développement logiciel: vous passez les dépendances .

Avec cela à l'écart, vous pouvez choisir de passer les dépendances directement en tant que types nus (comme int,Window* , etc.) ou vous pouvez choisir de les passer dans un ou plusieurs types personnalisés emballage (comme EntityInitializationOptions).

La première façon peut devenir ennuyeuse (comme vous l'avez découvert), tandis que la seconde vous permettra de tout passer dans un seul objet et de modifier les champs (et même de spécialiser le type d'options) sans contourner et changer chaque constructeur d'entité. Je pense que la dernière façon est meilleure.


3

Les singletons ne sont pas mauvais. Au lieu de cela, ils sont faciles à abuser. D'autre part, les globaux sont encore plus faciles à abuser et ont beaucoup plus de problèmes.

La seule raison valable de remplacer un singleton par un global est de pacifier les haineux religieux singleton.

Le problème est d'avoir une conception qui inclut des classes dont il n'existe qu'une seule instance globale et qui doivent être accessibles de partout. Cela se désagrège dès que vous finissez par avoir plusieurs instances du singleton, par exemple dans un jeu lorsque vous implémentez un écran partagé, ou dans une application d'entreprise suffisamment grande lorsque vous remarquez qu'un seul enregistreur n'est pas toujours une si bonne idée après tout .

En bout de ligne, si vous avez vraiment une classe où vous avez une seule instance globale que vous ne pouvez pas raisonnablement faire circuler par référence , singleton est souvent l'une des meilleures solutions dans un pool de solutions sous-optimales.


1
Je suis un haineux religieux singleton et je ne considère pas non plus une solution globale. : S
Dan Pantry

1

Injectez les dépendances. Vous pouvez désormais créer différents types de ces dépendances via une usine. Malheureusement, arracher des singletons d'une classe qui les utilise, c'est comme tirer un chat par ses pattes de derrière sur un tapis. Mais si vous les injectez, vous pouvez échanger des implémentations, peut-être à la volée.

RenderSystem(IWindow* window);

Vous pouvez maintenant injecter différents types de fenêtres. Cela vous permet d'écrire des tests sur le RenderSystem avec différents types de fenêtres afin que vous puissiez voir comment votre RenderSystem se cassera ou fonctionnera. Ce n'est pas possible, ou plus difficile, si vous utilisez des singletons directement à l'intérieur de "RenderSystem".

Maintenant, il est plus testable, modulaire et il est également découplé d'une implémentation spécifique. Cela ne dépend que d'une interface, pas d'une implémentation concrète.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.