Tách dữ liệu / logic trò chơi khỏi kết xuất


21

Tôi đang viết một trò chơi bằng C ++ và OpenGL 2.1. Tôi đã suy nghĩ làm thế nào tôi có thể tách dữ liệu / logic khỏi kết xuất. Hiện tại tôi sử dụng một lớp cơ sở 'Renderable' cung cấp một phương thức ảo thuần túy để thực hiện vẽ. Nhưng mọi đối tượng đều có mã chuyên biệt như vậy, chỉ có đối tượng biết cách thiết lập đúng đồng phục shader và tổ chức dữ liệu bộ đệm mảng đỉnh. Tôi kết thúc với rất nhiều hàm gọi gl * trên toàn bộ mã của mình. Có cách nào chung để vẽ các đối tượng không?


4
Sử dụng bố cục để thực sự đính kèm một kết xuất vào đối tượng của bạn và để đối tượng của bạn tương tác với m_renderablethành viên đó . Bằng cách đó, bạn có thể tách logic của bạn tốt hơn. Không thực thi "giao diện" có thể kết xuất trên các đối tượng chung cũng có vật lý, ai và không có gì .. Sau đó, bạn có thể quản lý kết xuất đồ họa một cách riêng biệt. Bạn cần một lớp trừu tượng hóa qua các lệnh gọi hàm OpenGL để tách rời mọi thứ hơn nữa. Vì vậy, đừng mong đợi một công cụ tốt sẽ có bất kỳ lệnh gọi API GL nào bên trong các triển khai có thể kết xuất khác nhau của nó. Đó là nó, trong một tóm tắt vi mô.
teodron

1
@teodron: Tại sao bạn không đặt nó làm câu trả lời?
Tapio

1
@Tapio: bởi vì đó không phải là nhiều câu trả lời; thay vào đó là một gợi ý.
teodron

Câu trả lời:


20

Một ý tưởng là sử dụng mẫu thiết kế của Khách truy cập. Bạn cần một triển khai Renderer biết cách kết xuất đạo cụ. Mọi đối tượng có thể gọi đối tượng kết xuất để xử lý công việc kết xuất.

Trong một vài dòng mã giả:

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 ) };
}

Các công cụ gl * được triển khai bằng các phương thức của trình kết xuất và các đối tượng chỉ lưu trữ dữ liệu cần được hiển thị, vị trí, loại kết cấu, kích thước ... vv.

Ngoài ra, bạn có thể thiết lập các trình kết xuất khác nhau (debugRenderer, hqRenderer, ... vv) và sử dụng chúng một cách linh hoạt, mà không thay đổi các đối tượng.

Điều này cũng có thể dễ dàng kết hợp với các hệ thống Thực thể / Thành phần.


1
Đây là một câu trả lời khá tốt! Bạn có thể đã nhấn mạnh sự Entity/Componentthay thế nhiều hơn một chút vì nó có thể giúp tách các nhà cung cấp hình học khỏi các bộ phận động cơ khác (AI, Vật lý, Mạng hoặc trò chơi nói chung). +1!
teodron

1
@teodron, tôi sẽ không giải thích về sự thay thế E / C bởi vì nó sẽ tuân thủ mọi thứ. Nhưng, tôi nghĩ rằng bạn nên thay đổi ObjectAObjectBmỗi DrawableComponentADrawableComponentB, và bên trong làm cho phương pháp, sử dụng các thành phần khác nếu bạn cần nó, như: position = component->getComponent("Position");Và trong vòng lặp chính, bạn có một danh sách các thành phần có thể vẽ được gọi vẽ với.
Zhen

Tại sao không chỉ có một giao diện (như Renderable) có draw(Renderer&)chức năng và tất cả các đối tượng có thể được kết xuất thực hiện chúng? Trong trường hợp nào Rendererchỉ cần một chức năng chấp nhận bất kỳ đối tượng nào thực hiện giao diện chung và gọi renderable.draw(*this);?
Vite Falcon

1
@ViteFalcon, Xin lỗi nếu tôi không làm rõ, nhưng để giải thích chi tiết, tôi cần thêm dung lượng và mã. Về cơ bản, giải pháp của tôi di chuyển các gl_*chức năng vào trình kết xuất (tách logic khỏi kết xuất), nhưng giải pháp của bạn di chuyển các gl_*cuộc gọi vào các đối tượng.
Zhen

Bằng cách này, các hàm gl * thực sự được chuyển ra khỏi mã đối tượng, nhưng tôi vẫn giữ các biến xử lý được sử dụng trong kết xuất, như vị trí của bộ đệm / kết cấu id, vị trí đồng nhất / thuộc tính.
felipe

4

Tôi biết bạn đã chấp nhận câu trả lời của Zhen nhưng tôi muốn đưa một câu hỏi khác ra ngoài trong trường hợp nó giúp được ai khác.

Để nhắc lại vấn đề, OP muốn có khả năng giữ mã kết xuất tách biệt với logic và dữ liệu.

Giải pháp của tôi là sử dụng tất cả một lớp khác nhau để kết xuất thành phần, tách biệt với lớp Renderervà lớp logic. Trước tiên cần có một Renderablegiao diện có chức năng bool render(Renderer& renderer);Rendererlớp sử dụng mẫu khách truy cập để truy xuất tất cả các Renderablethể hiện, đưa ra danh sách GameObjects và kết xuất các đối tượng có Renderablethể hiện. Theo cách này, Renderer không cần biết từng loại đối tượng ngoài kia và trách nhiệm của từng loại đối tượng là phải thông báo cho nó Renderablethông qua getRenderable()chức năng. Hoặc theo cách khác, bạn có thể tạo một RenderableVisitorlớp truy cập tất cả các GameObject và dựa trên GameObjectđiều kiện riêng lẻ mà họ có thể chọn để thêm / không thêm vào kết xuất của họ cho khách truy cập. Dù bằng cách nào, ý chính làgl_*các cuộc gọi là tất cả bên ngoài của chính đối tượng và nằm trong một lớp biết các chi tiết thân mật của chính đối tượng đó, thay vì đó là một phần của Renderer.

TUYÊN BỐ TỪ CHỐI : Tôi đã viết tay các lớp này trong trình soạn thảo để có cơ hội tốt rằng tôi đã bỏ lỡ điều gì đó trong mã, nhưng hy vọng, bạn sẽ hiểu ý tưởng.

Để hiển thị một ví dụ (một phần):

Renderable giao diện

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

GameObject lớp học:

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;
};

(Một phần) Rendererlớp.

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 lớp học:

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 lớp học:

// 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 lớp học:

#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

Xây dựng một hệ thống chỉ huy kết xuất. Một đối tượng cấp cao, có quyền truy cập vào cả hai OpenGLRenderervà các khung cảnh / trò chơi, sẽ lặp lại biểu đồ cảnh hoặc trò chơi và xây dựng một lô RenderCmds, sau đó sẽ được gửi tới OpenGLRendererlần lượt sẽ vẽ từng cái, và do đó chứa tất cả OpenGL mã liên quan trong đó.

Có nhiều lợi thế cho điều này hơn là chỉ trừu tượng; cuối cùng khi độ phức tạp kết xuất của bạn tăng lên, bạn có thể sắp xếp và nhóm từng lệnh kết xuất theo kết cấu hoặc đổ bóng Render()để loại bỏ nhiều nút thắt trong các lệnh gọi rút thăm có thể tạo ra sự khác biệt lớn về hiệu suất.

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

Nó hoàn toàn phụ thuộc vào việc bạn có thể đưa ra các giả định về những gì phổ biến cho tất cả các thực thể có thể kết xuất hay không. Trong công cụ của tôi, tất cả các đối tượng được kết xuất theo cùng một cách, vì vậy chỉ cần cung cấp vbos, kết cấu và biến đổi. Sau đó, trình kết xuất tìm nạp tất cả chúng, do đó không cần gọi các hàm OpenGL trong các đối tượng khác nhau.


1
thời tiết = mưa, nắng, nóng, lạnh: P -> wether
Tobias Kienzler

3
@TobiasKienzler Nếu bạn định sửa lỗi chính tả của mình, hãy thử đánh vần xem có đúng không :-)
TASagent

@TASagent Cái gì, và phanh Muphry's Law ? m- /
Tobias Kienzler

1
đã sửa lỗi đánh máy đó
danijar

2

Chắc chắn đặt mã kết xuất và logic trò chơi trong các lớp khác nhau. Thành phần (như teodron đề xuất) có lẽ là cách tốt nhất để làm điều này; mỗi Thực thể trong thế giới trò chơi sẽ có Kết xuất riêng - hoặc có thể là một bộ chúng.

Bạn vẫn có thể có nhiều lớp con của Renderable, ví dụ để xử lý hoạt hình khung xương, bộ phát hạt và trình tạo bóng phức tạp, ngoài trình tạo bóng kết cấu cơ bản của bạn. Lớp Renderable và các lớp con của nó chỉ nên chứa thông tin cần thiết để kết xuất: hình học, kết cấu và đổ bóng.

Hơn nữa, bạn nên tách một thể hiện của một lưới nhất định khỏi chính lưới đó. Giả sử bạn có một trăm cây trên màn hình, mỗi cây sử dụng cùng một lưới. Bạn chỉ muốn lưu trữ hình học một lần, nhưng bạn sẽ cần ma trận vị trí & xoay riêng biệt cho mỗi cây. Các đối tượng phức tạp hơn, chẳng hạn như hình người hoạt hình, cũng sẽ có thêm thông tin trạng thái (như bộ xương, bộ hoạt hình hiện đang được áp dụng, v.v.).

Để kết xuất, cách tiếp cận ngây thơ là lặp đi lặp lại trên mọi thực thể trò chơi và bảo nó tự kết xuất. Luân phiên, mỗi thực thể (khi nó sinh sản) có thể chèn (các) đối tượng có thể kết xuất của nó vào một đối tượng cảnh. Sau đó, chức năng kết xuất của bạn cho cảnh để kết xuất. Điều này cho phép cảnh thực hiện những thứ liên quan đến kết xuất phức tạp mà không cần nhúng mã đó vào thực thể trò chơi hoặc một lớp con có thể kết xuất cụ thể.


2

Lời khuyên này không thực sự cụ thể để kết xuất nhưng sẽ giúp đưa ra một hệ thống giúp mọi thứ tách biệt nhau. Trước hết hãy thử và tách dữ liệu 'GameObject' khỏi thông tin vị trí.

Điều đáng chú ý là thông tin vị trí XYZ đơn giản có thể không đơn giản như vậy. Nếu bạn đang sử dụng công cụ vật lý thì dữ liệu vị trí của bạn có thể được lưu trữ trong công cụ của bên thứ 3. Bạn có thể cần phải đồng bộ hóa giữa chúng (sẽ liên quan đến việc sao chép bộ nhớ vô nghĩa) hoặc truy vấn thông tin trực tiếp từ công cụ. Nhưng không phải tất cả các đối tượng đều cần vật lý, một số sẽ được cố định tại chỗ để một bộ phao đơn giản hoạt động tốt ở đó. Một số thậm chí có thể được gắn vào các đối tượng khác, vì vậy vị trí của chúng thực sự là phần bù của vị trí khác. Trong thiết lập nâng cao, bạn có thể chỉ có vị trí được lưu trữ trên GPU, lần duy nhất bên máy tính cần thiết là để tạo kịch bản, lưu trữ và sao chép mạng. Vì vậy, bạn có thể sẽ có một số lựa chọn có thể cho dữ liệu vị trí của bạn. Ở đây nó có ý nghĩa để sử dụng thừa kế.

Thay vì một đối tượng sở hữu vị trí của nó, thay vào đó, chính đối tượng đó phải được sở hữu bởi một cấu trúc dữ liệu lập chỉ mục. Ví dụ: 'Cấp độ' có thể có Octree hoặc có thể là 'cảnh' của động cơ vật lý. Khi bạn muốn kết xuất (hoặc thiết lập cảnh kết xuất), bạn truy vấn cấu trúc đặc biệt của mình cho các đối tượng hiển thị trước máy ảnh.

Điều này cũng giúp cung cấp cho quản lý bộ nhớ tốt. Bằng cách này, một đối tượng không thực sự ở trong một khu vực thậm chí không có vị trí nào có ý nghĩa hơn là trả lại 0,0 coords hoặc các coords mà nó có khi nó tồn tại trong một khu vực.

Nếu bạn không còn giữ tọa độ trong đối tượng, thay vì object.getX () bạn sẽ có level.getX (object). Vấn đề với việc tìm kiếm đối tượng ở cấp độ có thể sẽ là một hoạt động chậm vì nó sẽ phải xem qua tất cả các đối tượng của nó và khớp với đối tượng mà bạn truy vấn.

Để tránh điều đó có lẽ tôi sẽ tạo một lớp 'liên kết' đặc biệt. Một liên kết giữa một cấp độ và một đối tượng. Tôi gọi nó là "Địa điểm". Điều này sẽ chứa tọa độ xyz cũng như tay cầm ở mức và tay cầm đối tượng. Lớp liên kết này sẽ được lưu trữ trong cấu trúc / cấp độ không gian và đối tượng sẽ có một tham chiếu yếu đến nó (nếu mức / vị trí bị phá hủy, các đối tượng cần phải cập nhật thành null. 'sở hữu' đối tượng, theo cách đó nếu một mức bị xóa, thì cấu trúc chỉ mục đặc biệt, các vị trí mà nó chứa và các Đối tượng của nó cũng vậy.

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

Bây giờ thông tin vị trí chỉ được lưu trữ ở một nơi. Không trùng lặp giữa Object, cấu trúc lập chỉ mục Spacial, trình kết xuất, v.v.

Các cấu trúc dữ liệu không gian như Octrees thường không cần phải có tọa độ của các đối tượng mà chúng lưu trữ. Có vị trí được lưu trữ ở vị trí tương đối của các nút trong chính cấu trúc (nó có thể được coi là một dạng nén mất mát, hy sinh độ chính xác cho thời gian tra cứu nhanh). Với đối tượng vị trí trong Octree thì tọa độ thực được tìm thấy bên trong nó sau khi truy vấn được thực hiện.

Hoặc nếu bạn đang sử dụng một công cụ vật lý để quản lý các vị trí đối tượng của mình hoặc hỗn hợp giữa hai thứ đó, lớp Location sẽ xử lý một cách trong suốt trong khi giữ tất cả mã của bạn ở một nơi.

Một lợi thế khác bây giờ là vị trí và sự điều chỉnh theo cấp độ được lưu trữ trong cùng một vị trí. Bạn có thể triển khai object.TeleportTo (other_object) và để nó hoạt động ở các cấp. Tương tự như vậy, việc tìm đường AI có thể đi theo một thứ gì đó vào một khu vực khác.

Liên quan đến kết xuất. Kết xuất của bạn có thể có một ràng buộc tương tự với Vị trí. Ngoại trừ nó sẽ có các công cụ cụ thể kết xuất trong đó. Bạn có thể không cần 'Đối tượng' hoặc 'Cấp độ' để được lưu trữ trong cấu trúc này. Đối tượng có thể hữu ích nếu bạn đang cố gắng thực hiện một số thứ như chọn màu hoặc hiển thị một thanh hit nổi phía trên nó, nhưng nếu không thì trình kết xuất chỉ quan tâm đến lưới và như vậy. RenderableStuff sẽ là một Lưới, cũng có thể có các hộp giới hạn, v.v.

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);
}

Bạn có thể không cần phải làm điều này mọi khung hình, bạn có thể đảm bảo bạn có một vùng lớn hơn so với máy ảnh hiện đang hiển thị. Lưu trữ bộ nhớ cache, theo dõi chuyển động của đối tượng để xem có hộp giới hạn nào trong phạm vi, theo dõi chuyển động của camera hay không. Nhưng đừng bắt đầu lộn xộn với những thứ đó cho đến khi bạn đã điểm chuẩn nó.

Bản thân động cơ vật lý của bạn có thể có một sự trừu tượng tương tự, vì nó cũng không cần dữ liệu Object, chỉ cần lưới va chạm và các thuộc tính vật lý.

Tất cả dữ liệu đối tượng cốt lõi của bạn sẽ chứa tên lưới mà đối tượng sử dụng. Sau đó, công cụ trò chơi có thể tiếp tục và tải nó ở bất kỳ định dạng nào mà nó thích mà không làm gánh nặng lớp đối tượng của bạn với một loạt các thứ cụ thể kết xuất (có thể cụ thể cho API kết xuất của bạn, ví dụ DirectX vs OpenGL).

Nó cũng giữ các thành phần khác nhau riêng biệt. Điều này giúp bạn dễ dàng thực hiện những việc như thay thế động cơ vật lý của mình vì những thứ đó chủ yếu được chứa trong một vị trí. Nó cũng làm cho việc bỏ qua dễ dàng hơn nhiều. Bạn có thể kiểm tra những thứ như truy vấn vật lý mà không cần phải có bất kỳ thiết lập đối tượng giả thực tế nào vì tất cả những gì bạn cần là lớp Location. Bạn cũng có thể tối ưu hóa công cụ dễ dàng hơn. Nó làm cho rõ ràng hơn những truy vấn bạn cần thực hiện trên các lớp và vị trí đơn lẻ nào để tối ưu hóa chúng (ví dụ: level.getVisibleObject sẽ là nơi bạn có thể lưu trữ mọi thứ nếu máy ảnh không di chuyển đến nhiều).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.