Come implementare l'interazione tra le parti del motore?


10

Voglio fare una domanda su come dovrebbe essere implementato lo scambio di informazioni tra le parti del motore di gioco.

Il motore è diviso in quattro parti: logica, dati, interfaccia utente, grafica. All'inizio ho fatto questo scambio attraverso le bandiere. Ad esempio, se il nuovo oggetto viene aggiunto nei dati, il flag isNewnella classe di un oggetto verrà impostato come true. E dopo che la parte grafica del motore controllerà questa bandiera e aggiungerà l'oggetto nel mondo di gioco.

Tuttavia, con questo approccio avrei dovuto scrivere molto codice per elaborare ogni bandiera di ogni tipo di oggetto.

Ho pensato di utilizzare un sistema di eventi, ma non ho abbastanza esperienza per sapere se questa sarebbe la soluzione giusta.

Il sistema di eventi è l'unico approccio appropriato o dovrei usare qualcos'altro?

Sto usando Ogre come motore grafico, se è importante.


Questa è una domanda molto vaga. Il modo in cui i tuoi sistemi interagiranno sarà molto accoppiato a come i tuoi sistemi sono progettati e che tipo di incapsulamento stai finendo di fare. Ma una cosa spicca: "E dopo che la parte grafica del motore controllerà questa bandiera e aggiungerà l'oggetto al mondo di gioco". Perché la grafica fa parte del motore e aggiunge cose al mondo ? Sembra che il mondo dovrebbe dire al modulo grafico cosa rendere.
Tetrad,

Nel motore la parte "grafica" controlla l'Ogre (ad esempio, gli dice di aggiungere oggetto nella scena). Ma per farlo cerca anche i "dati" per l'oggetto che è nuovo (e in seguito dice a Ogre di aggiungerlo alla scena) Ma non so se questo approccio sia giusto o sbagliato a causa della mancanza di esperienza.
Userr,

Risposte:


20

La mia struttura di motore di gioco preferita è l'interfaccia e l'oggetto <-> modello di componente che utilizza la messaggistica per la comunicazione tra quasi tutte le parti.

Hai più interfacce per le parti principali del motore come il tuo gestore di scene, caricatore di risorse, audio, renderer, fisica, ecc.

Ho il responsabile della scena responsabile di tutti gli oggetti della scena / mondo 3D.

L'oggetto è una classe molto atomica, contenente solo alcune cose comuni a quasi tutto nella scena, nel mio motore la classe oggetto contiene solo posizione, rotazione, un elenco di componenti e un ID univoco. L'ID di ogni oggetto viene generato da un int statico, in modo che nessun oggetto abbia lo stesso ID, questo ti consente di inviare messaggi a un oggetto tramite il suo ID, anziché dover avere un puntatore all'oggetto.

L'elenco dei componenti sull'oggetto è ciò che dà che gli oggetti sono le proprietà principali. Ad esempio, per qualcosa che puoi vedere nel mondo 3D, assegneresti al tuo oggetto un componente di rendering che contiene le informazioni sulla mesh di rendering. Se vuoi che un oggetto abbia una fisica, gli daresti una componente fisica. Se vuoi che qualcosa funzioni come una videocamera, dagli un componente della videocamera. L'elenco dei componenti può continuare all'infinito.

La comunicazione tra interfacce, oggetti e componenti è la chiave. Nel mio motore ho una classe di messaggi generica che contiene solo un ID univoco e un ID del tipo di messaggio. L'ID univoco è l'ID dell'oggetto a cui si desidera inviare il messaggio e l'ID del tipo di messaggio viene utilizzato dall'oggetto che riceve il messaggio in modo che sappia quale tipo di messaggio è.

Gli oggetti possono gestire il messaggio se necessario e possono trasmettere il messaggio a ciascuno dei loro componenti, e i componenti spesso fanno cose importanti con il messaggio. Ad esempio, se si desidera modificare la posizione dell'oggetto e inviare un messaggio SetPosition all'oggetto, l'oggetto potrebbe aggiornare la sua variabile di posizione quando riceve il messaggio, ma potrebbe essere necessario che il componente di rendering debba inviare un messaggio per aggiornare la posizione della mesh di rendering e il componente fisico potrebbe aver bisogno del messaggio per aggiornare la posizione del corpo fisico.

Ecco un layout molto semplice di scene manager, oggetto e componente e flusso di messaggi, che ho montato in circa un'ora, scritto in C ++. Quando eseguito imposta la posizione su un oggetto e il messaggio passa attraverso il componente di rendering, quindi recupera la posizione dall'oggetto. Godere!

Inoltre, ho scritto una versione C # e una versione Scala del codice seguente per chiunque possa essere fluente in quelle piuttosto che in C ++.

#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';
}

1
Questo codice sembra davvero carino. Mi ricorda l'Unità.
Tili,

So che questa è una vecchia risposta, ma ho alcune domande. Un gioco "reale" non avrebbe centinaia di tipi di messaggi, creando un incubo di programmazione? Inoltre, cosa fai se hai bisogno (ad esempio) del modo in cui il personaggio principale è rivolto per disegnarlo correttamente. Non avresti bisogno di creare un nuovo GetSpriteMessage e inviarlo ogni volta che esegui il rendering? Non diventa troppo costoso? Mi chiedo solo! Grazie.
tu786,

Nel mio ultimo progetto abbiamo usato XML per scrivere i messaggi e uno script Python ha creato tutto il codice per noi durante il tempo di compilazione. È possibile separare in più XML per diverse categorie di messaggi. Puoi creare macro per l'invio di messaggi, rendendoli quasi concisi come una chiamata di funzione, se ti servisse il modo in cui un personaggio si trovava di fronte senza messaggistica, dovrai comunque ottenere il puntatore al componente e quindi conoscere la funzione su cui chiamare (se non stavi usando la messaggistica). RenderComponent può registrarsi con il renderer in modo da non doverlo interrogare ogni frame.
Nic Foster,

2

Penso che sia il modo migliore per utilizzare Scene Manager e Interfaces. Ho implementato la messaggistica ma lo userei come approccio secondario. La messaggistica è utile per la comunicazione tra thread. Usa l'astrazione (interfacce) dove puoi.

Non so molto di Ogre, quindi sto parlando in generale.

Al centro, hai il loop di gioco principale. Riceve segnali di input, calcola l'IA (dal semplice movimento all'intelligenza artificiale complessa e alla logica di gioco), carica le risorse [, ecc.] E rende lo stato corrente. Questo è un esempio di base, quindi puoi separare il motore in queste parti (InputManager, AIManager, ResourceManager, RenderManager). E dovresti avere SceneManager che contiene tutti gli oggetti presenti nel gioco.

Ognuna di queste parti e le loro sottoparti hanno interfacce. Quindi cerca di organizzare queste parti per fare il loro e solo il loro lavoro. Dovrebbero utilizzare le parti secondarie che interagiscono internamente ai fini della parte madre. In questo modo non ti sbaglierai senza possibilità di srotolarti senza riscrivere totalmente.

ps se stai usando C ++ considera l'utilizzo del modello RAII


2
RAII non è un modello, è un modo di vivere.
Fucile Ninja
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.