dynamic_cast böse



  • dot schrieb:

    Wenn das passiert, dann hat man was falsch gemacht. Denn eigentlich sollte die Methode tuwas() wohl nicht in all diesen Interfaces auftreten, sondern all diese Objekte das Interface mit der tuwas() Methode implementieren...

    Verstehe ich nicht. Vielleicht war .tuwas() zu abstrakt.

    Nimm .zeichneDichAufDenBildschirm() statt .tuwas().



  • Das ändert doch nichts an meinem Argument 😕
    Wenn ich alle Dinge die sich zeichnen sollen gleich behandeln will, dann sollen die doch auch bitte alle das Interface Drawable implementieren. Dann hab ich einen Container der alle Drawables enthält und kann alle Drawables zeichnen!? Ob manche der Objekte, die hinter diesen Drawables stehen, auch noch das Interface Player, Enemy oder Cake implementieren, ist für den Zeichencode doch irrelevant!?



  • dot schrieb:

    Das ändert doch nichts an meinem Argument 😕
    Wenn ich alle Dinge die sich zeichnen sollen gleich behandeln will, dann sollen die doch auch bitte alle das Interface Drawable implementieren. Dann hab ich einen Container der alle Drawables enthält und kann alle Drawables zeichnen!?

    Alle können zeichneDich() und alle können tuDenNächstenSchrittAnhandDeinerKI(). Und mache davon sind Gegner.
    Also alle liegen in zwei Containern und manche sogar in drei.
    Willst Du mir jetzt vorschlagen, daß ich für jedes Interface einen nichtbesitzenden Zeiger-Container anlege? Und noch einen vierten besitzenden Zeiger-Container?



  • volkard schrieb:

    dot schrieb:

    Das ändert doch nichts an meinem Argument 😕
    Wenn ich alle Dinge die sich zeichnen sollen gleich behandeln will, dann sollen die doch auch bitte alle das Interface Drawable implementieren. Dann hab ich einen Container der alle Drawables enthält und kann alle Drawables zeichnen!?

    Alle können zeichneDich() und alle können tuDenNächstenSchrittAnhandDeinerKI(). Und mache davon sind Gegner.
    Also alle liegen in zwei Containern und manche sogar in drei.
    Willst Du mir jetzt vorschlagen, daß ich für jedes Interface einen nichtbesitzenden Zeiger-Container anlege? Und noch einen vierten besitzenden Zeiger-Container?

    Ja, das ist imo die natürliche Lösung (wobei all diese Container natürlich nur die entsprechenden Interface-Pointer enthalten und nicht die Objekte an sich). Denn der Renderer arbeitet mit völlig anderern Datenstrukturen wie die KI oder die Spiellogik. Die Dinge, die in einem Frame gerendert werden sollen, liegen vielleicht in einem vector, für ein möglichst effizientes Traversal. Die Spiellogik hält die Gegner aber doch besser in einem Grid, um möglichst schnell die Gegner im Umkreis des Spielers finden zu können. Die KI wird ihre Agenten vielleicht durch ein Navigation-Mesh schicken wollen.
    Alle Objekte in den selben Container zu stopfen mach hier von vornherein keinen Sinn.
    So eine Gameloop, wo einfach alle Objekte dies im Spiel gibt das selbe Interface besitzen und ständig Update() und Draw() aufgerufen wird, funktioniert vielleicht für Pong und Tetris. Aber spätestens bei Super-Mario ist Schluss...



  • dot schrieb:

    Ja, das ist imo die natürliche Lösung. Denn der Renderer arbeitet mit völlig anderern Datenstrukturen wie die KI oder die Spiellogik.

    Der Renderer kann ja andere Datenstrukturen haben. Und ein Reiter kann sein Reiterbild im Renderer kennen. Unwichtige Details.

    dot schrieb:

    Die Dinge, die in einem Frame gerendert werden sollen, liegen vielleicht in einem vector, für ein möglichst effizientes Traversal. die Spiellogik hält die Gegner aber doch besser in einem Grid, um möglichst schnell die Gegner im Umkreis des Spielers finden zu können. Die KI wird ihre Agenten vielleicht durch ein Navigation-Mesh schicken wollen.

    Nichtbesitzende Zeigercontainer für beschleinigten Zugriff. Das ist auch eine ganz andere Ebene.

    dot schrieb:

    Alle Objekte in den selben Container zu stopfen mach hier von vornherein keinen Sinn.

    Da bin ich halt ganz anderer Meinung.



  • volkard schrieb:

    dot schrieb:

    Alle Objekte in den selben Container zu stopfen mach hier von vornherein keinen Sinn.

    Da bin ich halt ganz anderer Meinung.

    Und was genau sind da die Argumente dafür? Genau damit bringt man sich doch erst in die Situation, wo man ständig irgendwelche Typabfragen und Downcasts braucht!?
    Ich halt es jedenfalls für eine notwendige Eigenschaft von gutem Design, dass die Dinge völlig natürlich ineinandergreifen und das Typsystem der Sprache als Ausdrucksmittel dient und nicht ständig dagegen gekämpft oder gar ein eigenes erfunden werden muss...



  • dot schrieb:

    Und was genau sind da die Argumente dafür?

    Es klappt auch.

    dot schrieb:

    Genau damit bringt man sich doch erst in die Situation, wo man ständig irgendwelche Typabfragen und Downcasts braucht!?

    Nicht ständig.

    dot schrieb:

    Ich halt es jedenfalls für eine notwendige Eigenschaft von gutem Design, dass die Dinge völlig natürlich ineinandergreifen und das Typsystem der Sprache als Ausdrucksmittel dient und nicht ständig dagegen gekämpft oder gar ein eigenes erfunden werden muss...

    Du übertreibst.



  • Ich sehe nicht, wie diese parellele pointer haltung sinn machen soll - hab ich 27 verschiedene vectoren statt ein einfaches virtual void update = 0 das ich einfach überall drüberschmeißen kann. In 95% der Fälle braucht man da gar keine casts (und tatsächlich mache ich das so, und hab de facto momentan 0 dynamic_casts). Das ist für mich Kapselung - die Hauptschliefe weiß einfach gar nichts über die details der verhandenen Objekte, und es INTERESSIERT sie auch nicht. Bei deiner Lösung muss ich im Zweifelsfall auch jedesmal die Hauptschleife ändern, wenn ich neue Objekttypen einführe. Neuen vector erstellen, daten da neu einfügen, alles nochmal extra machen. Selbst wenn man das generisch recht einfach machen kann. Irgendwie kommt mir das etwas nach "dynamic_cast vermeiden als reiner selbstzweck" vor. Ob ich jetzt 2 container hab AIObject und OtherObject, oder einen Object, bei dem ich dynamic_cast mache ändert doch NICHTS an der Kapselung, was doch eigentlich das Hauptargument war. Dazu brauch ich in der single-variante in 95% der Fälle eben nichtmal diesen dynamic_cast (oder eben typeid), während ich bei 2 oder mehr vectoren sie nicht nur in 5% der Fälle unterschiedlich behandele, sondern in 100%. Vorallem behandle ich das ganze an Stellen unterschiedlich, wo es völlig unnötig ist. Ich verstehe ja durchaus, das dynamic_cast wegen Kapselung ein prinzipielles Problem hat, aber "ich behandel von vorneherein alles unterschiedlich, hab also gar nicht mehr das Problem das ich manche Dinge in seltenen Fällen auch unterschiedlich behandeln muss" seh ich nicht gerade als Lösung. Hat ein bißchen was von "das Baby mit dem Badewasser ausschütten". Nur um klarzustellen - wenn sich 2 Dinge tatsächlich in den meisten Fällen unterschiedlich Verhalten, halte ich 2 oder mehr vectoren auch für die natürliche Lösung. Das ist aber in dem Beispiel eben nicht der Fall.



  • Warum müssen in-game Objekte unbedingt durch c++ Objekte repräsentiert werden?
    Wie soll man eine Hierarchie vernünftig aufbauen (um dynamic_cast verwenden zu können), wenn man z.B. solche Objekte hat:

    Lastwagen: beweglich, unbewaffnet
    Panzer: beweglich, bewaffnet
    Geschützturm: unbeweglich, bewaffnet

    GameObjekt -> Beweglich -> Lastwagen
    Beweglich -> Bewaffnet -> Panzer
    GameObjekt -> ???? -> Geschützturm

    Abgesehen davon, wie würdet ihr etwas globales, z.B. Physik, implementieren? In update() durch die Liste der Objekte gehen, schauen ob sie kollidieren können (also dynamic_cast), und dann auf Kollision überprüfen?
    Wie sieht Interaktion zwischen 2 Objekten über ein Interface wie

    class GameObjekt
    {
    public:
        virtual void update() = 0;
    };
    

    aus?



  • GorbGorb schrieb:

    Warum müssen in-game Objekte unbedingt durch c++ Objekte repräsentiert werden?

    Müssen nicht. Man muss auch nicht c++ schreiben sondern kanns in asm machen. Es ist halt praktisch.

    Wie soll man eine Hierarchie vernünftig aufbauen (um dynamic_cast verwenden zu können), wenn man z.B. solche Objekte hat:

    Man baut doch bitte nicht eine Hierarchie auf, nur um dynamic_cast zu verwenden. Dafür wäre dynamic_cast ja wirklich fehl am Platz.

    Aber zu dem Beispiel selbst. Das ist in der tat ein Problem. Da kann man dann funktionalität ausgliedern (aber dann aus konkreten gründen, und nicht hauptsache damit man nicht eventuell irgendwann in die Gelegenheit kommen könnte, dynamic_cast zu brauchen) oder man sagt, falls es kein grundlegenderes Problem gibt, das ein Geschützturm beweglich ist (was ja nur heißt, dass das Programm in logisch bewegen darf, nicht muss). Gibt dann halt seltsames Verhalten wenn man z.B. alle beweglichen Objekte entfernt. Man kann auch einen pointer in Object haben auf "Bewaffnung" (0 wenn keine bewaffnung)- dann hat sich das Problem quasi in Luft aufgelöst - und da der pointer private sein kann, sieht man ihn nach außen nichtmal 😉
    Es gibt gute argumente, bewaffnet oder nicht in der Klassenstruktur zu trennen, aber auch andere Wege. Letztlich beantwortet das ganze aber gar nicht die Frage, ob man denn nur um eventuell dynamic_cast zu vermeiden, es trennen soll, obwohl man NICHT muss.

    Abgesehen davon, wie würdet ihr etwas globales, z.B. Physik, implementieren?
    In update() durch die Liste der Objekte gehen, schauen ob sie kollidieren können (also dynamic_cast), und dann auf Kollision überprüfen?

    Weiß nicht obs bessere designs gibt, aber ne globale physics map, auf der kollisionen gespeichert werden. Da braucht man dann auch keinerlei dynamic_casts, da die objekte selbst natürlich wissen was für ein typ sie sind (und deren typ eigentlich auch gar nicht gebraucht wird) und der Typ von anderen Objekten gar nicht interessiert. Entweder sie stehen in der collision map, oder sie stehen da nicht drin. Über diese Kollisionmap können dann auch interaktionen laufen.

    virtual void Collision(Parameter) = 0; //Parameter falls relevant ist, was kollidiert, pure virtual für jedes Object, das collision hat.
    virtual Object* CheckCollision() = CheckCollision //vector, falls es mehrere Collisionen auf einmal geben kann. 
    //Falls das ausgeschlossen ist, braucht man den vector nicht
    {
     //prüfe auf der map, welche objecte collidieren, angenommen mal es ist nur eins
     ...
     return collidingObject; //0 wenns keins gibt
    }
    
    Object* check = object.CheckCollision();
    if(check) {
    check->Collision(parameter);
    }
    

    Beispiel für eine update funktion für ein AI object:

    void update() {
    Move(AI.CalculateNextMove()); //funktion Move ist (indirekt) für collisionen verantwortlich, wird aber an sich ein ähnlicher 2-6 zeiler sein
    Draw();
    }
    

    Ich will nicht sagen dass das immer der beste Weg ist - häufen sich die Probleme mit Lastwafen/Turm/Panzer muss man eventuell stellenweise umbauen.



  • Das "= CheckCollision" oben ist natürlich falsch. Der code war auf die schnelle mal hingerotzt, um das Konzept zu erklären.



  • @Topic: ich hab in meinem Projekt bisher 15k Zeilen Code (steht noch ganz am Anfang), darin hab ich ganze 4 dynamic_casts gefunden:
    1x im tatsächlichen Anwendungscode, 3x in Unit-Tests. Es handelt sich ausschließlich um die Rückgabe einer abstrakten generischen Fabrik, bei der ich in der Anwendung erstens weiß, dass sie ein Objekt eines bestimmten konkreten Typs zurückgibt (weil ich es direkt vorher angefordert habe) und zweitens eine spezielle Methode dieses konkreten Typs brauche, die nur dieser eine Typ hat:

    class ClientCommunicator /*...*/;
    class LocalClientCommunicator : public ClientCommunicator /*...*/;
    
    /* ... */
    
        data->createClientCommunicator(communication::ConnectionType::LOCAL);
    
        communication::LocalClientCommunicator* localComm = 
          dynamic_cast<communication::LocalClientCommunicator*>(data->clientCommunicator.get());
        if (localComm)
        {
          data->server.reset(new server::Server(localComm->getServerCommunicator())); //getServerCommunicator only avialable on LocalClientCommunicator
          localComm->setServer(data->server);
        }
    

    Die 3 dynamic_casts in den Unit-tests sind Überprüfungen, dass die Factory auch liefert, was ich erwarte, bzw. auf konkrete Mocking-Typen, die ich der Factory vorher "injiziert" habe.



  • pumuckl schrieb:

    [...] Es handelt sich ausschließlich um die Rückgabe einer abstrakten generischen Fabrik, bei der ich in der Anwendung erstens weiß, dass sie ein Objekt eines bestimmten konkreten Typs zurückgibt (weil ich es direkt vorher angefordert habe) und zweitens eine spezielle Methode dieses konkreten Typs brauche, die nur dieser eine Typ hat[...]

    Wieso gibt die Fabrik dann nicht einfach gleich ein Objekt des konkreteren Typs zurück?
    Wenn du an der Stelle die konkrete Fabrik kennen würdest, könntest du ganz einfach einen kovariaten Rückgabetyp verwenden und bist komplett statisch Typsicher. Wenn du die konkrete Fabrik nicht kennst, dann hast du imo einen Widerspruch in deiner Abstraktion. Denn dann sagt deine Schnittstelle, dass es egal ist welche Fabrik daherkommt, aber die Implementierung der Schnittstelle funktioniert tatsächlich nur, wenn es sich um eine ganz bestimmte Art von Fabrik handelt.

    Die dynamic_casts in den Unit-Tests klingen evtl. sinnvoll.



  • kleiner Troll schrieb:

    Wie soll man eine Hierarchie vernünftig aufbauen (um dynamic_cast verwenden zu können), wenn man z.B. solche Objekte hat:

    Man baut doch bitte nicht eine Hierarchie auf, nur um dynamic_cast zu verwenden. Dafür wäre dynamic_cast ja wirklich fehl am Platz.

    Das sehe ich genauso. Mir geht es ja darum, dass Polymorphie und dynamic_cast nicht unbedingt die richtigen für eine Simulation sind.

    Weiß nicht obs bessere designs gibt, aber ne globale physics map, auf der kollisionen gespeichert werden.

    Das meinte dot doch, wenn ich ihn richtig verstanden habe: Für unterschiedliche Typen unterschiedliche Container (mit nicht besitzenden pointern).

    Ich würde stelle mir ein Objekt einfach zusammengesetzt aus mehreren Komponenten vor, d.h.

    Tank
    {
        RenderComponent
        PhysicsComponent
        ArmedComponent
        InputComponent
        ...
    }
    

    Diese Komponenten registrieren sich dann bei einem zentralen System. Im main loop updated man dann nacheinander jedes zentrale System, die sich jeweils um alle Komponenten eines bestimmten Typs kümmern.
    Was tricky an diesem Konzept ist, ist die Frage, wie verschiedene Komponenten/Systeme miteinander kommunizieren. Da bin ich immernoch auf der Suche nach dem Heiligen Gral.
    Toller Blog zum Thema: http://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/



  • Das meinte dot doch, wenn ich ihn richtig verstanden habe: Für unterschiedliche Typen unterschiedliche Container (mit nicht besitzenden pointern).

    Dem würd ich widersprechen, da die physics map eben keinerlei informationen über den Typ hat! Und auch nicht braucht. Das macht das ganze eben sehr leicht erweiter, und modifizierbar.

    Ich würde stelle mir ein Objekt einfach zusammengesetzt aus mehreren Komponenten vor, d.h.

    Tank
    {
        RenderComponent
        PhysicsComponent
        ArmedComponent
        InputComponent
        ...
    }
    

    Diese Komponenten registrieren sich dann bei einem zentralen System. Im main loop updated man dann nacheinander jedes zentrale System, die sich jeweils um alle Komponenten eines bestimmten Typs kümmern.

    Das ganze ohne Vererbung? Soll dann deine zentrale Stelle wirklich so aussehen?

    class Zentral {
    vector<Tank> tanks;
    vector<Buggy> buggys;
    vector<Car> cars;
    vector<Truck> trucks;
    vector<SpecOps> specOps;
    vector<Infanterie> infanterie;
    vector<Stone> stones;
    vector<Airplane> airplanes;
    //...
    void RegisterTanks(const Tank&);
    //...
    
    void loop()
    {
       for(vector<Tank>::iterator i = tanks.begin(); i != tanks.end(); ++i)
       {
          //...
       }
       for(vector<Tank>::iterator i = buggys.begin(); i != buggys.end(); ++i)
       {
          //...
       }
          //...
    }
    }
    

    Das sind ja rieseige code-Mengen, die eigentlich alle fast dasselbe machen. Und fügt man einen neuen Typ hinzu, darf man wieder erneut alles in der Hauptschleife ändern und modifizieren. Ich hab mir grad mal den Blog, und daraufhin entity systems angeschaut. Das wird durchaus auch mit virtual vererbung und einem einzelnen container mit einer update funktion implementiert, was letztlich wieder zu demselben Problem führt, wo man mal dynamic cast brauchen könnte. Ist aber imho deutlich sinnvoller als alle Typen in die main loop reinzuschreiben 😕
    Das mit entity systems hört sich ineteressant an - das dynamic cast Problem umgeht man damit aber prinzipiell nicht, falls es nicht um biegen und brechen darum geht, das auf jedenfall zu vermeiden.



  • kleiner Troll schrieb:

    Das sind ja rieseige code-Mengen, die eigentlich alle fast dasselbe machen. Und fügt man einen neuen Typ hinzu, darf man wieder erneut alles in der Hauptschleife ändern und modifizieren.

    Wenn du sowas hast, dann hast du genau was falsch gemacht. Eine Klasse kann nicht nur ein Interface implementieren...



  • @kleiner Troll
    Ein Komponentensystem kannst du so aufbauen, dass du eine Klasse "Entity" hast, welche einen Container mit Komponenten hat. (Alle Komponenten erben natürlich von einer virtuellen Basisklasse.)
    Wenn das Objekt geupdated wird, durchläuft es alle seine Komponenten. Die Klassen selbst kannst du dann auch beliebig zur Laufzeit einlesen.

    Edit:
    Ich stelle mal mein (gerade erdachtes) System für Komponenten als Ansatz vor:

    enum ComponentType
    {
      physics_component, 
      graphics_component, 
      // ...
    };
    
    class Entity
    {
      std::map<ComponentType, std::unique_ptr<Component>> components_;
    public:
      bool deleted;
      float position[3]; // wird vec3 (position)
      float rotation[4]; // wird quaternion (rotation)
      Entity();
      Entity(Entity &&entity);
      void addComponent(std::unique_ptr<Component> component);
      void removeComponent(ComponentType type);
      Component *getComponent(ComponentType type); // Unschön, caller muss casten. -.-
      void tick(std::list<Entity> &entities, float elapsed_time);
    private:
      Entity(const Entity &);
      Entity &operator = (const Entity &);
    };
    
    class Component
    {
      const ComponentType component_type_;
    public:
      virtual ~Component()
      {}
      ComponentType type() const
      {
        return component_type_;
      }
      virtual void tick(Entity &entity, std::list<Entity> &entities, float elapsed_time) = 0;
    protected:
      Component(ComponentType component_type)
        : component_type_(component_type)
      {}
    private:
      Component(const Component &);
      Component &operator = (const Component &);
    };
    

    Ich glaube die Kommunikation untereinander regel ich ganz einfach über Interfaces. Das führt im Gegensatz zu einem Messagesystem natürlich dazu, dass bestimmte Komponennten voneinander wissen müssen, aber das ist dann halt so. Also die Grafikkomponente muss im Zweifelsfall halt etwas von der Animationskomponente wissen.

    Ich habe das hier mal reingestellt, da ich das System auch noch nicht als wirklich schön empfinde, aber vielleicht haben ja andere noch ein paar richtig gute Ideen dazu.



  • ? Erkläre dich. Will auch darauf hinweisen, das ich "fast" geschrieben - unterschiede gibts schon, sonst hätte man ja keine unterschiedlichen klassen - nur grafik rendern muss jeder. Und wenn du das auch noch explizit in der hauptschleife machst, riechts langsam nach gottklasse...:/
    Anders ist es eh nur n call auf eine Render-Methode. Ähnlich bei der Physik.

    Falls du was andres gemeint hast, versteh ich nicht, worauf du hinauswillst. Soweit ich sehe ist von Object ableiten (oder mit ähnlichem Namen) so ziemlich der gebräuchlichste Weg in der Spieleindustrie (Das von Gorb erwähnte entity model kommt erst langsam auf). Ist ja nicht so als wärs nur meine Idee.



  • Nachtrag:
    Böse dazwischen gepostet cooky!
    Die variante gefällt mir schon deutlich besser als das, was Gorb vorgeschlagen hat. Aber:

    Component *getComponent(ComponentType type); // Unschön, caller muss casten. -.-
    

    Womit wir wieder in einer Situation wären, bei der man wie ich ursprünglich gemeint habe dynamic_cast brauchen kann (z.B. Entities mit bestimmten componenten aussortieren, Fehlerprüfung wenn man komponente vom typ "Type" holt...). Das Design legt wieder dynamic_cast nahe - langsam Zweifel ich etwas dran, wie ernst man "wenn du dynamic_cast brauchen kannst, hast du ein designproblem" eigentlich nehmen kann.



  • Jain. dynamic_cast braucht man hier zumindest nie, der Typ ist ja immer klar durch type() gegeben. Also static_cast kann man schon ziemlich ungefährlich nutzen. (Der Typ wird ja auch im Konstruktor der abgeleiteten Klasse übergeben, da kann also auch nichts schief gehen.)
    Der cast bleibt natürlich unschön, aber logisch gesehen ist der Typ immer gegeben.


Anmelden zum Antworten