Como implementar a interação entre as peças do motor?

10

Quero fazer uma pergunta sobre como a troca de informações entre as partes do motor do jogo deve ser implementada.

O mecanismo é separado em quatro partes: lógica, dados, interface do usuário, gráficos. No começo eu fiz essa troca através das bandeiras. Por exemplo, se o novo objeto for adicionado aos dados, o sinalizador isNewna classe de um objeto será definido como true. E depois disso, a parte gráfica do mecanismo verificará essa bandeira e adicionará o objeto ao mundo do jogo.

Porém, com essa abordagem, escrevi muito código para processar todos os sinalizadores de cada tipo de objeto.

Pensei em usar algum sistema de eventos, mas não tenho experiência suficiente para saber se essa seria a solução certa.

O sistema de eventos é a única abordagem apropriada ou devo usar outra coisa?

Estou usando o Ogre como o mecanismo gráfico, se isso importa.

Userr
fonte
Esta é uma pergunta muito vaga. O modo como seus sistemas interagem será muito associado à maneira como seus sistemas são projetados e que tipo de encapsulamento você está fazendo. Mas uma coisa se destaca: "E depois disso, a parte gráfica do mecanismo verificará essa bandeira e adicionará o objeto ao mundo do jogo". Por que a parte gráfica do mecanismo está adicionando coisas ao mundo ? Parece que o mundo deveria dizer ao módulo gráfico o que renderizar.
Tétrada
No mecanismo, a parte "gráficos" controla o Ogre (por exemplo, diz para ele adicionar um objeto à cena). Mas, ao fazer isso, ele também procura nos "dados" o objeto que é novo (e depois diz ao Ogre para adicioná-lo à cena). Mas não sei se essa abordagem é certa ou errada por falta de experiência.
UserR

Respostas:

20

Minha estrutura de mecanismo de jogo favorita é a interface e o objeto <-> modelo de componente usando mensagens para comunicação entre quase todas as partes.

Você tem várias interfaces para as principais partes do mecanismo, como o gerenciador de cenas, o carregador de recursos, o áudio, o renderizador, a física etc.

Eu tenho o gerente de cena encarregado de todos os objetos na cena / mundo 3D.

Objeto é uma classe muito atômica, contendo apenas algumas coisas comuns a quase tudo em sua cena. No meu mecanismo, a classe de objeto mantém apenas posição, rotação, uma lista de componentes e um ID exclusivo. O ID de cada objeto é gerado por um int estático, de modo que nenhum objeto tenha o mesmo ID; isso permite que você envie mensagens para um objeto por seu ID, em vez de precisar de um ponteiro para o objeto.

A lista de componentes no objeto é o que fornece a esses objetos as principais propriedades. Por exemplo, para algo que você pode ver no mundo 3D, você daria ao seu objeto um componente de renderização que contém as informações sobre a malha de renderização. Se você deseja que um objeto possua física, daria a ele um componente de física. Se você quiser que algo atue como uma câmera, forneça um componente para ela. A lista de componentes pode continuar indefinidamente.

A comunicação entre interfaces, objetos e componentes é fundamental. No meu mecanismo, tenho uma classe de mensagem genérica que contém apenas um ID exclusivo e um ID do tipo de mensagem. O ID exclusivo é o ID do objeto para o qual você deseja que a mensagem e o ID do tipo de mensagem é usado pelo objeto que recebe a mensagem para que ele saiba que tipo de mensagem é.

Os objetos podem manipular a mensagem, se necessário, e podem passar a mensagem para cada um de seus componentes, e os componentes geralmente fazem coisas importantes com a mensagem. Por exemplo, se você deseja alterar a posição do objeto e enviar uma mensagem SetPosition ao objeto, o objeto pode atualizar sua variável de posição ao receber a mensagem, mas o componente de renderização pode precisar de uma mensagem para atualizar a posição da malha de renderização e o componente físico pode precisar da mensagem para atualizar a posição do corpo físico.

Aqui está um layout muito simples do gerenciador de cenas, objeto e componente e fluxo de mensagens, que criei em cerca de uma hora, escrito em C ++. Quando executada, define a posição em um objeto e a mensagem passa pelo componente de renderização e recupera a posição do objeto. Aproveitar!

Além disso, eu escrevi uma versão em C # e uma versão em Scala do código abaixo para qualquer pessoa que seja fluente naqueles e não em 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
fonte
11
Este código parece muito bom. Lembra-me da unidade.
Tili
Sei que essa é uma resposta antiga, mas tenho algumas perguntas. Um jogo 'real' não teria centenas de tipos de mensagem, criando um pesadelo de codificação? Além disso, o que você faz se precisar (por exemplo) da maneira como o personagem principal está enfrentando para desenhá-lo corretamente. Você não precisaria criar um novo GetSpriteMessage e enviá-lo sempre que renderizar? Isso não se torna muito caro? Apenas me perguntando! Obrigado.
you786
No meu último projeto, usamos XML para escrever as mensagens e um script python criou todo o código para nós durante o tempo de construção. Você pode separar em vários XMLs para diferentes categorias de mensagens. Você pode criar macros para o envio de mensagens, tornando-as quase tão concisas quanto uma chamada de função, se precisar da maneira que um personagem está enfrentando sem enviar mensagens, você ainda precisará direcionar o ponteiro para o componente e conhecer a função a ser chamada (se você não estivesse usando mensagens). O RenderComponent pode se registrar no renderizador, para que você não precise consultá-lo em todos os quadros.
21412 Nic Foster
2

Eu acho que é a melhor maneira de usar o Scene Manager e Interfaces. As mensagens foram implementadas, mas eu a usaria como abordagem secundária. As mensagens são boas para a comunicação entre threads. Use abstração (interfaces) sempre que puder.

Eu não sei muito sobre o Ogre, então estou falando em geral.

No núcleo, você tem o loop principal do jogo. Ele recebe sinais de entrada, calcula a IA (do movimento simples à IA complexa e lógica do jogo), carrega recursos [, etc] e renderiza o estado atual. Este é um exemplo básico, para que você possa separar o mecanismo nessas partes (InputManager, AIManager, ResourceManager, RenderManager). E você deve ter o SceneManager, que contém todos os objetos presentes no jogo.

Cada uma dessas partes e suas sub-partes têm interfaces. Portanto, tente organizar essas partes para fazer seu e somente seu trabalho. Eles devem usar sub-partes que interagem internamente com o objetivo de sua parte-pai. Dessa forma, você não se envolverá sem chance de desenrolar sem reescrever totalmente.

ps se você estiver usando C ++, considere usar o padrão RAII

edin-m
fonte
2
RAII não é um padrão, é um modo de vida.
Shotgun Ninja