エンジン部品間の相互作用を実装する方法は?


10

ゲームエンジンパーツ間の情報交換の実装方法について質問したい。

エンジンは、ロジック、データ、UI、グラフィックの4つの部分に分かれています。初めに私は旗を通してこの交換をしました。たとえば、新しいオブジェクトがデータに追加された場合、isNewオブジェクトのクラスのフラグはとして設定されtrueます。その後、エンジンのグラフィックス部分がこのフラグをチェックし、オブジェクトをゲームの世界に追加します。

ただし、このアプローチでは、さまざまな種類のオブジェクトのすべてのフラグを処理するために、多くのコードを記述する必要がありました。

イベントシステムを使用することを考えましたが、これが適切なソリューションになるかどうかを知るのに十分な経験がありません。

イベントシステムが唯一の適切なアプローチですか、それとも別のものを使用する必要がありますか?

それが問題なら、私はグラフィックスエンジンとしてOgreを使用しています。


これは非常にあいまいな質問です。システムがどのように相互作用するかは、システムの設計方法、および最終的にどのようなカプセル化を行うかと非常に関連します。しかし、1つ目立った点があります。「その後、エンジンのグラフィックス部分がこのフラグをチェックし、オブジェクトをゲームの世界に追加します。」なぜエンジンのグラフィックス部分が世界に何かを追加しているのですか?何をレンダリングするかを世界がグラフィックモジュールに指示する必要があるようです。
テトラッド

エンジンでは、「グラフィックス」パーツがOgreを制御します(たとえば、シーンにオブジェクトを追加するように指示します)。しかし、それを行うために、新しいオブジェクトの「データ」も検索します(その後、Ogreにそれをシーンに追加するよう指示します)しかし、経験の欠如のため、このアプローチが正しいか間違っているかはわかりません。
ユーザー、2012

回答:


20

私のお気に入りのゲームエンジンの構造は、ほぼすべてのパーツ間の通信にメッセージングを使用するインターフェイスとオブジェクト<->コンポーネントモデルです。

シーンマネージャー、リソースローダー、オーディオ、レンダラー、物理などの主要なエンジンパーツに複数のインターフェイスがあります。

3Dシーン/ワールドのすべてのオブジェクトを担当するシーンマネージャーがあります。

オブジェクトは非常にアトミックなクラスであり、シーン内のほとんどすべてに共通するものをいくつか含みます。私のエンジンでは、オブジェクトクラスは位置、回転、コンポーネントのリスト、および一意のIDのみを保持します。すべてのオブジェクトのIDは静的intによって生成されるため、2つのオブジェクトがすべて同じIDを持つことはありません。これにより、オブジェクトへのポインターを持たずに、そのIDでオブジェクトにメッセージを送信できます。

オブジェクトのコンポーネントのリストは、オブジェクトがメインプロパティであることを示しています。たとえば、3Dの世界で見ることができるものの場合、レンダーメッシュに関する情報を含むレンダーコンポーネントをオブジェクトに与えます。オブジェクトに物理を持たせたい場合は、物理コンポーネントを与えます。何かをカメラとして機能させたい場合は、カメラコンポーネントを割り当てます。コンポーネントのリストはどんどん増えていきます。

インターフェイス、オブジェクト、コンポーネント間の通信が重要です。私のエンジンには、一意のIDとメッセージタイプIDのみを含む汎用メッセージクラスがあります。一意のIDは、メッセージを送信する先のオブジェクトのIDです。メッセージタイプIDは、メッセージを受信するオブジェクトによって使用されるため、メッセージのタイプがわかります。

オブジェクトは必要に応じてメッセージを処理でき、各コンポーネントにメッセージを渡すことができます。コンポーネントは多くの場合、メッセージで重要なことを行います。たとえば、オブジェクトの位置を変更したい場合、オブジェクトにSetPositionメッセージを送信すると、オブジェクトはメッセージを取得したときに位置変数を更新しますが、レンダーコンポーネントはレンダーメッシュの位置を更新するためにメッセージを送信する必要がある場合があります。物理コンポーネントは、物理ボディの位置を更新するためにメッセージを必要とする場合があります。

シーンマネージャー、オブジェクト、コンポーネント、メッセージフローの非常にシンプルなレイアウトを以下に示します。これらは、C ++で記述して約1時間で作成したものです。実行すると、オブジェクトの位置が設定され、メッセージがレンダリングコンポーネントを通過して、オブジェクトから位置が取得されます。楽しい!

また、以下のコードの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
このコードは本当によさそうだ。Unityを思い出します。
Tili

これは古い答えですが、いくつか質問があります。「本当の」ゲームには何百ものメッセージタイプがあり、コーディングの悪夢にならないでしょうか?また、(たとえば)メインキャラクターが正しく描画するために正面を向いている状態が必要な場合はどうしますか。新しいGetSpriteMessageを作成し、レンダリングするたびに送信する必要はありませんか?これは高額になりませんか?ただ疑問に思います!ありがとう。
you786

私の最後のプロジェクトでは、XMLを使用してメッセージを記述し、ビルド時にPythonスクリプトがすべてのコードを作成しました。メッセージカテゴリごとに複数のXMLに分割できます。メッセージ送信用のマクロを作成して、関数呼び出しと同じくらい簡潔にすることができます。メッセージなしでキャラクターが直面している方法が必要な場合は、コンポーネントへのポインターを取得する必要があり、呼び出す関数を知っています。 (メッセージングを使用していない場合)。RenderComponentはレンダラーに登録できるため、フレームごとにクエリを実行する必要はありません。
ニックフォスター

2

Scene ManagerとInterfacesを使用する最良の方法だと思います。メッセージングを実装しましたが、それを二次的なアプローチとして使用します。メッセージングは​​、スレッド間通信に適しています。できる限り抽象化(インターフェース)を使用します。

私はオーガについてあまり知らないので、一般的に言っています。

コアでは、メインのゲームループがあります。入力信号の取得、AIの計算(単純なモーションから複雑なAIやゲームロジックまで)、リソースのロードなどを行い、現在の状態をレンダリングします。これは基本的な例なので、エンジンをこれらの部分(InputManager、AIManager、ResourceManager、RenderManager)に分離できます。そして、ゲームに存在するすべてのオブジェクトを保持するSceneManagerが必要です。

これらのパーツとそのサブパーツのすべてにインターフェースがあります。したがって、これらのパーツを整理して、自分の仕事だけを行うようにしてください。親パートの目的で内部的に相互作用するサブパートを使用する必要があります。そうすれば、完全に書き直さずに展開する機会がなければ、巻き込まれることはありません。

PS C ++を使用している場合は、RAIIパターンの使用を検討してください


2
RAIIはパターンではなく、生き方です。
Shotgun Ninja
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.