Jak wdrożyć interakcję między częściami silnika?

10

Chcę zadać pytanie o sposób wymiany informacji między częściami silnika gry.

Silnik jest podzielony na cztery części: logikę, dane, interfejs użytkownika, grafikę. Na początku dokonałem tej wymiany za pomocą flag. Na przykład, jeśli nowy obiekt zostanie dodany do danych, flaga isNeww klasie obiektu zostanie ustawiona jako true. A potem część graficzna silnika sprawdzi tę flagę i doda obiekt do świata gry.

Jednak przy takim podejściu miałem napisać dużo kodu, aby przetworzyć każdą flagę każdego rodzaju obiektu.

Myślałem o użyciu jakiegoś systemu zdarzeń, ale nie mam wystarczającego doświadczenia, aby wiedzieć, czy byłoby to właściwe rozwiązanie.

Czy system zdarzeń jest jedynym właściwym podejściem, czy powinienem użyć czegoś innego?

Jeśli to ma znaczenie, używam Ogre jako silnika graficznego.

Userr
źródło
To bardzo niejasne pytanie. To, w jaki sposób współdziałają twoje systemy, będzie bardzo powiązane z tym, jak są zaprojektowane i jakie rodzaje enkapsulacji kończysz. Ale jedna rzecz się wyróżnia: „Następnie część graficzna silnika sprawdzi tę flagę i doda obiekt do świata gry”. Dlaczego grafika jest częścią silnika, który dodaje światu rzeczy ? Wygląda na to, że świat powinien powiedzieć modułowi graficznemu, co ma renderować.
Tetrad
W silniku część „graficzna” kontroluje Ogra (na przykład każe mu dodać obiekt do sceny). Ale robiąc to, przeszukuje również „dane” dla nowego obiektu (a następnie mówi Ogreowi, aby dodał go do sceny). Ale nie wiem, czy to podejście jest dobre, czy złe z powodu braku doświadczenia.
Użytkownik

Odpowiedzi:

20

Moją ulubioną strukturą silnika gry jest model interfejsu i obiektu <-> wykorzystujący komunikację do komunikacji między prawie wszystkimi częściami.

Masz wiele interfejsów dla głównych części silnika, takich jak menedżer scen, moduł ładujący zasoby, dźwięk, renderer, fizyka itp.

Mam menedżera scen odpowiedzialnego za wszystkie obiekty w scenie / świecie 3D.

Obiekt jest bardzo atomową klasą, zawierającą tylko kilka rzeczy, które są wspólne dla prawie wszystkiego w twojej scenie, w moim silniku klasa obiektów ma tylko pozycję, obrót, listę komponentów i unikalny identyfikator. Identyfikator każdego obiektu jest generowany przez statyczny int, dzięki czemu żaden z dwóch obiektów nie będzie miał tego samego identyfikatora, co pozwala na wysyłanie wiadomości do obiektu według jego identyfikatora, zamiast konieczności posiadania wskaźnika do obiektu.

Lista składników obiektu daje główne właściwości obiektu. Na przykład, dla czegoś, co można zobaczyć w świecie 3D, można dać obiektowi komponent renderowania, który zawiera informacje o siatce renderowania. Jeśli chcesz, aby obiekt miał fizykę, nadaj mu komponent fizyki. Jeśli chcesz, aby coś działało jak kamera, daj mu element kamery. Lista komponentów może się zmieniać.

Kluczem jest komunikacja między interfejsami, obiektami i komponentami. W moim silniku mam ogólną klasę wiadomości, która zawiera tylko unikalny identyfikator i identyfikator typu wiadomości. Unikalny identyfikator to identyfikator obiektu, do którego wiadomość ma się udać, a identyfikator typu wiadomości jest używany przez obiekt odbierający wiadomość, aby wiedział, jaki to jest typ wiadomości.

Obiekty mogą obsłużyć komunikat, jeśli są potrzebne, i mogą przekazać wiadomość do każdego ze swoich komponentów, a komponenty często wykonują ważne czynności z komunikatem. Na przykład, jeśli chcesz zmienić i pozycję obiektu, wysyłasz obiektowi komunikat SetPosition, obiekt może zaktualizować swoją zmienną pozycji, gdy otrzyma komunikat, ale komponent renderujący może potrzebować komunikatu, aby zaktualizować pozycję siatki renderowania, i element fizyki może potrzebować komunikatu, aby zaktualizować pozycję ciała fizyki.

Oto bardzo prosty układ menedżera scen, obiektu i komponentu oraz przepływu komunikatów, który wymyśliłem w około godzinę, napisany w C ++. Po uruchomieniu ustawia pozycję na obiekcie, a wiadomość przechodzi przez komponent renderowania, a następnie pobiera pozycję z obiektu. Cieszyć się!

Napisałem również wersję C # i Scala poniższego kodu dla każdego, kto może biegle posługiwać się nimi, a nie 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';
}
Nic Foster
źródło
1
Ten kod wygląda naprawdę ładnie. Przypomina mi Jedność.
Tili
Wiem, że to stara odpowiedź, ale mam kilka pytań. Czy „prawdziwa” gra nie miałaby setek rodzajów wiadomości, co byłoby koszmarem kodowania? Co również robisz, jeśli potrzebujesz (na przykład) sposobu, w jaki główny bohater stoi, aby poprawnie go narysować. Czy nie musisz tworzyć nowego GetSpriteMessage i wysyłać go przy każdym renderowaniu? Czy to nie staje się zbyt drogie? Zastanawiam się! Dzięki.
you786
W moim ostatnim projekcie wykorzystaliśmy XML do napisania wiadomości, a skrypt Pythona utworzył dla nas cały kod w czasie kompilacji. Możesz podzielić na wiele plików XML dla różnych kategorii wiadomości. Możesz tworzyć makra do wysyłania wiadomości, dzięki czemu są one tak zwięzłe jak wywołanie funkcji, jeśli potrzebowałeś sposobu, w jaki znak był skierowany bez wysyłania wiadomości, nadal musisz dostać wskaźnik do komponentu, a następnie znać funkcję do wywołania to (jeśli nie korzystasz z wiadomości). RenderComponent może zarejestrować się w module renderującym, więc nie musisz wyszukiwać go w każdej ramce.
Nic Foster,
2

Myślę, że to najlepszy sposób na użycie Menedżera scen i interfejsów. Zaimplementowano przesyłanie wiadomości, ale użyłbym go jako drugiego podejścia. Wiadomości są dobre do komunikacji między wątkami. Używaj abstrakcji (interfejsów) gdziekolwiek możesz.

Nie wiem wiele o Ogre, więc mówię ogólnie.

Zasadniczo masz główną pętlę gry. Odbiera sygnały wejściowe, oblicza AI (od prostego ruchu do złożonej AI i logiki gry), ładuje zasoby [itp.] I wyświetla aktualny stan. Jest to podstawowy przykład, dzięki któremu można podzielić silnik na te części (InputManager, AIManager, ResourceManager, RenderManager). I powinieneś mieć SceneManager, który przechowuje wszystkie obiekty obecne w grze.

Każda z tych części i ich części składowe mają interfejsy. Staraj się więc zorganizować te części, aby wykonały swoją i tylko swoją pracę. Powinny używać podelementów, które oddziałują wewnętrznie do celów części nadrzędnej. W ten sposób nie będziesz się wplątać bez szansy na rozwinięcie się bez całkowitego przepisania.

ps, jeśli używasz C ++, rozważ użycie wzorca RAII

edin-m
źródło
2
RAII nie jest wzorem, jest sposobem na życie.
Shotgun Ninja