Làm thế nào để thực hiện sự tương tác giữa các bộ phận động cơ?


10

Tôi muốn hỏi một câu hỏi về cách trao đổi thông tin giữa các bộ phận công cụ trò chơi nên được thực hiện.

Động cơ được tách thành bốn phần: logic, dữ liệu, giao diện người dùng, đồ họa. Ban đầu tôi đã thực hiện trao đổi này thông qua các cờ. Ví dụ: nếu đối tượng mới được thêm vào dữ liệu, cờ isNewtrong lớp của đối tượng sẽ được đặt là true. Và sau đó, phần đồ họa của công cụ sẽ kiểm tra lá cờ này và sẽ thêm đối tượng vào thế giới trò chơi.

Mặc dù, với cách tiếp cận này, tôi đã viết nhiều mã để xử lý mọi cờ của từng loại đối tượng.

Tôi đã nghĩ đến việc sử dụng một số hệ thống sự kiện, nhưng tôi không có đủ kinh nghiệm để biết liệu đây có phải là giải pháp phù hợp hay không.

Là hệ thống sự kiện là cách tiếp cận phù hợp duy nhất, hay tôi nên sử dụng cái gì khác?

Tôi đang sử dụng Ogre làm công cụ đồ họa, nếu điều đó quan trọng.


Đây là một câu hỏi rất mơ hồ. Cách các hệ thống của bạn tương tác sẽ được kết hợp rất chặt chẽ với cách các hệ thống của bạn được thiết kế và loại đóng gói mà bạn đang thực hiện. Nhưng có một điều nổi bật: "Và sau đó, phần đồ họa của công cụ sẽ kiểm tra lá cờ này và sẽ thêm đối tượng vào thế giới trò chơi." Tại sao phần đồ họa của động cơ thêm mọi thứ vào thế giới ? Có vẻ như thế giới nên nói với mô-đun đồ họa những gì sẽ kết xuất.
Tetrad

Trong công cụ, phần "đồ họa" điều khiển Ogre (ví dụ, bảo nó thêm đối tượng vào cảnh). Nhưng để làm điều đó, nó cũng tìm kiếm "dữ liệu" cho đối tượng mới (và sau đó nói với Ogre để thêm nó vào cảnh) Nhưng tôi không biết cách tiếp cận này là đúng hay sai vì thiếu kinh nghiệm.
Userr

Câu trả lời:


20

Cấu trúc công cụ trò chơi yêu thích của tôi là mô hình thành phần và giao diện <-> sử dụng tin nhắn để liên lạc giữa hầu hết các bộ phận.

Bạn có nhiều giao diện cho các bộ phận động cơ chính như trình quản lý cảnh, trình tải tài nguyên, âm thanh, trình kết xuất, vật lý, v.v.

Tôi có người quản lý cảnh phụ trách tất cả các đối tượng trong thế giới / thế giới 3D.

Đối tượng là một lớp rất nguyên tử, chỉ chứa một vài thứ phổ biến cho hầu hết mọi thứ trong cảnh của bạn, trong công cụ của tôi, lớp đối tượng chỉ giữ vị trí, xoay, danh sách các thành phần và ID duy nhất. Mọi ID của đối tượng được tạo bởi một int int, do đó sẽ không có hai đối tượng nào có cùng một ID, điều này cho phép bạn gửi tin nhắn đến một đối tượng bằng ID của nó, thay vì phải có một con trỏ tới đối tượng.

Danh sách các thành phần trên đối tượng là những gì mang lại cho các đối tượng đó là các thuộc tính chính. Ví dụ, đối với một cái gì đó mà bạn có thể nhìn thấy trong thế giới 3D, bạn sẽ cung cấp cho đối tượng của mình một thành phần kết xuất có chứa thông tin về lưới kết xuất. Nếu bạn muốn một đối tượng có vật lý, bạn sẽ cung cấp cho nó một thành phần vật lý. Nếu bạn muốn một cái gì đó hoạt động như một máy ảnh, hãy cung cấp cho nó một thành phần máy ảnh. Danh sách các thành phần có thể đi và về.

Giao tiếp giữa các giao diện, đối tượng và các thành phần là chìa khóa. Trong công cụ của tôi, tôi có một lớp thông báo chung chỉ chứa một ID duy nhất và ID loại thông báo. ID duy nhất là ID của đối tượng bạn muốn gửi tin nhắn và ID loại tin nhắn được sử dụng bởi đối tượng nhận tin nhắn để nó biết loại tin nhắn đó là gì.

Các đối tượng có thể xử lý tin nhắn nếu họ cần và họ có thể truyền tin nhắn cho từng thành phần của mình và các thành phần thường sẽ làm những việc quan trọng với tin nhắn. Ví dụ: nếu bạn muốn thay đổi và vị trí của đối tượng bạn gửi cho đối tượng một thông báo SetPocation, đối tượng có thể cập nhật biến vị trí của nó khi nhận được thông báo, nhưng thành phần kết xuất có thể cần thông báo để cập nhật vị trí của lưới kết xuất và thành phần vật lý có thể cần thông điệp để cập nhật vị trí của cơ thể vật lý.

Đây là một bố cục rất đơn giản của trình quản lý cảnh, đối tượng và thành phần và luồng thông báo mà tôi đã đánh dấu trong khoảng một giờ, được viết bằng C ++. Khi chạy, nó đặt vị trí trên một đối tượng và thông báo đi qua thành phần kết xuất, sau đó lấy vị trí từ đối tượng. Thưởng thức!

Ngoài ra, tôi đã viết một phiên bản C #phiên bản Scala của mã dưới đây cho bất kỳ ai có thể thông thạo những thứ đó hơn là 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
Mã này trông thực sự tốt đẹp. Nhắc nhở tôi về sự thống nhất.
Tili

Tôi biết đây là một câu trả lời cũ, nhưng tôi có một vài câu hỏi. Không phải một trò chơi 'thực' sẽ có hàng trăm loại Tin nhắn, tạo ra một cơn ác mộng mã hóa? Ngoài ra, bạn sẽ làm gì nếu bạn cần (ví dụ) cách nhân vật chính đối mặt để vẽ chính xác. Bạn có cần tạo GetSpriteMessage mới và gửi nó mỗi lần bạn kết xuất không? Điều này có trở nên quá đắt không? Chỉ cần tự hỏi! Cảm ơn.
bạn786

Trong dự án cuối cùng của chúng tôi, chúng tôi đã sử dụng XML để viết các thông điệp và một tập lệnh python đã tạo ra tất cả các mã cho chúng tôi trong thời gian xây dựng. Bạn có thể tách thành nhiều XML cho các loại thông báo khác nhau. Bạn có thể tạo các macro để gửi tin nhắn, làm cho chúng gần như ngắn gọn như một cuộc gọi chức năng, nếu bạn cần cách mà một nhân vật đang đối mặt mà không nhắn tin, bạn vẫn cần phải đưa con trỏ đến thành phần, và sau đó biết chức năng để gọi nó (nếu bạn không sử dụng nhắn tin). RenderComponent có thể đăng ký với trình kết xuất để bạn không phải truy vấn nó mọi khung hình.
Nic Foster

2

Tôi nghĩ đó là cách tốt nhất để sử dụng Trình quản lý cảnh và Giao diện. Có nhắn tin thực hiện nhưng tôi sẽ sử dụng nó như là cách tiếp cận thứ cấp. Nhắn tin là tốt cho giao tiếp liên chủ đề. Sử dụng trừu tượng (giao diện) bất cứ nơi nào bạn có thể.

Tôi không biết nhiều về Ogre, vì vậy tôi đang nói chung.

Tại cốt lõi, bạn có vòng lặp trò chơi chính. Nó nhận được tín hiệu đầu vào, tính toán AI (từ chuyển động đơn giản đến AI phức tạp và logic trò chơi), tải tài nguyên [, v.v.] và hiển thị trạng thái hiện tại. Đây là ví dụ cơ bản, vì vậy bạn có thể tách công cụ thành các phần này (InputManager, AIManager, ResourceManager, RenderManager). Và bạn nên có Trình quản lý cảnh chứa tất cả các đối tượng có trong trò chơi.

Mỗi phần và các phần phụ của chúng có giao diện. Vì vậy, cố gắng tổ chức những phần này để làm và chỉ công việc của họ. Họ nên sử dụng các phần phụ tương tác nội bộ cho mục đích của phần cha mẹ của họ. Bằng cách đó, bạn sẽ không bị lôi kéo mà không có cơ hội hủy đăng ký mà không viết lại toàn bộ.

ps nếu bạn đang sử dụng C ++, hãy cân nhắc sử dụng mẫu RAII


2
RAII không phải là một khuôn mẫu, đó là một cách sống.
Shotgun Ninja
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.