Einführung
Im vorherigen Artikel haben wir den Erstellungsprozess für benutzerdefinierte Aspekte überprüft und gezeigt, wie (die meisten) Frontend-Funktionen erstellt werden. In diesem Artikel bauen wir unsere Benutzerfacette weiter auf, indem wir die entsprechenden Backend-Typen implementieren, die Typen registrieren und die Beziehung zwischen Frontend-Objekten und Backend-Objekten einrichten. Dies wird den größten Teil dieses Artikels einnehmen. Im nächsten Artikel sehen wir uns an, wie man Jobs implementiert, um die Komponenten unseres Aspekts zu verarbeiten.
Zur Erinnerung an das, was wir im Sinn haben, hier ist das Architekturdiagramm aus Teil 1:
Erstellen Sie ein Backend
Eines der schönen Dinge an Qt 3D ist, dass es zu einem sehr hohen Durchsatz fähig ist. Dies wird durch die Verwendung von Jobs erreicht, die auf einem Thread-Pool im Backend ausgeführt werden. Um dies tun zu können, ohne ein kompliziertes Netzwerk von Synchronisationspunkten einzuführen (was die Parallelität einschränken würde), erstellen wir die klassische Computersituation mit Kompromissen und Speicheropfern im Interesse der Geschwindigkeit nach. Indem jeder Aspekt an seiner eigenen Kopie der Daten arbeitet, kann er Jobs sicher planen, da er weiß, dass nichts anderes seine Daten berührt.
Es ist nicht so teuer wie es klingt. Backend-Knoten werden nicht von QObject abgeleitet. Die Basisklasse für Backend-Knoten ist Qt3DCore::QBackendNode , eine ziemlich einfache Klasse . Beachten Sie auch, dass Aspekte nur die Daten speichern, die sie im Backend benötigen. Dem Animationsaspekt ist es beispielsweise egal, welche Material -Komponente Entity hat, sodass keine Daten dieser Komponente gespeichert werden müssen. Im Gegensatz dazu betrifft der Rendering-Aspekt keine Animationsclips oder Animator -Komponenten.
In unserem kleinen benutzerdefinierten Aspekt haben wir nur einen Frontend-Komponententyp, FpsMonitor . Logischerweise haben wir nur einen entsprechenden Backend-Typ, den wir bildlich FpsMonitorBacken nennen:
fpsmonitorbackend.h
class FpsMonitorBackend : public Qt3DCore::QBackendNode { public: FpsMonitorBackend() : Qt3DCore::QBackendNode(Qt3DCore::QBackendNode::ReadWrite) , m_rollingMeanFrameCount(5) {} private: void initializeFromPeer(const Qt3DCore::QNodeCreatedChangeBasePtr &change) override { // TODO: Implement me! } int m_rollingMeanFrameCount; };
Die Klassendeklaration ist sehr einfach. Wir erben von Qt3DCore::QBackendNode , wie Sie es erwarten würden; fügen Sie ein Datenelement hinzu, um Informationen von der Front-End-Komponente FpsMonitor widerzuspiegeln; und überschreibe die virtuelle Funktion initializeFromPeer() . Diese Funktion wird direkt aufgerufen, nachdem Qt 3D unseren Backend-Typ instanziiert hat. Das Argument ermöglicht es uns, die vom entsprechenden Frontend-Objekt gesendeten Daten zu erhalten, wie wir gleich sehen werden.
Registrierung von Typen
Wir haben jetzt einfache Implementierungen von Frontend- und Backend-Komponenten. Der nächste Schritt besteht darin, sie beim Aspekt zu registrieren, damit dieser weiß, wie der Backend-Knoten zu instanziieren ist, wenn ein Frontend-Knoten erstellt wird. Ebenso zur Vernichtung. Wir tun dies mit einem Zwischenhelfer, der als Node-Matcher bekannt ist.
Um einen Node-Mapper zu erstellen, erben wir von Qt3DCore::QNodeMapper und überschreiben virtuelle Funktionen zum Erstellen, Finden und Zerstören von Backend-Objekten bei Bedarf. Wie Sie Objekte erstellen, speichern, finden und zerstören, liegt ganz bei Ihnen als Entwickler. Qt 3D zwingt Ihnen kein bestimmtes Steuerungsschema auf. Der Rendering-Aspekt macht einige ziemlich ausgefallene Sachen mit verwalteten Speichermanagern und Speicherausrichtung für SIMD-Typen, aber wir können hier etwas viel Einfacheres tun.
Wir speichern Zeiger auf Backend-Knoten in QHash innerhalb von CustomAspect und indizieren sie mithilfe des Qt3DCore::QNodeId -Knotens. Die Knoten-ID wird verwendet, um einen bestimmten Knoten eindeutig zu identifizieren, auch zwischen dem Frontend und allen verfügbaren Backends. In Qt3DCore::QNode ist die Kennung über die Funktion id() verfügbar, während für QBackendNode Sie greifen darauf über die Funktion peerId() zu. Für zwei entsprechende Objekte, die eine Komponente darstellen, geben die Funktionen id() und peerId() denselben QNodeId -Wert zurück.
Lassen Sie uns fortfahren und etwas Speicher für die Backend-Knoten in CustomAspect zusammen mit einigen Hilfsfunktionen hinzufügen:
customaspect.h
class CustomAspect : public Qt3DCore::QAbstractAspect { Q_OBJECT public: ... void addFpsMonitor(Qt3DCore::QNodeId id, FpsMonitorBackend *fpsMonitor) { m_fpsMonitors.insert(id, fpsMonitor); } FpsMonitorBackend *fpsMonitor(Qt3DCore::QNodeId id) { return m_fpsMonitors.value(id, nullptr); } FpsMonitorBackend *takeFpsMonitor(Qt3DCore::QNodeId id) { return m_fpsMonitors.take(id); } ... private: QHash<Qt3DCore::QNodeId, FpsMonitorBackend *> m_fpsMonitors; };
Wir können jetzt einen einfachen Node-Matcher wie folgt implementieren:
fpsmonitorbackend.h
class FpsMonitorMapper : public Qt3DCore::QBackendNodeMapper { public: explicit FpsMonitorMapper(CustomAspect *aspect); Qt3DCore::QBackendNode *create(const Qt3DCore::QNodeCreatedChangeBasePtr &change) const override { auto fpsMonitor = new FpsMonitorBackend; m_aspect->addFpsMonitor(change->subjectId(), fpsMonitor); return fpsMonitor; } Qt3DCore::QBackendNode *get(Qt3DCore::QNodeId id) const override { return m_aspect->fpsMonitor(id); } void destroy(Qt3DCore::QNodeId id) const override { auto fpsMonitor = m_aspect->takeFpsMonitor(id); delete fpsMonitor; } private: CustomAspect *m_aspect; };
Um dieses Puzzleteil zu vervollständigen, müssen wir darüber sprechen, wie diese Typen und Matcher miteinander in Beziehung stehen. Dazu rufen wir die Vorlagenfunktion QAbstractAspect::registerBackendType() auf und übergeben einen gemeinsam genutzten Zeiger an den zu erstellenden Matcher , finden und zerstören Sie die entsprechenden Backend-Knoten. Das Template-Argument ist der Typ des Frontend-Knotens, auf dem dieser Matcher aufgerufen werden soll. Ein geeigneter Ort dafür ist der CustomAspect-Konstruktor. In unserem Fall sieht das so aus:
customaspect.cpp
CustomAspect::CustomAspect(QObject *parent) : Qt3DCore::QAbstractAspect(parent) { // Register the mapper to handle creation, lookup, and destruction of backend nodes auto mapper = QSharedPointer<FpsMonitorMapper>::create(this); registerBackendType<FpsMonitor>(mapper); }
Das ist alles! Wenn die FpsMonitor -Bean mit dieser direkten Registrierung zur Frontend-Objektstruktur (Szene) hinzugefügt wird, sucht der Aspekt nach einem Node-Mapper für diesen Objekttyp. Hier findet es unser registriertes FpsMonitorMapper -Objekt und ruft seine create() -Funktion auf, um den Backend-Knoten zu erstellen und seinen Speicher zu verwalten. Die Geschichte ist ähnlich mit der Zerstörung (technisch gesehen ist dies die Entfernung von der Szene) des Front-End-Knotens. Die get() -Funktion des Resolvers wird intern verwendet, um die virtuellen Funktionen des Backend-Knotens zu geeigneten Zeiten aufrufen zu können (z. B. wenn Eigenschaften melden, dass sie geändert wurden).
Front-Back-Kommunikation
Jetzt, da wir den Backend-Knoten eines beliebigen Frontend-Knotens erstellen, darauf zugreifen und zerstören können, sehen wir uns an, wie wir sie miteinander kommunizieren lassen können. Es gibt drei Hauptpunkte, wenn Front-End- und Back-End-Knoten miteinander kommunizieren:
- Initialisierung – Wenn unser Backend-Knoten erstellt wird, haben wir die Möglichkeit, ihn mit den vom Frontend-Knoten gesendeten Daten zu initialisieren.
- Von Frontend zu Backend – Wenn sich Eigenschaften auf dem Frontend-Knoten ändern, möchten wir normalerweise den neuen Wert der Eigenschaft an den Backend-Knoten senden, damit er mit aktuellen Informationen arbeitet.
- Vom Backend zum Frontend - Wenn unsere Jobs Daten verarbeiten, die in Backend-Knoten gespeichert sind, kann es vorkommen, dass dies zu aktualisierten Werten führt, die an den Frontend-Knoten gesendet werden müssen.
Hier betrachten wir die ersten beiden Fälle. Der dritte Fall wird auf den nächsten Artikel verschoben, wenn wir die Aufgaben präsentieren.
Initialisieren Sie den Backend-Knoten
Die gesamte Kommunikation zwischen Frontend- und Backend-Objekten erfolgt durch Senden einer Unterklasse von Qt3DCore::QSceneChanges . Sie ähneln in Art und Konzept QEvent , aber der Änderungsarbiter, der die Änderungen handhabt, hat die Fähigkeit, sie im Falle von Konflikten aus mehreren Aspekten zu manipulieren, sie auf die Priorität neu auszurichten oder andere Manipulationen vorzunehmen, die möglicherweise erforderlich sind die Zukunft.
Um den Backend-Knoten bei der Erstellung zu initialisieren, verwenden wir Qt3DCore::QNodeCreatedChange . Dies ist eine Vorlage, mit der wir Daten eines bestimmten Typs umschließen können. Wenn Qt 3D das Backend über den Anfangszustand Ihres Frontend-Knotens informieren möchte, ruft es die private virtuelle Funktion QNode::createNodeCreationChange() auf. Diese Funktion gibt eine vom Knoten erstellte Änderung zurück, die alle Informationen enthält, auf die wir im Backend-Knoten zugreifen möchten. Wir müssen dies tun, indem wir die Daten kopieren, anstatt einfach den Zeiger auf das Frontend-Objekt zu dereferenzieren, denn bis das Backend die Anfrage verarbeitet, kann das Frontend-Objekt gelöscht sein – ein klassisches Datenrennen. Für unsere einfache Komponente sieht unsere Implementierung so aus:
fpsmonitor.h
struct FpsMonitorData { int rollingMeanFrameCount; };
fpsmonitor.cpp
Qt3DCore::QNodeCreatedChangeBasePtr FpsMonitor::createNodeCreationChange() const { auto creationChange = Qt3DCore::QNodeCreatedChangePtr<FpsMonitorData>::create(this); auto &data = creationChange->data; data.rollingMeanFrameCount = m_rollingMeanFrameCount; return creationChange; }
Die von unserem Frontend-Knoten erstellte Änderung wird an den Backend-Knoten (über den Änderungsarbiter) weitergegeben und von der virtuellen Funktion initializeFromPeer() verarbeitet:
fpsmonitorbackend.cpp
void FpsMonitorBackend::initializeFromPeer(const Qt3DCore::QNodeCreatedChangeBasePtr &change) { const auto typedChange = qSharedPointerCast<Qt3DCore::QNodeCreatedChange<FpsMonitorData>>(change); const auto &data = typedChange->data; m_rollingMeanFrameCount = data.rollingMeanFrameCount; }
Kommunikations-Frontend mit Backend
An diesem Punkt spiegelt der Backend-Knoten den Anfangszustand des Frontend-Knotens wider. Was aber, wenn der Benutzer eine Eigenschaft im Frontend-Knoten ändert? In diesem Fall speichert unser Backend-Knoten veraltete Daten.
Die gute Nachricht ist, dass dies einfach zu handhaben ist. Die Qt3DCore::QNode -Implementierung erledigt für uns die erste Hälfte des Problems. Intern lauscht es auf Q_PROPERTY-Benachrichtigungssignale, und wenn es sieht, dass sich eine Eigenschaft geändert hat, erstellt es eine [QPropertyUpdatedChange] für uns (http://code.qt.io/cgit/qt/qt3d.git/tree/src/ core/changes/qpropertyupdatedchange.h#n51) und sendet sie an den Änderungsarbiter, der sie wiederum an die sceneChangeEvent() -Funktion im Backend-Knoten liefert.
Alles, was wir als Autoren des Backend-Knotens tun müssen, ist diese Funktion zu überschreiben, die Daten aus dem Änderungsobjekt abzurufen und unseren internen Status zu aktualisieren. Oft möchten Sie den Backend-Knoten auf irgendeine Weise markieren, damit der Aspekt weiß, dass er im nächsten Frame verarbeitet werden muss. Hier aktualisieren wir jedoch nur den Status, um den neuesten Wert vom Frontend anzuzeigen:
fpsmonitorbackend.cpp
void FpsMonitorBackend::sceneChangeEvent(const Qt3DCore::QSceneChangePtr &e) { if (e->type() == Qt3DCore::PropertyUpdated) { const auto change = qSharedPointerCast<Qt3DCore::QPropertyUpdatedChange>(e); if (change->propertyName() == QByteArrayLiteral("rollingMeanFrameCount")) { const auto newValue = change->value().toInt(); if (newValue != m_rollingMeanFrameCount) { m_rollingMeanFrameCount = newValue; // TODO: Update fps calculations } return; } } QBackendNode::sceneChangeEvent(e); }
Wenn Sie den integrierten automatischen Versand von Eigenschaftsänderungen von Qt3DCore::QNode nicht verwenden möchten, können Sie ihn deaktivieren, indem Sie die Ausgabe eines Eigenschaftsbenachrichtigungssignals umbrechen, wenn [QNode::blockNotifications()] aufgerufen wird (https ://doc.qt.io/qt-5/qt3dcore-qnode.html#blockNotifications). Dies funktioniert genau wie QObject::blockSignals() , außer dass es nur blockiert, dass Benachrichtigungen an den zugrunde liegenden Knoten gesendet werden, nicht das Signal selbst. Das bedeutet, dass andere Verbindungen oder Eigenschaftsbindungen, die auf Ihre Signale angewiesen sind, weiterhin funktionieren.
Wenn Sie Benachrichtigungen standardmäßig auf diese Weise blockieren, müssen Sie sie senden, um sicherzustellen, dass der zugrunde liegende Knoten über aktualisierte Informationen verfügt. Fühlen Sie sich frei, von jeder Klasse in der Qt3DCore::QSceneChange -Hierarchie zu erben und sie an Ihre Bedürfnisse anzupassen. Der allgemeine Ansatz besteht darin, Qt3DCore::QStaticPropertyUpdatedChangeBase zu erben, das den Eigenschaftsnamen und behandelt in einer Unterklasse fügt ein stark typisiertes Klassenelement zur Eigenschaft payload value hinzu. Der Vorteil gegenüber dem eingebauten Mechanismus besteht darin, dass die Verwendung von QVariant vermieden wird, das in Bezug auf die Leistung etwas unter Kontexten mit vielen Threads leidet. Normalerweise ändern sich Frontend-Eigenschaften nicht zu oft, und das ist standardmäßig in Ordnung.
Fazit
In diesem Artikel haben wir gezeigt, wie die meisten Backend-Knoten implementiert werden. wie man einen Node-Mapper registriert, um Backend-Knoten zu erstellen, zu finden und zu zerstören; wie man einen Backend-Knoten sicher von einem Frontend-Knoten aus initialisiert und wie man seine Daten mit dem Frontend synchronisiert.
Im nächsten Artikel werden wir endlich unsere Benutzerseite erledigen, tatsächlich etwas echte Arbeit leisten und lernen, wie man den Backend-Knoten dazu bringt, Aktualisierungen an den Frontend-Knoten zu senden (durchschnittliche fps). Wir stellen sicher, dass die schweren Teile im Kontext des Qt-3D-Thread-Pools ausgeführt werden, damit Sie verstehen, wie er skaliert werden kann. Bis bald.
Artikel geschrieben von: Sean Harmer | Mittwoch, 13. Dezember 2017