Idee: Praktisches Messaging System mit C++11

Benutzeravatar
NeoArmageddon
Beiträge: 1165
Registriert: 13.02.2012 20:34
Wohnort: Göttingen
Kontaktdaten:

Idee: Praktisches Messaging System mit C++11

Beitragvon NeoArmageddon » 29.08.2016 15:50

Ich bin mal wieder dabei Grundlagen zu basteln und habe mich nun (einmal wieder) einem Messaging-System angenommen, welches unterschiedliche Systeme wie Grafik, Eingabe, Audio, Spielwelt, etc in einem Spiel miteinander verknüpfen soll. Folgende Anforderungen sehe ich an ein solches System:

  • Gut lesbare Syntax bei der Benutzung.
  • Einfaches Senden und Empfangen von Nachrichten.
  • Entkopplung der Systeme (z.B. AudioManager und Spielwelt sollen nichts voneinander wissen, aber die Spielwelt kann trotzdem das Abspielen von Sounds in Auftrag geben).
  • Einfaches erweitern der Nachrichten.

Für das System benutze ich das Observer-Pattern. Das bedeutet, Systeme/Klassen können sich an einem zentralen Punkt für bestimmte Ereignisse anmelden und werden dann beim Eintreten eines solchen Ereignisses benachrichtigt.

Okay, hier ist meine Schnittstelle für ein Observer:

Code: Alles auswählen

   
   class Message;
   class IObserver
   {
   public:
      IObserver();
      ~IObserver();

      virtual void onNotify(std::shared_ptr<Message>) {
         LOG << "Unhandled Message redirected to Observer.";
      };
   };


Aber nun kommen wir zum Eingemachten. Dem sogenannten "Subject". Eine Klasse welche die Liste von Observern zu einem bestimmten Ereignis bereithält und im Falle des Ereignisses die Nachricht an alle Observer weiterleitet:

Code: Alles auswählen

   template<class T>
   class Subject
   {
   public:
      static void registerObserver(IObserver* o)
      {
         observers.insert(o);
      };
      static void unregisterObserver(IObserver* o)
      {
         observers.erase(observers.find(o));
      };
      static void notify(std::shared_ptr<T> e)
      {

         for (IObserver* i : observers) {
            auto ptr = std::dynamic_pointer_cast<Message>(e);
            i->onNotify(ptr);
         }
      };
   private:
      static std::set<IObserver*> observers;
   };
   template<class T>
   std::set<IObserver*> Subject<T>::observers;


Die Klasse beinhaltet nur statische Methoden und muss daher nicht instanziert werden. Außerdem ist sie eine Templateklasse, bedeutet, der Compiler erstellt automatisch versch. Variationen der Klasse abhänging von der Klasse "T", welche für uns ein konkretes Ereignis oder eine Nachricht darstellt.
registerObserver und unregisterObserver übernimmt das an- und abmelden von Klassen, welche die IObserver-Schnittstelle implementieren und notify reicht eine Klasse, welche von "Message" erbt an alle Observer weiter.

Die Nachrichten/Ereignis-Basisklasse ist auch nicht weiter kompliziert. Sie beinhaltet ausschließlich einen String zum unterscheiden zwischen versch. Nachrichten:

Code: Alles auswählen

class Message
   {
   public:
      Message(std::wstring _type) {
         type = _type;
      };
      virtual ~Message() {};
      virtual bool isType(std::wstring _t) {
         if (type.compare(_t) == 0)
            return true;
         return false;
      }
      virtual std::wstring getType() {
         return type;
      }
   protected:
      std::wstring type;
   };
   
   class PlayMusic : public Message {
   public:
      PlayMusic(std::string _file) : file(_file), Message(L"PlayMusic") {};
      ~PlayMusic() { LOG << "PlayMusic message destroyed."; };
      std::string file;
   };
   

PlayMusic ist bereits ein hiervon konkret abgeleitet Objekt mit zusätzlichem Parameter "file".

So, nun noch eine Beispielimplementation von einem Observer der Musik abspielt, wenn ein PlayMusic erkannt wird:

Code: Alles auswählen

AudioSystem::AudioSystem()
{
   Subject<PlayMusic>::registerObserver(this);
   Subject<StopMusic>::registerObserver(this);
}


AudioSystem::~AudioSystem()
{
   Subject<PlayMusic>::unregisterObserver(this);
   Subject<StopMusic>::unregisterObserver(this);
}

void PFF::AudioSystem::onNotify(std::shared_ptr<Message> msg)
{
   if(msg->isType(L"PlayMusic"))
      playMusic(std::dynamic_pointer_cast<PlayMusic>(msg)->file);
   if (msg->isType(L"StopMusic"))
      stopMusic();
}
void AudioSystem::playMusic(std::string file) {
   if (music.openFromFile(file))
      music.play();
   LOG << "Playing Music";
}
void AudioSystem::stopMusic() {
   music.stop();
   LOG << "Music stopped";
}


Damit können wir alles in unserer main-Methode zusammen fügen:

Code: Alles auswählen

auto as = std::make_shared<AudioSystem>(); //AudioSystem erstellen
   Subject<PlayMusic>::notify(std::make_shared<PlayMusic>("./Menu Music.ogg"));
   system("pause");
   Subject<StopMusic>::notify(std::make_shared<StopMusic>());
   system("pause");


Das wirkt hier nun etwas übertrieben, könnte man dich einfach as->playMusic("./Menu Music.ogg") aufrufen. Jedoch kann man Zeile 2 ÜBERALL im Programm aufrufen und die Musik fängt an zu spielen, das könnte in der Welt-Klasse, der Physik-Klasse oder in einem Scriptsystem passieren. All diese Klassen müssen nichts vom Audiosystem wissen, was dieses total entkoppelt.

Was haltet ihr von diesem Design?
Ich finde es so eigentlich recht praktisch.
Einzig bei den Message-Klassen bin ich mir noch nicht sicher, wie ich das am besten löse. Der "type"-String erscheint mir nicht als die perfekte Lösung und type-Checks der Klassen sind vielleicht ein wenig aufwendig/rechenintensiv.

Benutzeravatar
Glatzemann
Administrator
Beiträge: 3230
Registriert: 08.02.2012 13:35
Wohnort: Leverkusen
Kontaktdaten:

Re: Idee: Praktisches Messaging System mit C++11

Beitragvon Glatzemann » 31.08.2016 11:20

Ich habe vor kurzem eine Firmware für einen 3D-Drucker entwickelt und habe die unterschiedlichsten Subsysteme über ein ähnliches System kommunizieren lassen. Das funktionierte sehr gut und auch auf einem kleinen Arduino problemlos.

Als Argument habe ich einfach einen void* genommen. Somit kann jeder Event einen eigenen Datentyp als Argument verwenden.

Benutzeravatar
NeoArmageddon
Beiträge: 1165
Registriert: 13.02.2012 20:34
Wohnort: Göttingen
Kontaktdaten:

Re: Idee: Praktisches Messaging System mit C++11

Beitragvon NeoArmageddon » 31.08.2016 21:39

An den voidpointer dachte ich auch und dann einfach eine Struktur mit den Parametern übergeben allerdings ist das dann weder typensicher noch ist dort sinnvoll geregelt, wie die Parameter initialisiert und später wieder deallokiert werden. Momentan gehören meine Events in einem weak_ptr dem Subject, welches an alle Observer einen shared_ptr ausgibt. Im Grunde müsst ich das dann auch in einem Event mit den Parametern machen, aber dann reicht es wohl auch einfach die Events mit versch. Membervariablen einfach zu vererben.

Ich dachte auch daran vielleicht eine KeyValue-Liste wie in libSon an die Events hänge. Das würde das ableiten von neuen Eventklassen überflüssig machen, aber dann verliere ich mit dem Subject<T> auch de Möglichkeit von eindeutigen und schnellen Bindungen.

Benutzeravatar
Glatzemann
Administrator
Beiträge: 3230
Registriert: 08.02.2012 13:35
Wohnort: Leverkusen
Kontaktdaten:

Re: Idee: Praktisches Messaging System mit C++11

Beitragvon Glatzemann » 01.09.2016 15:55

Ich hab das sehr simpel gelöst - allerdings könnte das in einer parallelisierten Umgebung zu Problemen führen:

Derjenige der den Event "auslöst" kümmert sich darum, daß das Parameter-Objekt initialisiert wird und kümmert sich auch später um das Aufräumen. Also in der Art von:

Code: Alles auswählen

MyEventArgs* eventArgs = createEventArgs();
eventHandler->fireEvent(eventArgs);
delete eventArgs;


Klar müssen dabei einige Regeln beachtet werden, wie z.B. das die Zeiger innerhalb von Event-Routinen nicht gespeichert werden dürfen, etc. Das ist aber meist auch kein Problem.

Und die Typisierung bekommt man mit ein paar kleinen Ableitungen, Spezialisierungen und ein wenig Template-Magic eigentlich auch ganz gut hin...


Das wichtigste bei so einem System ist eigentlich nur, daß man sich sinnvolle Gedanken über die Schnittstellen macht. Wenn sich da später was ändert, kann dies zu ganz seltsamen Fehlern führen...


Zurück zu „C / C++“

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder und 1 Gast