Klassenschachtelung für Operatorüberladung ohne Dynamikverlust



  • Hallo zusammen,

    leider ist mir kein besserer Titel eingefallen. Möchte gern Folgendes machen:

    Für ein Spiel stehen eine Reihe verschiedener Ressourcen zur Verfügung. Diese können einerseits pro Spieler gehortet werden, andererseits kosten Aktionen im Spiel auch wieder eine bestimmte Menge an Ressourcen.

    Meine Idee wäre nun für jede Ressourcenart eine eigene spezielle Klasse zu spendieren, die eine gemeinsame Überklasse haben. Allerdings unterscheiden sich die einzelnen Ressourcen nur in ihrem Typ voneinander (derzeit ist noch keine Verbindung mit z.B. den jeweiligen Feldern auf der Spielfläche vorgesehen, die diese Ressourcen repräsentieren), daher ist der Ansatz mit den spezialisierten Klassen ggf. Overkill, oder?

    Für die Operatorüberladung würde ich nun eine Klasse Amount oder so machen, die z.B. für einen Spieler die Summe aller seiner Ressourcen enthält (also die Summe für jede einzelne Ressourcenart). Ein Gebäude hätte dann ebenfalls ein Objekt Amount, welches seine Kosten repräsentiert. Beim Bau könnten diese dann beim Amount des Spielers abgezogen werden (nach vorheriger Prüfung). Hier käme dann die Operatorüberladung ins Spiel!

    In Amount müssten dann aber für Additionen und Subtraktionen immer die einzelnen Ressourcen gefunden werden.

    Also z.B.:

    Ressourcenklassen für:

    • Holz
    • Wasser
    • ...

    Klasse Amount mit je einem Member für je eine Ressource. Bei Überladung von + oder - können dann jeweils Amount-Objekte miteinander addiert oder voneinander subtrahiert werden. Da hierbei aber immer nur z.B. Wasser mit Wasser verrechnet werden darf muss man in der Klasse Amount natürlich auch die einzelnen Ressourcen finden. Dies könnte nun wieder durch definierte Konstanten geschehen (je eine für jeden Ressourcentyp), mit dennen man die entsprechenden Ressourcenobjekte in einer HashMap findet. Allerdings verliert man dadurch wieder Dynamik (für jede neue Ressourcenart muss man den Code wieder anpassen)!

    Habt ihr hierzu evtl. ne geeignetere Idee? Hoffe, ich konnte es enigermaßen erklären.

    Dank euch schon mal!

    Ciao



  • Reth schrieb:

    Meine Idee wäre nun für jede Ressourcenart eine eigene spezielle Klasse zu spendieren, die eine gemeinsame Überklasse haben. Allerdings unterscheiden sich die einzelnen Ressourcen nur in ihrem Typ voneinander (derzeit ist noch keine Verbindung mit z.B. den jeweiligen Feldern auf der Spielfläche vorgesehen, die diese Ressourcen repräsentieren), daher ist der Ansatz mit den spezialisierten Klassen ggf. Overkill, oder?

    Vor diesem Problem steht man ab und zu. Die Entscheidung hängt vor allem davon ab, wie stark sich die einzelnen Ressourcentypen unterscheiden und ob es Kontexte gibt, in denen eigene Klassen geeigneter wären. Ich weiss auch nicht, wofür du Ressourcen abgesehen zum Zählen noch benötigst, eventuell wäre ein Template (mit Ressourcentyp als Templateparameter) eine weitere Möglichkeit.

    Ansonsten könnte ein statischer Ansatz so aussehen:

    namespace Resource
    {
        enum Type
        {
            Wood,
            Water,
            ...
            Count // Anzahl Ressourcentypen
        };
    }
    
    class Amount
    {
        private:
            // std::tr1::array ist eine intelligente Array-Klasse
            std::tr1::array<int, Resource::Count> myResources;
    
        public:
            Amount()
            : myResources() // initialisiere alles mit 0
            {
            }
    
            Amount& operator+= (const Amount& rhs)
            {
                for (size_t i = 0; i < Resource::Count; ++i)
                    myResources[i] += rhs.myResources[i];
    
                return *this;
            }
    };
    
    Amount operator+ (const Amount& lhs, const Amount& rhs)
    {
        Amount tmp(lhs);
        tmp += rhs;
        return tmp;
    }
    

    Hier hast du Typsicherheit zur Kompilierzeit und maximale Geschwindigkeit, dafür musst du bei einer neuen Ressource das enum ändern. Die Frage ist, wie dynamisch das Ganze sein soll?

    Eine Möglichkeit, bei der die Ressourcentypen erst zur Laufzeit bekannt sein müssen:

    class Amount
    {
        private:
            typedef std::map<std::string, int> ResourceMap;
            ResourceMap myResources;
    
            static void InitResources(ResourceMap& resources);
    
      public:
            Amount()
            {
                InitResources(myResources);
            }
    
            Amount& operator+= (const Amount& rhs)
            {
                assert(/* Keys von myResources und rhs.myResources gleich */);
    
                // Addiere alle Ressourcen (du kannst auch eine Schleife nehmen). 
                // Für std::transform() siehe www.cplusplus.com
                std::transform(myResources.begin(), myResources.end(), rhs.myResources.begin(),
                    myResources.begin(), std::plus<int>());
    
                return *this;
            }
    };
    
    Amount operator+ (const Amount& lhs, const Amount& rhs); // wie gehabt
    

    Hier sollte InitResources() dafür sorgen, dass jedes Amount -Objekt mit den gleichen Keys initialisiert wird. Am besten lädt es beim ersten Aufruf irgendwelche dynamischen Daten (z.B. aus einer Datei) und benutzt später für jede Instanz die gleichen Daten zur Initialisierung. So kannst du für Konsistenz sorgen. Falls du Ressourcen auch sonst noch benötigst, bietet es sich an, InitResources aus der Klasse zu nehmen. Eventuell sogar eine eigene Klasse zu schreiben, die alle Ressourcentypen verwaltet.

    Übrigens würde ich die Klasse vielleicht eher ResourceAmount statt Amount nennen, letzteres kommt mir etwas zu allgemein vor.



  • Danke für die Tips!

    Nexus schrieb:

    Vor diesem Problem steht man ab und zu. Die Entscheidung hängt vor allem davon ab, wie stark sich die einzelnen Ressourcentypen unterscheiden und ob es Kontexte gibt, in denen eigene Klassen geeigneter wären. Ich weiss auch nicht, wofür du Ressourcen abgesehen zum Zählen noch benötigst, eventuell wäre ein Template (mit Ressourcentyp als Templateparameter) eine weitere Möglichkeit.

    Aber die benötige ich ja auch nur, wenn sich Operationen in der Templateklasse in Abhängigkeit vom Ressourcentyp anders verhalten, oder?

    Ansonsten könnte ein statischer Ansatz so aussehen:

    namespace Resource
    {
        enum Type
        {
            Wood,
            Water,
            ...
            Count // Anzahl Ressourcentypen
        };
    }
    
    class Amount
    {
        private:
            // std::tr1::array ist eine intelligente Array-Klasse
            std::tr1::array<int, Resource::Count> myResources;
    
        public:
            Amount()
            : myResources() // initialisiere alles mit 0
            {
            }
    
            Amount& operator+= (const Amount& rhs)
            {
                for (size_t i = 0; i < Resource::Count; ++i)
                    myResources[i] += rhs.myResources[i];
                
                return *this;
            }
    };
    
    Amount operator+ (const Amount& lhs, const Amount& rhs)
    {
        Amount tmp(lhs);
        tmp += rhs;
        return tmp;
    }
    

    Hier hast du Typsicherheit zur Kompilierzeit und maximale Geschwindigkeit, dafür musst du bei einer neuen Ressource das enum ändern. Die Frage ist, wie dynamisch das Ganze sein soll?

    Das gefällt mir! Habe aber was Enums und Namespaces angeht noch keine Erfahrung. Ich weiss, das ist nur ein Codebeispiel, aber für mich zum Verständnis ein paar Fragen:
    Muss denn der Count nicht außerhalb der Enum stehen und bereits mit einem Wert initialisiert werden? Und warum wird += in der Amount-Klasse überladen, + aber außerhalb? Werden denn die Enum-Werte überhaupt benötigt? Welche Aufgabe übernimmt denn hier der Namespace, außer für Eindeutigkeit zu sorgen?

    Die einzelnen Ressourcen stammen bei mir von unterschiedlichen Spielfeldteilen (einzelnen Hexagons). Diese Spielfeldteile haben einen Typ, der auch die Ressource repräsentiert. An dieser Stelle könnte ich mir vorstellen, die Enum-Werte einzusetzen.

    Beim Klassennamen gebe ich Dir recht, Amount ist zu allgemein!



  • Reth schrieb:

    Aber die benötige ich ja auch nur, wenn sich Operationen in der Templateklasse in Abhängigkeit vom Ressourcentyp anders verhalten, oder?

    Nein, Templates haben den genau entgegengesetzten Anwendungszweck – man möchte verschiedene Dinge einheitlich ansprechen. Natürlich gibt es Techniken wie Templatespezialisierung, aber der Grundgedanke der Abstraktion bleibt. Bei grundverschiedenen Implementierungen wäre man mit einzelnen Klassen besser beraten.

    Reth schrieb:

    Muss denn der Count nicht außerhalb der Enum stehen und bereits mit einem Wert initialisiert werden?

    Nein. Für die Werte des enum s gilt, dass wenn keine Initialisierer angegeben sind, der erste Enumerator dem integralen Wert 0 und jeder darauf folgende dem Wert des Vorgängers + 1 entspricht. Count bezeichnet also gerade die Anzahl der Ressourcentypen.

    Beispiel mit 3 Ressourcen:

    enum Type
    {
        Wood,  // Wert 0
        Water, // Wert 1
        Gold,  // Wert 2
        Count  // Wert 3 == Anzahl der Ressourcen
    };
    

    Reth schrieb:

    Und warum wird += in der Amount-Klasse überladen, + aber außerhalb?

    Weil operator+= Zugriff auf private Member braucht, operator+ aber nicht (wird ja über den öffentlichen operator+= implementiert). Ausserdem sollten symmetrische Funktionen wie eine Addition tendenziell eher global sein, weil sie z.B. auch eine implizite Konvertierung des ersten Operanden ermöglichen. Ausführlicheres findest du im Artikel über Operatorüberladung.

    Reth schrieb:

    Werden denn die Enum-Werte überhaupt benötigt?

    Gute Frage. Du hast damit mehr Möglichkeiten, z.B. sowas:

    class ResourceAmount
    {
        public:
            int GetAmount(Resource::Type type) const
            {
                 return myResources[type];
            }
    
            void SetAmount(Resource::Type type, int amount)
            {
                 myResources[type] = amount;
            }
    
            void AddAmount(Resource::Type type, int increase)
            {
                 myResources[type] += amount;
            }
    
        // Rest bleibt
    };
    
    // Etwas zu bauen kostet 245 Gold und 20 Holz
    ResourceAmount costs;
    costs.SetAmount(Resource::Gold, 245);
    costs.SetAmount(Resource::Wood, 20);
    

    Aber das nur als Beispiel. Du solltest nicht leichtsinnig zu viele öffentliche Methoden anbieten. Wenn du z.B. eine Ressourcenmenge nur bei der Initialisierung (also im Konstruktor) festlegen willst und nachher nicht mehr verändern möchtest, solltest du z.B. die beiden modifizierenden Funktionen weglassen.

    Reth schrieb:

    Welche Aufgabe übernimmt denn hier der Namespace, außer für Eindeutigkeit zu sorgen?

    Den Namensraum habe ich nur genommen, weil ein enum seine Enumeratoren (also Wood , Water etc.) über den umliegenden Scope ausleert. Ich finde es schöner, wenn die Zugehörigkeit angegeben wird, also Resource::Wood statt nur Wood . Du verhinderst damit auch Namenskonflikte. Besonders wenn du viele enum s und auch Klassen hast, zeigt sich in dieser Gliederung ein Vorteil. Im Bezug auf Bezeichner im globalen Scope solltest du immer skeptisch sein.

    Reth schrieb:

    Die einzelnen Ressourcen stammen bei mir von unterschiedlichen Spielfeldteilen (einzelnen Hexagons). Diese Spielfeldteile haben einen Typ, der auch die Ressource repräsentiert. An dieser Stelle könnte ich mir vorstellen, die Enum-Werte einzusetzen.

    Meinst du die Deklaration des enum s oder nur die Übergabe der Werte?



  • Also so wie ich das sehe würde ich dir empfehlen ein Strategiemuster um zu setzen und zwar delegiert das auf die einzelnen typen das funktioniert super gut. Die implementierung würde am anfang etwas dauern aber die erweiterung hinterher bei neuen Typen wäre super schnell 🙂

    würde dir gerne bspl. nennen aber das wäre zu ausführlich 🙂



  • Besucher1987 schrieb:

    Also so wie ich das sehe würde ich dir empfehlen ein Strategiemuster um zu setzen und zwar delegiert das auf die einzelnen typen das funktioniert super gut. Die implementierung würde am anfang etwas dauern aber die erweiterung hinterher bei neuen Typen wäre super schnell 🙂

    Ist grundsätzlich keine schlechte Idee, aber dürfte hier etwas Overkill sein. Reths Ressourcentypen sind ja keine eigenen Klassen. Aber Alternativvorschläge sind immer gut. 😉

    Falls sich das enum -Konzept bewährt, ist Erweiterbarkeit auch kein Problem. Man muss lediglich einen neuen Enumerator hinzufügen, dank Count passt sich sogar die Arraygrösse an.



  • Nexus schrieb:

    Du hast damit mehr Möglichkeiten, z.B. sowas:

    class ResourceAmount
    {
        public:
            int GetAmount(Resource::Type type) const
            {
                 return myResources[type];
            }
    
            void SetAmount(Resource::Type type, int amount)
            {
                 myResources[type] = amount;
            }
    
            void AddAmount(Resource::Type type, int increase)
            {
                 myResources[type] += amount;
            }
    
        // Rest bleibt
    };
    
    // Etwas zu bauen kostet 245 Gold und 20 Holz
    ResourceAmount costs;
    costs.SetAmount(Resource::Gold, 245);
    costs.SetAmount(Resource::Wood, 20);
    

    Aber das nur als Beispiel. Du solltest nicht leichtsinnig zu viele öffentliche Methoden anbieten. Wenn du z.B. eine Ressourcenmenge nur bei der Initialisierung (also im Konstruktor) festlegen willst und nachher nicht mehr verändern möchtest, solltest du z.B. die beiden modifizierenden Funktionen weglassen.

    Da würde sich die Aufteilung in Ressourcen und Kosten anbieten. Kosten stehen fest und können nicht mehr verändert werden, Ressourcen dagegen, wie von Dir beschrieben. Kosten würden z.B. bei Gebäuden fest stehen (als Teil davon, oder im Konstruktor übergeben). Hier würde ich eine ganz normale Klassenbeziehung herstellen, wobei die "fachliche" Semantik ja eher nicht so einer Vererbung entspricht (Kosten als übergeordnete Klasse mit weniger öffentlichen Methoden, Ressourcen davon abgeleitet).

    Nexus schrieb:

    Reth schrieb:

    Die einzelnen Ressourcen stammen bei mir von unterschiedlichen Spielfeldteilen (einzelnen Hexagons). Diese Spielfeldteile haben einen Typ, der auch die Ressource repräsentiert. An dieser Stelle könnte ich mir vorstellen, die Enum-Werte einzusetzen.

    Meinst du die Deklaration des enum s oder nur die Übergabe der Werte?

    Hier meine ich die Enum-Werte. Ein Hexagon wird bei seiner Erzeugung mit einem der Enum-Werte versehen, der seinen Ressourcentyp kennzeichnet. Bisher habe ich so etwas schon, nur ohne Enum. Habe mir entsprechende Konstanten definiert.

    Besucher1987 schrieb:

    Also so wie ich das sehe würde ich dir empfehlen ein Strategiemuster um zu setzen und zwar delegiert das auf die einzelnen typen das funktioniert super gut. Die implementierung würde am anfang etwas dauern aber die erweiterung hinterher bei neuen Typen wäre super schnell 🙂

    würde dir gerne bspl. nennen aber das wäre zu ausführlich 🙂

    Das würde mich auf alle Fälle interessieren. Kann entweder gern einen neuen Thread dazu aufmachen, oder vielleicht hast Du ja ein paar gute Links zu dem Thema?



  • Reth schrieb:

    Da würde sich die Aufteilung in Ressourcen und Kosten anbieten. Kosten stehen fest und können nicht mehr verändert werden, Ressourcen dagegen, wie von Dir beschrieben. Kosten würden z.B. bei Gebäuden fest stehen (als Teil davon, oder im Konstruktor übergeben). Hier würde ich eine ganz normale Klassenbeziehung herstellen, wobei die "fachliche" Semantik ja eher nicht so einer Vererbung entspricht (Kosten als übergeordnete Klasse mit weniger öffentlichen Methoden, Ressourcen davon abgeleitet).

    Entweder nur eine Klasse und je nachdem const -qualifizieren. Oder zwei Klassen, eine für Ressourcen mit allen Möglichkeiten und eine eingeschränkte für Kosten, die ein Ressourcen-Objekt als Member hat (keine Vererbung!). Vorteil bei zwei Klassen ist, dass du Typsicherheit strikter durchziehen kannst. Sodass z.B. nur Ressourcen - Kosten , aber nicht Kosten - Ressourcen oder Ressourcen - Ressourcen möglich ist. Eventuell lohnt es sich sogar, eine weitere Klasse für den Ertrag als Gegenstück zu Kosten einzuführen. Sodass dann Ressourcen + Ertrag möglich wird. Die Implementierung der Kosten- und Ertrag-Klassen wäre trivial, da du lediglich Funktionen an die interne Ressourcen-Instanz weiterleiten müsstest. Dann würde ich auch gar keine Operatoren + und - mit zwei Ressourcen-Typen anbieten.

    Reth schrieb:

    Hier meine ich die Enum-Werte. Ein Hexagon wird bei seiner Erzeugung mit einem der Enum-Werte versehen, der seinen Ressourcentyp kennzeichnet. Bisher habe ich so etwas schon, nur ohne Enum. Habe mir entsprechende Konstanten definiert.

    Ich würde enum nehmen. Das ist typsicherer als int oder andere Ganzzahlen. Falls du #define genommen hast: Das solltest du in C++ eigentlich nie für Konstanten verwenden.

    Reth schrieb:

    Das würde mich auf alle Fälle interessieren. Kann entweder gern einen neuen Thread dazu aufmachen, oder vielleicht hast Du ja ein paar gute Links zu dem Thema?

    Vielleicht schaust du dir mal den Wikipedia-Artikel an. Wenn dann noch Fragen bleiben, kannst du einen neuen Thread eröffnen.



  • Eine Frage noch: Mir steht das intelligente Array nicht zur Verfügung, dann müsste doch auch Folgendes gehen (obwohl hier implizit angenommen wird, dass der Datentyp der Enum-Werte int ist):

    class RessourceAmount
    {
        private:
        int myResources[Ressources::Count] = {0};
    ...
    }
    

    Nexus schrieb:

    Entweder nur eine Klasse und je nachdem const -qualifizieren. Oder zwei Klassen, eine für Ressourcen mit allen Möglichkeiten und eine eingeschränkte für Kosten, die ein Ressourcen-Objekt als Member hat (keine Vererbung!). Vorteil bei zwei Klassen ist, dass du Typsicherheit strikter durchziehen kannst. Sodass z.B. nur Ressourcen - Kosten , aber nicht Kosten - Ressourcen oder Ressourcen - Ressourcen möglich ist. Eventuell lohnt es sich sogar, eine weitere Klasse für den Ertrag als Gegenstück zu Kosten einzuführen. Sodass dann Ressourcen + Ertrag möglich wird. Die Implementierung der Kosten- und Ertrag-Klassen wäre trivial, da du lediglich Funktionen an die interne Ressourcen-Instanz weiterleiten müsstest. Dann würde ich auch gar keine Operatoren + und - mit zwei Ressourcen-Typen anbieten.

    Klingt gut. Allerdings muss mann dann doch die Methoden für die arithmetischen Operationen einzeln ausprogrammieren? Oder kann man das immer noch mit Operatorüberladung erledigen (in dem man + und - für die erlaubten Typkombinationen überlädt)?
    Wie kann ich denn in so einem Fall (einzelne Klassen) Kosten (z.B. für Errichtung und Reparatur) fest im Konstruktor der unterschiedlichen Gebäudeklassen mitgeben? (Das Gleiche gilt für Erträge, die man den einzelnen Feldern mitgeben kann, wobei hier immer nur der Wert für die Ressource des jeweiligen Feldes auf 1 steht, alle anderen auf 0).

    Nexus schrieb:

    Vielleicht schaust du dir mal den Wikipedia-Artikel an. Wenn dann noch Fragen bleiben, kannst du einen neuen Thread eröffnen.

    Vielen Dank für den Link!



  • Reth schrieb:

    Eine Frage noch: Mir steht das intelligente Array nicht zur Verfügung, dann müsste doch auch Folgendes gehen (obwohl hier implizit angenommen wird, dass der Datentyp der Enum-Werte int ist):

    class RessourceAmount
    {
        private:
        int myResources[Ressources::Count] = {0};
    ...
    }
    

    Die Initialisierung darfst du nicht in der Klassendefinition selber lassen. Benutze für Null-Initialisierung eines Arrays die Initialisierungsliste des Konstruktors (so wie in meinem 2. Post). Aber ansonsten sollte es gehen. Der Typ des Arrays ist int , weil die Anzahl Ressourcen eine Ganzzahl ist. Man könnte auch unsigned int nehmen, falls keine negativen Werte erlaubt sind. Das "intelligente" Array std::tr1::array hast du, wenn deine Standardbibliotheks-Implementierung TR1 unterstützt. Es bietet z.B. Bereichsprüfungen im Debug-Modus und ein Iterator-Interface (ähnlich zu std::vector ), oder eine size() -Methode.

    Übrigens heisst "Ressource" auf englisch "resource", also mit einem "s". 😉

    Reth schrieb:

    Klingt gut. Allerdings muss mann dann doch die Methoden für die arithmetischen Operationen einzeln ausprogrammieren? Oder kann man das immer noch mit Operatorüberladung erledigen (in dem man + und - für die erlaubten Typkombinationen überlädt)?

    Also + und - kannst du sicher über += und -= implementieren (das sollte man eigentlich immer tun). Diese sehen dann z.B. so aus:

    // Ressource + Ertrag
    ResourceAmount& ResourceAmount::operator+= (const Income& rhs)
    {
        for (size_t i = 0; i < Resource::Count; ++i)
            myResources[i] += /* Ressource an Stelle i von rhs */;
    
        return *this;
    }
    
    // Ressource - Kosten
    ResourceAmount& ResourceAmount::operator-= (const Costs& rhs);
    

    Reth schrieb:

    Wie kann ich denn in so einem Fall (einzelne Klassen) Kosten (z.B. für Errichtung und Reparatur) fest im Konstruktor der unterschiedlichen Gebäudeklassen mitgeben? (Das Gleiche gilt für Erträge, die man den einzelnen Feldern mitgeben kann, wobei hier immer nur der Wert für die Ressource des jeweiligen Feldes auf 1 steht, alle anderen auf 0).

    Deklariere einen Parameter der entsprechenden Klasse.

    Building::Building(const Costs& costs)
    : myCosts(costs) // myCosts ist Member vom Typ Costs
    {
    }
    
    Costs towerCosts; // mit Werten füllen
    Building tower1(towerCosts);
    

    Wobei vielleicht eine Indirektion keine schlechte Idee wäre. Also dass es irgendwo eine Tabelle gibt, in der für jedes Gebäude die Kosten stehen. Vielleicht was in der Art:

    Costs GetBuildingCosts(BuildingType type);
    

    Aber das ist dir überlassen. Mit der Zeit wirst du merken, was sich bewährt und was nicht. 🙂



  • Nexus schrieb:

    Reth schrieb:

    Wie kann ich denn in so einem Fall (einzelne Klassen) Kosten (z.B. für Errichtung und Reparatur) fest im Konstruktor der unterschiedlichen Gebäudeklassen mitgeben? (Das Gleiche gilt für Erträge, die man den einzelnen Feldern mitgeben kann, wobei hier immer nur der Wert für die Ressource des jeweiligen Feldes auf 1 steht, alle anderen auf 0).

    Deklariere einen Parameter der entsprechenden Klasse.

    Building::Building(const Costs& costs)
    : myCosts(costs) // myCosts ist Member vom Typ Costs
    {
    }
    
    Costs towerCosts; // mit Werten füllen
    Building tower1(towerCosts);
    

    Hier frage ich mich, ob es denn keine Möglichkeit gibt, das Ganze ohne externes Cost-Objekt zu füllen, so dass im Konstruktor von Building automatisch immer die Costs-Objekte für Erstellung und Reparatur gefüllt werden (vielleicht noch ohne die ganzen Setter-Aufrufe)?

    Generell hatte ich es bisher bei den Resourcen immer umgekehrt: Eine Klasse Resource, die man mit Typ und Anzahl initialisieren kann. Dazu dann Methoden, zum Ausgeben der Anzahl und des Typs (getter) und zum Erhöhen bzw. Erniedrigen der Anzahl. Allerdings werde ich so eine Klasse mit diesem Ansatz hier eher nicht mehr benötigen.



  • Reth schrieb:

    Hier frage ich mich, ob es denn keine Möglichkeit gibt, das Ganze ohne externes Cost-Objekt zu füllen, so dass im Konstruktor von Building automatisch immer die Costs-Objekte für Erstellung und Reparatur gefüllt werden (vielleicht noch ohne die ganzen Setter-Aufrufe)?

    Wahrscheinlich ist es wie angedeutet nicht sinnvoll, in jeder Gebäude-Instanz die Bau- und Reparaturkosten zu speichern. Denn diese sind ja für die gleichen Gebäudetypen gleich, oder? Also wenn du z.B. 10 Türme hast, musst du die Kosten nicht 10 Mal speichern.

    Vielleicht ist die erwähnte Funktion angebracht, die für jeden Gebäudetyp die Kosten zurückgibt. Wie unterscheidest du Gebäudetypen? Auch enum s? Oder eigene Klassen?

    Es gibt grundsätzlich viele Möglichkeiten. Und ich kenne dein genaues Design zu wenig. Zum Beispiel auch, wie viele Ressourcentypen es gibt. Wenn es z.B. nur 3 oder so sind, kannst du einen ResourceAmount -Konstruktor einrichten, der für jeden Typ die initiale Anzahl Ressourcen festlegt. Bei vielen Ressourcen wird das aber zu umständlich und dann ist ein Setter einfacher.



  • Nexus schrieb:

    Wahrscheinlich ist es wie angedeutet nicht sinnvoll, in jeder Gebäude-Instanz die Bau- und Reparaturkosten zu speichern. Denn diese sind ja für die gleichen Gebäudetypen gleich, oder? Also wenn du z.B. 10 Türme hast, musst du die Kosten nicht 10 Mal speichern.

    Das ist auch nicht meine Intention. Allerdings dachte ich mir, dass man bei feststehenden Werten vllt. den Aufruf der Setter vermeiden kann!?

    Nexus schrieb:

    Vielleicht ist die erwähnte Funktion angebracht, die für jeden Gebäudetyp die Kosten zurückgibt. Wie unterscheidest du Gebäudetypen? Auch enum s? Oder eigene Klassen?

    Bisher nur eine Klasse und 2 Gebäudetypen, die noch über #define-Konstanten unterschieden werden. Hier wird dann zukünftig auch ein ähnliches enum-Konstrukt werkeln dürfen! Evtl. werde ich für die Gebäude auch 2 spezielle Subklassen anlegen, falls sie zu sehr auseinander driften, was das Verhalten anbelangt.
    Für die Kosten und generell dachte ich, dass ich evtl. einen Gebäudemanager mache, der dann auch als Factory agiert (nur er kann Gebäude instanziieren). In diesem kann ich dann auch einmalig die Kosten für Errichtung und Reperatur anlegen um beim Erstellen der Gebäude mitgeben. Nach dem Instanziieren von Gebäuden werden dann deren Referenzen herausgeben, so dass andere Klassen/Objekte damit arbeiten können.

    Factories in C++ habe ich bisher mit privaten Konstruktoren der Klassen designed, deren Objekte fabriziert werden sollen. Die Factory-Klassen selbst sind für diese dann als Friend-Klassen angelegt. In den Factories werden dann die Instanzen in Listen/Maps verwaltet, sowie beim Aufruf des Factory-Destrukturs wieder zerstört.



  • Factory klingt gut. Aber ich würde vielleicht die Reparaturkosten doch nicht in den Gebäude-Objekten selbst speichern, sondern an einem zentralen Ort. Vielleicht bietet sich hier gerade die Factory an.

    Zur Festlegung der Kosten an einem Ort wäre z.B. sowas möglich:

    void BuildingFactory::InitializeCosts() // static
    {
        SetConstructionCosts(Building::Tower)
            .SetAmount(Resource::Gold, 245)
            .SetAmount(Resource::Wood, 20);
    
        SetRepairCosts(Building::Bridge)
            .SetAmount(Resource::Wood, 50)
            .SetAmount(Resource::Iron, 35);
    }
    

    Realisierbar wäre das folgendermassen:

    class BuildingFactory
    {
        public:
            static void InitializeCosts();
    
        private:
            static ResourceAmount& SetConstructionCosts(Building::Type type)
            {
                return myConstructionCosts[type];
            }
    
            static ResourceAmount& SetRepairCosts(Building::Type type)
            {
                return myRepairCosts[type];
            }
    
        private:
            static std::tr1::array<ResourceAmount, Building::Count> myConstructionCosts;
            static std::tr1::array<ResourceAmount, Building::Count> myRepairCosts;
    };
    

    Dann müsste der Setter von ResourceAmount eine Referenz auf sich selbst zurückgeben ( return *this; ). Wenn das in der Klasse unangebracht ist, kannst du eine abgeleitete Klasse mit dieser Zusatzfunktion erstellen. Aber das nur so als Idee. 😉

    Edit: "Construction costs" ist hier für Baukosten vielleicht der bessere Begriff als "building costs".



  • Nexus schrieb:

    Factory klingt gut. Aber ich würde vielleicht die Reparaturkosten doch nicht in den Gebäude-Objekten selbst speichern, sondern an einem zentralen Ort.

    Zur Festlegung der Kosten an einem Ort wäre z.B. sowas möglich:

    BuildingFactory::SetBuildingCosts(Building::Tower)
        .SetAmount(Resource::Gold, 245)
        .SetAmount(Resource::Wood, 20);
    
    BuildingFactory::SetRepairCosts(Building::Bridge)
        .SetAmount(Resource::Wood, 50)
        .SetAmount(Resource::Iron, 35);
    

    Bin leider auch noch C++-Anfänger (hat man sicher gemerkt). Die Schreibweise hier (zweimal den Setter aufrufen, ohne das referenzierende Objekt wieder vor den Punkt zu setzen) ist mir fremd (oder ist das nur zur Verkürzung des Codebeispiels gedacht?

    Nexus schrieb:

    Realisierbar wäre das folgendermassen:

    class BuildingFactory
    {
        public:
            static ResourceAmount& SetBuildingCosts(Building::Type type)
            {
                return myBuildingCosts[type];
            }
    
            static ResourceAmount& SetRepairCosts(Building::Type type)
            {
                return myRepairCosts[type];
            }
    
        private:
            static std::tr1::array<ResourceAmount, Building::Count> myBuildingCosts;
            static std::tr1::array<ResourceAmount, Building::Count> myRepairCosts;
    };
    

    Aber das nur so als Idee. 😉

    Die Setter irritieren mich in dem Bsp. etwas! Da wird doch letztendlich nix gesetzt, sondern es werden die Kosten für Erstellung/Reperatur des jeweiligen Gebäudetyps zurückgegeben!?

    Was ist denn der Vorteil der zentralen Instanz außerhalb der Factory/des Managers, da dieser ja nur für Gebäude zuständig ist?



  • Reth schrieb:

    Bin leider auch noch C++-Anfänger (hat man sicher gemerkt).

    Naja, so wenig Ahnung scheint du mir nicht zu haben. 😉
    Aber tut mir leid, wenn ich dich teilweise etwas überfordere. Wahrscheinlich sind viele Dinge neu, und ein anderer Programmierer als ich würde vieles unterschiedlich lösen. Frag auf jeden Fall ruhig nach. Und wenn dir eine Möglichkeit von mir nicht passt, sei ruhig kritisch! Du musst nicht alles wie von mir vorgeschlagen lösen, fasse meine Posts als Tipps und Anregungen auf.

    Reth schrieb:

    Die Schreibweise hier (zweimal den Setter aufrufen, ohne das referenzierende Objekt wieder vor den Punkt zu setzen) ist mir fremd (oder ist das nur zur Verkürzung des Codebeispiels gedacht?

    Nein, das ist gültiger C++-Code. Der Setter müsste dann eine Referenz auf das Objekt selbst zurückgeben (hab ich im 1. Edit noch geschrieben). So kannst du beliebig viele Aufrufe in einer Anweisung verketten.

    ResourceAmount& ResourceAmount::SetAmount(Resource::Type type, int amount)
    {
        ... // setze Anzahl
        return *this;
    }
    

    Reth schrieb:

    Die Setter irritieren mich in dem Bsp. etwas! Da wird doch letztendlich nix gesetzt, sondern es werden die Kosten für Erstellung/Reperatur des jeweiligen Gebäudetyps zurückgegeben!?

    Ja, aber die Rückgabe erfolgt als Referenz. Das heisst, man greift auf das originale Objekt (das im Array) zu. Wenn man dann den Setter aufruft, verändert man das entsprechende Bau- oder Reparaturkosten-Element im Array.

    Reth schrieb:

    Was ist denn der Vorteil der zentralen Instanz außerhalb der Factory/des Managers, da dieser ja nur für Gebäude zuständig ist?

    Das verstehe ich jetzt nicht ganz. Wieso ausserhalb? Ich habe die Kostenfestlegung ja zentral (als statische Methode) in der Factory implementiert.



  • So, nachdem das Forum gestern fast den ganzen Tag leidern nicht erreichbar war funkioniert es heut zum Glück wieder!

    Nexus schrieb:

    Naja, so wenig Ahnung scheint du mir nicht zu haben. 😉

    Habe einige Jahre Erfahrung in der Java-/JEE-Webprogrammierung. In Sachen C++ stehe ich noch ziemlich vorm Anfang (meiner Meinung nach, und dazu öfters aufm Schlauch). Mein Java-Background macht es mir da gerade schwer. Der C++-Code, den ich fabriziere ist äußerst Java-Like!

    Nexus schrieb:

    Aber tut mir leid, wenn ich dich teilweise etwas überfordere. Wahrscheinlich sind viele Dinge neu, und ein anderer Programmierer als ich würde vieles unterschiedlich lösen. Frag auf jeden Fall ruhig nach. Und wenn dir eine Möglichkeit von mir nicht passt, sei ruhig kritisch! Du musst nicht alles wie von mir vorgeschlagen lösen, fasse meine Posts als Tipps und Anregungen auf.

    Keine Angst! Fasse das Ganze immer als Anregungen, Tips usw. auf und wenn ich was nicht verstehe frage auch nach! An dieser Stelle nochmals Vielen Dank an Dich (und alle anderen Antworter)!

    Nexus schrieb:

    Reth schrieb:

    Die Schreibweise hier (zweimal den Setter aufrufen, ohne das referenzierende Objekt wieder vor den Punkt zu setzen) ist mir fremd (oder ist das nur zur Verkürzung des Codebeispiels gedacht?

    Nein, das ist gültiger C++-Code. Der Setter müsste dann eine Referenz auf das Objekt selbst zurückgeben (hab ich im 1. Edit noch geschrieben). So kannst du beliebig viele Aufrufe in einer Anweisung verketten.

    Ist das ne übliche Vorgehensweise in C++?

    Nexus schrieb:

    Reth schrieb:

    Die Setter irritieren mich in dem Bsp. etwas! Da wird doch letztendlich nix gesetzt, sondern es werden die Kosten für Erstellung/Reperatur des jeweiligen Gebäudetyps zurückgegeben!?

    Ja, aber die Rückgabe erfolgt als Referenz. Das heisst, man greift auf das originale Objekt (das im Array) zu. Wenn man dann den Setter aufruft, verändert man das entsprechende Bau- oder Reparaturkosten-Element im Array.

    Hier schlägt wieder mein Java-Hintergrund zu und das dortige Bean-Konzept. Ein Getter ist dabei immer eine Methode, die mir ein Member der Klasse zurückliefert. Auch wenn ich dieses dann manipulieren kann wird aus der zurückliefernden Methode kein Setter. Dies Entspricht auch Deinem Bsp. hier:

    static ResourceAmount& SetConstructionCosts(Building::Type type)
    {
        return myConstructionCosts[type];
    }
    

    Dabei wäre die Methode

    SetConstructionCosts(Building::Type type)
    

    eigentlich der Getter. Auf dem Objekt, dessen Referenz zurückgegeben wird werden dann die Setter ausgeführt.

    Nexus schrieb:

    Reth schrieb:

    Was ist denn der Vorteil der zentralen Instanz außerhalb der Factory/des Managers, da dieser ja nur für Gebäude zuständig ist?

    Das verstehe ich jetzt nicht ganz. Wieso ausserhalb? Ich habe die Kostenfestlegung ja zentral (als statische Methode) in der Factory implementiert.

    Stimmt. Aber gäbe es noch eine Möglichkeit in der Factory die Kosten schon für die einzelnen Gebäudetypen fertig vorzuhalten (also ein Cost-Objekt so zu erstellen, dass man einfach Werte wie für ein Array initialisiert, ohne Konstruktorparameter oder Setter-Aufrufe) und bei Erstellen der Gebäudeinstanzen einfach ne Referenz darauf in den Konstruktor zu übergeben? (Schwer zu beschreiben)



  • Reth schrieb:

    Ist das ne übliche Vorgehensweise in C++?

    Das ist kein typisches C++-Idiom oder so, aber ich halte die Verkettung teilweise für eine elegante Möglichkeit. Allerdings hast du dieses Vorgehen in C++ bestimmt schon angetroffen: Die Stream-Operatoren << und >> tun nämlich genau das Gleiche. Man kanns auch übertreiben, ich würde z.B. nie jeden Setter eine Referenz auf das Objekt zurückliefern lassen, weil es eben meist keinen Mehrwert bietet. Sowas finde ich hässlich:

    Player player;
    player.SetPosition(25, 30).SetVelocity(3, 4).SetHitpoints(100).SetEquipment(XY);
    

    Reth schrieb:

    Hier schlägt wieder mein Java-Hintergrund zu und das dortige Bean-Konzept. Ein Getter ist dabei immer eine Methode, die mir ein Member der Klasse zurückliefert. Auch wenn ich dieses dann manipulieren kann wird aus der zurückliefernden Methode kein Setter. [...] Auf dem Objekt, dessen Referenz zurückgegeben wird werden dann die Setter ausgeführt.

    Ja, "Set" bei SetBuildingCosts() und SetRepairCosts() ist vielleicht ein unglücklicher Name, du hast Recht. Allerdings fände ich "Get" auch nicht sehr treffend, da man eine Referenz zurückbekommt und Schreibzugriff hat. Vielleicht wäre "Modify" oder so passender.

    In Java verhält sich das Ganze etwas anders, da es keine Const-Correctness gibt und man zurückbekommene Referenzen immer beschreiben kann, sofern keine Immutable-Klasse im Spiel ist. Dort kopiert man auch viel eher, um eine Veränderung des Originalobjekts zu verhindern. Ich selbst halte es in C++ relativ strikt: Referenzen auf nicht-konstante Objekte werden so wenig wie möglich zurückgegeben, und die entsprechende Funktion heisst dann normalerweise nicht GetXY() , sondern operator[] oder irgendwas Passendes.

    Was SetAmount() betrifft, da wird tatsächlich direkt etwas verändert, das würde ich so lassen. Die Referenzrückgabe hat eine nebensächliche syntaktische Funktion (Verkettung) und steht als Funktionalität daher nicht im Vordergrund.

    Reth schrieb:

    Stimmt. Aber gäbe es noch eine Möglichkeit in der Factory die Kosten schon für die einzelnen Gebäudetypen fertig vorzuhalten (also ein Cost-Objekt so zu erstellen, dass man einfach Werte wie für ein Array initialisiert, ohne Konstruktorparameter oder Setter-Aufrufe) und bei Erstellen der Gebäudeinstanzen einfach ne Referenz darauf in den Konstruktor zu übergeben? (Schwer zu beschreiben)

    Ja. Das Prinzip ist momentan insofern das Gleiche, als die Kosten in der Factory vorberechnet und später nachgeschaut werden. Nur dass ich eben Setter verwende, um die Eigenschaften festzulegen. Das Problem bei array-ähnlichen Initialisierungen besteht darin, dass diese schnell unübersichtlich werden und du nicht mehr weisst, was wofür steht. Bei vielen Ressourcentypen hast du zudem einen Grossteil an nutzlosen Informationen drin (nämlich jene Ressourcentypen, von denen nichts benötigt wird). Dafür hast du aber mehr Code. Kurzes Beispiel, soll die Problematik nur verdeutlichen (kein 1:1 Code):

    int towerCosts[] = {245, 0, 20, 0, 35, 0, 0, 0}; // ??
    // vs.
    Costs towerCosts;
    towerCosts.SetBuildingCosts(Resource::Gold, 245);
    towerCosts.SetBuildingCosts(Resource::Wood, 20);
    towerCosts.SetRepairCosts(Resource::Gold, 35);
    

    Was die Übergabe betrifft, du kannst natürlich auch eine Const-Referenz auf die Kosten an den Konstruktor des Gebäudes übergeben. Das alles hängt davon ab, wem du die Aufgabe für Gebäudekonstruktionen und Ressourcenverwaltung übertragen willst. Beispielsweise wäre es auch möglich, dass du keine Factory hättest und ein Gebäude beim Konstruieren selbst prüft, ob genügend Ressourcen vorhanden sind. Allerdings müsstest du die Konstruktion im Fehlerfall mit einer Exception abbrechen, während die Factory mehr Möglichkeiten bietet (z.B. Nullzeiger zurückgeben). Generell halte ich es für eine gute Idee, wenn eine separate Klasse hierfür zuständig ist. So kannst du prüfen, ob genügend Ressourcen vorhanden sind, bevor du überhaupt mit dem Bau beginnst.

    Wie sieht eigentlich deine momentane Schnittstelle zu vorhandenen Ressourcen aus? Würdest du ein ResourceAmount -Objekt an die Factory-Baumethode übergeben und dort prüfen?



  • Nexus schrieb:

    Ich selbst halte es in C++ relativ strikt: Referenzen auf nicht-konstante Objekte werden so wenig wie möglich zurückgegeben, und die entsprechende Funktion heisst dann normalerweise nicht GetXY() , sondern operator[] oder irgendwas Passendes.

    Das mit operator[] verstehe ich in diesem Zusammenhang nicht. Wie machst Du es dann, wenn Du änderbare Objekte hast? Gibst Du nur Zeiger darauf zurück anstelle von Referenzen (z.B. wenn die Gebäudefactory ein neues Gebäude erzeugt hat, muss sich dessen Zustand ja auch von außen ändern lassen - es sei denn die Factory ist mehr als nur ne Fabrik, nämlich ein Manager, der auch den Zustand der produzierten Gebäude ändern kann!)?

    Nexus schrieb:

    Das Problem bei array-ähnlichen Initialisierungen besteht darin, dass diese schnell unübersichtlich werden und du nicht mehr weisst, was wofür steht. Bei vielen Ressourcentypen hast du zudem einen Grossteil an nutzlosen Informationen drin (nämlich jene Ressourcentypen, von denen nichts benötigt wird). Dafür hast du aber mehr Code. Kurzes Beispiel, soll die Problematik nur verdeutlichen (kein 1:1 Code):

    int towerCosts[] = {245, 0, 20, 0, 35, 0, 0, 0}; // ??
    // vs.
    Costs towerCosts;
    towerCosts.SetBuildingCosts(Resource::Gold, 245);
    towerCosts.SetBuildingCosts(Resource::Wood, 20);
    towerCosts.SetRepairCosts(Resource::Gold, 35);
    

    Dachte hier eher an ne Möglichkeit, das Costs-Objekt so ähnlich wie ein Array zu initialisieren, aber das scheidet wohl aus!

    Nexus schrieb:

    Was die Übergabe betrifft, du kannst natürlich auch eine Const-Referenz auf die Kosten an den Konstruktor des Gebäudes übergeben. Das alles hängt davon ab, wem du die Aufgabe für Gebäudekonstruktionen und Ressourcenverwaltung übertragen willst.

    So war meine Idee: Jedes Gebäudeobjekt bekommt ne konstante Referenz auf seine Reparaturkosten (die Baukosten muss es ja nicht kennen, die werden vor seiner Erzeugung geprüft).

    Nexus schrieb:

    Generell halte ich es für eine gute Idee, wenn eine separate Klasse hierfür zuständig ist. So kannst du prüfen, ob genügend Ressourcen vorhanden sind, bevor du überhaupt mit dem Bau beginnst.

    Wie sieht eigentlich deine momentane Schnittstelle zu vorhandenen Ressourcen aus? Würdest du ein ResourceAmount -Objekt an die Factory-Baumethode übergeben und dort prüfen?

    Nein, aber ich finde die Idee super (und einleuchtend!)!



  • Reth schrieb:

    Das mit operator[] verstehe ich in diesem Zusammenhang nicht.

    Ich meine das speziell für Container, damit sie wie Arrays benutzt werden können. Selbst implementiere ich sowas selten.

    T& std::vector<T>::operator[] (size_t index);
    

    Reth schrieb:

    Wie machst Du es dann, wenn Du änderbare Objekte hast? Gibst Du nur Zeiger darauf zurück anstelle von Referenzen (z.B. wenn die Gebäudefactory ein neues Gebäude erzeugt hat, muss sich dessen Zustand ja auch von außen ändern lassen - es sei denn die Factory ist mehr als nur ne Fabrik, nämlich ein Manager, der auch den Zustand der produzierten Gebäude ändern kann!)?

    Das Problem ist, dass die Rückgabe von Non-Const-Referenzen in vielen Fällen die Kapselung verletzt und die Aufteilung in Getter und Setter hinfällig macht, weil man Objekte auch über den Getter verändern kann.

    Eine Factory-Methode ist ja was anderes, da sie Objekte erzeugt. Diese Objekte gehören nicht zu privaten Implementierungsdetails, die gekapselt sein müssen. Allerdings ist "Get" bei einer Factory ohnehin unangebracht. Gebräuchlich ist z.B. "Create". Als Rückgabetyp kann man bei kopierbaren Objekte direkt eine Kopie nehmen, oder einen besitzenden Zeiger, oder einen Smart Pointer wie std::auto_ptr .

    Falls du an eine Factory denkst, die die erzeugten Objekte intern abspeichert, dann ist die Rückgabe von Referenzen durchaus angebracht. Ich bezog mich hauptsächlich auf Methoden, welche direkt Attribute (Membervariablen) der Klasse zurückliefern. Dort hat ein Getter mit einer Non-Const-Referenz als Rückgabetyp keinen grossen Vorteil gegenüber einem öffentlichen Attribut.

    Reth schrieb:

    Das Dachte hier eher an ne Möglichkeit, das Costs-Objekt so ähnlich wie ein Array zu initialisieren, aber das scheidet wohl aus!

    Wie stellst du dir das konkret vor? Möglich ist es grundsätzlich schon...

    Reth schrieb:

    So war meine Idee: Jedes Gebäudeobjekt bekommt ne konstante Referenz auf seine Reparaturkosten (die Baukosten muss es ja nicht kennen, die werden vor seiner Erzeugung geprüft).

    Ja, das kannst du so machen. Ich würde einfach dafür sorgen, dass die Schnittstelle bei Bauen und Reparieren ähnlich ist. Vielleicht bietet es sich auch an, für die Reparatur trotzdem die Factory zu verwenden – semantisch macht es nämlich mehr Sinn, wenn sich ein Gebäude nicht selbst repariert. Dann hast du eben eine Fabrik und Reparaturwerkstatt in einem. 😉

    Übrigens, der Begriff "konstante Referenz" wird von vielen Leuten falsch verwendet. Eine Referenz in C++ ist immer konstant – worauf sich das "konstant" bezieht, ist das referenzierte Objekt. Deshalb spreche ich meistens von "Const-Referenz" oder "Referenz auf const ".


Anmelden zum Antworten