렌더링에서 게임 데이터 / 논리 분리


21

C ++ 및 OpenGL 2.1을 사용하여 게임을 작성하고 있습니다. 데이터 / 논리를 렌더링에서 분리하는 방법을 생각하고있었습니다. 현재 나는 그리기를 구현하는 순수한 가상 방법을 제공하는 기본 클래스 'Renderable'을 사용합니다. 그러나 모든 객체에는 매우 특수한 코드가 있으며 객체 만 쉐이더 유니폼을 올바르게 설정하고 정점 배열 버퍼 데이터를 구성하는 방법을 알고 있습니다. 코드 전체에서 많은 gl * 함수 호출로 끝납니다. 객체를 그리는 일반적인 방법이 있습니까?


4
컴포지션을 사용하여 실제로 렌더링 가능 객체를 객체에 연결하고 객체가 해당 m_renderable멤버 와 상호 작용하도록합니다 . 그렇게하면 논리를 더 잘 분리 할 수 ​​있습니다. 물리학, 인공 지능 및 기타를 포함하는 일반 객체에는 렌더링 가능한 "인터페이스"를 적용하지 마십시오. 그 후에는 렌더링 가능한 객체를 개별적으로 관리 할 수 ​​있습니다. 사물을 더 많이 분리하려면 OpenGL 함수 호출에 대한 추상화 계층이 필요합니다. 따라서 좋은 엔진이 다양한 렌더링 가능한 구현 내에 GL API 호출을 가질 것으로 기대하지 마십시오. 그것은 소소한 일입니다.
teodron

1
@ teodron : 왜 대답으로 넣지 않았습니까?
Tapio

1
@Tapio : 그다지 답이 아니기 때문에; 대신 제안이 더 있습니다.
teodron

답변:


20

아이디어는 방문자 디자인 패턴을 사용하는 것입니다. 소품을 렌더링하는 방법을 알고있는 Renderer 구현이 필요합니다. 모든 객체는 렌더러 인스턴스를 호출하여 렌더 작업을 처리 할 수 ​​있습니다.

몇 줄의 의사 코드에서 :

class Renderer {
public:
    void render( const ObjectA & obj );
    void render( const ObjectB & obj );
};


class ObjectA{
public:
    void draw( Renderer & r ){ r.render( *this ) };
}

class ObjectB{
public:
    void draw( Renderer & r ){ r.render( *this ) };
}

gl * 스터프는 렌더러의 메소드로 구현되며 객체는 렌더링에 필요한 데이터, 위치, 텍스처 유형, 크기 등 만 저장합니다.

또한 다른 렌더러 (debugRenderer, hqRenderer 등)를 설정하고 객체를 변경하지 않고도 동적으로 사용할 수 있습니다.

이것은 또한 엔티티 / 컴포넌트 시스템과 쉽게 결합 될 수 있습니다.


1
이것은 좋은 대답입니다! 당신은 강조 수도 Entity/Component는 다른 엔진 부품 (AI, 물리, 네트워킹 또는 일반 게임 플레이)에서 별도의 구조 제공을 도울 수 있기 때문에 대안 a가 더 비트. +1!
teodron

1
@ teodron, 나는 E / C 대안을 설명하지 않을 것입니다. 그러나, 나는 당신이 변화해야한다고 생각 ObjectA하고 ObjectBDrawableComponentADrawableComponentB, 내부 방법을 렌더링, 당신이 좋아, 필요하면 다른 구성 요소를 사용 : position = component->getComponent("Position");그리고 메인 루프에서, 당신은 그릴 전화 당김 구성 요소 목록을 가지고있다.
Zhen

왜 그냥 (같은 인터페이스를 Renderable가 그) draw(Renderer&)기능을 구현 렌더링 할 수있는 모든 개체를? 어떤 경우 Renderer에는 공통 인터페이스를 구현하고 호출하는 객체를 허용하는 함수가 하나만 필요 renderable.draw(*this);합니까?
Vite Falcon

1
@ViteFalcon, 명확하지 않으면 죄송하지만 자세한 설명을 위해서는 더 많은 공간과 코드가 필요합니다. 기본적으로 내 솔루션은 gl_*함수를 렌더러로 옮기고 (논리와 렌더링 분리) 솔루션이 gl_*호출을 객체로 옮깁니다 .
Zhen

이런 식으로 gl * 함수가 실제로 객체 코드에서 벗어나지 만 버퍼 / 텍스처 ID, 균일 / 속성 위치와 같이 렌더링에 사용되는 핸들 변수를 계속 유지합니다.
felipe

4

Zhen의 답변을 이미 수락했지만 다른 사람을 돕기 위해 다른 것을 제시하고 싶습니다.

문제를 반복하기 위해 OP는 렌더링 코드를 논리 및 데이터와 별도로 유지하는 기능을 원합니다.

내 솔루션은 다른 클래스를 모두 사용하여 구성 요소를 렌더링하는 것 Renderer입니다.이 클래스는 로직 클래스와 분리되어 있습니다 . 먼저 Renderable함수가 있는 인터페이스가 있어야 bool render(Renderer& renderer);하고 Renderer클래스는 방문자 패턴을 사용하여 모든 Renderable인스턴스 를 검색하고 인스턴스가 GameObject있는 객체를 렌더링 Renderable합니다. 이런 식으로 Renderer는 모든 객체 유형을 알 필요가 없으며 함수 Renderable를 통해 전달한다는 것은 여전히 ​​각 객체 유형의 책임입니다 getRenderable(). 또는 RenderableVisitor모든 게임 오브젝트를 방문 하는 클래스를 생성하고 개별 GameObject조건에 따라 렌더러 블을 방문자에게 추가 / 추가하지 않도록 선택할 수 있습니다. 어느 쪽이든, 주요 요점은gl_*호출은 모두 객체 자체 외부에 있으며의 일부가 아닌 객체 자체의 세부 정보를 알고있는 클래스에 상주합니다 Renderer.

면책 조항 : 나는이 클래스를 편집기에서 손으로 작성 했으므로 코드에서 뭔가를 놓쳤을 가능성이 있지만 아이디어를 얻을 수 있기를 바랍니다.

(일부) 예를 표시하려면

Renderable 인터페이스

class Renderable {
public:
    Renderable(){}
    virtual ~Renderable(){}
    virtual void render(Renderer& renderer) const = 0;
};

GameObject 수업:

class GameObject {
public:
    GameObject()
        : mVisible(true)
        , mMarkedForDelete(false) {}

    virtual ~GameObject(){}

    virtual Renderable* getRenderable() {
        // By default, all GameObjects are missing their Renderable
        return NULL;
    }

    void setVisible(bool visible) {
        mVisible = visible;
    }

    bool isVisible() const {
        return getRenderable() != null && !isMarkedForDeletion() && mVisible;
    }

    void markForDeletion() {
        mMarkedForDelete = true;
    }

    bool isMarkedForDeletion() const {
        return mMarkedForDelete;
    }

    // More GameObject functions

private:
    bool mVisible;
    bool mMarkedForDelete;
};

(부분) Renderer수업.

class Renderer {
public:
    void renderObjects(std::vector<GameObject>& gameObjects) {
        // If you want to do something fancy with the renderable GameObjects,
        // create a visitor class to return the list of GameObjects that
        // are visible instead of rendering them straight-away
        std::list<GameObject>::iterator itr = gameObjects.begin(), end = gameObjects.end();
        while (itr != end) {
            GameObject* gameObject = *itr++;
            if (gameObject == null || !gameObject->isVisible()) {
                continue;
            }
            gameObject->getRenderable()->render(*this);
        }
    }

};

RenderableObject 수업:

template <typename T>
class RenderableObject : public Renderable {
public:
    RenderableObject(T& object)
        :mObject(object) {}
    virtual ~RenderableObject(){}

    virtual void render(Renderer& renderer) {
        return render(renderer, mObject);
    }

protected:
    virtual void render(Renderer& renderer, T& object) = 0;
};

ObjectA 수업:

// Forward delcare ObjectARenderable and make sure the constructor
// definition in the CPP file where ObjectARenderable gets included
class ObjectARenderable;

class ObjectA : public GameObject {
public:
    ObjectA()
        : mRenderable(new ObjectARenderable(*this)) {}

    // All data/logic

    Renderable* getRenderable() {
        return mRenderable.get();
    }

protected:
    // boost or std shared_ptr to make sure that the renderable instance is
    // cleaned up with the destruction of this object.
    shared_ptr<Renderable> mRenderable;
};

ObjectARenderable 수업:

#include "ObjectA.h"

class ObjectARenderable : public RenderableObject<ObjectA> {
public:
    ObjectARenderable(ObjectA& instance) {
        : RenderableObject<ObjectA>(instance) {}

protected:
    virtual void render(Renderer& renderer, T& object) {
        // gl_* class to render ObjectA
    }
};

4

렌더링 명령 시스템을 구축하십시오. 모두에 대한 액세스 권한이있는 높은 수준의 객체 OpenGLRenderer및 장면 그래프 / 게임 오브젝트는 장면 그래프 나 게임 오브젝트를 반복하고 일괄 구축 할 것 RenderCmds다음에 제출 될 것입니다, OpenGLRenderer다시 각을 그릴 것입니다, 따라서 모든 OpenGL을 포함 관련 코드.

이것에 대한 추상화는 단순한 추상화보다 더 많습니다. 결국 렌더링 복잡성이 증가함에 따라 렌더 Render()호출에서 많은 병목 현상을 제거하기 위해 텍스처 또는 쉐이더별로 각 렌더링 명령을 정렬하고 그룹화 하여 성능에 큰 차이를 만들 수 있습니다.

class OpenGLRenderer
{
public:
    typedef GLuint GeometryBuffer;
    typedef GLuint TextureID;
    typedef std::vector<RenderCmd> RenderBatch; 

    void Render(const RenderBatch& renderBatch);   // set shaders, set active textures, draw geometry, ...

    MeshID CreateGeometryBuffer(...);
    TextureID CreateTexture(...);

    // ....
}

struct RenderCmd
{
    GeometryBuffer mGeometryBuffer;
    TextureID mTexture;
    Mat4& mWorldMatrix;
    bool mLightingEnabled;
    // .....
}

std::vector<GameObject> gYourGameObjects;
RenderBatch BuildRenderBatch()
{
    RenderBatch ret;

    for (GameObject& object : gYourGameObjects)
    { 
        // ....
    }

    return ret;
}

3

렌더링 가능한 모든 엔티티에 공통적 인 사항을 가정 할 수 있는지 여부에 따라 달라집니다. 내 엔진에서 모든 객체는 동일한 방식으로 렌더링되므로 vbo, 텍스처 및 변환을 제공하면됩니다. 그런 다음 렌더러가 모든 객체를 가져 오므로 다른 객체에서 OpenGL 함수 호출이 전혀 필요하지 않습니다.


1
날씨 = 비, 태양, 따뜻한 물과 차가운 : P -> 거세한 숫양
토비아스 Kienzler

3
@TobiasKienzler 철자를 수정하려면 철자를 정확하게 입력하십시오 :-)
TASagent

@TASagent 무엇, 그리고 브레이크 머피의 법칙 ? m- /
Tobias Kienzler

1
오타 수정
danijar

2

렌더링 코드와 게임 로직을 다른 클래스에 확실히 넣습니다. 작곡 (teodron이 제안한대로)이 가장 좋은 방법 일 것입니다. 게임 세계의 각 엔티티에는 자체 렌더링 가능 또는 아마도 세트가 있습니다.

예를 들어 기본 질감 및 조명 셰이더 외에도 골격 애니메이션, 파티클 이미 터 및 복잡한 셰이더를 처리하기 위해 여러 하위 클래스의 렌더링 가능이있을 수 있습니다. Renderable 클래스 및 해당 서브 클래스에는 렌더링에 필요한 정보 (지오메트리, 텍스처 및 쉐이더) 만 포함되어야합니다.

또한 주어진 메시의 인스턴스를 메시 자체와 분리해야합니다. 화면에 각각 같은 메시를 사용하는 수백 그루의 나무가 있다고 가정 해 봅시다. 지오메트리를 한 번만 저장하려고하지만 각 트리마다 별도의 위치 및 회전 행렬이 필요합니다. 애니메이션 휴머노이드와 같은보다 복잡한 객체에는 추가 상태 정보 (예 : 골격, 현재 적용된 애니메이션 세트 등)가 있습니다.

렌더링을하려면 순진한 접근 방식이 모든 게임 개체를 반복하여 렌더링하도록 지시하는 것입니다. 또는 각 엔티티 (생성시)는 렌더링 가능한 오브젝트를 장면 오브젝트에 삽입 할 수 있습니다. 그런 다음 렌더링 함수는 장면에 렌더링을 지시합니다. 이를 통해 장면은 해당 코드를 게임 엔터티 또는 특정 렌더링 가능한 하위 클래스에 포함시키지 않고도 복잡한 렌더링 관련 작업을 수행 할 수 있습니다.


2

이 조언은 실제로 렌더링에만 국한된 것은 아니지만 크게 분리 된 시스템을 만드는 데 도움이됩니다. 먼저 'GameObject'데이터를 위치 정보와 분리하십시오.

간단한 XYZ 위치 정보는 그렇게 간단하지 않을 수도 있습니다. 물리 엔진을 사용하는 경우 위치 엔진을 타사 엔진에 저장할 수 있습니다. 많은 무의미한 메모리 복사가 필요한 동기화를 수행하거나 엔진에서 직접 정보를 쿼리해야합니다. 그러나 모든 물체에 물리학이 필요한 것은 아니며 일부는 고정되어 있으므로 간단한 수레 세트가 잘 작동합니다. 일부는 다른 객체에 부착 될 수도 있으므로 해당 위치는 실제로 다른 위치의 오프셋입니다. 고급 설정에서는 컴퓨터 측에서 스크립팅, 스토리지 및 네트워크 복제에 필요한 시간 만 GPU에만 위치를 저장할 수 있습니다. 따라서 위치 데이터에 대해 몇 가지 가능한 선택이있을 수 있습니다. 여기서 상속을 사용하는 것이 좋습니다.

위치를 소유하는 객체가 아니라 인덱싱 데이터 구조로 객체 자체를 소유해야합니다. 예를 들어 'Level'에는 Octree가 있거나 물리 엔진 'scene'이있을 수 있습니다. 렌더링하거나 렌더링 장면을 설정하려면 카메라에서 볼 수있는 객체에 대한 특수 구조를 쿼리합니다.

이것은 또한 좋은 메모리 관리에 도움이됩니다. 이런 식으로 실제로 영역에 있지 않은 객체는 0.0 좌표 또는 영역에서 마지막에있을 때의 좌표를 반환하는 것보다 의미가있는 위치조차 갖지 않습니다.

더 이상 객체에 좌표를 유지하지 않으면 object.getX () 대신 level.getX (object)가 생깁니다. 레벨에서 객체를 찾는 문제는 모든 객체를 살펴보고 쿼리하는 객체와 일치해야하기 때문에 느린 작동 일 것입니다.

이를 피하기 위해 특별한 '링크'클래스를 만들 것입니다. 레벨과 객체를 묶는 것. "위치"라고합니다. 여기에는 xyz 좌표와 레벨 핸들 및 객체 핸들이 포함됩니다. 이 링크 클래스는 공간 구조 / 레벨에 저장되며 오브젝트에 대한 참조가 약합니다 (레벨 / 위치가 파괴 된 경우 오브젝트 굴절은 null로 업데이트되어야 함). 레벨을 삭제하면 오브젝트를 '소유'합니다. 특별한 인덱스 구조, 포함 된 위치 및 오브젝트도 마찬가지입니다.

typedef std::tuple<Level, Object, PositionXYZ> Location;

이제 위치 정보는 한 곳에만 저장됩니다. Object, Spacial 인덱싱 구조, 렌더러 등에는 중복되지 않습니다.

Octrees와 같은 공간 데이터 구조는 종종 그들이 저장하는 객체의 좌표를 가질 필요가 없습니다. 위치는 구조 자체에서 노드의 상대 위치에 저장됩니다 (빠른 조회 시간의 정확도를 희생하는 손실 압축과 같은 것으로 생각할 수 있음). Octree에 location 객체를 사용하면 쿼리가 완료되면 실제 좌표가 그 안에 있습니다.

또는 물리 엔진을 사용하여 객체 위치 또는 둘 사이의 혼합을 관리하는 경우 Location 클래스는 모든 코드를 한 곳에 유지하면서 투명하게 처리해야합니다.

다른 장점은 이제 위치와 레벨에 대한 굴절이 같은 위치에 저장된다는 것입니다. object.TeleportTo (other_object)를 구현하고 여러 레벨에서 작동하도록 할 수 있습니다. 마찬가지로 AI 경로 찾기는 다른 영역을 따라갈 수 있습니다.

렌더링과 관련하여. 렌더는 위치와 비슷한 바인딩을 가질 수 있습니다. 거기에 렌더링 관련 내용이있는 것을 제외하고. 이 구조에 'Object'또는 'Level'을 저장하지 않아도됩니다. 색상 선택과 같은 작업을하거나 그 위에 떠있는 히트 바를 렌더링하려는 경우 Object가 유용 할 수 있습니다. RenderableStuff는 Mesh 일 수도 있고 경계 상자 등을 가질 수도 있습니다.

typedef std::pair<RenderableStuff, PositionXYZ> RenderThing;

renderer.render(level, camera);
renderer: object = level.getVisibleObjects(camera);
level: physics.getObjectsInArea(physics.getCameraFrustrum(camera));
for(object in objects) {
    //This could be depth sorted, meshes could be broken up and sorted by material for batch rendering or whatever
    rendering_que.addObjectToRender(object);
}

매 프레임마다이 작업을 수행 할 필요는 없습니다. 현재 표시된 카메라보다 더 큰 영역을 사용할 수 있습니다. 캐시하고 객체 이동을 추적하여 경계 상자가 범위 내에 있는지 확인하고 카메라 이동 등을 추적합니다. 그러나 벤치마킹 할 때까지 그런 종류의 물건을 엉망으로 만들지 마십시오.

물리 엔진 자체는 객체 데이터가 필요하지 않고 충돌 메시 및 물리 속성 만 필요하기 때문에 유사한 추상화를 가질 수 있습니다.

모든 핵심 객체 데이터에는 객체가 사용하는 메시의 이름이 포함됩니다. 그런 다음 게임 엔진은 객체 클래스에 렌더링 특정 항목 (Director vs OpenGL과 같은 렌더링 API에 따라 다름)에 부담을주지 않고 원하는 형식으로로드 할 수 있습니다.

또한 서로 다른 구성 요소를 별도로 유지합니다. 이것은 물리 엔진을 교체하는 것과 같은 일을 쉽게 할 수있게합니다. 또한 단위 테스트가 훨씬 쉬워집니다. Location 클래스 만 있으면 실제 가짜 개체를 설정할 필요없이 물리 쿼리와 같은 것을 테스트 할 수 있습니다. 물건을 더 쉽게 최적화 할 수도 있습니다. 최적화를 위해 어떤 클래스와 단일 위치에서 어떤 쿼리를 수행해야하는지 더 명확하게합니다 (예 : 위의 level.getVisibleObject는 카메라가 많이 움직이지 않으면 물건을 캐싱 할 수있는 위치입니다).

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.