Як реалізувати взаємодію між деталями двигуна?


10

Хочу задати питання про те, як повинен здійснюватися обмін інформацією між частинами ігрових двигунів.

Двигун розділений на чотири частини: логіка, дані, інтерфейс користувача, графіка. На початку я здійснив цей обмін через прапори. Наприклад, якщо новий об'єкт доданий у дані, прапор isNewу класі об’єкта буде встановлений як true. А після цього графічна частина двигуна перевірить цей прапор і додасть об’єкт у ігровий світ.

Хоча при такому підході я повинен був написати багато коду для обробки кожного прапора кожного виду об’єктів.

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

Чи є система заходів єдиним підходящим підходом, або я повинен використовувати щось інше?

Я використовую Ogre як графічний двигун, якщо це має значення.


Це дуже розпливчасте питання. Те, як взаємодіють ваші системи, буде дуже поєднане з тим, як розроблені ваші системи, і яку інкапсуляцію ви закінчуєте. Але виділяється одне: "А після цього графічна частина двигуна перевірить цей прапор і додасть об'єкт у ігровий світ". Чому графічна частина двигуна додає речі у світ ? Здається, що світ повинен підказати графічному модулю, що потрібно візуалізувати.
Тетрад

У двигуні "графічна" частина керує Ogre (наприклад, каже їй, щоб додати об'єкт до сцени). Але для цього він також шукає "дані" для нового об'єкта (і після цього каже Огре додати його на сцену). Але я не знаю, чи такий підхід є правильним чи неправильним через відсутність досвіду.
Користувач

Відповіді:


20

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

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

У мене є керівник сцени, який відповідає за всі об'єкти в 3D-сцені / світі.

Об'єкт - це дуже атомний клас, що містить лише декілька речей, які є загальними для майже всього у вашій сцені, в моєму двигуні об'єктний клас містить лише положення, обертання, список компонентів та унікальний ідентифікатор. Ідентифікатор кожного об'єкта генерується статичним int, так що жоден два об'єкти не матимуть однакового ідентифікатора, це дозволяє вам надсилати повідомлення об'єкту за його ідентифікатором, а не мати покажчик на об'єкт.

Список компонентів на об'єкті - це те, що дає цим об'єктам основні властивості. Наприклад, для чогось, що ви можете бачити в 3D-світі, ви надасте вашому об'єкту компонент візуалізації, який містить інформацію про сітку візуалізації. Якщо ви хочете, щоб об'єкт мав фізику, ви б дали йому фізичну складову. Якщо ви хочете, щоб щось виконувало роль камери, надайте їй компонент камери. Список компонентів можна продовжувати і продовжувати.

Зв'язок між інтерфейсами, об'єктами та компонентами є ключовим. У своєму двигуні у мене є загальний клас повідомлень, який містить лише унікальний ідентифікатор та ідентифікатор типу повідомлення. Унікальний ідентифікатор - це ідентифікатор об’єкта, на який ви хочете, щоб повідомлення перейшло, а ідентифікатор типу повідомлення використовується об'єктом, який отримує повідомлення, щоб він знав, який тип повідомлення це.

Об'єкти можуть обробляти повідомлення, якщо вони потребують, і вони можуть передавати повідомлення кожному своєму компоненту, а компоненти часто роблять важливі справи з повідомленням. Наприклад, якщо ви хочете змінити і позицію об'єкта, ви надсилаєте об'єкту повідомлення 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

Я думаю, що це найкращий спосіб використовувати Менеджер сценаріїв та інтерфейси. Увімкніть обмін повідомленнями, але я би використовував це як вторинний підхід. Повідомлення добре для міжпотокового спілкування. Використовуйте абстракцію (інтерфейси) де завгодно.

Я мало знаю про Огре, тому загалом кажу.

В основі, у вас є основний цикл гри. Він отримує вхідні сигнали, обчислює AI (від простого руху до складного AI та логіки гри), завантажує ресурси [тощо] та видає поточний стан. Це основний приклад, тому ви можете розділити двигун на ці частини (InputManager, AIManager, ResourceManager, RenderManager). І у вас повинен бути SceneManager, який містить усі об'єкти, які є в грі.

Кожна з цих частин та їх підрозділів мають інтерфейси. Тому спробуйте організувати ці частини, щоб вони виконували свою і єдину свою роботу. Вони повинні використовувати підрозділи, які взаємодіють внутрішньо для батьківської частини. Таким чином, ви не зможете потрапити у вишивку без шансів розкрутитись без повного переписування.

ps, якщо ви використовуєте C ++, розгляньте можливість використання шаблону RAII


2
РАІ - це не зразок, це спосіб життя.
Shotgun Ninja
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.