วิธีการใช้ปฏิสัมพันธ์ระหว่างชิ้นส่วนเครื่องยนต์?


10

ฉันต้องการถามคำถามเกี่ยวกับวิธีการแลกเปลี่ยนข้อมูลระหว่างชิ้นส่วนกลไกเกม

เครื่องยนต์ถูกแยกออกเป็นสี่ส่วน: ลอจิก, ข้อมูล, UI, กราฟิก ในตอนแรกฉันทำการแลกเปลี่ยนนี้ผ่านธง ตัวอย่างเช่นถ้าวัตถุใหม่จะถูกเพิ่มในข้อมูลธงในชั้นเรียนของวัตถุจะถูกกำหนดให้เป็นisNew trueและหลังจากนั้นส่วนกราฟิกของเครื่องยนต์จะตรวจสอบธงนี้และจะเพิ่มวัตถุลงในโลกของเกม

แม้ว่าด้วยวิธีนี้ฉันต้องเขียนโค้ดมากเพื่อประมวลผลค่าสถานะของวัตถุแต่ละชนิด

ฉันคิดว่าจะใช้ระบบเหตุการณ์บางอย่าง แต่ฉันไม่มีประสบการณ์เพียงพอที่จะรู้ว่านี่จะเป็นทางออกที่ถูกต้องหรือไม่

ระบบเหตุการณ์เป็นแนวทางที่เหมาะสมหรือฉันควรใช้อย่างอื่นหรือไม่

ฉันใช้ Ogre เป็นเอ็นจิ้นกราฟิคถ้ามันสำคัญ


นี่เป็นคำถามที่คลุมเครือมาก การที่ระบบของคุณโต้ตอบกันนั้นจะควบคู่ไปกับการออกแบบระบบของคุณและการห่อหุ้มแบบไหนที่คุณกำลังทำอยู่ แต่มีสิ่งหนึ่งที่โดดเด่น: "และหลังจากนั้นส่วนกราฟิกของเครื่องยนต์จะตรวจสอบธงนี้และจะเพิ่มวัตถุลงในโลกของเกม" คือทำไมกราฟิกเป็นส่วนหนึ่งของเครื่องยนต์เพิ่มสิ่งที่ไปของโลก ? ดูเหมือนว่าโลกควรบอกโมดูลกราฟิกว่าจะแสดงผลอย่างไร
Tetrad

ในเครื่องยนต์ส่วน "กราฟิก" จะควบคุม Ogre (ตัวอย่างเช่นบอกให้เพิ่มวัตถุเข้าไปในฉาก) แต่สำหรับการทำสิ่งนั้นมันก็ค้นหา "ข้อมูล" สำหรับวัตถุที่เป็นของใหม่ด้วย (และหลังจากนั้นก็บอกให้ Ogre เพิ่มเข้าไปในที่เกิดเหตุ) แต่ฉันไม่รู้ว่าวิธีการนี้ถูกหรือผิดเพราะขาดประสบการณ์
Userr

คำตอบ:


20

โครงสร้างเอ็นจิ้นเกมโปรดของฉันคืออินเตอร์เฟสและโมเดลวัตถุ <-> โดยใช้การส่งข้อความเพื่อการสื่อสารระหว่างเกือบทุกส่วน

คุณมีส่วนต่อประสานหลายอันสำหรับชิ้นส่วนเครื่องยนต์หลักเช่นตัวจัดการฉาก, ตัวโหลดทรัพยากร, เสียง, ตัวเรนเดอร์, ฟิสิกส์ ฯลฯ

ฉันมีผู้จัดการฉากที่รับผิดชอบทุกอย่างในฉาก 3 มิติ / โลก

Object เป็นคลาสอะตอมมิคที่บรรจุเพียงบางสิ่งที่พบได้ทั่วไปในฉากของคุณในเอ็นจินของฉันคลาสวัตถุจะมีเฉพาะตำแหน่งการหมุนรายการส่วนประกอบและ ID เฉพาะ ID ของวัตถุทุกชิ้นถูกสร้างขึ้นโดย int แบบสแตติกเพื่อให้ไม่มีวัตถุสองชิ้นที่จะมี ID เดียวกันทั้งหมดนี้ช่วยให้คุณสามารถส่งข้อความไปยังวัตถุด้วย ID ของมันแทนที่จะต้องมีตัวชี้ไปยังวัตถุ

รายการส่วนประกอบบนวัตถุคือสิ่งที่ทำให้วัตถุนั้นเป็นคุณสมบัติหลัก ตัวอย่างเช่นสำหรับบางสิ่งที่คุณสามารถเห็นได้ในโลก 3 มิติคุณจะต้องให้องค์ประกอบการเรนเดอร์วัตถุของคุณที่มีข้อมูลเกี่ยวกับตาข่ายการเรนเดอร์ ถ้าคุณต้องการให้วัตถุมีฟิสิกส์คุณจะต้องให้มันเป็นองค์ประกอบของฟิสิกส์ หากคุณต้องการบางสิ่งบางอย่างที่จะทำหน้าที่เป็นกล้องให้องค์ประกอบกล้อง รายการส่วนประกอบสามารถไปเรื่อย ๆ

การสื่อสารระหว่างอินเตอร์เฟสวัตถุและส่วนประกอบเป็นกุญแจสำคัญ ในเอ็นจิ้นของฉันฉันมีคลาสข้อความทั่วไปที่มีเฉพาะ ID ที่ไม่ซ้ำกันและ ID ประเภทข้อความ ID ที่ไม่ซ้ำกันคือ ID ของวัตถุที่คุณต้องการให้ข้อความไปถึงและ ID ประเภทข้อความจะถูกใช้โดยวัตถุที่ได้รับข้อความเพื่อให้ทราบว่าเป็นข้อความประเภทใด

วัตถุสามารถจัดการกับข้อความได้หากต้องการและพวกเขาสามารถส่งข้อความไปยังแต่ละส่วนประกอบได้และส่วนประกอบมักจะทำสิ่งสำคัญกับข้อความ ตัวอย่างเช่นหากคุณต้องการเปลี่ยนและตำแหน่งของวัตถุที่คุณส่งวัตถุเป็นข้อความ 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
รหัสนี้ดูดีจริงๆ เตือนฉันถึงความสามัคคี
Tili

ฉันรู้ว่านี่เป็นคำตอบเก่า แต่ฉันมีคำถามสองสามข้อ เกม 'ของจริง' จะมีประเภทของข้อความนับร้อย ๆ ชนิดทำให้ฝันร้ายเข้ารหัสหรือไม่ นอกจากนี้คุณจะทำอย่างไรถ้าคุณต้องการ (ตัวอย่าง) วิธีที่ตัวละครหลักหันหน้าไปวาดมันอย่างถูกต้อง คุณไม่จำเป็นต้องสร้าง GetSpriteMessage ใหม่และส่งในทุกครั้งที่คุณแสดง สิ่งนี้ไม่แพงเกินไปหรือ เพียงแค่สงสัย! ขอบคุณ
you786

ในโปรเจ็กต์สุดท้ายของฉันเราใช้ XML เพื่อเขียนข้อความและสคริปต์ไพ ธ อนสร้างโค้ดทั้งหมดให้เราในช่วงเวลาบิลด์ คุณสามารถแยกออกเป็นหลาย XML สำหรับประเภทข้อความที่แตกต่างกัน คุณสามารถสร้างมาโครสำหรับการส่งข้อความทำให้พวกมันเกือบจะเป็นคำย่อในการเรียกใช้ฟังก์ชันถ้าคุณต้องการวิธีที่ตัวละครหันหน้าไปทางโดยไม่ต้องส่งข้อความคุณยังต้องรับตัวชี้ไปยังส่วนประกอบแล้วจึงรู้ว่าฟังก์ชั่นโทร มัน (ถ้าคุณไม่ได้ใช้การส่งข้อความ) RenderComponent สามารถลงทะเบียนกับ renderer ดังนั้นคุณไม่ต้องค้นหามันทุกเฟรม
Nic Foster

2

ฉันคิดว่ามันเป็นวิธีที่ดีที่สุดในการใช้ Scene Manager และอินเทอร์เฟซ มีการใช้งานการส่งข้อความ แต่ฉันจะใช้เป็นวิธีการรอง การส่งข้อความนั้นดีสำหรับการสื่อสารระหว่างเธรด ใช้นามธรรม (อินเทอร์เฟซ) ทุกที่ที่คุณสามารถทำได้

ฉันไม่ค่อยรู้อะไรเกี่ยวกับ Ogre ดังนั้นฉันจึงพูดโดยทั่วไป

ที่แกนกลางคุณมีวนรอบเกมหลัก มันได้รับสัญญาณอินพุตคำนวณ AI (จากการเคลื่อนไหวอย่างง่ายไปจนถึง AI และตรรกะของเกมที่ซับซ้อน) โหลดทรัพยากร [ฯลฯ ] และแสดงสถานะปัจจุบัน นี่เป็นตัวอย่างพื้นฐานเพื่อให้คุณสามารถแยกเครื่องยนต์ออกเป็นส่วนต่าง ๆ เหล่านี้ (InputManager, AIManager, ResourceManager, RenderManager) และคุณควรมี SceneManager ซึ่งมีวัตถุทั้งหมดที่มีอยู่ในเกม

ทุกส่วนเหล่านี้และส่วนย่อยมีส่วนต่อประสาน ดังนั้นพยายามจัดระเบียบชิ้นส่วนเหล่านี้เพื่อทำงานของพวกเขาและงานของพวกเขาเท่านั้น พวกเขาควรใช้ส่วนย่อยที่มีการโต้ตอบภายในเพื่อจุดประสงค์ในส่วนของผู้ปกครอง ด้วยวิธีนี้คุณจะไม่เข้าไปยุ่งเกี่ยวโดยไม่มีโอกาสคลี่คลายโดยไม่มีการเขียนซ้ำทั้งหมด

ป.ล. ถ้าคุณใช้ C ++ ลองใช้รูปแบบRAII


2
RAII ไม่ใช่แบบแผน แต่เป็นวิถีชีวิต
ปืนลูกซองนินจา
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.