Was ist die "richtige OOP" -Methode, um mit einem Speicherpool von Elementen gemischter Typen umzugehen?

8

Dies wurde durch einen Kommentar zu meiner anderen Frage hier inspiriert:

Wie wiederholst du dich", wenn du einer Klasse in C ++ einen zugänglichen "Namen" gibst?

nvoight: "RTTI ist schlecht, weil es ein Hinweis ist, dass du keine gute OOP machst. Deine eigene Homebrew-RTTI macht es nicht besser, OOP, es bedeutet nur, dass du das Rad auf der Basis von schlechtem OOP neu erfindest."

Was ist also die "gute OOP" -Lösung? Das Problem ist das. Das Programm ist in C ++, daher gibt es auch C ++ spezifische Details, die unten erwähnt werden. Ich habe eine "Komponenten" -Klasse (eigentlich eine Struktur), die in eine Reihe von verschiedenen abgeleiteten Klassen unterklassifiziert ist, die verschiedene Arten von Komponentendaten enthalten. Es ist Teil eines "Entity Component System" -Designs für ein Spiel. Ich wundere mich über die Lagerung der Komponenten. Insbesondere hat das aktuelle Speichersystem:

  1. ein "Komponenten-Manager", der ein Array, eigentlich eine Hash-Map, eines einzelnen Komponententyps speichert. Die Hash-Map ermöglicht das Nachschlagen einer Komponente anhand der Entitäts-ID der Entität, zu der sie gehört. Dieser Komponentenmanager ist eine Vorlage, die von einer Basis erbt, und der Vorlagenparameter ist der Typ der zu verwaltenden Komponente.

  2. ein vollständiges Speicherpaket, bei dem es sich um eine Sammlung dieser Komponentenmanager handelt, die als ein Array von Zeigern für die Basisklasse des Komponentenmanagers implementiert sind. Dies hat Methoden zum Einfügen und Extrahieren einer Entität (beim Einfügen werden die Komponenten herausgenommen und in die Manager eingegeben, beim Entfernen werden sie extrahiert und in einem neuen Entitätsobjekt gesammelt), ebenso wie solche, um neue Komponentenmanager hinzuzufügen Wenn wir dem Spiel einen neuen Komponententyp hinzufügen wollen, müssen wir nur einen weiteren Befehl einfügen, um einen Komponentenmanager für das Spiel einzufügen.

Es ist das volle Speicherpaket, das dies ausgelöst hat. Insbesondere bietet es keine Möglichkeit, auf einen bestimmten Komponententyp zuzugreifen . Alle Komponenten sind als Basisklassenzeiger ohne Typinformation gespeichert. Ich dachte an eine Art von RTTI und speicherte die Komponentenmanager in einer Map, die Typnamen abbildet und somit das Nachschlagen und dann das korrekte Downcasting des Basisklassenzeigers auf die entsprechende abgeleitete Klasse ermöglicht (der Benutzer würde ein Template-Member aufrufen) auf dem Entity-Speicherpool, um dies zu tun).

Aber wenn diese RTTI schlechte OOP bedeutet, was wäre der richtige Weg, um dieses System so zu entwerfen, dass keine RTTI benötigt wird?

    
The_Sympathizer 19.08.2016, 09:13
quelle

3 Antworten

6

Disclaimer / Ressourcen: Meine BCS-These war über den Entwurf und die Implementierung eines C ++ 14 Bibliothek für Kompilierungszeit Entity-Component-System Mustererzeugung. Sie finden die Bibliothek hier auf GitHub .

Diese Antwort soll Ihnen einen umfassenden Überblick über einige Techniken / Ideen geben, die Sie anwenden können, um das Entity-Component-System-Muster zu implementieren, abhängig davon, ob Komponenten- / Systemtypen zur Kompilierungszeit bekannt sind.

Wenn Sie Implementierungsdetails sehen möchten, empfehle ich Ihnen, meine Bibliothek (oben verlinkt) für einen vollständig kompilierzeitbasierten Ansatz auszuprobieren. diana ist eine sehr nette C-Bibliothek, die Ihnen eine Vorstellung von einem laufzeitbasierten Ansatz geben kann.

Je nach Umfang / Umfang Ihres Projekts und der Art Ihrer Entitäten / Komponenten / Systeme gibt es verschiedene Ansätze.

  1. Alle Komponententypen und Systemtypen sind zur Kompilierungszeit bekannt.

    Dies ist der Fall, der in meiner BCS-These analysiert wurde - was Sie tun können, ist fortgeschrittene Metaprogrammierungstechniken zu verwenden (zB Boost.Hana <) / a>) um alle Komponententypen und Systemtypen in Kompilierzeitlisten zu setzen und Datenstrukturen zu erstellen, die zur Kompilierungszeit alles miteinander verknüpfen. Pseudocode Beispiel:

    %Vor%

    Nachdem Sie Ihre Komponenten definiert haben, können Sie Ihre Systeme definieren und ihnen sagen, welche Komponenten zu verwenden sind:

    %Vor%

    Alles, was übrig bleibt, ist die Verwendung einer Art Kontextobjekt und Lambda-Überladung, um die Systeme zu besuchen und ihre Verarbeitungsmethoden aufzurufen:

    %Vor%

    Sie können das gesamte Wissen zur Kompilierzeit verwenden, um geeignete Datenstrukturen für Komponenten und Systeme innerhalb des Kontextobjekts zu generieren.

Dies ist der Ansatz, den ich in meiner Diplomarbeit und Bibliothek verwendet habe - ich habe darüber auf C ++ Now 2016 gesprochen: "Implementierung einer multithreaded Kompilierzeit ECS in C ++ 14 ".

  1. Alle Komponententypen und Systemtypen sind zur Laufzeit bekannt.

    Das ist eine völlig andere Situation - Sie müssen eine Art von Löschtechnik verwenden, um dynamisch mit Komponenten und Systemen umzugehen. Eine geeignete Lösung besteht darin, eine Skriptsprache wie LUA zu verwenden, um Systemlogik und / oder Komponentenstruktur zu behandeln (eine effizientere einfache Komponentendefinitionssprache kann auch handgeschrieben sein, so dass sie Eins-zu-Eins-C ++ - Typen oder zu den Motortypen) .

    Sie benötigen eine Art Kontextobjekt, in dem Sie Komponententypen und Systemtypen zur Laufzeit registrieren können . Ich empfehle entweder eindeutige inkrementierende IDs oder eine Art von UUIDs, um Komponenten- / Systemtypen zu identifizieren. Nach dem Zuordnen von Systemlogik und Komponentenstrukturen zu IDs können Sie diese in Ihrer ECS-Implementierung weitergeben, um Daten und Prozesselemente abzurufen. Sie können Komponentendaten in generischen resizierbaren Puffern speichern (oder assoziative Zuordnungen für große Container) , die zur Laufzeit dank des Komponentenstrukturwissens geändert werden können - hier ein Beispiel dafür, was ich meine:

    > %Vor%
  1. Einige Komponententypen und Systemtypen sind zur Kompilierzeit bekannt, andere sind zur Laufzeit bekannt.

    Verwenden Sie approach (1) und erstellen Sie eine Art "bridge" Komponenten- und Systemtypen, die eine beliebige Löschmethode implementieren, um auf die Komponentenstruktur oder das System zuzugreifen Logik zur Laufzeit. Ein std::map<runtime_system_id, std::function<...>> kann für die Verarbeitung der Laufzeit-Systemlogik verwendet werden. Ein std::unique_ptr<runtime_component_data> oder ein std::aligned_storage_t<some_reasonable_size> kann für die Laufzeitkomponentenstruktur funktionieren.

Um Ihre Frage zu beantworten:

  

Aber wenn diese RTTI schlechte OOP bedeutet, was wäre der richtige Weg, um dieses System so zu entwerfen, dass keine RTTI benötigt wird?

Sie benötigen eine Möglichkeit zum Zuordnen von Typen zu Werten, die Sie zur Laufzeit verwenden können: RTTI ist eine geeignete Methode dafür.

Wenn Sie RTTI nicht verwenden möchten und immer noch eine polymorphe Vererbung verwenden möchten, um Ihre Komponententypen zu definieren, müssen Sie eine Möglichkeit implementieren, eine Art von Laufzeittyp-ID aus einem abgeleiteten Komponententyp abzurufen. Hier ist eine primitive Art, das zu tun:

%Vor%

Erläuterung: get_next_type_id ist eine nicht static Funktion (zwischen den Übersetzungseinheiten geteilt) , die einen static inkrementellen Zähler vom Typ IDs speichert. Um die eindeutige Typ-ID abzurufen, die einem bestimmten Komponententyp entspricht, können Sie Folgendes aufrufen:

%Vor%

Die Funktion get_type_id "public" ruft die eindeutige ID aus der entsprechenden Instanziierung von impl::type_id_storage ab, die get_next_type_id() für die Konstruktion aufruft, die wiederum ihren aktuellen next_type_id -Zähler zurückgibt Wert und erhöht es für den nächsten Typ.

Besondere Vorsicht ist bei dieser Vorgehensweise geboten, um sicherzustellen, dass sie sich über mehrere Übersetzungseinheiten korrekt verhält und Race-Bedingungen vermieden werden (falls Ihre ECS Multithread-basiert ist) . ( Weitere Informationen hier .)

Nun, um Ihr Problem zu lösen:

  

Es ist das volle Speicherpaket, das dies ausgelöst hat. Insbesondere bietet es keine Möglichkeit, auf einen bestimmten Komponententyp zuzugreifen.

%Vor%

Sie können dieses Muster in meiner alten und verlassenen SSVentitySystem -Bibliothek verwenden. Sie können einen RTTI-basierten Ansatz in meiner alten und veralteten "Implementierung eines komponentenbasierten Entitätssystems in modernem C ++" sehen. CppCon 2015 Gespräch.

    
Vittorio Romeo 19.08.2016 09:41
quelle
1

Trotz der guten und langen Antwort von @VittorioRomeo möchte ich eine andere mögliche Herangehensweise an das Problem zeigen.
Grundlegende Konzepte, die hier involviert sind, sind Typ Löschen und doppeltes Senden .
Die folgende ist ein minimales, funktionierendes Beispiel:

%Vor%

Ich habe mich bemüht, den wenigen vom OP erwähnten Punkten gerecht zu werden, um eine Lösung zu finden, die dem wirklichen Problem entspricht.

Lassen Sie mich mit einem Kommentar wissen, ob das Beispiel für sich selbst klar genug ist oder ob einige weitere Details benötigt werden, um zu erklären, wie und warum es tatsächlich funktioniert.

    
skypjack 19.08.2016 12:13
quelle
0

Wenn ich richtig verstanden habe, möchten Sie eine Sammlung, z. B. eine Karte, wo die Werte von unterschiedlichem Typ sind, und Sie möchten wissen, welcher Typ der jeweilige Wert ist (damit Sie ihn reduzieren können).

Nun ist ein "guter OOP" ein Entwurf, den Sie nicht ablehnen müssen. Sie rufen einfach die Mothods (die der Basisklasse und den Ableitungen gemeinsam sind) auf, und die abgeleitete Klasse führt für dieselbe Methode eine andere Operation aus als ihr Parent.

Wenn dies beispielsweise nicht der Fall ist, wenn Sie andere Daten vom Kind verwenden müssen und daher eine Downcast-Funktion ausführen möchten, bedeutet dies, dass Sie in den meisten Fällen nicht intensiv genug am Design gearbeitet haben. Ich sage nicht, dass es immer möglich ist, aber Sie müssen es so gestalten, dass der Polymorphismus Ihr einziges Werkzeug ist. Das ist ein "gutes OOP".

Wenn Sie wirklich Downcast benötigen, müssen Sie RTTI nicht verwenden. Sie können ein gemeinsames Feld (string) in der Basisklasse verwenden, das den Klassentyp kennzeichnet.

    
Israel Unterman 19.08.2016 09:23
quelle

Tags und Links