
Einführung
Eine der Eigenschaften der C ++ - Sprache, für die sie häufig kritisiert wird, ist das Fehlen eines Mechanismus zur Ereignisverarbeitung im Standard. In der Zwischenzeit ist dieser Mechanismus eine der Hauptmethoden für die Interaktion einiger Softwarekomponenten mit anderen Softwarekomponenten und Hardware und wird auf der Ebene eines bestimmten Betriebssystems implementiert. Natürlich hat jede Plattform ihre eigenen Nuancen bei der Implementierung des beschriebenen Mechanismus.
In Verbindung mit all dem muss bei der Entwicklung in C ++ die Ereignisverarbeitung auf die eine oder andere Weise implementiert werden, die mithilfe von Bibliotheken und Frameworks von Drittanbietern gelöst wird. Das bekannte Qt-Framework bietet einen Signal- und Slot-Mechanismus zum Organisieren der Interaktion von Klassen, die von QObject geerbt wurden. Die Implementierung von Ereignissen ist auch in der Boost-Bibliothek vorhanden. Und natürlich könnte die OpenSceneGraph-Engine nicht auf ein eigenes „Fahrrad“ verzichten, dessen Anwendung im Artikel erläutert wird.
OSG ist eine abstrakte Grafikbibliothek. Einerseits abstrahiert es von der OpenGL-Prozedurschnittstelle und bietet dem Entwickler eine Reihe von Klassen, die die gesamte Mechanik der OpneGL-API kapseln. Andererseits abstrahiert es auch von einer bestimmten grafischen Benutzeroberfläche, da die Implementierungsansätze für verschiedene Plattformen unterschiedlich sind und Funktionen sogar innerhalb derselben Plattform aufweisen (z. B. MFC, Qt, .Net für Windows).
Unabhängig von der Plattform wird aus Sicht der Anwendung die Interaktion des Benutzers mit der grafischen Oberfläche darauf reduziert, Elemente einer Folge von Ereignissen zu generieren, die dann innerhalb der Anwendung verarbeitet werden. Die meisten grafischen Frameworks verwenden diesen Ansatz, aber selbst innerhalb derselben Plattform sind sie leider nicht miteinander kompatibel.
Aus diesem Grund bietet OSG eine eigene Basisoberfläche für die Verarbeitung von Widget-Widget-Ereignissen und Benutzereingaben basierend auf der Klasse osgGA :: GUIEventHandler. Dieser Handler kann durch Aufrufen der Methode addEventHandler () an den Viewer angehängt und durch die Methode removeEventHandler () entfernt werden. Natürlich sollte die konkrete Handlerklasse von der osgGA :: GUIEventHandler-Klasse geerbt und die handle () -Methode darin neu definiert werden. Diese Methode akzeptiert zwei Argumente: osgGA :: GUIEventAdapter, der die Warteschlange der Ereignisse aus der GUI enthält, und osg :: GUIActionAdepter, die für das Feedback verwendet werden. Typisch in der Definition ist ein solches Design
bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdepter &aa) {
Mit dem Parameter osgGA :: GUIActionAdapter kann der Entwickler die GUI auffordern, als Reaktion auf das Ereignis Maßnahmen zu ergreifen. In den meisten Fällen ist ein Betrachter von diesem Parameter betroffen, dessen Zeiger durch dynamische Zeigerkonvertierung erhalten werden kann
osgViewer::Viewer* viewer = dynamic_cast<osgViewer::Viewer *>(&aa);
1. Behandlung von Tastatur- und Mausereignissen
Die Klasse osgGA :: GUIEventAdapter () verwaltet alle von OSG unterstützten Ereignistypen und stellt Daten zum Festlegen und Abrufen ihrer Parameter bereit. Die Methode getEventType () gibt das aktuelle GUI-Ereignis zurück, das in der Ereigniswarteschlange enthalten ist. Jedes Mal, wenn Sie die handle () -Methode des Handlers überschreiben, sollten Sie beim Aufrufen dieser Methoden diesen Getter verwenden, um das Ereignis zu empfangen und seinen Typ zu bestimmen.
In der folgenden Tabelle werden alle verfügbaren Ereignisse beschrieben.
Ereignistyp | Beschreibung | Methoden zum Abrufen von Ereignisdaten |
---|
PUSH / RELEASE / DOUBLECLICK | Klicken / Loslassen und Doppelklicken mit der Maus | getX (), getY () - Ermittelt die Cursorposition. getButton () - Code der gedrückten Taste (LEFT_MOUSE_BUTTON, RIGHT_MOUSE_BUTTON, MIDDLE_MOUSE_BUTTON |
Scrol | Scrolling Mausrad (e) | getScrollingMotion () - gibt SCROOL_UP, SCROLL_DOWN, SCROLL_LEFT, SCROLL_RIGHT zurück |
DRAG | Maus ziehen | getX (), getY () - Cursorposition; getButtonMask () - Werte ähnlich wie getButton () |
BEWEGEN | Maus bewegen | getX (), getY () - Cursorposition |
KEYDOWN / KEYUP | Drücken / Loslassen einer Taste auf einer Tastatur | getKey () - ASCII-Code der gedrückten Taste oder der Wert des Key_Symbol-Enumerators (z. B. KEY_BackSpace) |
RAHMEN | Ereignis, das beim Rendern eines Frames generiert wird | keine Eingabe |
USER | Benutzerdefiniertes Ereignis | getUserDataPointer () - gibt einen Zeiger auf einen Benutzerdatenpuffer zurück (der Puffer wird von einem intelligenten Zeiger gesteuert) |
Es gibt auch eine getModKeyMask () -Methode zum Abrufen von Informationen über die gedrückte Modifizierertaste (gibt Werte der Form MODKEY_CTRL, MODKEY_SHIFT, MODKEY_ALT usw. zurück), mit der Sie Tastenkombinationen verarbeiten können, die Modifikatoren verwenden
if (ea.getModKeyMask() == osgGA::GUIEventAdapter::MODKEY_CTRL) {
Beachten Sie, dass Setter-Methoden wie setX (), setY (), setEventType () usw. Wird im handle () - Handler nicht verwendet. Sie werden vom grafischen OSG-Fenstersystem auf niedriger Ebene aufgerufen, um das Ereignis in die Warteschlange zu stellen.
2. Wir steuern die Cessna über die Tastatur
Wir wissen bereits, wie Szenenobjekte durch die osg :: MatrixTransform-Klassen transformiert werden. Wir haben verschiedene Arten von Animationen mit den Klassen osg :: AnimationPath und osg :: Animation untersucht. Für die Interaktivität einer Anwendung (z. B. eines Spiels) reichen Animation und Transformationen jedoch eindeutig nicht aus. Der nächste Schritt besteht darin, die Position von Objekten auf der Bühne über Benutzereingabegeräte zu steuern. Versuchen wir, das Management an unserer geliebten Cessna zu befestigen.
Tastaturbeispielmain.h #ifndef MAIN_H #define MAIN_H #include <osg/MatrixTransform> #include <osgDB/ReadFile> #include <osgGA/GUIEventHandler> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h"
Um dieses Problem zu lösen, schreiben wir eine Klasse für Eingabeereignishandler
class ModelController : public osgGA::GUIEventHandler { public: ModelController( osg::MatrixTransform *node ) : _model(node) {} virtual bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa); protected: osg::ref_ptr<osg::MatrixTransform> _model; };
Beim Erstellen dieser Klasse als Parameter wird ein Zeiger auf den Transformationsknoten übergeben, auf den im Handler reagiert wird. Die handle () - Handlermethode selbst wird wie folgt neu definiert
bool ModelController::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { (void) aa; if (!_model.valid()) return false; osg::Matrix matrix = _model->getMatrix(); switch (ea.getEventType()) { case osgGA::GUIEventAdapter::KEYDOWN: switch (ea.getKey()) { case 'a': case 'A': matrix *= osg::Matrix::rotate(-0.1, osg::Z_AXIS); break; case 'd': case 'D': matrix *= osg::Matrix::rotate( 0.1, osg::Z_AXIS); break; case 'w': case 'W': matrix *= osg::Matrix::rotate(-0.1, osg::X_AXIS); break; case 's': case 'S': matrix *= osg::Matrix::rotate( 0.1, osg::X_AXIS); break; default: break; } _model->setMatrix(matrix); break; default: break; } return false; }
Unter den wesentlichen Details seiner Implementierung sollte beachtet werden, dass wir zuerst die Transformationsmatrix von dem Knoten erhalten müssen, den wir steuern
osg::Matrix matrix = _model->getMatrix();
Als nächstes analysieren zwei verschachtelte switch () -Anweisungen die Art des Ereignisses (Tastenanschlag) und den Code der gedrückten Taste. Abhängig vom Code der gedrückten Taste wird die aktuelle Transformationsmatrix mit einer zusätzlichen Rotationsmatrix um die entsprechende Achse multipliziert
case 'a': case 'A': matrix *= osg::Matrix::rotate(-0.1, osg::Z_AXIS); break;
- Drehen Sie die Ebene mit einem Gierwinkel von -0,1 Bogenmaß, wenn Sie die Taste "A" drücken.
Vergessen Sie nach der Verarbeitung der Tastenanschläge nicht, eine neue Transformationsmatrix auf den Transformationsknoten anzuwenden
_model->setMatrix(matrix);
Laden Sie in der Funktion main () das Flugzeugmodell und erstellen Sie einen übergeordneten Transformationsknoten dafür. Fügen Sie den resultierenden Untergraphen zum Stammknoten der Szene hinzu
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform; mt->addChild(model.get()); osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(mt.get());
Erstellen und initialisieren Sie den Benutzereingabehandler
osg::ref_ptr<ModelController> mcontrol = new ModelController(mt.get());
Erstellen Sie einen Viewer, indem Sie unseren Handler hinzufügen
osgViewer::Viewer viewer; viewer.addEventHandler(mcontrol.get());
Richten Sie die Kameraansichtsmatrix ein
viewer.getCamera()->setViewMatrixAsLookAt( osg::Vec3(0.0f, -100.0f, 0.0f), osg::Vec3(), osg::Z_AXIS );
Verbieten Sie der Kamera, Ereignisse von Eingabegeräten zu empfangen
viewer.getCamera()->setAllowEventFocus(false);
Wenn dies nicht erfolgt, fängt der an der Kamera hängende Handler standardmäßig alle Benutzereingaben ab und stört unseren Handler. Wir setzen die Szenendaten auf den Viewer und führen sie aus
viewer.setSceneData(root.get()); return viewer.run();
Nachdem wir das Programm gestartet haben, können wir die Ausrichtung des Flugzeugs im Weltraum durch Drücken der Tasten A, D, W und S steuern.

Eine interessante Frage ist, was die handle () -Methode beim Beenden zurückgeben soll. Wenn true zurückgegeben wird, geben wir OSG an, dann haben wir bereits Eingabeereignisse verarbeitet und eine weitere Verarbeitung ist nicht erforderlich. In den meisten Fällen passt dieses Verhalten nicht zu uns. Daher empfiehlt es sich, false vom Handler zurückzugeben, um die Verarbeitung von Ereignissen durch andere Handler nicht zu unterbrechen, wenn diese an andere Knoten in der Szene angehängt sind.
3. Verwendung von Besuchern in der Ereignisverarbeitung
Ähnlich wie es beim Durchlaufen eines Szenendiagramms beim Aktualisieren implementiert wird, unterstützt OSG Rückrufe zur Behandlung von Ereignissen, die Knoten und geometrischen Objekten zugeordnet werden können. Dazu werden Aufrufe von setEventCallback () und addEventCallback () verwendet, die als Parameter einen Zeiger auf das untergeordnete osg :: NodeCallback verwenden. Um Ereignisse im Operator operator () zu empfangen, können wir den an den Site-Besucher übergebenen Zeiger in einen Zeiger auf osgGA :: EventVisitor konvertieren, beispielsweise wie folgt
#include <osgGA/EventVisitor> ... void operator()( osg::Node *node, osg::NodeVisitor *nv ) { std::list<osg::ref_ptr<osgGA::GUIEventAdapter>> events; osgGA::EventVisitor *ev = dynamic_cast<osgGA::EventVisitor *>(nv); if (ev) { events = ev->getEvents(); // } }
4. Erstellung und Verarbeitung von benutzerdefinierten Ereignissen
OSG verwendet eine interne Ereigniswarteschlange (FIFO). Ereignisse am Anfang der Warteschlange werden verarbeitet und daraus gelöscht. Neu generierte Ereignisse werden am Ende der Warteschlange platziert. Die handle () -Methode jedes Ereignishandlers wird so oft ausgeführt, wie sich Ereignisse in der Warteschlange befinden. Die Ereigniswarteschlange wird von der Klasse osgGA :: EventQueue beschrieben, mit der Sie unter anderem jederzeit ein Ereignis in die Warteschlange stellen können, indem Sie die Methode addEvent () aufrufen. Das Argument für diese Methode ist ein Zeiger auf osgGA :: GUIEventAdapter, der mit setEventType () -Methoden usw. auf ein bestimmtes Verhalten festgelegt werden kann.
Eine der Methoden der osgGA :: EventQueue-Klasse ist userEvent (), mit der ein Benutzerereignis festgelegt wird, indem es Benutzerdaten zugeordnet wird, auf die ein Zeiger als Parameter übergeben wird. Diese Daten können verwendet werden, um jedes benutzerdefinierte Ereignis darzustellen.
Eigene Instanz der Ereigniswarteschlange kann nicht erstellt werden. Diese Instanz wurde bereits erstellt und an die Viewer-Instanz angehängt, sodass Sie nur einen Zeiger auf diesen Singleton erhalten können
viewer.getEventQueue()->userEvent(data);
Benutzerdaten sind ein Objekt des Erben von osg :: Referenced, dh Sie können einen intelligenten Zeiger darauf erstellen.
Wenn ein benutzerdefiniertes Ereignis empfangen wird, kann der Entwickler Daten daraus extrahieren, indem er die Methode getUserData () aufruft und sie nach eigenem Ermessen verarbeitet.
5. Implementierung des Benutzer-Timers
Viele Bibliotheken und Frameworks, die die GUI implementieren, bieten einen Klassenentwickler zum Implementieren von Timern, die nach einem bestimmten Zeitintervall ein Ereignis generieren. OSG enthält keine regulären Mittel zum Implementieren von Timern. Versuchen wir daher, eine Art Timer selbst zu implementieren, indem wir über die Schnittstelle benutzerdefinierte Ereignisse erstellen.
Worauf können wir uns bei der Lösung dieses Problems verlassen? Für ein bestimmtes periodisches Ereignis, das vom Render ständig generiert wird, z. B. in FRAME, das Ereignis des Zeichnens des nächsten Frames. Dafür verwenden wir das gleiche Beispiel mit dem Umschalten des Cessna-Modells von normal auf brennend.
Timer Beispielmain.h #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgGA/GUIEventHandler> #include <osgViewer/Viewer> #include <iostream> #endif
main.cpp #include "main.h"
Lassen Sie uns zunächst das Format der in der Benutzernachricht gesendeten Daten bestimmen und diese als Struktur definieren
struct TimerInfo : public osg::Referenced { TimerInfo(unsigned int c) : _count(c) {} unsigned int _count; };
Der Parameter _count enthält die ganzzahlige Anzahl von Millisekunden, die seit dem Start des Programms bis zum nächsten Timer-Ereignis vergangen sind. Die Struktur erbt von der Klasse osg :: Referenced, sodass sie über intelligente OSG-Zeiger gesteuert werden kann. Erstellen Sie nun einen Ereignishandler
class TimerHandler : public osgGA::GUIEventHandler { public: TimerHandler(osg::Switch *sw, unsigned int interval = 1000) : _switch(sw) , _count(0) , _startTime(0.0) , _interval(interval) , _time(0) { } virtual bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa); protected: osg::ref_ptr<osg::Switch> _switch; unsigned int _count; double _startTime; unsigned int _interval; unsigned int _time; };
Dieser Handler hat mehrere spezifische geschützte Mitglieder. Die Variable _switch gibt einen Knoten an, der Flugzeugmodelle wechselt. _count - Der relative Countdown der seit der letzten Generation des Timer-Ereignisses verstrichenen Zeit dient zum Zählen der Zeitintervalle. _startTime - eine temporäre Variable zum Speichern des vorherigen Countdowns, die vom Viewer ausgeführt wird; _time - Die Gesamtbetriebszeit des Programms in Millisekunden. Der Klassenkonstruktor akzeptiert einen Schaltknoten als Parameter und optional das erforderliche Zeitintervall für den Betrieb des Schaltzeitgebers.
In dieser Klasse überschreiben wir die handle () -Methode
bool TimerHandler::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { switch (ea.getEventType()) { case osgGA::GUIEventAdapter::FRAME: { osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer *>(&aa); if (!viewer) break; double time = viewer->getFrameStamp()->getReferenceTime(); unsigned int delta = static_cast<unsigned int>( (time - _startTime) * 1000.0); _startTime = time; if ( (_count >= _interval) || (_time == 0) ) { viewer->getEventQueue()->userEvent(new TimerInfo(_time)); _count = 0; } _count += delta; _time += delta; break; } case osgGA::GUIEventAdapter::USER: if (_switch.valid()) { const TimerInfo *ti = dynamic_cast<const TimerInfo *>(ea.getUserData()); std::cout << "Timer event at: " << ti->_count << std::endl; _switch->setValue(0, !_switch->getValue(0)); _switch->setValue(1, !_switch->getValue(1)); } break; default: break; } return false; }
Hier analysieren wir die Art der empfangenen Nachricht. Wenn es sich um FRAME handelt, werden die folgenden Aktionen ausgeführt:
- Holen Sie sich einen Zeiger auf den Betrachter
osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer *>(&aa);
- Lesen Sie nach Erhalt des richtigen Zeigers die seit dem Start des Programms verstrichene Zeit ab
double time = viewer->getFrameStamp()->getReferenceTime();
Berechnen Sie den Zeitaufwand für das Rendern eines Frames in Millisekunden
unsigned int delta = static_cast<unsigned int>( (time - _startTime) * 1000.0);
und erinnere dich an die aktuelle Zeitzählung
_startTime = time;
Wenn der Wert des Zählers _count das erforderliche Zeitintervall überschritten hat (oder dies der erste Aufruf ist, wenn _time noch Null ist), stellen wir die Benutzernachricht in die Warteschlange und übergeben in der obigen Struktur die Programmzeit in Millisekunden. Der Zähler _count wird auf Null zurückgesetzt
if ( (_count >= _interval) || (_time == 0) ) { viewer->getEventQueue()->userEvent(new TimerInfo(_time)); _count = 0; }
Unabhängig vom Wert von _count müssen wir ihn und _time um die Verzögerung erhöhen, die zum Zeichnen eines Frames erforderlich ist
_count += delta; _time += delta;
Auf diese Weise wird das Timer-Ereignis generiert. Die Ereignisbehandlung wird wie folgt implementiert
case osgGA::GUIEventAdapter::USER: if (_switch.valid()) { const TimerInfo *ti = dynamic_cast<const TimerInfo *>(ea.getUserData()); std::cout << "Timer event at: " << ti->_count << std::endl; _switch->setValue(0, !_switch->getValue(0)); _switch->setValue(1, !_switch->getValue(1)); } break;
Hier überprüfen wir die Gültigkeit des Zeigers auf den Vermittlungsknoten, subtrahieren die Daten vom Ereignis, das von der TimerInfo-Struktur führt, zeigen den Inhalt der Struktur auf dem Bildschirm an und wechseln den Zustand des Knotens.
Der Code in der Funktion main () ähnelt dem Code in den beiden vorherigen Schaltbeispielen, mit dem Unterschied, dass wir in diesem Fall einen Ereignishandler an den Viewer hängen
viewer.addEventHandler(new TimerHandler(root.get(), 1000));
Übergeben des Zeigers an den Wurzelknoten und des erforderlichen Schaltintervalls in Millisekunden an den Handlerkonstruktor. Wenn Sie das Beispiel ausführen, werden Sie sehen, dass die Modelle im Abstand von einer Sekunde wechseln, und in der Konsole finden wir die Ausgabe der Zeiten, zu denen der Wechsel stattgefunden hat
Timer event at: 0 Timer event at: 1000 Timer event at: 2009 Timer event at: 3017 Timer event at: 4025 Timer event at: 5033
Ein benutzerdefiniertes Ereignis kann jederzeit während der Ausführung des Programms generiert werden, und zwar nicht nur beim Empfang des FRAME-Ereignisses. Dies bietet einen sehr flexiblen Mechanismus für den Datenaustausch zwischen Teilen des Programms und ermöglicht die Verarbeitung von Signalen von nicht standardmäßigen Eingabegeräten wie z. B. Joysticks oder VR-Handschuhen.
Fortsetzung folgt...