Kompilierzeitprobleme mit modularer Programmierung



  • Nexus schrieb:

    Wie macht ihr das eigentlich? #include -Direktiven fast nur in CPP-Dateien?

    Sollte nicht durch saubere Trennung der Klassen mit Schnittstellen vermieden werden, dass du in Headerdateien andere Header einbindest?

    In der Headerdatei sollte doch dann nur die Klasse als Schnittstelle stehen und bei der Implementation der Klassenmethoden (in einer cpp Datei) sollte dann auf andere Header verwiesen werden. Sonst bindest du den anderen Header immer mit ein, wenn du eine bestimmte Headerdatei einbindest, die andere Headerdateien includiert.

    Also so weit ich weiß, sollte man #include-Direktiven möglichst nur in cpp-Dateien verwenden. Sonst mal das Design überdenken...



  • MFK schrieb:

    Hast du dir das Pimpl-Idiom mal angesehen?

    Ich hab es mir einmal angeschaut (auf en.wikibooks.org), aber irgendwie bin ich nicht so richtig davon überzeugt. Erstens müsste ich dann für jede Klasse noch eine separate mit dem Body erzeugen, und zweitens ist es gerade bei meinen Anwendungen nicht so extrem tragisch, wenn man als Anwender etwas sieht, das eigentlich zur Implementation gehört. Aber der Vorteil der Kompilierungsverkürzung ist natürlich schon vorhanden.

    MFK schrieb:

    Das klingt danach, als hättest du angefangen zu programmieren, bevor das Design fertig war. Das sollte nicht "relativ häufig" vorkommen, sondern ziemlich selten. Wenn du natürlich Planung und Implementierung "gleichzeitig" machst, solltest du dich über erhöhte Kompilierungszeiten nicht wundern.

    Nein, das grobe Design stand eigentlich schon vorher. Ich weiss nicht, ob du selber schon Spiele programmiert hast, aber es kann schon vorkommen, dass man mitten in der Programmierung merkt, dass man noch ein neues Spielelement hinzufügen will, welches das halbe Spielkonzept über den Haufen wirft. Aber du hast Recht, häufig geschieht das eigentlich nicht.

    MFK schrieb:

    Erklär doch mal, warum du glaubst, dass der Code überhaupt nennenswert größer werden sollte. Von Inline-Funktionen mal abgesehen, steckt doch jede Funktion in genau einer Übersetzungseinheit.

    Naja, ich war mir nur nicht sicher, weil ich mich eben nicht so gut auskenne bei der Codegenerierung und Linkage... Aber dann brauche ich mir ja keine Gedanken darum zu machen.

    #include in Header schrieb:

    Also so weit ich weiß, sollte man #include-Direktiven möglichst nur in cpp-Dateien verwenden. Sonst mal das Design überdenken...

    Ich meine nur, wenn man z.B. eine Klasse hat, die als Member Objekte von anderen Klassen besitzt, dann kann man die benötigten Klassen doch gerade in dieser Headerdatei einbinden, oder?

    Aber die ganze Thematik scheint mir doch schwerwiegender, als ich zu Beginn dachte. Früher hatte ich immer eine CPP-Datei, und alles andere in Header ausgelagert. Da brauchte ich mir weder Gedanken um Linker-Mehrfachdefinitionen, noch um erhöhte Kompilierzeiten, spezielles Einbinden von Headern oder Pimpl-Idioms zu machen. Klar war es vielleicht nicht das beste Design, aber immerhin kam man noch zum Programmieren 😉

    Es ist nicht so, dass ich den Vorteil der modular-objektorientierten Programmierung nicht sehe. Ich kann durchaus nachvollziehen, dass man bei wirklich grossen Projekten (z.B. in der Softwareentwicklung) grossen Nutzen davon hat, aber mir persönlich scheint es eher mehr Probleme zu bereiten...



  • Trotzdem kann ich bei dir nicht nachvollziehen, warum du bei modularer Programmierung größere Kompilierzeiten hast als bei einer Datei.
    Denn bei Änderung an einer Datei müßte ja der ganze Code neu kompiliert werden (und dies jedesmal, anstatt nur einige wenige kleinere Dateien).

    Welchen Compiler setzt du denn ein, evtl. solltest du mal - wie vorgeschlagen - vorkompilierte Headerdateien einsetzen?

    Vllt. könntest du ja mal dein Klassendesign exemplarisch hier aufzeigen, d.h. die Header und deren Includes.

    Aus

    Das führt dazu, dass ich für jede kleine Klasse, die mehrfach vorkommen kann, einen eigenen Kopierkonstruktor, Destruktor und Zuweisungsoperator implementieren muss - was meiner Ansicht nach nicht gerade optimal ist.

    schließe ich, daß du noch einigen Nachholbedarf beim Design und deren Anwendung in C++ hast, denn das Verwenden von eingebetteten Objekten führt ja zu Kopien, so daß keine Eindeutigkeit der Objekte mehr gewährleistet ist.

    Ein gutes Design führt oftmals insgesamt zu kleinerem Code (trotz expliziter Implementierung bestimmter Methoden).

    Da ich 5 Jahre in einer Computerspiele-Firma gearbeitet habe, kenne ich mich mit den Problematiken bei großen Spieleprojekten sehr gut aus.



  • Th69 schrieb:

    Trotzdem kann ich bei dir nicht nachvollziehen, warum du bei modularer Programmierung größere Kompilierzeiten hast als bei einer Datei.
    Denn bei Änderung an einer Datei müßte ja der ganze Code neu kompiliert werden (und dies jedesmal, anstatt nur einige wenige kleinere Dateien).

    Ja, aber wie gesagt gibt es bei mir relativ viele Module, die voneinander abhängen (z.B. Spieler - Gegner, Spieler - Game, etc.)... Siehe dazu unten das Design.

    Th89 schrieb:

    Vllt. könntest du ja mal dein Klassendesign exemplarisch hier aufzeigen, d.h. die Header und deren Includes.

    Gut möglich, dass das Design so nicht gut ist, aber damit hab ich eben noch nicht so grosse Erfahrung (bzw. das ist mein erstes Spiel, wo ich richtig modular programmiere und aufs Design achte).

    Also es handelt sich um ein Jump'n'Run. Ich muss noch erwähnen, dass die Klassen Enemy und Weapon noch nicht implementiert sind, ich hab sie jedoch so im Design vorgesehen.

    Zuerst mal die Vererbungshierarchie:

    [b]Master[/b] (verwaltet alles)
            im Moment noch alles statische Member,
             soll jedoch noch Singleton werden
            sozusagen global, von überall zugreifbar
    
               [b]Surface[/b] (Oberfläche)
                         |
       +----------+------+-------+--------+
       |          |              |        |
    [b]MainMenu[/b]   [b]InfoMenu[/b]   ..    [b]Game[/b]    [b]Editor[/b]
    
             [b]Object[/b] (Spielelemente,                      [b]Tile[/b]
          die Kollision berücksichtigen)           (Bausteine der Welt)
                     |
      +--------------+---------------+
      |              |               |
    [b]Player[/b]         [b]Enemy[/b]          [b]Weapon[/b]
                            (abgefeuerte Waffen)
    

    Zusätzlich noch einige weniger relevante Klassen.

    Hier die Membertypen von wichtigen Klassen:

    Master
    --> Surface* (Basisklassenzeiger (Polymorphie))
    --> Grafikklassen (Fenster, Event, etc.), die überall benötigt werden
    
    Game
    --> Player
    --> Enemy
    --> Weapon
    --> Tile
    

    Nun ist es so, dass Kollisionsabfragen in Object passieren, d.h. da werden schon mal die Klassen Tile und Game (aktuelle Oberfläche) benötigt. Bei den Klassen Player , Enemy und Weapon muss ich wahrscheinlich eine Dreiecksabhängigkeit einrichten, weil sich diese Klassen jeweils gegenseitig beeinträchtigen können.
    Game selber benötigt natürlich alle Klassen, die Typen der Member sind.
    Was ein Problem darstellt, ist die Tatsache, dass z.B. Player indirekt über die Basisklasse Object auf Game Zugriff haben muss, um z.B. Kollisionsabfragen mit den Tiles zu handhaben. Ich habe das mit einem Zeiger gelöst, jedoch hab ich momentan das Problem, dass eine Vorwärtsdeklaration nicht ausreicht, weil ein dynamic_cast<Game*>(Surface*) durchgeführt werden muss. Aber das lässt sich evtl. dadurch lösen, dass beim Erstellen der Player -Instanz ein Zeiger auf Game übergeben wird. Was wiederum nicht geht, weil im Konstruktor von Game ja auch Player konstruiert wird, und der this -Zeiger von Game zu diesem Zeitpunkt noch auf die Basisklasse Surface verweist... 🙄

    Grundsätzlich wäre fast alles mit Vorwärtsdeklarationen und Zeigern als Member zu lösen. Was ich jedoch unnötig finde, weil auf ich eigene Speicherverwaltung lieber verzichte, wo sichs vermeiden lässt (und wie gesagt auf The Big Three). Oder ist das die gängige Variante (Pimpl scheint mir arg übertrieben für mein verhältnismässig kleines Spiel)?

    Dazu:

    Th89 schrieb:

    Aus

    Das führt dazu, dass ich für jede kleine Klasse, die mehrfach vorkommen kann, einen eigenen Kopierkonstruktor, Destruktor und Zuweisungsoperator implementieren muss - was meiner Ansicht nach nicht gerade optimal ist.

    schließe ich, daß du noch einigen Nachholbedarf beim Design und deren Anwendung in C++ hast

    Das will ich auch nicht bestreiten 😉
    Aber was ist denn daran so falsch?
    Und was meinst du genau mit folgender Aussage?

    Th89 schrieb:

    denn das Verwenden von eingebetteten Objekten führt ja zu Kopien, so daß keine Eindeutigkeit der Objekte mehr gewährleistet ist.



  • Nexus schrieb:

    Was Forward Declarations betrifft: Da hab ich auch einige drin. Jedoch impliziert das immer, dass man als Member nur Zeiger oder Referenzen haben kann. Das führt dazu, dass ich für jede kleine Klasse, die mehrfach vorkommen kann, einen eigenen Kopierkonstruktor, Destruktor und Zuweisungsoperator implementieren muss - was meiner Ansicht nach nicht gerade optimal ist.

    Warum sollte man wegen Forward Declaration mehr Kopierkonstruktor, Destruktor und Zuweisungsoperator implementieren müssen? Thema Smartpointer...

    shared_ptr<T> kann für einen unvollständigen Typ T verwendet werden:

    class X; // forward declaration
    shared_ptr<X> pX; // "X *"
    

    Zum Dereferenzieren und Freigeben von pX muss X aber vollständig bekannt sein - boost prüft das und wirft andernfalls eine Compile-Fehlermeldung.

    Nexus schrieb:

    Wie macht ihr das eigentlich? #include -Direktiven fast nur in CPP-Dateien?

    In meinen privaten Projekten: Ja.
    In Projekten an der Arbeit: Kommt auf den Codestil an, auch wenn ich immer versuchen werde, soviel wie möglich in den Source zu ziehen.

    cu André



  • Nexus schrieb:

    Master (verwaltet alles)
    im Moment noch alles statische Member,
    soll jedoch noch Singleton werden
    sozusagen global, von überall zugreifbar

    Hmm. Das würde ich schon mal anderst lösen. Mach da lieber ein Objekt draus, dass du dann direkt in der main initialisierst und hald einen Zeiger, oder eine Referenz an die restlichen Klassen übergibst, damit du darauf zugreifen kannst.

    Fände ich sauberer, als da mit Singletons zu arbeiten.


  • Mod

    drakon schrieb:

    Nexus schrieb:

    Master (verwaltet alles)
    im Moment noch alles statische Member,
    soll jedoch noch Singleton werden
    sozusagen global, von überall zugreifbar

    Hmm. Das würde ich schon mal anderst lösen. Mach da lieber ein Objekt draus, dass du dann direkt in der main initialisierst und hald einen Zeiger, oder eine Referenz an die restlichen Klassen übergibst, damit du darauf zugreifen kannst.

    Fände ich sauberer, als da mit Singletons zu arbeiten.

    Das hättest du anders zitieren sollen. So macht es den Eindruck, als wollte Nexus sich als Dichter betätigen.



  • camper schrieb:

    Das hättest du anders zitieren sollen. So macht es den Eindruck, als wollte Nexus sich als Dichter betätigen.

    😃
    Entweder ich verstehe dich falsch, oder du hast ein Smylie vergessen. 😉



  • drakon schrieb:

    Fände ich sauberer, als da mit Singletons zu arbeiten.

    Ich finde das hört sich schon ziemlich nach Singleton an. Überall Zeiger rumzureichen finde ich da viel viel ekliger.

    Nexus schrieb:

    Grundsätzlich wäre fast alles mit Vorwärtsdeklarationen und Zeigern als Member zu lösen.

    Mach das bloß nicht. Ist imo unsinning, genauso mit Smartpointern. Ich würde nie "reine" Membervariablen gegen Kompilierzeit eintauschen.


  • Mod

    drakon schrieb:

    camper schrieb:

    Das hättest du anders zitieren sollen. So macht es den Eindruck, als wollte Nexus sich als Dichter betätigen.

    😃
    Entweder ich verstehe dich falsch, oder du hast ein Smylie vergessen. 😉

    Du irrst, indem du recht hast.



  • camper schrieb:

    drakon schrieb:

    camper schrieb:

    Das hättest du anders zitieren sollen. So macht es den Eindruck, als wollte Nexus sich als Dichter betätigen.

    😃
    Entweder ich verstehe dich falsch, oder du hast ein Smylie vergessen. 😉

    Du irrst, indem du recht hast.

    😕



  • camper schrieb:

    Das hättest du anders zitieren sollen. So macht es den Eindruck, als wollte Nexus sich als Dichter betätigen.

    Dass man es als Gedicht erkennen könnte, war eigentlich nie meine Absicht - aber meine stichwortartige Beschreibung kommt da halt schon nahe 😉

    Badestrand schrieb:

    drakon schrieb:

    Fände ich sauberer, als da mit Singletons zu arbeiten.

    Ich finde das hört sich schon ziemlich nach Singleton an. Überall Zeiger rumzureichen finde ich da viel viel ekliger.

    Nexus schrieb:

    Grundsätzlich wäre fast alles mit Vorwärtsdeklarationen und Zeigern als Member zu lösen.

    Mach das bloß nicht. Ist imo unsinning, genauso mit Smartpointern. Ich würde nie "reine" Membervariablen gegen Kompilierzeit eintauschen.

    Puh, endlich mal jemand, der das Ganze so sieht wie ich 😉
    Was das Zeiger-Rumreichen betrifft, wäre das wirklich total eklig, da dann jede Funktion, die entweder Eingaben oder Bildschirmausgaben handhabt, noch einen zusätzlichen Parameter hätte (oder jede Klasse noch einen zusätzlichen Zeiger). Zum Teil wären auch noch ganz andere Funktionen betroffen, weil z.B. auch eine Instanz einer Zufallsgeneratorklasse in Master steht. Das würde sich hinten und vorne nicht lohnen.

    Naja, ich hab mich einfach gewundert; wenn man beginnt, modular zu programmieren, sind plötzlich wieder Unmengen "Hilfsmittel" erforderlich, dass man es nur schafft, nicht schlechtere Voraussetzungen als bei nicht-modularer Programmierung zu haben... Wahrscheinlich nehm ich doch lieber die längere Kompilierzeit in Kauf...

    Ach ja, noch was: Gibt es so was wie meine Masterklasse häufig in der Praxis? Ich finde es sehr praktisch, da ich in main() gerade mal zwei Anweisungen habe. Das Unschöne ist vielleicht, dass Master nur statische Member und Methoden hat, aber wie gesagt mach ich da vielleicht noch ein Singleton draus.



  • Nexus schrieb:

    Naja, ich hab mich einfach gewundert; wenn man beginnt, modular zu programmieren, sind plötzlich wieder Unmengen "Hilfsmittel" erforderlich, dass man es nur schafft, nicht schlechtere Voraussetzungen als bei nicht-modularer Programmierung zu haben... Wahrscheinlich nehm ich doch lieber die längere Kompilierzeit in Kauf...

    Was du aber bedenken solltest: In der Anfangsphase wo man viele Interfaces aendert und oft nur mit mockups herumwirft und jedweder code total instabil (aus aenderungssicht - nicht bug-sicht) ist, sind mehrere uebersetzungseinheiten natuerlich doof, da so das linken laenger dauert.

    sobald du aber einen stand erreicht hast wo ein teil des codes interface-stabil ist, sinken die compiletimes rapide. denk einfach nur daran, dass du ja einen grossteil der dateien nachdem sie einmal fertig sind kaum noch anfasst. am anfang, wo du aber jede datei dauernd aenderst - ists natuerlich etwas stoerend.

    und auch aktuell, also trotz grossem recompile kann ich mir nicht vorstellen dass die compilezeiten soviel groesser sind. denn alles was mehr gemacht wird sind mehrmals header parsen -> und das laesst sich mit precompiled headers sogar umgehen.

    wenn du compilezeiten extrem senken willst, dann schau dir das pimpl-idiom an. ich bin kein grosser fan davon, wenn es aber um reduzierung von abhaengigkeiten geht ist es einfach super.

    20 sekunden recompile deutet auf entweder einen sehr langsamen rechner hin oder auf viel code. und bei viel code: warum aenderst du die zentralen stellen dauernd?



  • und bei viel code: warum aenderst du die zentralen stellen dauernd?

    :p

    Weisst du, was mir da manchmal passiert?
    Ich schaue mal ein wenig im Core, ob alles stimmt usw. Und aus reiner Gewohnheit mache ich da auch aus dem Reflex raus ctrl+s.. Und ich bemerke meinen Fehler auch sogleich beim drücken. *grml*
    Lösung: ctrl+F7 und mal was trinken gehen. 😃



  • drakon schrieb:

    und bei viel code: warum aenderst du die zentralen stellen dauernd?

    :p

    Weisst du, was mir da manchmal passiert?
    Ich schaue mal ein wenig im Core, ob alles stimmt usw. Und aus reiner Gewohnheit mache ich da auch aus dem Reflex raus ctrl+s.. Und ich bemerke meinen Fehler auch sogleich beim drücken. *grml*
    Lösung: ctrl+F7 und mal was trinken gehen. 😃

    dann ist dein editor komisch... wenn es keine aenderung gibt, sollte er die datei nicht modifizieren, auch bei einem ctrl+s nicht...

    aber klar, sowas passiert mal dass man aus versehen eine zentrale datei unnoetigerweise "aendert" - aber das ist hoffentlich ausnahme und nicht regel 😉



  • dann ist dein editor komisch... wenn es keine aenderung gibt, sollte er die datei nicht modifizieren, auch bei einem ctrl+s nicht...

    aber klar, sowas passiert mal dass man aus versehen eine zentrale datei unnoetigerweise "aendert" - aber das ist hoffentlich ausnahme und nicht regel

    Meist ist hald so, dass ich dennoch eine Taste drücke und dann speichere.. 🙂

    Ja, ich schaue schon, dass das nicht all zu oft passiert. Es ist in der Tat recht mühsam nacher alles neu compilen zu müssen.. 🙂


  • Administrator

    Nexus schrieb:

    Nun ist es so, dass Kollisionsabfragen in Object passieren, d.h. da werden schon mal die Klassen Tile und Game (aktuelle Oberfläche) benötigt.

    Also wenn schon würde ich Tile auch von Object ableiten und somit Tile auch als Kollisionobjekt ansehen. In der Klasse Object gibt es eine Methode, welche ein anderes Object annimmt und dann die Kollisionsabfrage ausführt.

    Nexus schrieb:

    Bei den Klassen Player , Enemy und Weapon muss ich wahrscheinlich eine Dreiecksabhängigkeit einrichten, weil sich diese Klassen jeweils gegenseitig beeinträchtigen können.

    Wird wohl nicht nötig sein, zumindest nicht in den Headern. Da kannst du überall Forward Declaration anwenden.
    Du wirst ja wohl kaum ein Player, Enemy oder Weapon kopieren wollen. Von überall her, wo du eine Instanz auf den Player, Enemy oder Weapon übergeben tust, soll es ja auch die gleiche sein. Also wirst du in erster Linie Zeiger herumreichen. Vor allem auch, weil du die Spieler, Gegner und Waffen auf dem Heap erstellen wirst. Und es sicher Möglichkeiten gibt, dass es keine Gegner, keine Waffen oder keine Spieler hat, also Nullzeiger.

    Nexus schrieb:

    Game selber benötigt natürlich alle Klassen, die Typen der Member sind.

    Kommt drauf an, wie zentral du Game gestaltest. Vergiss nicht, dass man in Module auslagern sollte. Man kann auch Funktionsweisen oder Handlungen in Objekte packen, auch Beschreibungen sind möglich. Oder anders ausgedrückt:
    Nicht nur Nomen sind Objekte, sondern auch Adjektive, Verben, Adverben, Pronomen und was es sonst noch alles gibt 😉

    Nexus schrieb:

    Was ein Problem darstellt, ist die Tatsache, dass z.B. Player indirekt über die Basisklasse Object auf Game Zugriff haben muss, um z.B. Kollisionsabfragen mit den Tiles zu handhaben.

    Das leuchtet mir überhaupt nicht ein. Wieso kann der Player nicht direkt mit den Tiles interagieren? Game sollte höchstens den Player verschieben oder sowas ähnliches.

    Nexus schrieb:

    Was wiederum nicht geht, weil im Konstruktor von Game ja auch Player konstruiert wird, und der this -Zeiger von Game zu diesem Zeitpunkt noch auf die Basisklasse Surface verweist... 🙄

    Ah, da haben wir wieder so ein Problem. Du erstellst den Player als Eigenschaft von Game. Aber ist er das wirklich? Ich finde das irgendwie nicht. Einem Game Objekt sollte ein Player Objekt zugewiesen sein, vielleicht auch mehrere, aber es sollte den Player sicher nicht besitzen.
    Ein Spieler sollte meiner Meinung nach anderswo erstellt und dem Spiel dann übergeben werden.

    Nexus schrieb:

    Grundsätzlich wäre fast alles mit Vorwärtsdeklarationen und Zeigern als Member zu lösen. Was ich jedoch unnötig finde, weil auf ich eigene Speicherverwaltung lieber verzichte, wo sichs vermeiden lässt (und wie gesagt auf The Big Three).

    Auf "The Big Three" verzichtest du, in dem du Zeiger verwendest. Aber wenn du möglichst Zeiger meiden willst, weil du Angst vor der eigenen Speicherverwaltung hast, dann solltest du vielleicht eine andere Sprache wählen? C# oder Java? Bei C++ sind ja gerade solche Dinge ganz entscheidend und vieles wird darüber aufgebaut. Man verzichtet einfach auf einen ganz entscheidenden Teil der Sprache, wenn man keine eigene Speicherverwaltung einsetzt. Shared Pointer und ähnliches sollten nur Hilfskonstrukte für gewisse Situationen sein, aber nicht für alles.

    Ich hoffe ich konnte dir etwas helfen. Vor allem überwinde deine Angst vor der Speicherverwaltung!

    Eine "Master-Option"-Klasse, mache ich höchst selten. Das hat den einfach Grund, dass wenn man die Sache genügend aufsplittet in Objekte und Module, es irgendwann keine Option geben wird, welche für alle oder mehr als die Hälfte gilt. Man sollte zudem, meiner Meinung nach, nicht in solchen Dimensionen denken. Wenn eine Option verändert wird, dann betrifft das direkt entsprechende Objekte und dort sollten die Eigenschaften entsprechend gesetzt werden. Man sollte nicht Optionen zusammenschaufeln und in eine gemeinsame Klasse verfrachten. Ausser das sie Optionen sind, haben diese nämlich nichts gemeinsam!

    Grüssli



  • Vielen Dank für die hilfreichen Antworten! Es ist wirklich nett, wie ihr euch Zeit nehmt!

    Shade Of Mine schrieb:

    Was du aber bedenken solltest: In der Anfangsphase [...] sind mehrere uebersetzungseinheiten natuerlich doof, da so das linken laenger dauert.
    sobald du aber einen stand erreicht hast wo ein teil des codes interface-stabil ist, sinken die compiletimes rapide.

    Hmm, das hat in der Tat was, daran hab ich eigentlich gar nicht gedacht 😉

    Shade Of Mine schrieb:

    20 sekunden recompile deutet auf entweder einen sehr langsamen rechner hin oder auf viel code. und bei viel code: warum aenderst du die zentralen stellen dauernd?

    Nein, ich denke nicht, dass der Rechner zu langsam ist. Eher die zweite Möglichkeit. Und wie gesagt lassen sich häufige Änderungen nicht immer umgehen, weil man plötzlich Details bemerkt oder neue Features hinzufügen will, die man vorher noch nicht eingeplant hat. Aber es stimmt schon, ich sollte mich vielleicht ein bisschen einschränken mit zentralen Änderungen.

    drakon schrieb:

    Lösung: ctrl+F7 und mal was trinken gehen. 😃

    Hast du demnach auch das Problem mit langen Kompilierzeiten? Oder sind deine Projekte einfach so riesig? 🙂

    Dravere schrieb:

    Also wenn schon würde ich Tile auch von Object ableiten und somit Tile auch als Kollisionobjekt ansehen. In der Klasse Object gibt es eine Methode, welche ein anderes Object annimmt und dann die Kollisionsabfrage ausführt.

    Nein, dann würde es von der Logik her keinen Sinn mehr machen. Ein Tile ist ja ein fester Bestandteil der Welt, d.h. es kann nur rasterförmig ausgerichtet sein (eben in einer Tilemap) und bewegt sich nicht. Objekte wie der Spieler oder abgefeuerte Waffen hingegen sind mobil und reagieren auf Kollision mit der Welt (also den Tiles).

    Dravere schrieb:

    Nexus schrieb:

    Bei den Klassen Player , Enemy und Weapon muss ich wahrscheinlich eine Dreiecksabhängigkeit einrichten, weil sich diese Klassen jeweils gegenseitig beeinträchtigen können.

    Wird wohl nicht nötig sein, zumindest nicht in den Headern. Da kannst du überall Forward Declaration anwenden.
    Du wirst ja wohl kaum ein Player, Enemy oder Weapon kopieren wollen. Von überall her, wo du eine Instanz auf den Player, Enemy oder Weapon übergeben tust, soll es ja auch die gleiche sein. Also wirst du in erster Linie Zeiger herumreichen. Vor allem auch, weil du die Spieler, Gegner und Waffen auf dem Heap erstellen wirst. Und es sicher Möglichkeiten gibt, dass es keine Gegner, keine Waffen oder keine Spieler hat, also Nullzeiger.

    Ja, also momentan verwalte ich diese Klassen in STL-Containern innerhalb von Game (die eben leer sind falls kein entsprechendes Objekt existiert). Falls es etwas rumzureichen gibt, benutze ich eher Referenzen. Aber das muss ich eh noch ausbauen, jetzt wird z.B. auf die Tiles statt einfacher Übergabe relativ kompliziert zugegriffen - dynamic_cast<Game*>(Master::MySurface)->Tiles (natürlich noch abgekürzt) 😉

    Dravere schrieb:

    Nexus schrieb:

    Was ein Problem darstellt, ist die Tatsache, dass z.B. Player indirekt über die Basisklasse Object auf Game Zugriff haben muss, um z.B. Kollisionsabfragen mit den Tiles zu handhaben.

    Das leuchtet mir überhaupt nicht ein. Wieso kann der Player nicht direkt mit den Tiles interagieren? Game sollte höchstens den Player verschieben oder sowas ähnliches.

    Wie ich schon weiter oben angedeutet habe, hab ich das momentan noch relativ kompliziert (eher global) gelöst. Aber wenn der Spieler direkt mit den Tiles interagieren sollte, müsste ja bei dessen Erstellung im Konstruktor eine Referenz auf die Tiles übergeben werden, und auf Gegner genauso. Gegner wiederum hätten Referenzen auf Spieler und Tiles, was zu Konflikten führt, wenn eines der Objekte noch nicht konstruiert wurde und eine Referenz darauf übergeben wird.

    Dravere schrieb:

    Nexus schrieb:

    Game selber benötigt natürlich alle Klassen, die Typen der Member sind.

    Kommt drauf an, wie zentral du Game gestaltest.

    Dravere schrieb:

    Nexus schrieb:

    Was wiederum nicht geht, weil im Konstruktor von Game ja auch Player konstruiert wird, und der this -Zeiger von Game zu diesem Zeitpunkt noch auf die Basisklasse Surface verweist... 🙄

    Ah, da haben wir wieder so ein Problem. Du erstellst den Player als Eigenschaft von Game. Aber ist er das wirklich? Ich finde das irgendwie nicht. Einem Game Objekt sollte ein Player Objekt zugewiesen sein, vielleicht auch mehrere, aber es sollte den Player sicher nicht besitzen.
    Ein Spieler sollte meiner Meinung nach anderswo erstellt und dem Spiel dann übergeben werden.

    Daran hab ich auch relativ lange überlegt. Ich hab auch eine Klasse Map in Betracht gezogen (für ein Level/eine Karte). Schlussendlich hab ich mich für Game entschieden, weil das so schön zu der Polymorphie mit den Surface s (verschiedene Menüoberflächen) passt (Bei mir ist es so, dass es verschiedene gleichwertige Oberflächen gibt (Hauptmenü, Info, Hilfe, Editor, Game)). Zudem ist es auch logisch sinnvoll, wenn Spieler, Gegner, Tiles gerade erstellt werden, wenn man das Spiel startet (also eine Instanz von Game erstellt). Von mir aus gesehen macht es keinen Sinn, Spieler etc. separat zu erstellen und an Game zu übergeben, weil sie ja unmittelbar daran gebunden sind (welchen Sinn hat ein Spieler ohne Spiel?) 😉

    Dravere schrieb:

    Auf "The Big Three" verzichtest du, in dem du Zeiger verwendest.

    Ich meinte, ich müsse Copy-Ctor, Dtor und Op= selber implementieren, wenn ich Zeiger als Member habe - was ich bei Stackvariablen nicht muss.

    Dravere schrieb:

    Aber wenn du möglichst Zeiger meiden willst, weil du Angst vor der eigenen Speicherverwaltung hast, dann solltest du vielleicht eine andere Sprache wählen? C# oder Java? Bei C++ sind ja gerade solche Dinge ganz entscheidend und vieles wird darüber aufgebaut. Man verzichtet einfach auf einen ganz entscheidenden Teil der Sprache, wenn man keine eigene Speicherverwaltung einsetzt. Shared Pointer und ähnliches sollten nur Hilfskonstrukte für gewisse Situationen sein, aber nicht für alles.

    Vielleicht hab ich einen falschen Eindruck erweckt, es ist ja nicht so, dass ich ängstlich vor Zeigern und eigener Speicherverwaltung zurückschrecke - ich hab immer noch genügend Orte, wo ich mit new und delete arbeite 😉
    Ich meinte vielmehr, dass eigene Speicherverwaltung halt viele Fehlerrisiken birgt und man viel stärker aufpassen muss. Ich sehe deshalb automatische Speicherverwaltung als "Geschenk" an, das auch eingesetzt werden sollte, wenn es geht (im Übrigen ist es auch noch schneller). Wie Badestrand in seinem Post gesagt hat, würde ich Stackvariablen gegen "praktische" Vorwärtsdeklarationen lieber nicht eintauschen.

    Dravere schrieb:

    Eine "Master-Option"-Klasse, mache ich höchst selten. Das hat den einfach Grund, dass wenn man die Sache genügend aufsplittet in Objekte und Module, es irgendwann keine Option geben wird, welche für alle oder mehr als die Hälfte gilt. Man sollte zudem, meiner Meinung nach, nicht in solchen Dimensionen denken. Wenn eine Option verändert wird, dann betrifft das direkt entsprechende Objekte und dort sollten die Eigenschaften entsprechend gesetzt werden. Man sollte nicht Optionen zusammenschaufeln und in eine gemeinsame Klasse verfrachten. Ausser das sie Optionen sind, haben diese nämlich nichts gemeinsam!

    Hm... Was meinst du genau mit "Optionen"? Ich hab das Gefühl, wir stellen uns unter einer Masterklasse nicht ganz das Gleiche vor.
    Bei mir ist es so, dass nur die ganz zentralen Dinge dort geregelt werden. Der Master ist bezeichnenderweise (sorry, wusste keinen besseren Namen als "Master" :)) die oberste Direktive, die alles regelt. Er überwacht den Verlauf des Programms und gibt den einzelnen Oberflächen (abstrakte Basisklasse Surface ) die Kontrolle, wenn eine entsprechende Eingabe getätigt wurde. Abgesehen von der Kontrollfunktion besitzt er noch einige für Grafikroutinen benötigte Variablen (z.B. das Fenster, oder Event für Eingaben, Schriftarten), dient also auch als Ersatz für globale Variablen.

    Aber ganz dezentral kannst du das Programm ja auch nicht steuern (ich nehme nicht an, dass du sehr vieles in der main() -Funktion stehen hast), oder? Ich hab bei mir gerade mal zwei Funktionen in main() , nämlich eine für die Initialisierung und eine für den ständigen Verlauf.

    So, wieder viel geschrieben 🙂
    Ich hoffe, ihr könnt meine Sichtweise und mein Design einigermassen nachvollziehen - bei irgendwelchen Unklarheiten sofort fragen 😉


  • Administrator

    Nexus schrieb:

    Nein, dann würde es von der Logik her keinen Sinn mehr machen. Ein Tile ist ja ein fester Bestandteil der Welt, d.h. es kann nur rasterförmig ausgerichtet sein (eben in einer Tilemap) und bewegt sich nicht. Objekte wie der Spieler oder abgefeuerte Waffen hingegen sind mobil und reagieren auf Kollision mit der Welt (also den Tiles).

    Das bedeutet für mich eigentlich nur, dass du noch nicht genügend aufgesplittet hast. Dann gibt es eben Kollisionsobjekte und bewegliche Objekte. Ein Player wäre demnach beides, ein Tile nur ein Kollisionsobjekt. Durch die Auftrennung passiert dann auch der Logikfehler nicht mehr.

    Nexus schrieb:

    Ja, also momentan verwalte ich diese Klassen in STL-Containern innerhalb von Game (die eben leer sind falls kein entsprechendes Objekt existiert). Falls es etwas rumzureichen gibt, benutze ich eher Referenzen. Aber das muss ich eh noch ausbauen, jetzt wird z.B. auf die Tiles statt einfacher Übergabe relativ kompliziert zugegriffen - dynamic_cast<Game*>(Master::MySurface)->Tiles (natürlich noch abgekürzt) 😉

    Das ist nicht nur kompliziert, sondern sogar langsam! Ein dyamic_cast ist teuer, erst recht, wenn er immer wieder ausgeführt wird. Zudem zeigt ein dynamic_cast, wie so oft, darauf hin, dass ein Designfehler vorliegt. Es sollte ohne gehen!
    Ein Spieler ist nicht da, um sich zu bewegen, Regeln sind dafür da. Und das Spiel, welches die Tiles beinhaltet und auch den Spieler hat, sollte mit den Regeln den Spieler verschieben. Es kann also alles intern gelöst werden.

    Nexus schrieb:

    Wie ich schon weiter oben angedeutet habe, hab ich das momentan noch relativ kompliziert (eher global) gelöst.

    Ich würde eher sagen, sehr vereinfacht und global gemacht, anstatt genügend differenziert 🙂

    Nexus schrieb:

    Aber wenn der Spieler direkt mit den Tiles interagieren sollte, müsste ja bei dessen Erstellung im Konstruktor eine Referenz auf die Tiles übergeben werden, und auf Gegner genauso.

    Nein. Ich hätte da eher an eine Location gedacht. Also sowas wie:

    class Player
    {
      // ...
    private:
      Tile* m_location;
    
      // ...
    public:
      void set_location(Tile* location) { m_location = location; };
      Tile* get_location() const { return m_location; };
    };
    

    Wobei man das sicher ausbauen könnte. Vielleicht in das bewegliche Objekt nehmen. Zudem dann überprüfen, ob es eine Kollision gibt oder was auch immer, bzw. ob der Player dort stehen kann. Wenn nicht, muss das Spiel mit den Regeln richtig reagieren und den Spieler anders verschieben. Die Regeln können ja auch als Klassen eingebaut werden. Dann wird einem Regelobjekt der Spieler übergeben und gesagt, er müsse zum Punkt x,y,z gehen. Die Regel probiert das auszuführen und wenn es nicht geht, übergibt sie die Sache anderen Regeln usw.
    Hier werden dann auch nur Zeiger durchgereicht oder Referenzen.

    Nexus schrieb:

    Gegner wiederum hätten Referenzen auf Spieler und Tiles, was zu Konflikten führt, wenn eines der Objekte noch nicht konstruiert wurde und eine Referenz darauf übergeben wird.

    Wieso hat der Gegner Referenzen auf Spieler? Und wieso überhaupt Referenzen? Wieso nicht Pointer und dann die Übergabe eines Nullpointers erlauben? Ein Spieler könnte keine Waffe haben, ein Spieler oder Gegner kann nirgends stehen.

    Es muss nicht nur das Sinn machen, was der Spieler zu sehen bekommt. Die Funktionsweise eines Spieles sollte mehr können 😉

    Wieso willst du im übrigen alles mit Referenzen lösen? Referenzen kann man nur ein einziges Mal initialisieren, danach kannst du nie mehr ein anderes Objekt zuweisen. Wenn du deinen Klassen immer nur Referenzen übergibst als Member, dann bindest du sie viel zu stark an die entsprechenden Objekte. Das ist meistens gar nicht nötig. Zudem dürfte es Probleme beim "normalen" Kopieren geben.

    Nexus schrieb:

    Daran hab ich auch relativ lange überlegt. Ich hab auch eine Klasse Map in Betracht gezogen (für ein Level/eine Karte). Schlussendlich hab ich mich für Game entschieden, weil das so schön zu der Polymorphie mit den Surface s (verschiedene Menüoberflächen) passt (Bei mir ist es so, dass es verschiedene gleichwertige Oberflächen gibt (Hauptmenü, Info, Hilfe, Editor, Game)). Zudem ist es auch logisch sinnvoll, wenn Spieler, Gegner, Tiles gerade erstellt werden, wenn man das Spiel startet (also eine Instanz von Game erstellt). Von mir aus gesehen macht es keinen Sinn, Spieler etc. separat zu erstellen und an Game zu übergeben, weil sie ja unmittelbar daran gebunden sind (welchen Sinn hat ein Spieler ohne Spiel?) 😉

    Das der Spieler gerade im Warteraum ist? Bei den Optionen? In den Highscores? Jedenfalls nicht im Spiel.
    Zudem sollte man viel eher anschauen, was für Vorteile bringt einem eine entsprechende Kapselung. Sollte wirklich das Spiel den Spieler erstellen? Ich würde eher sagen, das Spiel soll mit dem Spieler interagieren. Erstellen tut sich der Spieler wahrscheinlich selbst, zum Beispiel über ein Profil oder ähnliches. Dann kann er den Namen selber festlegen und die Farbe seiner Spielfigur, oder sowas in der Art 😉

    Nexus schrieb:

    Ich meinte, ich müsse Copy-Ctor, Dtor und Op= selber implementieren, wenn ich Zeiger als Member habe - was ich bei Stackvariablen nicht muss.

    Also hier liegen irgendwie mehrere Missverständnisse vor:
    1. Eine Deep-Copy ist nicht immer nötig. Manchmal will man wirklich nur den Zeiger kopieren und nicht das, worauf der Zeiger zeigt. Siehe es am Beispiel von Player oben. Die Location sollte dort als Zeiger kopiert werden, also eine "flache" Kopie. Man möchte ja auf das gleiche Tile verweisen.
    2. Wenn du Zeiger verwendest, dann musst du ja nicht mehr kopieren! Wenn du dem Spiel ein Zeiger auf einen Spieler übergibst, dann soll der Zeiger gespeichert werden und nur der Zeiger weitergegeben werden (oder allenfalls eine Referenz). Der Spieler wird gar nie kopiert, wodurch er keiner der grossen Drei benötigt.
    3. Stackvariablen? Bei Stackvariablen brauchst du das je nach dem trotzdem. Und Membervariablen kommen nicht unbedingt auf den Stack! Die kommen dorthin, wo die Klasse erstellt wurde, also womöglich auch auf den Heap. Der Stack ist sowieso begrenzt, man sollte nicht zu viel dort drauf setzen.

    Nexus schrieb:

    Vielleicht hab ich einen falschen Eindruck erweckt, es ist ja nicht so, dass ich ängstlich vor Zeigern und eigener Speicherverwaltung zurückschrecke - ich hab immer noch genügend Orte, wo ich mit new und delete arbeite 😉
    Ich meinte vielmehr, dass eigene Speicherverwaltung halt viele Fehlerrisiken birgt und man viel stärker aufpassen muss. Ich sehe deshalb automatische Speicherverwaltung als "Geschenk" an, das auch eingesetzt werden sollte, wenn es geht ...

    Du hast doch Angst vor den Fehlerrisiken. Aber wenn du sie nie antriffst, dann wirst du immer Angst davor haben. Wenn du automatische Speicherverwaltung als ein "Geschenk" siehst, dann bist du, wie gesagt, in der falschen Sprache. Ich sehe viel eher die nicht automatische Speicherverwaltung als ein wundervolles Geschenk! Sicher, man kann sich damit den ganzen Körper wegsprengen, aber wenn man ein wenig Übung hat, und die sollte man sich aneignen, dann ist das kein Problem mehr.

    Nexus schrieb:

    ... (im Übrigen ist es auch noch schneller). Wie Badestrand in seinem Post gesagt hat, würde ich Stackvariablen gegen "praktische" Vorwärtsdeklarationen lieber nicht eintauschen.

    Stackvariablen können nur temporär erstellt werden oder müssen kopiert werden. Klar bei der Erstellung ist der Stack schneller, wenn du aber danach immer kopieren musst, ist die Übergabe eines Zeigers deutlich schneller, da du auf dem Heap nur ein einziges Mal allokierst.

    Nexus schrieb:

    Hm... Was meinst du genau mit "Optionen"? Ich hab das Gefühl, wir stellen uns unter einer Masterklasse nicht ganz das Gleiche vor.

    Scheint, dass du noch viel mehr darunter verstehst als ich.

    Nexus schrieb:

    Bei mir ist es so, dass nur die ganz zentralen Dinge dort geregelt werden.

    Alles? 🙂

    Nexus schrieb:

    Der Master ist bezeichnenderweise (sorry, wusste keinen besseren Namen als "Master" :)) die oberste Direktive, die alles regelt. Er überwacht den Verlauf des Programms und gibt den einzelnen Oberflächen (abstrakte Basisklasse Surface ) die Kontrolle, wenn eine entsprechende Eingabe getätigt wurde. Abgesehen von der Kontrollfunktion besitzt er noch einige für Grafikroutinen benötigte Variablen (z.B. das Fenster, oder Event für Eingaben, Schriftarten), dient also auch als Ersatz für globale Variablen.

    Eindeutig, alles 😉
    Schon nur das "Ersatz für globale Variablen" zeigt auf, dass da was nicht stimmen kann. Globale Variablen braucht man nicht und man braucht auch kein Ersatz dafür. Deshalb ist das Singleton-Pattern auch so umstritten, weil es oft als Ersatz für globale Variablen herangezogen wird, was es nicht sein sollte.

    Nexus schrieb:

    Aber ganz dezentral kannst du das Programm ja auch nicht steuern (ich nehme nicht an, dass du sehr vieles in der main() -Funktion stehen hast), oder? Ich hab bei mir gerade mal zwei Funktionen in main() , nämlich eine für die Initialisierung und eine für den ständigen Verlauf.

    Ich habe normalerweise kaum etwas in der Main, weil ich eben dezentral programmiere. Wenn ich viel in die Main schreiben würde, dann würde diese zu einem zentralen Punkt werden, nicht?

    Aber man kann dezentral programmieren, man muss nur genügend aufsplitten. Gerade deine Master und Game Klassen sind für mich eindeutige Hinweise darauf, dass du noch nicht genügend gesplittet hast.

    Aber mal ein wenig allgemeiner und ich bitte dich, die Frage nicht böse zu verstehen. Wieviel hast du in C++ schon gemacht? Und wieviel hast du schon zu C++ gelesen, welche Bücher? Versteh mich bitte nicht falsch, aber ich habe ein wenig das Gefühl, dass ein Spiel zu programmieren, vielleicht eine Stufe zu hoch ist.

    So, das war nun wirklich ein riesiger Post. Ich könnte noch mehr schreiben, muss aber essen gehen und möchte auch noch selber heute was programmieren 😃

    Grüssli



  • Versteh mich bitte nicht falsch, aber ich habe ein wenig das Gefühl, dass ein Spiel zu programmieren, vielleicht eine Stufe zu hoch ist.

    So, wie ich Nexus einschätze, reicht sein können für ein einfaches Spiel allemale. Sein Problem ist hald, wie ich vermute, dass er sich im vornherein zu viele Gedanken macht.
    Imhos sollte er einfach mal anfangen und dann wird er früher, oder später merken, was an dem Design Mist ist. Das prägt sich dann auch mehr ein und man kennt Alternativen. (Weil es hier vlt. schlecht war, heisst ja nicht, dass es wo anderst nicht mal von Nutzen sein kann).

    Hast du demnach auch das Problem mit langen Kompilierzeiten? Oder sind deine Projekte einfach so riesig?

    Es ist mittlerweile Recht gross geworden. (Die grösse .cpp hat ca. 1500 Zeilen. ) 🙂

    Um da jetzt mal den Vergleich zu machen. Dort ist mein gesamtes GUI System drin. Und das habe ich imho schon recht gut gemacht, aber ich sage dir, wenn ich, dass ich am liebsten alles nochmal neu schreiben würde, weil so richtig gefällt mir das ganze eigl. nicht. Aber ich habe auch keine Lust das ganze nochmal zu machen, vor allem, da ich dann wahrscheinlich auch nicht 100% zufrieden bin. 😉 - Ich denke mal, dass es da vielen gleich geht. Man ist irgendwie nie richtig zufriede mit dem, was man hat. 🙂


Anmelden zum Antworten