엔진 부품 간의 상호 작용을 구현하는 방법은 무엇입니까?


10

게임 엔진 파트 간의 정보 교환을 구현하는 방법에 대한 질문을하고 싶습니다.

엔진은 로직, 데이터, UI, 그래픽의 네 부분으로 구분됩니다. 처음에 나는 깃발을 통해이 교환을했다. 예를 들어 데이터에 새 객체가 추가 isNew되면 객체 클래스 의 플래그 가로 설정됩니다 true. 그리고 나서 엔진의 그래픽 부분이이 플래그를 확인하고 게임 월드에 오브젝트를 추가합니다.

그러나이 접근법으로 각 종류의 객체의 모든 플래그를 처리하는 많은 코드를 작성해야했습니다.

이벤트 시스템을 사용하려고 생각했지만 이것이 올바른 솔루션인지 여부를 알기에 충분한 경험이 없습니다.

이벤트 시스템이 유일한 적절한 접근법입니까, 아니면 다른 것을 사용해야합니까?

중요한 경우 Ogre를 그래픽 엔진으로 사용하고 있습니다.


이것은 매우 모호한 질문입니다. 시스템의 상호 작용 방식은 시스템의 설계 방식과 어떤 종류의 캡슐화가 끝나고 있는지에 매우 밀접한 관련이 있습니다. 그러나 한 가지 눈에 띄는 점은 "엔진의 그래픽 부분에서이 플래그를 확인하고 게임 세계에 오브젝트를 추가 할 것"이라고 지적합니다. 왜 엔진 의 그래픽 부분이 세상에 물건을 추가 합니까? 세계가 그래픽 모듈에 무엇을 렌더링해야하는지 알려 주어야합니다.
Tetrad

엔진에서 "graphics"부분은 Ogre를 제어합니다 (예 : 장면에 오브젝트를 추가하도록 지시합니다). 그러나 그렇게하기 위해 새로운 객체에 대한 "데이터"도 검색합니다 (그리고 그 후에 오우거에게 장면에 추가하도록 지시합니다). 그러나이 접근법이 경험 부족으로 옳은지 아닌지를 모르겠습니다.
사용자

답변:


20

내가 가장 좋아하는 게임 엔진 구조는 거의 모든 부분 간의 통신을 위해 메시징을 사용하는 인터페이스 및 객체 구성 요소 모델입니다.

장면 관리자, 리소스 로더, 오디오, 렌더러, 물리학 등 주요 엔진 부품에 대한 여러 인터페이스가 있습니다.

3D 장면 / 세계의 모든 객체를 담당하는 장면 관리자가 있습니다.

Object는 매우 원자적인 클래스이며 장면의 거의 모든 것에 공통되는 몇 가지 항목 만 포함합니다. 엔진에서 오브젝트 클래스는 위치, 회전, 구성 요소 목록 및 고유 ID 만 보유합니다. 모든 객체의 ID는 정적 int에 의해 생성되므로 두 객체가 모두 동일한 ID를 가지지 않으므로 객체에 대한 포인터가 없어도 ID로 메시지를 보낼 수 있습니다.

객체의 구성 요소 목록은 해당 객체가 주요 속성임을 나타냅니다. 예를 들어, 3D 세계에서 볼 수있는 것으로, 렌더링 메쉬에 대한 정보가 포함 된 렌더링 구성 요소를 객체에 제공합니다. 물체에 물리를 가지려면 물리 성분을 부여해야합니다. 무언가 카메라 역할을하려는 경우 카메라 구성 요소를 제공하십시오. 구성 요소 목록은 계속 진행될 수 있습니다.

인터페이스, 객체 및 구성 요소 간의 통신이 중요합니다. 내 엔진에는 고유 ID와 메시지 유형 ID 만 포함하는 일반 메시지 클래스가 있습니다. 고유 ID는 메시지를 보내려는 개체의 ID이며 메시지 유형 ID는 메시지를받는 개체에서 사용되므로 메시지 유형을 알 수 있습니다.

개체는 필요한 경우 메시지를 처리 ​​할 수 ​​있으며 각 구성 요소에 메시지를 전달할 수 있으며 구성 요소는 종종 메시지와 함께 중요한 작업을 수행합니다. 예를 들어, 객체의 위치를 ​​변경하고 객체의 위치를 ​​변경하려면 객체에 SetPosition 메시지를 보내면 객체가 메시지를받을 때 위치 변수를 업데이트 할 수 있지만 렌더 구성 요소는 렌더 메시의 위치를 ​​업데이트하기 위해 메시지를 보내야 할 수 있습니다. 물리 구성 요소는 물리 본체의 위치를 ​​업데이트하기 위해 메시지가 필요할 수 있습니다.

여기에 C ++로 작성된 장면 관리자, 객체 및 구성 요소 및 메시지 흐름의 간단한 레이아웃이 있습니다. 실행될 때 객체의 위치를 ​​설정하고 메시지가 렌더 구성 요소를 통과 한 다음 객체에서 위치를 검색합니다. 즐겨!

또한 C ++이 아닌 사람들에게 유창한 사람을 위해 아래 코드 의 C # 버전Scala 버전 을 작성했습니다 .

#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를 만들어 렌더링 할 때마다 보내지 않아도됩니까? 너무 비싸지 않습니까? 궁금해! 감사.
당신 786

마지막 프로젝트에서 우리는 XML을 사용하여 메시지를 작성했으며 파이썬 스크립트는 빌드 시간 동안 우리를 위해 모든 코드를 만들었습니다. 다른 메시지 범주에 대해 여러 XML로 분리 할 수 ​​있습니다. 메시지 전송을위한 매크로를 생성하여 거의 함수 호출처럼 간결하게 만들 수 있습니다. 메시지없이 문자가 직면하는 방식이 필요한 경우 여전히 컴포넌트에 대한 포인터를 가져 와서 호출 할 함수를 알아야합니다. (메시지를 사용하지 않는 경우). RenderComponent는 렌더러에 등록 할 수 있으므로 매 프레임마다 쿼리 할 필요가 없습니다.
Nic Foster

2

장면 관리자와 인터페이스를 사용하는 가장 좋은 방법이라고 생각합니다. 메시징을 구현했지만 보조 접근 방식으로 사용하려고합니다. 메시지는 스레드 간 통신에 좋습니다. 가능한 한 추상화 (인터페이스)를 사용하십시오.

나는 오우거에 대해 잘 모르므로 일반적으로 말하고 있습니다.

핵심에는 메인 게임 루프가 있습니다. 입력 신호를 받고, 간단한 모션에서 복잡한 AI 및 게임 로직에 이르기까지 AI를 계산하고, 리소스를로드하고 [등] 현재 상태를 렌더링합니다. 이것은 기본적인 예이므로 엔진을 이러한 부분 (InputManager, AIManager, ResourceManager, RenderManager)으로 분리 할 수 ​​있습니다. 그리고 게임에 존재하는 모든 오브젝트를 보유하는 SceneManager가 있어야합니다.

이러한 모든 부분과 하위 부분에는 인터페이스가 있습니다. 따라서 이들 부분 만 구성하여 자신의 업무 만 수행하십시오. 부모 부분의 목적을 위해 내부적으로 상호 작용하는 하위 부분을 사용해야합니다. 그렇게하면 완전히 다시 쓰지 않고 풀릴 수있는 기회없이 타협 할 수 없습니다.

ps C ++를 사용하는 경우 RAII 패턴 사용을 고려하십시오.


2
RAII는 패턴이 아니라 삶의 방식입니다.
Shotgun Ninja
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.