Tipps für die Nachrichtenbehandlung von Component Based Entity System

8

Ich versuche, ein komponentenbasiertes Entitätssystem zu implementieren, bin aber etwas verwirrt darüber, wie ich mit dem Messaging umgehen soll. Es gibt zwei Probleme, die ich lösen möchte, damit ich das System testen kann. Unten ist der Code, den ich bisher habe,

Die Entitätsklasse:

class Entity{
public:
    Entity(unsigned int id):
    id_(id)
    {};
    void handleMessage(BaseMessage &message){
        for(auto element: components_){
            element.second->handleMessage(message);
        }
    }
    template<class T>
    void attachComponent(T *component){
        //Consider making safer in case someone tries to attach same component type twice
        components_[typeid(T).hash_code()] = component;
    }
    template<class T>
    void detachComponent(void){
        components_.erase(typeid(T).hash_code());
    }
    template<class T>
    T* getComponent(void)const{
        return *components_.find(typeid(T).hash_code());
    }
    unsigned int getInstanceID(void)const{
        return id_;
    }
private:
    unsigned int id_;
    std::map<size_t, BaseComponent*> components_;
};

Die Klassen Basiskomponente und Nachricht:

class BaseComponent{
public:
    virtual void handleMessage(BaseMessage &message){};
};

class BaseMessage{
public:
    virtual int getType(void) = 0;
};

1. Behandlung von Nachrichtentypen

Meine erste Frage ist, wie ich mit den verschiedenen (von BaseMessage abgeleiteten) Nachrichtentypen umgehen soll.

Ich habe mir zwei Möglichkeiten ausgedacht, um mit den Nachrichtentypen der abgeleiteten Nachrichtentypen umzugehen. Eine besteht darin, einen Hash (dh unter Verwendung von FNV) aus einer Zeichenfolge zu generieren, die den Nachrichtentyp benennt, und diesen Hash zu verwenden, um den Nachrichtentyp zu bestimmen. Die handleMessage(BaseMessage &message)Funktion würde also zuerst diesen Hash aus der Nachricht extrahieren und dann einen static_cast für den entsprechenden Typ ausführen.

Die zweite Methode besteht darin, eine Vorlage wie folgt zu verwenden (ähnlich den attachComponentMethoden der Entitätsklasse):

template<class T>
handleMessage(T& message){};

und Spezialisierungen für jeden Nachrichtentyp vornehmen, den die jeweilige Komponente vornehmen wird.

Gibt es irgendwelche Nachteile bei der zweiten Methode? Was ist mit der Leistung? Warum sehe ich diese Art der Verwendung nicht öfter?

2. Eingabehandhabung

Meine zweite Frage ist, wie (in Bezug auf Latenz und Benutzerfreundlichkeit) Eingaben optimal verarbeitet werden können.

Mein Gedanke war, eine zu erstellen, InputHandlerComponentdie sich bei der Tastaturklasse registriert, um bestimmte Tastendrücke zu hören, die möglicherweise in einer Datei definiert sind. Zum Beispiel

keyboard.register( player.getComponent<InputHandler>() , 'W')

Ich wünschte, es gäbe eine präzisere Anleitung für komponentenbasierte Systeme, aber ich denke, es gibt viele verschiedene Möglichkeiten, die gleichen Dinge zu tun. Ich habe weitere Fragen, aber ich denke, es wäre klüger, zuerst zu versuchen, das zu implementieren, was ich kann.

Trauerherz
quelle

Antworten:

3

In einem typischen Entitätssystem überlassen Sie den Systemen die gesamte Logik für Ihr Spiel und die Komponenten, um nur Daten zu speichern , und Ihre Entitäten sind nur eine einfache Kennung. Die Handhabung von Eingaben wäre auf diese Weise viel einfacher und ganz zu schweigen von der Flexibilität Ihres Spiels.

Es gibt viele Möglichkeiten, mit Ereignissen umzugehen, z. B. das Beobachtermuster oder Signale / Slots. Sie könnten die Nachrichtenverarbeitung mit Vorlagen / virtuellen Funktionen oder Funktionszeigern durchführen, es wäre jedoch wahrscheinlich einfacher, Boost :: -Signale zu verwenden , auch wenn bekannt ist, dass dies für die Leistung nicht so gut ist. Wenn Sie Leistung * wünschen, empfehlen wir Ihnen, Vorlagen und virtuelle Funktionen oder Funktionszeiger zu verwenden.

* Ich habe festgestellt, dass Ihr Code wirklich optimiert werden kann. Es gibt Alternativen zum typeidBediener, die tatsächlich schneller sind als die Verwendung typeid. Verwenden Sie beispielsweise Vorlagen / Makros und einfache Ganzzahlen, um die ID einer Klasse zu definieren.

Sie können sich mein Entity System ansehen, wenn Sie Inspiration benötigen (die vom Java-Framework Artemis inspiriert ist ). Obwohl ich keine Möglichkeit implementiert habe, Ereignisse (außer entitätsbezogenen Ereignissen) zu behandeln, habe ich dies dem Benutzer überlassen, aber nachdem ich die Entityx- Bibliothek herausgearbeitet hatte, die ich auf reddit gefunden habe. Ich dachte, ich könnte meiner Bibliothek möglicherweise ein Ereignissystem hinzufügen. Bitte beachten Sie, dass mein Entitätssystem nicht zu 100% vollständig ist, was die gewünschten Funktionen betrifft, aber es funktioniert und eine anständige Leistung aufweist (aber ich könnte es optimieren, insbesondere hinsichtlich der Art und Weise, wie ich Entitäten speichere).

Folgendes können Sie möglicherweise tun, um Ereignisse zu behandeln (inspiriert von entityx ):

BaseReceiver / BaseListener

  • Eine Basisklasse, in der Sie Listener / Receiver speichern können.

Empfänger / Hörer

  • Dies ist eine Vorlagenklasse und erfordert, dass Sie die virtuelle Funktion überschreiben recieve(const T&), wobei T Ereignisinformationen sind.
  • Klassen, die von Ereignissen benachrichtigt werden möchten, die beim Eintreten eines Ereignisses bestimmte Informationen senden, müssen von dieser Klasse erben.

EventHandler

  • Sendet / feuert Ereignisse ab
  • Verfügt über eine Liste von Empfänger- / Listener-Objekten, die durch ausgelöste Ereignisse benachrichtigt werden

Ich habe dieses Design gerade in C ++ implementiert, ohne die Verwendung von boost :: Signalen . Sie können es hier sehen .

Vorteile

  • Statisch getippt
  • Virtuelle Funktionen (schneller als Boost :: Signale, sollte es auch sein)
  • Fehler bei der Kompilierung, wenn Sie Benachrichtigungen nicht korrekt ausgegeben haben
  • C ++ 98 kompatibel (glaube ich)

Nachteile

  • Erfordert die Definition von Funktoren (Sie können Ereignisse in einer globalen Funktion nicht verarbeiten).
  • Keine Ereigniswarteschlange; Einfach registrieren & wegfeuern. (was vielleicht nicht das ist, was du willst)
  • Keine direkten Anrufe (sollte nicht so schlecht für Ereignisse sein)
  • Keine Behandlung mehrerer Ereignisse in derselben Klasse (dies kann geändert werden, sodass mehrere Ereignisse über Mehrfachvererbung möglich sind, die Implementierung jedoch einige Zeit in Anspruch nehmen kann).

Außerdem habe ich ein Beispiel in meinem Entitätssystem, das sich mit dem Rendern und der Verarbeitung von Eingaben befasst. Es ist recht einfach, enthält jedoch viele Konzepte zum Verständnis meiner Bibliothek.

miguel.martin
quelle
Können Sie ein Beispiel für eine schnellere Methode zum Erkennen von Typen ohne Typ-ID geben?
Luke B.
"In einem typischen Entitätssystem überlassen Sie den Systemen die gesamte Logik für Ihr Spiel und die Komponenten, um nur Daten zu speichern" - ein "typisches" Entitätssystem gibt es nicht.
Den
@ LukeB. Sehen Sie sich den Quellcode meines Entitätssystems an, den ich nicht verwende typeid(siehe Component und ComponentContainer). Grundsätzlich speichere ich den "Typ" einer Komponente als Ganzzahl. Ich habe eine globale Ganzzahl, die ich pro Komponententyp inkrementiere. Und ich speichere Komponenten in einem 2d-Array, wobei der erste Index die ID der Entität und der zweite Index die ID des Komponententyps ist, dh Komponenten [entityId] [componentTypeId]
miguel.martin
@ miguel.martin. Vielen Dank für Ihre Antwort. Ich denke, meine Verwirrung beruht hauptsächlich auf der Tatsache, dass es verschiedene Möglichkeiten gibt , das Entitätssystem zu implementieren, und für mich haben alle ihre Nachteile und Vorteile. In dem von Ihnen verwendeten Ansatz müssen Sie beispielsweise neue Komponententypen mithilfe von CRTP registrieren, was mir persönlich nicht gefällt, da diese Anforderung nicht explizit ist. Ich denke, ich muss meine Implementierung überdenken.
Grieverheart
1
@MarsonMao behoben
miguel.martin
1

Ich arbeite an einem komponentenbasierten Entitätssystem in C #, aber die allgemeinen Ideen und Muster gelten weiterhin.

Ich gehe mit Nachrichtentypen um, indem jede Komponentenunterklasse die geschützte RequestMessage<T>(Action<T> action) where T : IMessageMethode der Komponente aufruft . Im Englischen bedeutet dies, dass die Komponentenunterklasse einen bestimmten Nachrichtentyp anfordert und eine Methode bereitstellt, die aufgerufen werden kann, wenn die Komponente eine Nachricht dieses Typs empfängt.

Diese Methode wird gespeichert, und wenn die Komponente eine Nachricht empfängt, verwendet sie Reflection, um den Nachrichtentyp abzurufen, der dann verwendet wird, um die zugehörige Methode nachzuschlagen und aufzurufen.

Sie können die Reflexion durch ein beliebiges anderes System ersetzen. Hashes sind die beste Wahl. Schauen Sie sich jedoch mehrere Versand- und Besuchermuster an, um weitere Ideen zu erhalten.

Was die Eingabe angeht, habe ich mich entschieden, etwas ganz anderes zu tun als das, was Sie denken. Ein Eingabehandler konvertiert die Eingabe von Tastatur / Maus / Gamepad separat in eine Flaggenaufzählung (Bitfeld) möglicher Aktionen (MoveForward, MoveBackwards, StrafeLeft, Use usw.) basierend auf den Benutzereinstellungen / dem, was der Player an den Computer angeschlossen hat. Jede Komponente erhält UserInputMessagejedes Bild, das sowohl das Bitfeld als auch die Blickachse des Spielers als 3D-Vektor enthält (Entwicklung der Ausrichtung auf einen Ego-Shooter). Dies macht es auch einfach, Spieler ihre Schlüsselbindungen ändern zu lassen.

Wie Sie am Ende Ihrer Frage gesagt haben, gibt es viele verschiedene Möglichkeiten, ein komponentenbasiertes Entitätssystem zu erstellen. Ich habe mein System stark auf das Spiel ausgerichtet, das ich mache, daher sind einige der Dinge, die ich tue, natürlich im Zusammenhang mit einem RTS möglicherweise nicht sinnvoll, aber es ist immer noch etwas, das implementiert wurde und für das gearbeitet wurde ich bisher.

Robert Rouhani
quelle