Wie verwende ich Singletons bei der Programmierung von C ++ - Engines richtig?

16

Ich weiß, Singletons sind schlecht. Meine alte Spiel-Engine hat ein einzelnes 'Game'-Objekt verwendet, das alles von der Speicherung aller Daten bis zur eigentlichen Spiel-Schleife verarbeitet. Jetzt mache ich einen neuen.

Das Problem ist, etwas in SFML zu zeichnen, das Sie dort verwenden, window.draw(sprite)wo ein Fenster ist sf::RenderWindow. Es gibt 2 Optionen, die ich hier sehe:

  1. Erstelle ein Singleton-Game-Objekt, das jede Entität im Spiel abruft (was ich vorher verwendet habe)
  2. Machen Sie dies zum Konstruktor für Entities: Entity(x, y, window, view, ...etc)(Das ist einfach lächerlich und nervig.)

Was wäre der richtige Weg, um dies zu tun, während der Konstruktor einer Entität auf x und y beschränkt bleibt?

Ich könnte versuchen, alles, was ich in der Hauptspielschleife mache, im Auge zu behalten und das Sprite einfach manuell in der Spielschleife zu zeichnen, aber auch das scheint chaotisch zu sein, und ich möchte auch die absolute vollständige Kontrolle über eine gesamte Zeichenfunktion für die Entität.

Akkumulator
quelle
1
Sie könnten das Fenster als Argument der 'Render'-Funktion übergeben.
Dari
25
Singletons sind nicht schlecht! Sie können nützlich und manchmal notwendig sein (natürlich ist es umstritten).
ExOfDe
3
Fühlen Sie sich frei, Singletons durch einfache Globals zu ersetzen. Es macht keinen Sinn, global benötigte Ressourcen "on demand" zu erstellen oder weiterzugeben. Für Entitäten können Sie jedoch eine "Level" -Klasse verwenden, um bestimmte Dinge zu speichern, die für alle relevant sind.
snake5
Ich deklariere mein Fenster und andere Abhängigkeiten in meinem Hauptfenster und habe dann Zeiger in meinen anderen Klassen.
KaareZ
1
@JAB Leicht zu beheben mit manueller Initialisierung von main (). Durch die verzögerte Initialisierung geschieht dies zu einem unbekannten Zeitpunkt, was für Kernsysteme niemals eine gute Idee ist.
snake5

Antworten:

3

Speichern Sie nur die Daten, die zum Rendern des Sprites in jeder Entität benötigt werden. Rufen Sie sie dann von der Entität ab und übergeben Sie sie zum Rendern an das Fenster. Sie müssen keine Fenster speichern oder Daten in Entitäten anzeigen.

Sie könnten eine Spiel- oder Engine- Klasse der obersten Ebene haben , die eine Level- Klasse (die alle derzeit verwendeten Entitäten enthält ) und eine Renderer- Klasse (die das Fenster, die Ansicht und alles andere zum Rendern enthält).

Die Spielaktualisierungsschleife in Ihrer höchsten Klasse könnte also so aussehen:

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());
}
Blauer Zauberer
quelle
3
Es gibt nichts Ideales an einem Singleton. Warum die Implementierungsinterna veröffentlichen, wenn Sie nicht müssen? Warum schreiben Logger::getInstance().Log(...)statt nur Log(...)? Warum die Klasse zufällig initialisieren, wenn Sie gefragt werden, ob Sie sie nur einmal manuell ausführen können? Eine globale Funktion, die auf statische Globale verweist, ist viel einfacher zu erstellen und zu verwenden.
snake5
@ snake5 Singletons auf Stack Exchange zu rechtfertigen ist wie mit Hitler zu sympathisieren.
Willy Goat
30

Der einfache Ansatz besteht darin, das, was früher Singleton<T>global war , einfach zu machen T. Globale Unternehmen haben ebenfalls Probleme, aber sie stellen keinen Haufen zusätzlicher Arbeit und zusätzlichen Code dar, um eine triviale Einschränkung durchzusetzen. Dies ist im Grunde die einzige Lösung, bei der der Entity-Konstruktor nicht (möglicherweise) berührt wird.

Der schwierigere, aber möglicherweise bessere Ansatz besteht darin , Ihre Abhängigkeiten an den Ort weiterzuleiten, an dem Sie sie benötigen . Ja, dies kann bedeuten, dass Sie eine Window *an eine Reihe von Objekten (wie Ihre Entität) auf eine Weise übergeben, die grob aussieht. Die Tatsache, dass es grob aussieht, sollte Ihnen etwas sagen: Ihr Design könnte grob sein.

Der Grund, warum dies schwieriger ist (abgesehen davon, dass mehr Eingaben erforderlich sind), ist, dass dies häufig zu einer Umgestaltung Ihrer Schnittstellen führt, so dass weniger Klassen auf Blattebene das benötigen, was Sie "übergeben" müssen. Dies führt dazu, dass ein Großteil der Hässlichkeit, die mit der Übergabe Ihres Renderers an alles verbunden ist, verschwindet und die allgemeine Wartbarkeit Ihres Codes verbessert wird, indem die Anzahl der Abhängigkeiten und Kopplungen verringert wird, deren Ausmaß Sie sehr deutlich gemacht haben, indem Sie die Abhängigkeiten als Parameter verwendet haben . Wenn es sich bei den Abhängigkeiten um Singletons oder Globals handelte, war es weniger offensichtlich, wie stark Ihre Systeme miteinander verbunden waren.

Aber es ist möglicherweise ein großes Unterfangen. Es kann geradezu schmerzhaft sein, es einem System nachträglich anzutun. Es mag für Sie weitaus pragmatischer sein, Ihr System vorerst mit dem Singleton in Ruhe zu lassen (insbesondere, wenn Sie versuchen, ein Spiel zu veröffentlichen, das ansonsten einwandfrei funktioniert; es ist den Spielern im Allgemeinen egal, ob Sie es haben oder nicht) ein Singleton oder vier drin).

Wenn Sie versuchen möchten, dies mit Ihrem vorhandenen Design zu tun, müssen Sie möglicherweise viel mehr Details zu Ihrer aktuellen Implementierung veröffentlichen, da es keine allgemeine Checkliste für diese Änderungen gibt. Oder kommen Sie und diskutieren Sie im Chat .

Nach dem, was Sie gepostet haben, besteht ein großer Schritt in Richtung "kein Singleton" darin, zu vermeiden, dass Ihre Entitäten Zugriff auf das Fenster oder die Ansicht haben müssen. Es deutet darauf hin, dass sie sich selbst zeichnen, und Sie müssen keine Entitäten haben, die sich selbst zeichnen . Sie können eine Methode übernehmen , wo die Einheiten nur die Informationen enthalten , die würde erlaubenSie müssen von einem externen System gezeichnet werden (mit Fenster- und Ansichtsreferenzen). Die Entität legt nur ihre Position und das zu verwendende Sprite offen (oder eine Art Verweis auf das Sprite, wenn Sie die tatsächlichen Sprites im Renderer selbst zwischenspeichern möchten, um doppelte Instanzen zu vermeiden). Dem Renderer wird einfach gesagt, dass er eine bestimmte Liste von Entitäten zeichnen soll, die er durchläuft, aus denen er die Daten liest und deren intern gehaltenes Fensterobjekt verwendet, drawum das nach der Entität gesuchte Sprite aufzurufen .

Gemeinschaft
quelle
3
Ich bin nicht mit C ++ vertraut, aber gibt es für diese Sprache keine komfortablen Frameworks für die Abhängigkeitsinjektion?
BGUSACH
1
Ich würde keines von ihnen als "bequem" bezeichnen, und ich finde sie im Allgemeinen nicht besonders nützlich, aber andere haben möglicherweise andere Erfahrungen mit ihnen, so dass es ein guter Grund ist, sie zur Sprache zu bringen.
1
Die Methode, die er so beschreibt, dass Entitäten sie nicht selbst zeichnen, sondern die Informationen enthalten, und ein einziges System, das alle Entitäten zeichnet, wird heutzutage in den meisten populären Spiele-Engines häufig verwendet.
Patrick W. McMahon
1
+1 für "Die Tatsache, dass es grob aussieht, sollte Ihnen etwas sagen: Ihr Design könnte grob sein."
Shadow503
+1 für den Idealfall und die pragmatische Antwort.
6

Erben Sie von sf :: RenderWindow

SFML ermutigt Sie tatsächlich, von seinen Klassen zu erben.

class GameWindow: public sf::RenderWindow{};

Von hier aus erstellen Sie Elementzeichnungsfunktionen zum Zeichnen von Objekten.

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

Jetzt können Sie dies tun:

GameWindow window;
Entity entity;

window.draw(entity);

Sie können sogar noch einen Schritt weiter gehen, wenn Ihre Entities ihre eigenen Sprites behalten, indem Sie Entity von sf :: Sprite erben lassen.

class Entity: public sf::Sprite{};

Jetzt sf::RenderWindowkönnen nur noch Entities gezeichnet werden und Entities haben nun Funktionen wie setTexture()und setColor(). Das Objekt könnte sogar die Position des Sprites als seine eigene Position verwenden, sodass Sie die setPosition()Funktion sowohl zum Bewegen des Objekts als auch seines Sprites verwenden können.


Am Ende ist es ziemlich schön, wenn Sie nur haben:

window.draw(game);

Im Folgenden finden Sie einige kurze Beispiele für Implementierungen

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);
 }
};

ODER

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

Sie vermeiden Singletons in der Spieleentwicklung genauso, wie Sie sie in jeder anderen Art der Softwareentwicklung vermeiden: Sie übergeben die Abhängigkeiten .

Mit diesem aus dem Weg, können Sie die Abhängigkeiten direkt als nackte Typen passieren (wie int, Window*etc) , oder Sie können sie wählen , in einer oder mehr benutzerdefinierten passieren Wrapper - Typen (wie EntityInitializationOptions).

Der erste Weg kann ärgerlich werden (wie Sie herausgefunden haben), während der zweite es Ihnen ermöglicht, alles in einem Objekt zu übergeben und die Felder zu ändern (und sogar den Optionstyp zu spezialisieren), ohne jeden Entitätskonstruktor zu umgehen und zu ändern. Ich denke, der letztere Weg ist besser.

TC
quelle
3

Singletons sind nicht schlecht. Stattdessen sind sie leicht zu missbrauchen. Auf der anderen Seite sind Globals noch leichter zu missbrauchen und haben noch viel mehr Probleme.

Der einzig gültige Grund, einen Singleton durch einen globalen zu ersetzen, ist die Befriedung der religiösen Singleton-Hasser.

Das Problem besteht darin, ein Design zu haben, das Klassen enthält, von denen es nur eine einzige globale Instanz gibt und auf die von überall aus zugegriffen werden muss. Dies bricht auseinander, sobald Sie mehrere Instanzen des Singletons haben, zum Beispiel in einem Spiel, in dem Sie einen geteilten Bildschirm implementieren, oder in einer ausreichend großen Unternehmensanwendung, in der Sie feststellen, dass ein einzelner Logger nicht immer eine so gute Idee ist .

Unterm Strich ist Singleton oft eine der besseren Lösungen in einem Pool suboptimaler Lösungen , wenn Sie wirklich eine Klasse haben, in der Sie eine einzige globale Instanz haben, die Sie nicht vernünftigerweise als Referenz weitergeben können.

Peter - Unban Robert Harvey
quelle
1
Ich bin ein religiöser Singleton-Hasser und ich halte eine globale Lösung auch nicht. : S
Dan Pantry
1

Abhängigkeiten injizieren. Dies hat den Vorteil, dass Sie jetzt verschiedene Arten dieser Abhängigkeiten über eine Factory erstellen können. Leider ist das Herausreißen von Singletons aus einer Klasse, in der sie verwendet werden, so, als würde man eine Katze mit den Hinterbeinen über einen Teppich ziehen. Aber wenn Sie sie injizieren, können Sie Implementierungen austauschen, vielleicht im laufenden Betrieb.

RenderSystem(IWindow* window);

Jetzt können Sie verschiedene Arten von Fenstern injizieren. Auf diese Weise können Sie Tests mit verschiedenen Fenstertypen gegen das RenderSystem schreiben, um zu sehen, wie Ihr RenderSystem beschädigt oder funktioniert. Dies ist nicht möglich oder schwieriger, wenn Sie Singletons direkt in "RenderSystem" verwenden.

Jetzt ist es testbarer, modularer und auch von einer bestimmten Implementierung entkoppelt. Es hängt nur von einer Schnittstelle ab, nicht von einer konkreten Implementierung.

Todd
quelle