Hệ thống thực thể và kết xuất


10

Okey, những gì tôi biết cho đến nay; Thực thể chứa một thành phần (lưu trữ dữ liệu) chứa thông tin như; - Texture / sprite - Shader - vv

Và sau đó tôi có một hệ thống renderer thu hút tất cả điều này. Nhưng điều tôi không hiểu là làm thế nào trình kết xuất nên được thiết kế. Tôi nên có một thành phần cho mỗi "loại hình ảnh". Một thành phần không có shader, một với shader, v.v?

Chỉ cần một số đầu vào trên "cách chính xác" để làm điều này. Lời khuyên và cạm bẫy để coi chừng.


2
Cố gắng đừng làm mọi thứ quá chung chung. Có vẻ lạ khi có một thực thể có thành phần Shader chứ không phải thành phần Sprite, vì vậy có lẽ Shader phải là một phần của thành phần Sprite. Đương nhiên sau đó bạn sẽ chỉ cần một hệ thống kết xuất.
Jonathan Connell

Câu trả lời:


7

Đây là một câu hỏi khó trả lời bởi vì mọi người đều có ý tưởng riêng về cách cấu trúc một hệ thống thành phần thực thể. Điều tốt nhất tôi có thể làm là chia sẻ với bạn một số điều tôi thấy hữu ích nhất đối với tôi.

Thực thể

Tôi áp dụng cách tiếp cận lớp chất béo vào ECS, có lẽ bởi vì tôi thấy các phương pháp lập trình cực đoan rất kém hiệu quả (về năng suất của con người). Cuối cùng, một thực thể đối với tôi là một lớp trừu tượng được kế thừa bởi các lớp chuyên biệt hơn. Thực thể có một số thuộc tính ảo và một cờ đơn giản cho tôi biết thực thể này có tồn tại hay không. Vì vậy, liên quan đến câu hỏi của bạn về một hệ thống kết xuất, đây là giao Entitydiện:

public abstract class Entity {
    public bool IsAlive = true;
    public virtual SpatialComponent   Spatial   { get; set; }
    public virtual ImageComponent     Image     { get; set; }
    public virtual AnimationComponent Animation { get; set; }
    public virtual InputComponent     Input     { get; set; }
}

Các thành phần

Các thành phần "ngu ngốc" ở chỗ chúng không làm hoặc không biết gì. Chúng không có tham chiếu đến các thành phần khác và chúng thường không có chức năng (Tôi làm việc trong C #, vì vậy tôi sử dụng các thuộc tính để xử lý getters / setters - nếu chúng có chức năng, chúng dựa trên việc truy xuất dữ liệu mà chúng giữ).

Hệ thống

Các hệ thống ít "ngu ngốc" hơn, nhưng vẫn là những máy tự động câm. Chúng không có ngữ cảnh của toàn bộ hệ thống, không có tham chiếu đến các hệ thống khác và không có dữ liệu ngoại trừ một vài bộ đệm mà chúng có thể cần thực hiện xử lý riêng lẻ. Tùy thuộc vào hệ thống, nó có thể có một chuyên ngành Update, hoặc Drawphương pháp, hoặc trong một số trường hợp, cả hai.

Giao diện

Giao diện là một cấu trúc quan trọng trong hệ thống của tôi. Chúng được sử dụng để xác định những gì Systemcó thể xử lý và những gì Entitycó khả năng. Các Giao diện có liên quan để kết xuất là: IRenderableIAnimatable.

Các giao diện chỉ đơn giản cho hệ thống biết các thành phần có sẵn. Ví dụ, hệ thống kết xuất cần biết hộp giới hạn của thực thể và hình ảnh cần vẽ. Trong trường hợp của tôi, đó sẽ là SpatialComponentImageComponent. Vì vậy, nó trông như thế này:

public interface IRenderable {
    SpatialComponent Component { get; }
    ImageComponent   Image     { get; }
}

Hệ thống kết xuất

Vậy làm thế nào để hệ thống kết xuất vẽ một thực thể? Nó thực sự khá đơn giản, vì vậy tôi sẽ chỉ cho bạn thấy lớp bị loại bỏ để cho bạn một ý tưởng:

public class RenderSystem {
    private SpriteBatch batch;
    public RenderSystem(SpriteBatch batch) {
        this.batch = batch;
    }
    public void Draw(List<IRenderable> list) {
        foreach(IRenderable obj in list) {
            this.batch.draw(
                obj.Image.Texture,
                obj.Spatial.Position,
                obj.Image.Source,
                Color.White);
        }
    }
}

Nhìn vào lớp, hệ thống kết xuất thậm chí không biết đó là gì Entity. Tất cả những gì nó biết là IRenderablevà nó chỉ đơn giản là đưa ra một danh sách chúng để vẽ.

Tất cả hoạt động như thế nào

Nó cũng có thể giúp tôi hiểu cách tôi tạo các đối tượng trò chơi mới và cách tôi cung cấp chúng cho các hệ thống.

Tạo các thực thể

Tất cả các đối tượng trò chơi được kế thừa từ Thực thể và bất kỳ giao diện áp dụng nào mô tả những gì đối tượng trò chơi đó có thể làm. Tất cả mọi thứ hoạt hình trên màn hình đều trông như thế này:

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

Nuôi dưỡng hệ thống

Tôi giữ một danh sách tất cả các thực thể tồn tại trong thế giới trò chơi trong một danh sách duy nhất được gọi List<Entity> gameObjects. Mỗi khung hình, sau đó tôi chọn lọc danh sách đó và sao chép các tham chiếu đối tượng vào nhiều danh sách hơn dựa trên loại giao diện, chẳng hạn như List<IRenderable> renderableObjects, và List<IAnimatable> animatableObjects. Bằng cách này, nếu các hệ thống khác nhau cần xử lý cùng một thực thể, chúng có thể. Sau đó, tôi chỉ cần đưa các danh sách đó cho từng hệ thống Updatehoặc Drawphương thức và để các hệ thống thực hiện công việc của chúng.

Hoạt hình

Bạn có thể tò mò làm thế nào hệ thống hoạt hình. Trong trường hợp của tôi, bạn có thể muốn xem giao diện IAnimizable:

public interface IAnimatable {
    public AnimationComponent Animation { get; }
    public ImageComponent Image         { get; set; }
}

Điều quan trọng cần chú ý ở đây là ImageComponentkhía cạnh của IAnimatablegiao diện không chỉ đọc; nó có một setter .

Như bạn có thể đoán, thành phần hoạt hình chỉ chứa dữ liệu về hoạt hình; một danh sách các khung (là các thành phần hình ảnh), khung hiện tại, số khung hình mỗi giây sẽ được vẽ, thời gian trôi qua kể từ lần tăng khung hình cuối cùng và các tùy chọn khác.

Hệ thống hoạt hình tận dụng hệ thống kết xuất và mối quan hệ thành phần hình ảnh. Nó chỉ đơn giản là thay đổi thành phần hình ảnh của thực thể khi nó tăng khung hình của hình ảnh động. Bằng cách đó, hình ảnh động được kết xuất gián tiếp bởi hệ thống kết xuất.


Tôi có lẽ nên lưu ý rằng tôi không thực sự biết nếu điều này thậm chí gần với những gì mọi người đang gọi là một hệ thống thành phần thực thể . Trong nỗ lực thực hiện một thiết kế dựa trên thành phần, tôi thấy mình rơi vào mô hình này.
Cypher

Hấp dẫn! Tôi không quá quan tâm đến lớp trừu tượng cho Thực thể của bạn nhưng giao diện IRenderable là một ý tưởng hay!
Jonathan Connell

5

Xem câu trả lời này để xem loại hệ thống mà tôi đang nói đến.

Thành phần nên chứa các chi tiết cho những gì cần vẽ và làm thế nào để vẽ nó. Hệ thống kết xuất sẽ lấy các chi tiết đó và vẽ thực thể theo cách được chỉ định bởi thành phần. Chỉ khi bạn sử dụng các công nghệ vẽ khác nhau đáng kể, bạn mới có các thành phần riêng biệt cho các kiểu riêng biệt.


3

Lý do chính để tách logic thành các thành phần là tạo ra một tập hợp dữ liệu mà khi kết hợp trong một thực thể chúng tạo ra hành vi hữu ích, có thể sử dụng lại. Ví dụ: tách một Thực thể thành Vật lý và Kết xuất có ý nghĩa vì có thể không phải tất cả các thực thể sẽ có Vật lý và một số thực thể có thể không có Sprite.

Để trả lời câu hỏi của bạn, bạn cần nhìn vào kiến ​​trúc của mình và tự hỏi mình hai câu hỏi:

  1. Liệu nó có ý nghĩa để có một Shader mà không có Texture
  2. Việc tách Shader khỏi Texture có cho phép tôi tránh sao chép mã không?

Khi tách một thành phần, điều quan trọng là phải hỏi câu hỏi này, nếu câu trả lời là 1. có thì bạn có thể có một ứng cử viên tốt để tạo hai thành phần riêng biệt, một với Shader và một với Texture. Câu trả lời cho 2. thường là có cho các thành phần như Position nơi nhiều thành phần có thể sử dụng vị trí.

Ví dụ: cả Vật lý và Âm thanh có thể sử dụng cùng một vị trí, thay vào đó cả hai thành phần lưu trữ các vị trí trùng lặp mà bạn cấu trúc lại chúng thành một PositionComponent và yêu cầu các thực thể sử dụng Vật lý / Âm thanh cũng có PositionComponent.

Dựa trên thông tin bạn đã cung cấp cho chúng tôi, có vẻ như RenderComponent của bạn là một ứng cử viên tốt để phân chia thành TextureComponent và ShaderComponent vì các shader hoàn toàn phụ thuộc vào Texture và không có gì khác.

Giả sử bạn đang sử dụng một cái gì đó tương tự như T-Machine: Entity Systems, một triển khai mẫu của RenderComponent & RenderSystem trong C ++ sẽ giống như thế này:

struct RenderComponent {
    Texture* textureData;
    Shader* shaderData;
};

class RenderSystem {
    public:
        RenderSystem(EntityManager& manager) :
            m_manager(manager) {
            // Initialize Window, rendering context, etc...
        }

        void update() {
            // Get all the entities with RenderComponent
            std::vector<RenderComponent>& components = m_manager.getComponents<RenderComponent>();

            for(auto component = components.begin(); entity != components.end(); ++components) {
                // Do something with the texture
                doSomethingWithTexture(component->textureData);

                // Do something with the shader if it's not null
                if(component->shaderData != nullptr) {
                    doSomethingWithShader(component->shaderData);
                }
            }
        }
    private:
        EntityManager& m_manager;
}

Điều đó hoàn toàn sai. Toàn bộ quan điểm của các thành phần là tách chúng ra khỏi các thực thể, không thực hiện tìm kiếm hệ thống kết xuất thông qua các thực thể để tìm thấy chúng. Hệ thống kết xuất nên kiểm soát hoàn toàn dữ liệu của riêng họ. PS Đừng đặt std :: vector (đặc biệt là với dữ liệu cá thể) vào các vòng lặp, đó là C ++ khủng khiếp (chậm).
rắn5

@ Snake5 bạn đúng cả hai số. Tôi gõ mã ra khỏi đỉnh đầu và có một số vấn đề, cảm ơn vì đã chỉ ra chúng. Tôi đã sửa mã bị ảnh hưởng để chậm hơn và sử dụng chính xác các thành ngữ hệ thống thực thể.
Jake Woods

2
@ Snake5 Bạn không tính toán lại dữ liệu mỗi khung hình, getComponents trả về một vectơ thuộc sở hữu của m_manager đã biết và chỉ thay đổi khi bạn thêm / xóa các thành phần. Đó là một lợi thế khi bạn có một hệ thống muốn sử dụng nhiều thành phần của cùng một thực thể, ví dụ như Hệ thống Vật lý muốn sử dụng PositionComponent và ChemistryComponent. Các hệ thống khác có thể sẽ muốn vị trí và bằng cách có PositionComponent, bạn không có dữ liệu trùng lặp. Chủ yếu nó giải quyết vấn đề làm thế nào để các thành phần giao tiếp.
Jake Woods

5
@ Snake5 Câu hỏi không phải là về cách hệ thống EC nên được đặt ra hoặc hiệu suất của nó. Câu hỏi là về việc thiết lập hệ thống kết xuất. Có nhiều cách để cấu trúc một hệ thống EC, đừng để bị cuốn vào các vấn đề hiệu năng của một hệ thống khác ở đây. OP có thể sử dụng cấu trúc EC hoàn toàn khác với câu trả lời của bạn. Mã được cung cấp trong câu trả lời này chỉ nhằm thể hiện tốt hơn ví dụ, không bị chỉ trích về hiệu suất của nó. Nếu câu hỏi liên quan đến hiệu suất hơn có lẽ điều này sẽ khiến câu trả lời "không hữu ích", nhưng thực tế không phải vậy.
MichaelHouse

2
Tôi thích thiết kế được đưa ra trong câu trả lời này hơn là trong Cyphers. Nó rất giống với cái tôi sử dụng. Các thành phần nhỏ hơn là imo tốt hơn, ngay cả khi chúng chỉ có một hoặc hai biến. Họ nên xác định một khía cạnh của một thực thể, như thành phần "Damagable" của tôi sẽ có 2, có thể là 4 biến (tối đa và hiện tại cho mỗi lượng máu và áo giáp). Những bình luận này đang trở nên dài, hãy chuyển sang trò chuyện nếu bạn muốn thảo luận thêm.
John McDonald

2

Cạm bẫy # 1: mã quá mức. Hãy suy nghĩ về việc bạn có thực sự cần mọi thứ bạn thực hiện hay không bởi vì bạn sẽ phải sống với nó khá lâu.

Cạm bẫy # 2: quá nhiều đối tượng. Tôi sẽ không sử dụng một hệ thống có quá nhiều đối tượng (một đối tượng cho mỗi loại, kiểu con và bất cứ điều gì) bởi vì nó chỉ khiến việc xử lý tự động trở nên khó khăn hơn. Theo tôi, sẽ tốt hơn nhiều khi mỗi đối tượng điều khiển một bộ tính năng nhất định (trái ngược với một tính năng). Ví dụ: làm cho các thành phần cho mỗi bit dữ liệu được bao gồm trong kết xuất (thành phần kết cấu, thành phần đổ bóng) quá phân chia - bạn thường cần phải có tất cả các thành phần đó cùng nhau, bạn có đồng ý không?

Cạm bẫy # 3: kiểm soát bên ngoài quá nghiêm ngặt. Thích thay đổi tên thành các đối tượng đổ bóng / kết cấu vì các đối tượng có thể thay đổi với trình kết xuất / kiểu kết cấu / định dạng đổ bóng / bất cứ điều gì. Tên là các định danh đơn giản - tùy thuộc vào trình kết xuất để quyết định những gì cần tạo ra từ đó. Một ngày nào đó bạn có thể muốn có các vật liệu thay vì các shader đơn giản (ví dụ thêm các shader, kết cấu và chế độ hòa trộn từ dữ liệu). Với giao diện dựa trên văn bản, việc thực hiện điều đó sẽ dễ dàng hơn nhiều.

Đối với trình kết xuất, nó có thể là một giao diện đơn giản tạo / hủy / duy trì / kết xuất các đối tượng được tạo bởi các thành phần. Đại diện nguyên thủy nhất của nó có thể là một cái gì đó như thế này:

class Renderer {
    function Draw() { ... }
    function AddSprite( ... ) { ... return sprite; }
    function RemoveSprite( sprite ) { ... }
    ...
};

Điều này sẽ cho phép bạn quản lý các đối tượng này từ các thành phần của mình và giữ chúng đủ xa để cho phép bạn kết xuất chúng theo bất kỳ cách nào bạn muốn.

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.