Wenn mehrere Klassen auf dieselben Daten zugreifen müssen, wo sollten die Daten deklariert werden?

39

Ich habe ein grundlegendes 2D-Tower-Defense-Spiel in C ++.

Jede Map ist eine eigene Klasse, die von GameState erbt. Die Karte delegiert die Logik und den Zeichencode an jedes Objekt im Spiel und legt Daten wie den Kartenpfad fest. Im Pseudocode könnte der Logikabschnitt ungefähr so ​​aussehen:

update():
  for each creep in creeps:
    creep.update()
  for each tower in towers:
    tower.update()
  for each missile in missiles:
    missile.update()

Die Objekte (Creeps, Tower und Missiles) werden in Zeigervektoren gespeichert. Die Türme müssen Zugriff auf den Kriechvektor und den Raketenvektor haben, um neue Raketen zu erstellen und Ziele zu identifizieren.

Die Frage ist: Wo deklariere ich die Vektoren? Sollten sie Mitglieder der Map-Klasse sein und als Argumente an die tower.update () -Funktion übergeben werden? Oder global deklariert? Oder gibt es andere Lösungen, die mir völlig fehlen?

Wenn mehrere Klassen auf dieselben Daten zugreifen müssen, wo sollten die Daten deklariert werden?

Saftig
quelle
1
Globale Mitglieder gelten als "hässlich", sind aber schnell und erleichtern die Entwicklung. Wenn es sich um ein kleines Spiel handelt, ist das kein Problem (IMHO). Sie können auch eine externe Klasse erstellen, die die Logik behandelt ( warum die Türme diese Vektoren benötigen) und Zugriff auf alle Vektoren hat.
Jonathan Connell
-1 Wenn dies mit dem Programmieren von Spielen zusammenhängt, ist es auch so, Pizza zu essen.
Besorgen
9
@Maik: Inwiefern hat Softwaredesign nichts mit Spieleprogrammierung zu tun? Nur weil es auch für andere Bereiche der Programmierung gilt, wird es nicht zum Thema.
BlueRaja - Danny Pflughoeft
@BlueRaja-Listen von Software-Design-Mustern sind besser für SO geeignet, dafür gibt es sie schließlich. GD.SE ist für die Programmierung von Spielen, nicht Software-Design
Maik Semder

Antworten:

53

Wenn Sie eine einzelne Instanz einer Klasse in Ihrem Programm benötigen, bezeichnen wir diese Klasse als Service . Es gibt verschiedene Standardmethoden zum Implementieren von Diensten in Programmen:

  • Globale Variablen . Diese sind am einfachsten zu implementieren, aber das schlechteste Design. Wenn Sie zu viele globale Variablen verwenden, werden Sie schnell feststellen, dass Module geschrieben werden, die zu stark aufeinander angewiesen sind ( starke Kopplung ), was es sehr schwierig macht, dem Logikfluss zu folgen. Globale Variablen sind nicht für Multithreading geeignet. Globale Variablen erschweren die Nachverfolgung der Lebensdauer von Objekten und überfrachten den Namespace. Sie sind jedoch die leistungsstärkste Option, sodass sie manchmal verwendet werden können und sollten, aber sparsam eingesetzt werden.
  • Singletons . Vor 10-15 Jahren waren Singletons das große Designmuster, über das man Bescheid wissen musste. Heutzutage wird jedoch auf sie herabgesehen. Multithreading ist viel einfacher, aber Sie müssen die Verwendung auf jeweils einen Thread beschränken. Dies ist nicht immer das, was Sie möchten. Das Verfolgen von Lebensdauern ist genauso schwierig wie bei globalen Variablen.
    Eine typische Singleton-Klasse sieht ungefähr so ​​aus:

    class MyClass
    {
    private:
        static MyClass* _instance;
        MyClass() {} //private constructor
    
    public:
        static MyClass* getInstance();
        void method();
    };
    
    ...
    
    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
        if(_instance == NULL)
            _instance = new MyClass(); //Not thread-safe version
        return _instance;
    
        //Note that _instance is *never* deleted - 
        //it exists for the entire lifetime of the program!
    }
  • Abhängigkeitsinjektion (DI) . Dies bedeutet nur, dass der Service als Konstruktorparameter übergeben wird. Es muss bereits ein Service vorhanden sein, um ihn einer Klasse zuzuordnen. Daher können sich zwei Services nicht aufeinander verlassen. In 98% der Fälle ist dies das, was Sie möchten (und für die anderen 2% können Sie jederzeit eine setWhatever()Methode erstellen und den Dienst später übergeben) . Aus diesem Grund hat DI nicht die gleichen Kopplungsprobleme wie die anderen Optionen. Es kann mit Multithreading verwendet werden, da jeder Thread einfach eine eigene Instanz jedes Dienstes haben kann (und nur die freigeben kann, die er unbedingt benötigt). Es macht auch Code Unit-testbar, wenn Sie das interessieren.

    Das Problem bei der Abhängigkeitsinjektion besteht darin, dass mehr Speicher belegt wird. Jetzt benötigt jede Instanz einer Klasse Verweise auf jeden Dienst, den sie verwenden wird. Außerdem wird es ärgerlich, wenn Sie zu viele Dienste haben. Es gibt Frameworks, die dieses Problem in anderen Sprachen mindern. Aufgrund der mangelnden Reflektion von C ++ sind DI-Frameworks in C ++ jedoch in der Regel noch aufwändiger als nur manuell.

    //Example of dependency injection
    class Tower
    {
    private:
        MissileCreationService* _missileCreator;
        CreepLocatorService* _creepLocator;
    public:
        Tower(MissileCreationService*, CreepLocatorService*);
    }
    
    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to 
    // add a new service to the Tower constructor, we must add it to the
    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.

    Auf dieser Seite (in der Dokumentation zu Ninject, einem C # DI-Framework) finden Sie ein weiteres Beispiel.

    Die Abhängigkeitsinjektion ist die übliche Lösung für dieses Problem und die Antwort, die Sie auf Fragen wie diese auf StackOverflow.com am häufigsten erhalten. DI ist eine Art von Inversion of Control (IoC).

  • Service Locator . Im Grunde genommen nur eine Klasse, die eine Instanz jedes Dienstes enthält. Sie können dies mit Reflection tun oder einfach jedes Mal, wenn Sie einen neuen Service erstellen möchten, eine neue Instanz hinzufügen. Sie haben immer noch das gleiche Problem wie zuvor - Wie greifen Klassen auf diesen Locator zu? - was auf eine der oben genannten Arten gelöst werden kann, aber jetzt müssen Sie es nur für Ihre ServiceLocatorKlasse tun , anstatt für Dutzende von Diensten. Diese Methode ist auch Unit-testbar, wenn Sie sich für so etwas interessieren.

    Service Locators sind eine weitere Form der Inversion of Control (IoC). In der Regel verfügen Frameworks, die die automatische Abhängigkeitsinjektion ausführen, auch über einen Service-Locator.

    XNA (Microsoft C # -Spielprogrammierframework) enthält einen Dienstfinder; Weitere Informationen finden Sie in dieser Antwort .


Übrigens, meiner Meinung nach sollten die Türme nichts von den Unheimlichen wissen. Wenn Sie nicht vorhaben, einfach die Liste der Creeps für jeden Turm zu durchlaufen, möchten Sie wahrscheinlich eine nicht-triviale Raumaufteilung implementieren . und diese Art von Logik gehört nicht in die Klasse der Türme.

BlueRaja - Danny Pflughoeft
quelle
Kommentare sind nicht für eine längere Diskussion gedacht. Diese Unterhaltung wurde in den Chat verschoben .
Josh
Eine der besten und klarsten Antworten, die ich je gelesen habe. Gut gemacht. Ich dachte, ein Service sollte immer geteilt werden.
Nikos
5

Ich persönlich würde hier Polymorphismus verwenden. Warum einen missileVektor, einen towerVektor und einen Vektor ... creepwenn alle die gleiche Funktion aufrufen? update? Warum nicht einen Vektor von Zeigern auf eine Basisklasse Entityoder GameObject?

Ich finde, ein guter Weg, um zu entwerfen, ist zu denken, ob dies in Bezug auf die Eigentümerschaft Sinn macht. Offensichtlich besitzt ein Turm eine Möglichkeit, sich selbst zu aktualisieren, aber besitzt eine Karte alle Objekte darauf? Wenn Sie sich für global entscheiden, sagen Sie dann, dass nichts die Türme besitzt und schleicht? Global ist normalerweise eine schlechte Lösung - es fördert schlechte Entwurfsmuster, aber es ist viel einfacher, damit zu arbeiten. Überlegen Sie, ob Sie abwägen möchten, ob Sie dies beenden möchten. und "will ich etwas, das ich wiederverwenden kann"?

Ein Weg, dies zu umgehen, ist eine Art Nachrichtensystem. Der towerkann eine Nachricht an den senden map(auf die er Zugriff hat, vielleicht einen Verweis auf seinen Besitzer?), Dass er a getroffen hat creep, und der mapmeldet dann, creepdass er getroffen wurde. Dies ist sehr sauber und trennt Daten.

Eine andere Möglichkeit besteht darin, die Karte selbst nach dem zu durchsuchen, was sie will. Hier können jedoch Probleme mit der Aktualisierungsreihenfolge auftreten.

Die kommunistische Ente
quelle
1
Ihr Vorschlag zum Polymorphismus ist nicht wirklich relevant. Ich habe sie in separaten Vektoren gespeichert, damit ich jeden Typ einzeln durchlaufen kann, z. B. im Zeichnungscode (wobei bestimmte Objekte zuerst gezeichnet werden sollen) oder im Kollisionscode.
Juicy
Für meine Zwecke besitzt die Karte die Entitäten, da die Karte hier analog zu "Ebene" ist. Ich werde Ihre Idee zu Nachrichten prüfen, danke.
Juicy
1
In einem Spiel zählt die Leistung. Vektoren gleicher Objektzeiten haben also eine bessere Referenzlokalität. Außerdem haben polymorphe Objekte mit virtuellen Zeigern eine schlechte Leistung, da sie nicht in die Aktualisierungsschleife eingefügt werden können.
Zan Lynx
0

Dies ist ein Fall, in dem die strikte objektorientierte Programmierung (OOP) zusammenbricht.

Gemäß den Prinzipien von OOP sollten Sie Daten mit verwandtem Verhalten mithilfe von Klassen gruppieren. Aber Sie haben ein Verhalten (Targeting) , die Daten benötigt , die nicht miteinander verwandt sind (Türme und kriecht). In dieser Situation werden viele Programmierer versuchen, das Verhalten mit einem Teil der benötigten Daten zu verknüpfen (z. B. Türme behandeln das Targeting, kennen sich jedoch nicht mit Creeps aus), aber es gibt eine andere Option: Gruppieren Sie das Verhalten nicht mit den Daten.

Anstatt das Targeting-Verhalten zu einer Methode der Tower-Klasse zu machen, machen Sie es zu einer freien Funktion, die Türme und Creeps als Argumente akzeptiert. Dies erfordert möglicherweise, dass mehr Mitglieder, die in den Tower- und Creep-Klassen verbleiben, veröffentlicht werden, und das ist in Ordnung. Das Verstecken von Daten ist nützlich, aber es ist ein Mittel, kein Selbstzweck, und Sie sollten kein Sklave dafür sein. Außerdem sind private Mitglieder nicht die einzige Möglichkeit, den Zugriff auf Daten zu steuern. Wenn Daten nicht an eine Funktion übergeben werden und nicht global sind, ist sie dieser Funktion effektiv verborgen. Wenn Sie mit dieser Technik globale Daten vermeiden können, verbessern Sie möglicherweise die Kapselung.

Ein extremes Beispiel für diesen Ansatz ist die Entitätssystemarchitektur .

Steve S
quelle