dynamic_cast böse
-
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.
-
Was spricht an der Stelle gegen ein Template und ein wenig TMP Magie?
Den Typ musst du ja eh angeben und den Cast konkret hinschreiben.template <unsigned ComponentType> typename tmp_get_component_type<ComponentType>::type * getComponent() { return static_cast<typename tmp_get_component_type<ComponentType::type *>(getComponent(ComponentType)); }
Dabei bildet die MetaFunktion tmp_get_component_type den unsiged (enum) auf den dazugehören konkreten ComponentType ab.
-
cooky451 schrieb:
std::map<ComponentType, std::unique_ptr<Component>> components_;
-
dot schrieb:
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.Es handelt sich nicht um eine Fabrik im "Objektorientierten" Sinne mit Laufzeitpolymorphismus etc., sondern um ein kleines Template-Biest auf Basis von std::function's an Stelle der konkreten Fabrik, gewürzt mit optionaler pre/post-construction/destruction Policy. Zur Veranschaulichung mal ein bisschen Code:
template <class Signature, class DefaultCreator, class DefaultDeleter, class ManagementPolicy = detail::NoManagemetPolicy> class GenericAbstractFactory : public ManagementPolicy { typedef typename boost::function_types::result_type< Signature >::type ResultType; typedef typename boost::mpl::if_< typename std::is_pointer<ResultType>::type, ResultType, typename std::add_reference<ResultType>::type >::type DeleterArg; public: typedef Signature CreatorSignature; typedef void DeleterSignature(DeleterArg); typedef std::function<CreatorSignature> Creator; typedef std::function<DeleterSignature> Deleter; private: Creator theCreator; Deleter theDeleter; public: void replace(Creator const& newCreator, Deleter const& newDeleter) { theCreator = newCreator; theDeleter = newDeleter; } void restore() { replace(DefaultCreator(), DefaultDeleter()); } ResultType create(/* passende Argumente... */) { ManagementPolicy::preCreation(); ResultType r = theCreator(/* argumente */); ManagementPolicy::postCreation(r); return r; } void destroy(DeleterArg r) { ManagementPolicy::preDestruction(r); theDeleter(r); ManagementPolicy::postDestruction(); } };
Benutzung etwa wie folgt:
//communicatorfactory.h struct DefaultCommunicatorCreator { ClientCommunicator* operator()(ServerEventCallback const& serverEventCallback, ConnectionType connType) { switch (connType.underlying()) { case ConnectionType::LOCAL: return new LocalClientCommunicator(serverEventCallback); /* ... */ } } }; typedef util::GenericAbstractFactory< ClientCommunicator*(communication::ServerEventCallback const&, communication::ConnectionType), DefaultCommunicatorCreator, std::default_delete<ClientCommunicator>, util::SingleProductPolicy<ClientCommunicator> > CommunicatorFactory; /* ... */ //install logging CommunicatorFactory::instance().replace( [&myLogger](communication::ServerEventCallback const& sec, communication::ConnectionType ct) { myLogger << "Creation!\n"; DefaultCommunicatorCreator dcc; return dcc(sec, ct); }, [&myLogger](ClientCommunicator* cc) { myLogger << "Deletion!\n"; delete cc; }); ClientCommunicator* myCc = CommunicatorFactory::instance().create(/* ... */); //logged CommunicatorFactory::instance().destroy(myCc); //logged CommunicatorFactory::instance().restore(); //return to default creation
Kurz: Es gibt nur die eine Factory, sie wird nicht durch eine konkrete Fabrik ersetzt, sondern lediglich die Erzeugerfunktion wird ersetzt. Der Rückgabetyp ist immer gleich.
-
Ja gut, wenn man es unbedingt genau so machen muss, dann geht das natürlich nicht. Ich geh mal davon aus, dass du verdammt gute Gründe hast das so zu machen
-
dot schrieb:
Ich geh mal davon aus, dass du verdammt gute Gründe hast das so zu machen
Verschiedene - ob sie so verdammt gut sind, mag ich nicht beurteilen
So kann ich wie angedeutet zum Beispiel diverse Mechanismen relativ einfach einschleusen, ohne jedesmal ein ganzes Klasseninterface neu implementieren zu müssen. (Hab ich schonmal erwähnt, dass ich überzeugt bin, dass vieles, was bisher mit Laufzeitpolymorphie und einer Basisklasse ohne Membervariablen und ein oder zwei virtuellen Funktionen implementiert wurde, künftig mit Funktionsobjekten und lambdas implementiert wird? ;))
Meine Unit-Tests ersetzen zum Beispiel die Funktoren durch Mocks aus einem Mocking-Framework, so kann ich sicherstellen, dass die Factory so aufgerufen wird, wie ichs erwarte, und genau das zurückliefern, was ich grade benötige - nämlich meistens auch Mocks.
Ich benutze mehrere Factories in meinem Projekt, die zwar verschiedene Signaturen und Produkt-Typen haben, aber gleiche/ähnliche Management-Policies benutzen und auch das Replacement der Creator/Deleter-Funktoren benötigen.Zu guter Letzt: selbst wenn ich das gute alte objektorientierte Abstract Factory Pattern bemüht hätte, müsste ich immernoch auf die abstrakte Fabrik zugreifen oder aber per dynamic_cast die konkrete Fabrik herauszaubern - der Cast würde nur auf eine andere Ebene verlagert.
-
aber per dynamic_cast die konkrete Fabrik herauszaubern - der Cast würde nur auf eine andere Ebene verlagert.
Um das hochcasten kommst immer dann nicht drumherum, wenn du spezialisierte Schnittstellen durch generisches tarrain transportieren musst.
Plugins sind da wohl der Präzedenz-Fall ...
Nur zieht das nicht zwangslauefig nen dynamic cast hinter sich ... im gegenteil, meist wuerde das nicht mal funktionieren.
Man baut oft Schnittstellen mehrstufig, und am ende hat man immer noch die Implementation ...
Beispiel:
generische Schnittstelle: IPlugin
Spezialisierte Schnittstelle: IMyViewIrgendwasPlugin
ImplementationsKlasse: XYZPluginMyViewImplOft ist nicht ma IMyViewIrgendwasPlugin von IPlugin abgeleitet, sondern man macht nen Schnittstelenwechsel in dem man sich nen generischen Zeiger auf das interface geben laesst, meist vom typ void * und dann sofort hochcastet auf das richtige Interface, aber nicht auf die Implmentation.
Mit dynamic_cast kannst sowas gar ned machen ...Auf deine eigene Typcodierung musst dich aber wiederum schon verlassen koennen (sonst gibts datenmuell, iss aber auch klar). Warum dann also RTTI dazu verwenden ?
Ciao ...
-
pumuckl schrieb:
So kann ich wie angedeutet zum Beispiel diverse Mechanismen relativ einfach einschleusen, ohne jedesmal ein ganzes Klasseninterface neu implementieren zu müssen.
Du könntest aber auch deine Factory von einem allgemeineren Factory-Interface ableiten:
template <typename T, typename... Args> class IFactory { public: virtual T* create(Args... args); };
Deine Generatoren könnten dann eine konkrete Instanz liefern und die create Methode der konkreten Factory einen kovarianten Returntype haben. An Stellen wo die konkrete Factory bekannt ist, kennst du so komplett statisch Typsicher und ganz ohne irgendeinen cast die erzeugten Objekte über ihren konkreten Typ, wobei du immer noch dieses Template und deinen Creator und Deleter verwenden kannst. Nachdem dann alles statisch Typsicher ist, brauchst du dann auch keine Tests mehr, die überprüfen ob die dynamischen Typen den Erwartungen entsprechen.
pumuckl schrieb:
Zu guter Letzt: selbst wenn ich das gute alte objektorientierte Abstract Factory Pattern bemüht hätte, müsste ich immernoch auf die abstrakte Fabrik zugreifen oder aber per dynamic_cast die konkrete Fabrik herauszaubern - der Cast würde nur auf eine andere Ebene verlagert.
Der Punkt ist doch, dass du die konkrete Fabrik kennen musst. Wenn du nur die abstrakte Fabrik kennst, dann hast du imo (aus bereits genannten Gründen) von vornherein einen Widerspruch in deinem Design.
-
volkard schrieb:
cooky451 schrieb:
std::map<ComponentType, std::unique_ptr<Component>> components_;
Hm. Was schlägst du als Verbesserung vor? Oder würdest du weiter ein OOP Modell bevorzugen?
-
cooky451 schrieb:
volkard schrieb:
cooky451 schrieb:
std::map<ComponentType, std::unique_ptr<Component>> components_;
Hm. Was schlägst du als Verbesserung vor? Oder würdest du weiter ein OOP Modell bevorzugen?
Du machst hier einen Baukasten, der Layering ermöglicht, falls ich den Sinn recht verstehe, obwohl das viel besser als Sprachmittel schon da ist.
Was ich als Verbesserung vorschlage? Na, Löschen natürlich.
-
Erstmal zu einer Implementierung von sowas wie der entity Klasse:
Das kann man noch etwas sauberer machen, indem man type erasure verwendet. Ich selbst habe eine Klasse die dieses interface anbietet:class entity { template< class Type > bool add_component( Type && ); //false falls Type schon in entity template< class Type > Type *query(); //gibt pointer auf die interne Instanz von Type zurück (oder 0) }; //////////////////////////////////////////////////////////////////// class A{}; entity ent; ent.add_component( A() ); ent.query< A >();
Allerdings ist das nicht Sinn eines entity systems.
Ich speichere jeden Komponententyp in einem seperaten Container ab (nicht nur Pointer, die eigentlichen Instanzen). Jedes Komponentobjekt hat zusätzlich einen Pointer auf eine Struktur, die nicht mehr beinhaltet als eine id und Referenzzähler und damit die entity repräsentiert. Jeder id können so mehrer Komponenten zugeordnet sein (aber immer nur 1 je Typ).
Nachdem eine entity zum entity system hinzugefügt wurde, übergebe ich jedem Komponenten eine Klasse, in der Pointer auf alle zur entity gehörigen Komponenten perquery
(s.o.) abgefragt werden können. Die Komponente kann damit anstellen, was sie will, z.B. pointer auf andere Komponenten speichern (die RenderComponent braucht einen pointer auf die TransformComponent, um an der richtigen Stelle gezeichnet werden zu können), oder callbacks in anderen Komponenten setzen (z.B. sich bei onCollision in der TransformComponent registrieren).
Im mainloop muss jetzt nur jedes sub system einmal geupdatet werden. Der Renderer wird beispielsweise über den Container mit RenderComponent s iterieren und jedes Objekt zeichnen (oder auch nicht, je nachdem). Das PhysicsSystem wird sich darum kümmern, dass auf Kollision überprüft wird und evtl. callbacks aufrufen.
Man spart sich virtual function calls ohne Ende, und hat zusätzlich verschiedene Aspekte der Simulation voneinander getrennt (Warum sollte Grafik irgendwas mit Input zu tun haben?). Das eröffnet die Möglichkeit, mehr parallel abzuarbeiten.volkard schrieb:
cooky451 schrieb:
std::map<ComponentType, std::unique_ptr<Component>> components_;
Das sehe ich nicht so. Wie würdest du denn das Problem mit Lastwagen, Panzer und Geschützturm von vorhin mit Polymorphie lösen?
Außerdem ist rtti viel langsamer als es sein könnte, weil die Compilerbauer Rücksicht auf dlls nehmen. Da kann es sinnvoller sein, sich selbst was zu bauen, vor allem wenn es von außen mindestens genauso gut aussieht (und nichtmal besonders komplex ist).
-
GorbGorb schrieb:
volkard schrieb:
]
Das sehe ich nicht so. Wie würdest du denn das Problem mit Lastwagen, Panzer und Geschützturm von vorhin mit Polymorphie lösen?Also das da:
Lastwagen: beweglich, unbewaffnet
Panzer: beweglich, bewaffnet
Geschützturm: unbeweglich, bewaffneta) bewegeDich() auf Geschütztürmen tut nichts.
a1) Geschütztürme haben eine Geschwindigkeit von 0
c) Lastwagen und Panzer erben von BeweglichesDing
c1) und getBeweglichesDingPtr(){return this;}
Ach, mir fiele schon was passendes ein, denke ich.GorbGorb schrieb:
Außerdem ist rtti viel langsamer als es sein könnte, weil die Compilerbauer Rücksicht auf dlls nehmen. Da kann es sinnvoller sein, sich selbst was zu bauen,
Ups, hab ich in c1) ja schon gemacht.
GorbGorb schrieb:
vor allem wenn es von außen mindestens genauso gut aussieht (und nichtmal besonders komplex ist).
Im Gegensatz zu Dir, sehe ich nicht, was durch solche Entities gelöst wird, was sich anders nicht viel besser anfühlen würde. Zum Beispiel denke ich beim Bedarf einer Liste aller beweglichen Objekte zuerst an eine intrusive doppelt verkettete Liste.
-
@volkard
Hast du das schon mal mit etwas komplexeren Dingen ausprobiert? Bei mir endet das dann so:class Object {}; class MovingObject : virtual public Object {}; class VisibleObject : virtual public Object {}; class PhysicalObject : virtual public Object {}; class ActiveObject : virtual public Object {}; ...
Und jedes neue Objekt muss weiter hart gecoded werden. Modulbasiert kannst du die Objekte z.B. einfach aus einem Script parsen - der C++ Code muss nicht mal wissen, was für Objekte es in dem Spiel eigentlich gibt.
Es geht natürlich auch so, aber wie verhinderst du dann die dynamic_cast Orgie?
-
cooky451 schrieb:
@volkard
Hast du das schon mal mit etwas komplexeren Dingen ausprobiert?Nein. Komplexere Dinge vermeide ich.