¿Cómo implementar la interacción entre las partes del motor?

10

Quiero hacer una pregunta sobre cómo se debe implementar el intercambio de información entre las partes del motor del juego.

El motor está separado en cuatro partes: lógica, datos, interfaz de usuario, gráficos. Al principio hice este intercambio a través de las banderas. Por ejemplo, si el nuevo objeto se agrega a los datos, la bandera isNewen la clase de un objeto se establecerá como true. Y después de eso, la parte de gráficos del motor verificará esta bandera y agregará el objeto al mundo del juego.

Sin embargo, con este enfoque, debía escribir mucho código para procesar cada bandera de cada tipo de objeto.

Pensé en usar algún sistema de eventos, pero no tengo suficiente experiencia para saber si esta sería la solución correcta.

¿Es el sistema de eventos el único enfoque apropiado, o debería usar algo más?

Estoy usando Ogre como motor gráfico, si eso importa.

Usuario
fuente
Esta es una pregunta muy vaga. La forma en que interactúan sus sistemas va a estar muy unida a cómo están diseñados sus sistemas y qué tipo de encapsulación está terminando haciendo. Pero una cosa destaca: "Y después de eso, la parte de gráficos del motor verificará esta bandera y agregará el objeto al mundo del juego". ¿Por qué la parte gráfica del motor agrega cosas al mundo ? Parece que el mundo debería decirle al módulo de gráficos qué renderizar.
Tetrad
En el motor, la parte de "gráficos" controla al Ogro (por ejemplo, le dice que agregue un objeto a la escena). Pero para hacer eso también busca en los "datos" el objeto que es nuevo (y luego le dice a Ogre que lo agregue a la escena). Pero no sé si este enfoque es correcto o incorrecto debido a la falta de experiencia.
Usuario

Respuestas:

20

Mi estructura de motor de juego favorita es la interfaz y el modelo de componente de objeto <-> que usa mensajes para la comunicación entre casi todas las partes.

Tiene múltiples interfaces para las partes principales del motor, como su administrador de escena, cargador de recursos, audio, renderizador, física, etc.

Tengo al administrador de escena a cargo de todos los objetos en la escena / mundo 3D.

Object es una clase muy atómica, que contiene solo algunas cosas que son comunes a casi todo en su escena, en mi motor, la clase de objeto tiene solo posición, rotación, una lista de componentes y una ID única. La ID de cada objeto es generada por un int estático, de modo que no habrá dos objetos que tengan la misma ID, esto le permite enviar mensajes a un objeto por su ID, en lugar de tener que tener un puntero al objeto.

La lista de componentes en el objeto es lo que le da a los objetos sus propiedades principales. Por ejemplo, para algo que puede ver en el mundo 3D, le daría a su objeto un componente de representación que contiene la información sobre la malla de representación. Si desea que un objeto tenga física, le daría un componente de física. Si desea que algo actúe como una cámara, dele un componente de cámara. La lista de componentes puede seguir y seguir.

La comunicación entre interfaces, objetos y componentes es clave. En mi motor, tengo una clase de mensaje genérico que contiene solo una ID única y una ID de tipo de mensaje. La identificación única es la identificación del objeto al que desea que vaya el mensaje, y la identificación del tipo de mensaje es utilizada por el objeto que recibe el mensaje para que sepa qué tipo de mensaje es.

Los objetos pueden manejar el mensaje si lo necesitan, y pueden pasar el mensaje a cada uno de sus componentes, y los componentes a menudo harán cosas importantes con el mensaje. Por ejemplo, si desea cambiar la posición del objeto y le envía un mensaje SetPosition, el objeto puede actualizar su variable de posición cuando recibe el mensaje, pero el componente de representación puede necesitar un mensaje para actualizar la posición de la malla de representación, y el componente de física puede necesitar el mensaje para actualizar la posición del cuerpo de física.

Aquí hay un diseño muy simple de administrador de escena, objeto, componente y flujo de mensajes, que preparé en aproximadamente una hora, escrito en C ++. Cuando se ejecuta, establece la posición en un objeto, y el mensaje pasa a través del componente de representación, luego recupera la posición del objeto. ¡Disfrutar!

Además, he escrito una versión de C # y una versión de Scala del código a continuación para cualquier persona que pueda hablar con fluidez en lugar de 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
fuente
1
Este código se ve muy bien. Me recuerda a la Unidad.
Tili
Sé que esta es una respuesta anterior, pero tengo algunas preguntas. ¿No sería un juego 'real' tener cientos de tipos de mensajes, haciendo una pesadilla de codificación? Además, ¿qué hacer si necesita (por ejemplo) la forma en que se enfrenta el personaje principal para dibujarlo correctamente. ¿No necesitaría crear un nuevo GetSpriteMessage y enviarlo cada vez que renderice? ¿No se vuelve demasiado caro? ¡Sólo me preguntaba! Gracias.
you786
En mi último proyecto, usamos XML para escribir los mensajes y un script de Python creó todo el código para nosotros durante el tiempo de compilación. Puede separarse en múltiples XML para diferentes categorías de mensajes. Puede crear macros para el envío de mensajes, haciéndolos casi tan concisos como una llamada a la función, si necesita la forma en que se enfrentaba un personaje sin mensajes, aún necesitaría obtener el puntero al componente y luego conocer la función para llamar (si no estabas usando la mensajería). RenderComponent puede registrarse con el renderizador para que no tenga que consultarlo cada fotograma.
Nic Foster
2

Creo que es la mejor manera de usar Scene Manager e Interfaces. Tiene implementado el mensaje pero lo usaría como un enfoque secundario. La mensajería es buena para la comunicación entre hilos. Utilice la abstracción (interfaces) siempre que pueda.

No sé mucho sobre Ogre, así que estoy hablando en general.

En esencia, tienes el bucle principal del juego. Obtiene señales de entrada, calcula la IA (desde el movimiento simple hasta la IA compleja y la lógica del juego), carga recursos [, etc.] y representa el estado actual. Este es un ejemplo básico, por lo que puede separar el motor en estas partes (InputManager, AIManager, ResourceManager, RenderManager). Y deberías tener SceneManager que contiene todos los objetos que están presentes en el juego.

Cada una de estas partes y sus subpartes tienen interfaces. Así que trate de organizar estas partes para hacer su trabajo y solo el suyo. Deben usar subpartes que interactúen internamente con el propósito de su parte principal. De esa manera no te enredarás sin posibilidad de desenrollarte sin una reescritura total.

ps si estás usando C ++ considera usar el patrón RAII

edin-m
fuente
2
RAII no es un patrón, es una forma de vida.
Shotgun Ninja