Komponentenbasiertes Design: Umgang mit der Interaktion von Objekten

9

Ich bin mir nicht sicher, wie genau Objekte Dinge mit anderen Objekten in einem komponentenbasierten Design tun.

Angenommen, ich habe eine ObjKlasse. Ich mache:

Obj obj;
obj.add(new Position());
obj.add(new Physics());

Wie könnte ich dann ein anderes Objekt haben, das nicht nur den Ball bewegt, sondern auch diese Physik anwendet? Ich suche nicht nach Implementierungsdetails, sondern abstrakt, wie Objekte kommunizieren. In einem entitätsbasierten Design haben Sie möglicherweise nur Folgendes:

obj1.emitForceOn(obj2,5.0,0.0,0.0);

Jeder Artikel oder jede Erklärung, um ein komponentengesteuertes Design besser zu verstehen und grundlegende Dinge zu tun, wäre wirklich hilfreich.

jmasterx
quelle

Antworten:

10

Dies geschieht normalerweise mithilfe von Nachrichten. Sie können viele Details in anderen Fragen auf dieser Site finden, wie hier oder dort .

Um auf Ihr spezielles Beispiel zu antworten, müssen Sie eine kleine MessageKlasse definieren , die Ihre Objekte verarbeiten können, z.

struct Message
{
    Message(const Objt& sender, const std::string& msg)
        : m_sender(&sender)
        , m_msg(msg) {}
    const Obj* m_sender;
    std::string m_msg;
};

void Obj::Process(const Message& msg)
{
    for (int i=0; i<m_components.size(); ++i)
    {
        // let components do some stuff with msg
        m_components[i].Process(msg);
    }
}

Auf diese Weise "verschmutzen" Sie Ihre ObjKlassenschnittstelle nicht mit komponentenbezogenen Methoden. Einige Komponenten können die Nachricht verarbeiten, andere ignorieren sie möglicherweise einfach.

Sie können beginnen, indem Sie diese Methode direkt von einem anderen Objekt aus aufrufen:

Message msg(obj1, "EmitForce(5.0,0.0,0.0)");
obj2.ProcessMessage(msg);

In diesem Fall obj2wählt s Physicsdie Nachricht aus und führt die erforderliche Verarbeitung durch. Wenn Sie fertig sind, wird es entweder:

  • Senden Sie eine "SetPosition" -Nachricht an sich selbst, die von der PositionKomponente ausgewählt wird.
  • Oder greifen Sie direkt auf die PositionKomponente zu, um Änderungen vorzunehmen (völlig falsch für ein reines komponentenbasiertes Design, da Sie nicht davon ausgehen können, dass jedes Objekt eine PositionKomponente hat, die PositionKomponente jedoch möglicherweise eine Anforderung von ist Physics).

Es ist generell eine gute Idee , verzögern die eigentliche Verarbeitung der Nachricht an die nächste Komponente zu aktualisieren. Die sofortige Verarbeitung kann das Senden von Nachrichten an andere Komponenten anderer Objekte bedeuten. Das Senden von nur einer Nachricht kann also schnell einen untrennbaren Spaghetti-Stapel bedeuten.

Sie müssen sich wahrscheinlich später für ein erweitertes System entscheiden: asynchrone Nachrichtenwarteschlangen, Senden von Nachrichten an Objektgruppen, Registrieren / Aufheben der Registrierung von Nachrichten pro Komponente usw.

Die MessageKlasse kann ein generischer Container für eine einfache Zeichenfolge sein, wie oben gezeigt, aber die Verarbeitung von Zeichenfolgen zur Laufzeit ist nicht wirklich effizient. Sie können einen Container mit generischen Werten verwenden: Zeichenfolgen, Ganzzahlen, Gleitkommazahlen ... Mit einem Namen oder noch besser einer ID, um verschiedene Arten von Nachrichten zu unterscheiden. Sie können auch eine Basisklasse ableiten, die bestimmten Anforderungen entspricht. In Ihrem Fall können Sie sich einen vorstellen EmitForceMessage, der von Messagedem gewünschten Kraftvektor abgeleitet ist und diesen hinzufügt. Beachten Sie jedoch die Laufzeitkosten von RTTI, wenn Sie dies tun.

Laurent Couvidou
quelle
3
Ich würde mir keine Sorgen über die "Unreinheit" des direkten Zugriffs auf Komponenten machen. Komponenten werden verwendet, um funktionale und gestalterische Anforderungen zu erfüllen, nicht im akademischen Bereich. Sie möchten überprüfen, ob eine Komponente vorhanden ist (z. B., dass der Rückgabewert für den Aufruf der Komponente get nicht null ist).
Sean Middleditch
Ich habe immer daran gedacht, wie Sie zuletzt gesagt haben, mit RTTI, aber so viele Leute haben so viele schlechte Dinge über RTTI
gesagt
@ SeanMiddleditch Sicher, ich würde es so machen und nur erwähnen, um zu verdeutlichen, dass Sie immer überprüfen sollten, was Sie tun, wenn Sie auf andere Komponenten derselben Entität zugreifen.
Laurent Couvidou
@Milo Das vom Compiler implementierte RTTI und sein dynamic_cast kann zu einem Engpass werden, aber darüber würde ich mir vorerst keine Sorgen machen. Sie können dies später noch optimieren, wenn es zu einem Problem wird. CRC-basierte Klassenkennungen wirken wie ein Zauber.
Laurent Couvidou
´template <typename T> uint32_t class_id () {static uint32_t v; return (uint32_t) & v; } ´ - kein RTTI erforderlich.
Arul
3

Um ein ähnliches Problem wie das von Ihnen gezeigte zu lösen, habe ich einige spezifische Komponentenhandler und eine Art Ereignisauflösungssystem hinzugefügt.

Im Fall Ihres "Physik" -Objekts würde es sich bei seiner Initialisierung einem zentralen Manager von Physikobjekten hinzufügen. In der Spielschleife haben diese Art von Managern ihren eigenen Aktualisierungsschritt. Wenn dieser PhysicsManager aktualisiert wird, berechnet er alle physischen Interaktionen und fügt sie einer Ereigniswarteschlange hinzu.

Nachdem Sie alle Ihre Ereignisse erstellt haben, können Sie Ihre Ereigniswarteschlange auflösen, indem Sie einfach überprüfen, was passiert ist, und entsprechende Maßnahmen ergreifen. In Ihrem Fall sollte ein Ereignis vorhanden sein, das besagt, dass Objekt A und B irgendwie interagiert haben, sodass Sie Ihre emitForceOn-Methode aufrufen.

Vorteile dieser Methode:

  • Konzeptionell ist es wirklich einfach zu folgen.
  • Bietet Ihnen Raum für spezifische Optimierungen wie die Verwendung von Quadtress oder was auch immer Sie benötigen würden.
  • Es endet wirklich "Plug and Play". Objekte mit Physik interagieren nicht mit Objekten außerhalb der Physik, da sie für den Manager nicht vorhanden sind.

Nachteile:

  • Am Ende bewegen sich viele Referenzen, so dass es etwas chaotisch werden kann, alles korrekt zu bereinigen, wenn Sie nicht vorsichtig sind (von Ihrer Komponente zum Komponentenbesitzer, vom Manager zur Komponente, von der Veranstaltung zu den Teilnehmern usw.) ).
  • Sie müssen besonders über die Reihenfolge nachdenken, in der Sie alles lösen. Ich denke, es ist nicht Ihr Fall, aber ich sah mich mehr als einer Endlosschleife gegenüber, in der ein Ereignis ein anderes Ereignis erstellt hat, und ich habe es nur direkt zur Ereigniswarteschlange hinzugefügt.

Ich hoffe das hilft.

PS: Wenn jemand einen saubereren / besseren Weg hat, dies zu lösen, würde ich es wirklich gerne hören.

Carlos
quelle
1
obj->Message( "Physics.EmitForce 0.0 1.1 2.2" );
// and some variations such as...
obj->Message( "Physics.EmitForce", "0.0 1.1 2.2" );
obj->Message( "Physics", "EmitForce", "0.0 1.1 2.2" );

Ein paar Dinge, die Sie bei diesem Design beachten sollten:

  • Der Name der Komponente ist der erste Parameter - dies soll verhindern, dass zu viel Code für die Nachricht verarbeitet wird - wir können nicht wissen, welche Komponenten eine Nachricht auslösen könnte - und wir möchten nicht, dass alle eine Nachricht mit 90% Fehler kauen Rate, die in viele unnötige Zweige und Strcmps konvertiert .
  • Der Name der Nachricht ist der zweite Parameter.
  • Der erste Punkt (in Nr. 1 und Nr. 2) ist nicht erforderlich, um das Lesen zu erleichtern (für Personen, nicht für Computer).
  • Es ist sscanf, iostream, you-name-it-kompatibel. Kein syntaktischer Zucker, der die Verarbeitung der Nachricht nicht vereinfacht.
  • Ein Zeichenfolgenparameter: Das Übergeben der nativen Typen ist im Hinblick auf den Speicherbedarf nicht billiger, da Sie eine unbekannte Anzahl von Parametern mit relativ unbekanntem Typ unterstützen müssen.
Schlange5
quelle