Motor parçaları arasındaki etkileşim nasıl uygulanır?


10

Oyun motoru parçaları arasındaki bilgi alışverişinin nasıl uygulanması gerektiği hakkında bir soru sormak istiyorum.

Motor dört bölüme ayrılmıştır: mantık, veri, kullanıcı arayüzü, grafik. Başlangıçta bu değişimi bayraklarla yaptım. Örneğin, yeni nesne verilere eklenirse isNew, bir nesnenin sınıfındaki bayrak olarak ayarlanır true. Bundan sonra motorun grafik kısmı bu bayrağı kontrol edecek ve nesneyi oyun dünyasına ekleyecektir.

Yine de, bu yaklaşımla, her bir nesnenin her bayrağını işlemek için çok kod yazacaktım.

Bir olay sistemi kullanmayı düşündüm, ancak bunun doğru çözüm olup olmayacağını bilmek için yeterli deneyimim yok.

Etkinlik sistemi tek uygun yaklaşım mı yoksa başka bir şey mi kullanmalıyım?

Eğer önemliyse grafik motoru olarak Ogre kullanıyorum.


Bu çok belirsiz bir soru. Sistemlerinizin etkileşimi, sistemlerinizin nasıl tasarlandığına ve ne tür bir kapsülleme yaptığınıza çok bağlı olacaktır. Ama bir şey dikkat çekiyor: "Ve bundan sonra motorun grafik kısmı bu bayrağı kontrol edecek ve nesneyi oyun dünyasına ekleyecek." Grafikler neden motorun dünyaya bir şeyler katıyor ? Görünüşe göre dünya, grafik modülüne ne render edeceğini söylemeli.
Tetrad

Motorda "grafik" kısmı Dev'i kontrol eder (örneğin, sahneye nesne eklemesini söyler). Ancak bunu yapmak için yeni olan nesnenin “verilerini” de arar (ve bundan sonra Ogre'ye sahneye eklemesini söyler) Ama deneyim eksikliğinden dolayı bu yaklaşımın doğru mu yanlış mı olduğunu bilmiyorum.
Userr

Yanıtlar:


20

En sevdiğim oyun motoru yapısı, neredeyse tüm parçalar arasındaki iletişimi kullanan arayüz ve nesne <-> bileşen modelidir.

Sahne yöneticiniz, kaynak yükleyiciniz, ses, oluşturucu, fizik vb.Gibi ana motor parçaları için birden fazla arayüzünüz var.

3D sahnede / dünyadaki tüm nesnelerden sorumlu sahne yöneticisi var.

Nesne çok atomik bir sınıftır, sahnenizdeki hemen hemen her şey için ortak olan sadece birkaç şey içerir, motorumda nesne sınıfı sadece konum, döndürme, bileşen listesi ve benzersiz bir kimlik tutar. Her nesnenin kimliği statik bir int tarafından oluşturulur, böylece hiçbir nesnenin her biri aynı kimliğe sahip olmaz, bu, nesneye bir işaretçi kullanmak yerine nesneye kimliğiyle mesaj göndermenize olanak tanır.

Nesnedeki bileşenlerin listesi, nesnelerin ana özellikler olduğunu verir. Örneğin, 3B dünyada görebileceğiniz bir şey için, nesnenize oluşturma ağı hakkındaki bilgileri içeren bir oluşturma bileşeni verirsiniz. Bir nesnenin fiziğe sahip olmasını istiyorsanız, ona bir fizik bileşeni verirsiniz. Bir şeyin kamera gibi davranmasını istiyorsanız, ona bir kamera bileşeni verin. Bileşenlerin listesi uzayıp gidebilir.

Arayüzler, nesneler ve bileşenler arasındaki iletişim önemlidir. Motorumda, yalnızca benzersiz bir kimlik ve bir ileti türü kimliği içeren genel bir mesaj sınıfım var. Benzersiz kimlik, iletinin gitmesini istediğiniz nesnenin kimliğidir ve ileti türü kimliği, iletiyi alan nesne tarafından kullanılır, böylece ne tür bir ileti olduğunu bilir.

Nesneler gerektiğinde iletiyi işleyebilir ve iletiyi bileşenlerinin her birine iletebilir ve bileşenler iletiyle genellikle önemli şeyler yapar. Örneğin, nesnenin konumunu değiştirmek isterseniz, nesneye bir SetPosition iletisi gönderirseniz, nesne iletiyi aldığında konum değişkenini güncelleştirebilir, ancak oluşturma bileşeninin oluşturma kafesinin konumunu güncelleştirmek için ileti göndermesi gerekebilir ve fizik bileşeninin fizik bedeninin konumunu güncellemek için mesaja ihtiyacı olabilir.

İşte C ++ ile yazılmış yaklaşık bir saat içinde çırptığım sahne yöneticisi, nesne ve bileşen ve mesaj akışının çok basit bir düzeni. Çalıştırıldığında nesne üzerindeki konumu ayarlar ve ileti oluşturma bileşeninden geçer, ardından konumu nesneden alır. Zevk almak!

Ayrıca, C ++ yerine akıcı olabilecek herkes için aşağıdaki kodun bir C # sürümünü ve Scala sürümünü yazdım .

#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
Bu kod gerçekten hoş görünüyor. Bana Birliği hatırlatıyor.
Tili

Bunun eski bir cevap olduğunu biliyorum, ama birkaç sorum var. 'Gerçek' bir oyunda kodlama kabusu yapan yüzlerce Mesaj türü olmaz mıydı? Ayrıca, doğru karakteri çizmek için ana karakterin karşı karşıya olduğu yola (örneğin) ihtiyacınız varsa ne yaparsınız. Her oluşturduğunuzda yeni bir GetSpriteMessage oluşturmanız ve göndermeniz gerekmez mi? Bu çok pahalı değil mi? Sadece merak ediyorum! Teşekkürler.
you786

Son projemde, mesajları yazmak için XML kullandık ve bir python betiği, oluşturma süresi boyunca bizim için tüm kodu oluşturdu. Farklı mesaj kategorileri için birden çok XML'ye ayırabilirsiniz. İleti göndermek için makrolar oluşturabilir, bu da onları bir işlev çağrısı kadar kısa yapar, eğer bir karakterin mesajlaşma olmadan yüzyüze gelme biçimine ihtiyacınız varsa, işaretçiyi bileşene götürmeniz ve ardından çağırılacak işlevi bilmeniz gerekir (eğer mesajlaşmadıysanız). RenderComponent oluşturucuya kaydolabilir, böylece her kareyi sorgulamanız gerekmez.
Nic Foster

2

Sahne Yöneticisi ve Arayüzlerini kullanmanın en iyi yolu olduğunu düşünüyorum. Mesajlaşma uygulandı, ancak ikincil yaklaşım olarak kullanırım. Mesajlaşma zincirler arası iletişim için iyidir. Mümkün olan her yerde soyutlama (arayüzler) kullanın.

Ogre hakkında çok şey bilmiyorum, bu yüzden genel olarak konuşuyorum.

Çekirdekte, ana oyun döngüsüne sahipsiniz. Giriş sinyallerini alır, AI'yi hesaplar (basit hareketten karmaşık AI ve oyun mantığına kadar), kaynakları yükler [, vb.) Ve mevcut durumu oluşturur. Bu temel örnektir, bu nedenle motoru bu parçalara ayırabilirsiniz (InputManager, AIManager, ResourceManager, RenderManager). Ve oyunda bulunan tüm nesneleri tutan SceneManager'a sahip olmalısınız.

Bu parçaların ve alt parçalarının her birinin arayüzleri vardır. Bu yüzden sadece ve sadece işlerini yapmak için bu parçaları organize etmeye çalışın. Ana bölümleri için dahili olarak etkileşime giren alt bölümleri kullanmalıdırlar. Bu şekilde, tamamen yeniden yazmadan açma şansı olmadan embrole olmayacaksınız.

ps C ++ kullanıyorsanız RAII kalıbı kullanmayı düşünün


2
RAII bir desen değil, bir yaşam biçimi.
Shotgun Ninja
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.