Wie implementiere ich die Interaktion zwischen Motorteilen?

10

Ich möchte eine Frage stellen, wie der Informationsaustausch zwischen Game-Engine-Teilen implementiert werden soll.

Die Engine ist in vier Teile unterteilt: Logik, Daten, Benutzeroberfläche, Grafiken. Am Anfang habe ich diesen Austausch durch die Flaggen gemacht. Wenn beispielsweise das neue Objekt zu den Daten hinzugefügt wird, wird das Flag isNewin der Klasse eines Objekts als gesetzt true. Danach überprüft der Grafikteil der Engine dieses Flag und fügt das Objekt der Spielwelt hinzu.

Bei diesem Ansatz sollte ich jedoch viel Code schreiben, um jedes Flag jeder Art von Objekt zu verarbeiten.

Ich habe überlegt, ein Ereignissystem zu verwenden, aber ich habe nicht genug Erfahrung, um zu wissen, ob dies die richtige Lösung wäre.

Ist das Ereignissystem der einzig geeignete Ansatz oder sollte ich etwas anderes verwenden?

Ich benutze Ogre als Grafik-Engine, wenn das wichtig ist.

Userr
quelle
Dies ist eine sehr vage Frage. Wie Ihre Systeme interagieren, hängt stark davon ab, wie Ihre Systeme entworfen sind und welche Art von Kapselung Sie am Ende durchführen. Eines fällt jedoch auf: "Danach überprüft der Grafikteil der Engine dieses Flag und fügt das Objekt der Spielwelt hinzu." Warum fügt die Grafik der Engine der Welt Dinge hinzu ? Es scheint, als sollte die Welt dem Grafikmodul mitteilen, was gerendert werden soll.
Tetrad
In der Engine steuert der Teil "Grafik" den Ogre (weist ihn beispielsweise an, der Szene ein Objekt hinzuzufügen). Dazu durchsucht es aber auch die "Daten" nach dem neuen Objekt (und fordert Ogre anschließend auf, es der Szene hinzuzufügen). Ich weiß jedoch nicht, ob dieser Ansatz aufgrund mangelnder Erfahrung richtig oder falsch ist.
Userr

Antworten:

20

Meine bevorzugte Struktur der Game Engine ist das Schnittstellen- und Objekt <-> Komponentenmodell, das Messaging für die Kommunikation zwischen fast allen Teilen verwendet.

Sie haben mehrere Schnittstellen für Haupt-Engine-Teile wie Ihren Szenenmanager, Ressourcenlader, Audio, Renderer, Physik usw.

Ich habe den Szenenmanager, der für alle Objekte in der 3D-Szene / Welt verantwortlich ist.

Objekt ist eine sehr atomare Klasse, die nur einige Dinge enthält, die fast allem in Ihrer Szene gemeinsam sind. In meiner Engine enthält die Objektklasse nur Position, Drehung, eine Liste von Komponenten und eine eindeutige ID. Die ID jedes Objekts wird von einem statischen int generiert, sodass keine zwei Objekte dieselbe ID haben. Auf diese Weise können Sie Nachrichten anhand ihrer ID an ein Objekt senden, anstatt einen Zeiger auf das Objekt zu haben.

Die Liste der Komponenten des Objekts gibt den Objekten die Haupteigenschaften. Für etwas, das Sie in der 3D-Welt sehen können, würden Sie Ihrem Objekt beispielsweise eine Renderkomponente geben, die die Informationen über das Rendernetz enthält. Wenn Sie möchten, dass ein Objekt Physik hat, geben Sie ihm eine Physikkomponente. Wenn Sie möchten, dass etwas als Kamera fungiert, geben Sie ihm eine Kamerakomponente. Die Liste der Komponenten kann weiter und weiter gehen.

Die Kommunikation zwischen Schnittstellen, Objekten und Komponenten ist der Schlüssel. In meiner Engine habe ich eine generische Nachrichtenklasse, die nur eine eindeutige ID und eine Nachrichtentyp-ID enthält. Die eindeutige ID ist die ID des Objekts, an das die Nachricht gesendet werden soll, und die Nachrichtentyp-ID wird von dem Objekt verwendet, das die Nachricht empfängt, damit es weiß, um welchen Nachrichtentyp es sich handelt.

Objekte können die Nachricht bei Bedarf verarbeiten und sie können die Nachricht an jede ihrer Komponenten weiterleiten, und Komponenten tun häufig wichtige Dinge mit der Nachricht. Wenn Sie beispielsweise die Position des Objekts ändern möchten und dem Objekt eine SetPosition-Nachricht senden, aktualisiert das Objekt möglicherweise seine Positionsvariable, wenn es die Nachricht erhält. Die Renderkomponente muss jedoch möglicherweise eine Nachricht senden, um die Position des Rendernetzes zu aktualisieren Die Physikkomponente benötigt möglicherweise die Nachricht, um die Position des Physikkörpers zu aktualisieren.

Hier ist ein sehr einfaches Layout von Szenenmanager, Objekt, Komponente und Nachrichtenfluss, das ich in ungefähr einer Stunde erstellt und in C ++ geschrieben habe. Beim Ausführen wird die Position für ein Objekt festgelegt, und die Nachricht durchläuft die Renderkomponente und ruft dann die Position vom Objekt ab. Genießen!

Außerdem habe ich eine C # -Version und eine Scala-Version des folgenden Codes für alle geschrieben, die diese eher fließend als C ++ beherrschen.

#include <iostream>
#include <stdio.h>

#include <list>
#include <map>

using namespace std;

struct Vector3
{
public:
    Vector3() : x(0.0f), y(0.0f), z(0.0f)
    {}

    float x, y, z;
};

enum eMessageType
{
    SetPosition,
    GetPosition,    
};

class BaseMessage
{
protected: // Abstract class, constructor is protected
    BaseMessage(int destinationObjectID, eMessageType messageTypeID) 
        : m_destObjectID(destinationObjectID)
        , m_messageTypeID(messageTypeID)
    {}

public: // Normally this isn't public, just doing it to keep code small
    int m_destObjectID;
    eMessageType m_messageTypeID;
};

class PositionMessage : public BaseMessage
{
protected: // Abstract class, constructor is protected
    PositionMessage(int destinationObjectID, eMessageType messageTypeID, 
                    float X = 0.0f, float Y = 0.0f, float Z = 0.0f)
        : BaseMessage(destinationObjectID, messageTypeID)
        , x(X)
        , y(Y)
        , z(Z)
    {

    }

public:
    float x, y, z;
};

class MsgSetPosition : public PositionMessage
{
public:
    MsgSetPosition(int destinationObjectID, float X, float Y, float Z)
        : PositionMessage(destinationObjectID, SetPosition, X, Y, Z)
    {}
};

class MsgGetPosition : public PositionMessage
{
public:
    MsgGetPosition(int destinationObjectID)
        : PositionMessage(destinationObjectID, GetPosition)
    {}
};

class BaseComponent
{
public:
    virtual bool SendMessage(BaseMessage* msg) { return false; }
};

class RenderComponent : public BaseComponent
{
public:
    /*override*/ bool SendMessage(BaseMessage* msg)
    {
        // Object has a switch for any messages it cares about
        switch(msg->m_messageTypeID)
        {
        case SetPosition:
            {                   
                // Update render mesh position/translation

                cout << "RenderComponent handling SetPosition\n";
            }
            break;
        default:
            return BaseComponent::SendMessage(msg);
        }

        return true;
    }
};

class Object
{
public:
    Object(int uniqueID)
        : m_UniqueID(uniqueID)
    {
    }

    int GetObjectID() const { return m_UniqueID; }

    void AddComponent(BaseComponent* comp)
    {
        m_Components.push_back(comp);
    }

    bool SendMessage(BaseMessage* msg)
    {
        bool messageHandled = false;

        // Object has a switch for any messages it cares about
        switch(msg->m_messageTypeID)
        {
        case SetPosition:
            {               
                MsgSetPosition* msgSetPos = static_cast<MsgSetPosition*>(msg);
                m_Position.x = msgSetPos->x;
                m_Position.y = msgSetPos->y;
                m_Position.z = msgSetPos->z;

                messageHandled = true;
                cout << "Object handled SetPosition\n";
            }
            break;
        case GetPosition:
            {
                MsgGetPosition* msgSetPos = static_cast<MsgGetPosition*>(msg);
                msgSetPos->x = m_Position.x;
                msgSetPos->y = m_Position.y;
                msgSetPos->z = m_Position.z;

                messageHandled = true;
                cout << "Object handling GetPosition\n";
            }
            break;
        default:
            return PassMessageToComponents(msg);
        }

        // If the object didn't handle the message but the component
        // did, we return true to signify it was handled by something.
        messageHandled |= PassMessageToComponents(msg);

        return messageHandled;
    }

private: // Methods
    bool PassMessageToComponents(BaseMessage* msg)
    {
        bool messageHandled = false;

        auto compIt = m_Components.begin();
        for ( compIt; compIt != m_Components.end(); ++compIt )
        {
            messageHandled |= (*compIt)->SendMessage(msg);
        }

        return messageHandled;
    }

private: // Members
    int m_UniqueID;
    std::list<BaseComponent*> m_Components;
    Vector3 m_Position;
};

class SceneManager
{
public: 
    // Returns true if the object or any components handled the message
    bool SendMessage(BaseMessage* msg)
    {
        // We look for the object in the scene by its ID
        std::map<int, Object*>::iterator objIt = m_Objects.find(msg->m_destObjectID);       
        if ( objIt != m_Objects.end() )
        {           
            // Object was found, so send it the message
            return objIt->second->SendMessage(msg);
        }

        // Object with the specified ID wasn't found
        return false;
    }

    Object* CreateObject()
    {
        Object* newObj = new Object(nextObjectID++);
        m_Objects[newObj->GetObjectID()] = newObj;

        return newObj;
    }

private:
    std::map<int, Object*> m_Objects;
    static int nextObjectID;
};

// Initialize our static unique objectID generator
int SceneManager::nextObjectID = 0;

int main()
{
    // Create a scene manager
    SceneManager sceneMgr;

    // Have scene manager create an object for us, which
    // automatically puts the object into the scene as well
    Object* myObj = sceneMgr.CreateObject();

    // Create a render component
    RenderComponent* renderComp = new RenderComponent();

    // Attach render component to the object we made
    myObj->AddComponent(renderComp);

    // Set 'myObj' position to (1, 2, 3)
    MsgSetPosition msgSetPos(myObj->GetObjectID(), 1.0f, 2.0f, 3.0f);
    sceneMgr.SendMessage(&msgSetPos);
    cout << "Position set to (1, 2, 3) on object with ID: " << myObj->GetObjectID() << '\n';

    cout << "Retreiving position from object with ID: " << myObj->GetObjectID() << '\n';

    // Get 'myObj' position to verify it was set properly
    MsgGetPosition msgGetPos(myObj->GetObjectID());
    sceneMgr.SendMessage(&msgGetPos);
    cout << "X: " << msgGetPos.x << '\n';
    cout << "Y: " << msgGetPos.y << '\n';
    cout << "Z: " << msgGetPos.z << '\n';
}
Nic Foster
quelle
1
Dieser Code sieht wirklich gut aus. Erinnert mich an die Einheit.
Tili
Ich weiß, dass dies eine alte Antwort ist, aber ich habe ein paar Fragen. Hätte ein "echtes" Spiel nicht Hunderte von Nachrichtentypen, was einen Alptraum für die Codierung darstellt? Was machst du auch, wenn du (zum Beispiel) die Art und Weise brauchst, in die die Hauptfigur schaut, um sie richtig zu zeichnen? Müssten Sie nicht eine neue GetSpriteMessage erstellen und diese jedes Mal senden, wenn Sie rendern? Wird das nicht zu teuer? Ich wundere mich nur! Vielen Dank.
you786
In meinem letzten Projekt haben wir XML verwendet, um die Nachrichten zu schreiben, und ein Python-Skript hat den gesamten Code während der Erstellungszeit für uns erstellt. Sie können für verschiedene Nachrichtenkategorien in mehrere XMLs trennen. Sie können Makros für das Senden von Nachrichten erstellen, die fast so knapp wie ein Funktionsaufruf sind. Wenn Sie die Art und Weise benötigen, wie ein Charakter ohne Nachrichtenübermittlung angezeigt wird, müssen Sie immer noch den Zeiger auf die Komponente abrufen und dann die Funktion kennen, die aufgerufen werden soll es (wenn Sie keine Nachrichten verwenden). RenderComponent kann sich beim Renderer registrieren, sodass Sie ihn nicht bei jedem Frame abfragen müssen.
Nic Foster
2

Ich denke, es ist der beste Weg, um Scene Manager und Interfaces zu verwenden. Habe Messaging implementiert, aber ich würde es als sekundären Ansatz verwenden. Messaging ist gut für die Kommunikation zwischen Threads. Verwenden Sie Abstraktion (Schnittstellen), wo immer Sie können.

Ich weiß nicht viel über Ogre, also spreche ich allgemein.

Im Kern haben Sie die Hauptspielschleife. Es empfängt Eingangssignale, berechnet die KI (von einfachen Bewegungen bis hin zu komplexen KI- und Spiellogiken), lädt Ressourcen [usw.] und gibt den aktuellen Status wieder. Dies ist ein grundlegendes Beispiel, damit Sie die Engine in diese Teile (InputManager, AIManager, ResourceManager, RenderManager) unterteilen können. Und Sie sollten SceneManager haben, der alle Objekte enthält, die im Spiel vorhanden sind.

Jeder dieser Teile und ihre Unterteile haben Schnittstellen. Versuchen Sie also, diese Teile so zu organisieren, dass sie ihre und nur ihre Arbeit erledigen. Sie sollten Unterteile verwenden, die für den Zweck ihres übergeordneten Teils intern interagieren. Auf diese Weise werden Sie nicht in die Stickerei geraten, ohne die Möglichkeit zu haben, sich ohne vollständiges Umschreiben abzuwickeln.

ps Wenn Sie C ++ verwenden, sollten Sie ein RAII- Muster verwenden

edin-m
quelle
2
RAII ist kein Muster, es ist eine Lebensweise.
Schrotflinte Ninja