Как реализовать взаимодействие между частями двигателя?

10

Я хочу задать вопрос о том, как должен осуществляться обмен информацией между частями игрового движка.

Движок разделен на четыре части: логика, данные, пользовательский интерфейс, графика. В начале я сделал этот обмен через флаги. Например, если новый объект добавлен в данные, флаг isNewв классе объекта будет установлен как true. И после этого графическая часть движка проверит этот флаг и добавит объект в игровой мир.

Тем не менее, при таком подходе я должен был написать много кода для обработки каждого флага каждого типа объекта.

Я думал об использовании некоторой системы событий, но у меня недостаточно опыта, чтобы понять, будет ли это правильным решением.

Является ли система событий единственным подходящим подходом, или я должен использовать что-то еще?

Я использую Ogre в качестве графического движка, если это имеет значение.

Userr
источник
Это очень расплывчатый вопрос. То, как взаимодействуют ваши системы, будет очень сильно зависеть от того, как спроектированы ваши системы и какой тип инкапсуляции вы в итоге делаете. Но выделяется одно: «И после этого графическая часть движка проверит этот флаг и добавит объект в игровой мир». Почему графическая часть движка добавляет вещи в мир ? Кажется, что мир должен сказать графическому модулю, что делать.
Тетрад
В движке часть «графика» управляет Огром (например, говорит ему добавить объект в сцену). Но для этого он также ищет в «данных» новый объект (и после этого говорит Огре добавить его в сцену). Но я не знаю, является ли этот подход правильным или неправильным из-за отсутствия опыта.
Userr

Ответы:

20

Моя любимая структура игрового движка - это компонентная модель интерфейса и объекта <->, использующая обмен сообщениями для связи практически между всеми частями.

У вас есть несколько интерфейсов для основных частей движка, таких как менеджер сцены, загрузчик ресурсов, аудио, рендер, физика и т. Д.

У меня есть менеджер сцены, отвечающий за все объекты в 3D сцене / мире.

Объект - это очень атомарный класс, содержащий всего несколько вещей, которые являются общими для почти всей вашей сцены. В моем движке класс объекта содержит только положение, вращение, список компонентов и уникальный идентификатор. Идентификатор каждого объекта генерируется статическим int, так что никакие два объекта не будут иметь одинаковые идентификаторы, что позволяет отправлять сообщения объекту по его идентификатору, а не указывать на объект.

Список компонентов на объекте - это то, что дает объектам основные свойства. Например, для чего-то, что вы можете видеть в трехмерном мире, вы бы дали своему объекту компонент рендеринга, который содержит информацию о сетке рендеринга. Если вы хотите, чтобы объект имел физику, вы бы дали ему физический компонент. Если вы хотите, чтобы что-то действовало как камера, добавьте в нее компонент камеры. Список компонентов можно продолжать и продолжать.

Связь между интерфейсами, объектами и компонентами является ключевой. В моем движке у меня есть общий класс сообщений, который содержит только уникальный идентификатор и идентификатор типа сообщения. Уникальный идентификатор - это идентификатор объекта, к которому вы хотите отправить сообщение, а идентификатор типа сообщения используется объектом, получающим сообщение, чтобы он знал, к какому типу это относится.

Объекты могут обрабатывать сообщение, если они нуждаются, и они могут передавать сообщение каждому из своих компонентов, и компоненты часто будут делать важные вещи с сообщением. Например, если вы хотите изменить положение объекта и отправить ему сообщение SetPosition, объект может обновить свою переменную положения, когда он получает сообщение, но компоненту рендеринга может потребоваться сообщение, чтобы обновить положение сетки рендеринга, и физическому компоненту может потребоваться сообщение для обновления положения физического тела.

Вот очень простая компоновка менеджера сцены, объекта и компонента, а также потока сообщений, которую я написал примерно за час, написанная на C ++. При запуске он устанавливает позицию на объекте, и сообщение проходит через компонент рендеринга, а затем извлекает позицию из объекта. Наслаждайтесь!

Кроме того , я написал C # версию и Scala версию кода ниже для тех, кто может быть свободно в тех , а не 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
Этот код выглядит действительно красиво. Напоминает мне о Единстве.
Тили
Я знаю, что это старый ответ, но у меня есть несколько вопросов. Разве в «настоящей» игре не было бы сотен типов сообщений, что превращало бы в кошмар кодирования? Кроме того, что вы делаете, если вам нужно (например) то, как смотрится главный герой, чтобы нарисовать его правильно. Разве вам не нужно создавать новый GetSpriteMessage и отправлять его каждый раз при рендеринге? Разве это не становится слишком дорогим? Просто интересуюсь! Спасибо.
you786
В моем последнем проекте мы использовали XML для написания сообщений, а скрипт на python создал весь код для нас во время сборки. Вы можете разделить на несколько XML-файлов для разных категорий сообщений. Вы можете создавать макросы для отправки сообщений, делая их почти такими же краткими, как и вызов функции. Если вам нужно, чтобы персонаж обращался без обмена сообщениями, вам все равно нужно получить указатель на компонент, а затем узнать функцию для вызова. это (если вы не использовали обмен сообщениями). RenderComponent может зарегистрироваться у средства визуализации, поэтому вам не нужно запрашивать его каждый кадр.
Ник Фостер
2

Я думаю, что это лучший способ использовать Scene Manager и интерфейсы. Реализован ли обмен сообщениями, но я бы использовал его как вторичный подход. Обмен сообщениями хорош для связи между потоками. Используйте абстракцию (интерфейсы) везде, где можете.

Я не знаю много об Огре, поэтому я говорю в целом.

По сути, у вас есть основной игровой цикл. Он получает входные сигналы, вычисляет AI (от простого движения до сложного AI и игровой логики), загружает ресурсы [и т. Д.] И отображает текущее состояние. Это базовый пример, поэтому вы можете разделить движок на эти части (InputManager, AIManager, ResourceManager, RenderManager). И у вас должен быть SceneManager, который содержит все объекты, присутствующие в игре.

У каждой из этих частей и их частей есть интерфейсы. Поэтому постарайтесь организовать эти части, чтобы сделать их и только свою работу. Они должны использовать части, которые взаимодействуют внутри с целью их родительской части. Таким образом, вы не попадете в замешательство без шанса развернуться без полного переписывания.

PS, если вы используете C ++, подумайте об использовании шаблона RAII

Эдин-м
источник
2
RAII это не шаблон, это образ жизни.
Дробовик ниндзя