Thiết kế một trò chơi dựa trên thành phần


16

Tôi đang viết một game bắn súng (như 1942, đồ họa 2D cổ điển) và tôi muốn sử dụng một phê duyệt dựa trên thành phần. Cho đến nay tôi nghĩ về thiết kế sau:

  1. Mỗi yếu tố trò chơi (khí cầu, đạn, sức mạnh, kẻ thù) là một Thực thể

  2. Mỗi Thực thể là một tập hợp các thành phần có thể được thêm hoặc xóa trong thời gian chạy. Ví dụ như Position, Sprite, Health, IA, Damage, BoundingBox, v.v.

Ý tưởng là Airship, Projectile, Enemy, Powerup KHÔNG phải là các lớp trò chơi. Một thực thể chỉ được xác định bởi các thành phần mà nó sở hữu (và có thể thay đổi trong thời gian). Vì vậy, Airship của người chơi bắt đầu với các thành phần Sprite, Position, Health và Input. Một powerup có Sprite, Position, BoundingBox. Và như thế.

Vòng lặp chính quản lý trò chơi "vật lý", tức là cách các thành phần tương tác với nhau:

foreach(entity (let it be entity1) with a Damage component)
    foreach(entity (let it be entity2) with a Health component)
    if(the entity1.BoundingBox collides with entity2.BoundingBox)
    {
        entity2.Health.decrease(entity1.Damage.amount());
    }

foreach(entity with a IA component)
    entity.IA.update(); 

foreach(entity with a Sprite component)
    draw(entity.Sprite.surface()); 

...

Các thành phần được mã hóa cứng trong ứng dụng C ++ chính. Các thực thể có thể được định nghĩa trong một tệp XML (phần IA trong tệp lua hoặc python).

Vòng lặp chính không quan tâm nhiều đến các thực thể: nó chỉ quản lý các thành phần. Thiết kế phần mềm nên cho phép:

  1. Cho một thành phần, có được thực thể nó thuộc về

  2. Đưa ra một thực thể, lấy thành phần của loại "loại"

  3. Đối với tất cả các thực thể, làm một cái gì đó

  4. Đối với tất cả thành phần của thực thể, hãy làm một cái gì đó (ví dụ: serialization)

Tôi đã suy nghĩ về những điều sau đây:

class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component {...};
class Position : public Component {...};
class IA : public Component {... virtual void update() = 0; };

// I don't remember exactly the boost::fusion map syntax right now, sorry.
class Entity
{
   int id; // entity id
   boost::fusion::map< pair<Sprite, Sprite*>, pair<Position, Position*> > components;
   template <class C> bool has_component() { return components.at<C>() != 0; }
   template <class C> C* get_component() { return components.at<C>(); }
   template <class C> void add_component(C* c) { components.at<C>() = c; }
   template <class C> void remove_component(C* c) { components.at<C>() = 0; }
   void serialize(filestream, op) { /* Serialize all componets*/ }
...
};

std::list<Entity*> entity_list;

Với thiết kế này, tôi có thể nhận được # 1, # 2, # 3 (nhờ các thuật toán boost :: fusion :: map) và # 4. Ngoài ra mọi thứ đều là O (1) (ok, không chính xác, nhưng nó vẫn rất nhanh).

Ngoài ra còn có một sự chấp thuận "phổ biến" hơn:

class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component { static const int type_id = 0; };
class Position : public Component { static const int type_id = 1; };

class Entity
{
   int id; // entity id
   std::vector<Component*> components;
   bool has_component() { return components[i] != 0; }
   template <class C> C* get_component() { return dynamic_cast<C> components[C::id](); } // It's actually quite safe
...
};

Một cách chấp nhận khác là loại bỏ lớp Thực thể: mỗi loại Thành phần sống trong danh sách riêng của nó. Vì vậy, có một danh sách Sprite, danh sách Sức khỏe, danh sách Thiệt hại, v.v ... Tôi biết chúng thuộc cùng một thực thể logic vì id thực thể. Điều này đơn giản hơn, nhưng chậm hơn: các thành phần IA cần truy cập cơ bản vào tất cả các thành phần của thực thể khác và điều đó sẽ yêu cầu tìm kiếm danh sách của các thành phần khác ở mỗi bước.

Mà bạn nghĩ là tốt hơn? Bản đồ boost :: fusion có phù hợp để sử dụng theo cách đó không?


2
Tại sao một downvote? Có gì sai với câu hỏi này?
Emiliano

Câu trả lời:


6

Tôi đã thấy rằng thiết kế dựa trên thành phần và thiết kế hướng dữ liệu đi đôi với nhau. Bạn nói rằng việc có các danh sách các thành phần đồng nhất và loại bỏ đối tượng thực thể hạng nhất (thay vào đó chọn một ID thực thể trên chính các thành phần) sẽ "chậm hơn", nhưng đó không phải ở đây và ở đó vì bạn chưa thực sự tìm hiểu bất kỳ mã thực nào thực hiện cả hai cách tiếp cận để đi đến kết luận đó. Trên thực tế, tôi gần như có thể đảm bảo với bạn rằng việc đồng nhất hóa các thành phần của bạn và tránh ảo hóa nặng truyền thống sẽ nhanh hơn do các lợi thế khác nhau của thiết kế hướng dữ liệu - song song dễ dàng hơn, sử dụng bộ đệm, mô đun hóa, v.v.

Tôi không nói rằng cách tiếp cận này là lý tưởng cho tất cả mọi thứ, nhưng các hệ thống thành phần về cơ bản là các bộ sưu tập dữ liệu cần các phép biến đổi giống nhau được thực hiện trên mỗi khung hình, chỉ đơn giản là hét lên để được định hướng dữ liệu. Sẽ có lúc các thành phần cần giao tiếp với các thành phần khác thuộc các loại khác nhau, nhưng đây cũng sẽ là một điều ác cần thiết. Tuy nhiên, nó không nên điều khiển thiết kế vì có nhiều cách để giải quyết vấn đề này ngay cả trong trường hợp cực đoan là tất cả các thành phần được xử lý song song như hàng đợi tin nhắn và tương lai .

Chắc chắn Google xoay quanh thiết kế hướng dữ liệu vì nó liên quan đến các hệ thống dựa trên thành phần, bởi vì chủ đề này xuất hiện rất nhiều và có khá nhiều thảo luận và dữ liệu giai thoại ngoài kia.


"Định hướng dữ liệu" nghĩa là gì?
Emiliano

Có rất nhiều thông tin trên Google, nhưng đây là một bài báo hay xuất hiện để cung cấp một cái nhìn tổng quan cấp cao, sau đó là một cuộc thảo luận vì nó liên quan đến các hệ thống thành phần: gamesfromwithin.com/data-oriented-design , gamedev. net / topic /
Khải

Tôi không thể đồng ý với tất cả những điều liên quan đến DOD, vì tôi nghĩ rằng nó không thể tự hoàn thiện, ý tôi là chỉ DOD mới có thể đề xuất một aprroch rất tốt để lưu trữ dữ liệu nhưng để gọi các chức năng và thủ tục bạn cần sử dụng theo thủ tục hoặc Sự chấp thuận của OOP, ý tôi là vấn đề là làm thế nào để kết hợp hai phương pháp này để mang lại lợi ích cao nhất cho cả hiệu suất và tính dễ mã hóa, ví dụ. trong cấu trúc tôi đề nghị sẽ có vấn đề về hiệu năng khi tất cả các thực thể không chia sẻ một số thành phần nhưng nó có thể dễ dàng được giải quyết bằng DOD, bạn chỉ phải tạo các mảng khác nhau cho các loại liên kết khác nhau.
Ali1S 232

Điều này không trả lời trực tiếp câu hỏi của tôi nhưng nó rất nhiều thông tin. Tôi nhớ một vài điều về Dataflows trong thời đại học của tôi. Đó là câu trả lời tốt nhất cho đến nay và nó "chiến thắng".
Emiliano

-1

Nếu tôi viết một mã như vậy, tôi sẽ sử dụng sự chấp thuận này (và tôi sẽ không sử dụng bất kỳ sự tăng cường nào nếu nó quan trọng đối với bạn), vì nó có thể làm tất cả những gì bạn muốn nhưng vấn đề là khi có quá nhiều mục nhập mà không chia sẻ một số thành phần mạng, việc tìm kiếm những người có nó sẽ tiêu tốn một thời gian. ngoài ra không có vấn đề nào khác tôi có thể điều:

// declare components here------------------------------
class component
{
};

class health:public component
{
public:
    int value;
};

class boundingbox:public component
{
public :
    int left,right,top,bottom;
    bool collision(boundingbox& other)
    {
        if (left < other.right || right > other.left)
            if (top < other.bottom || bottom > other.top)
                return true;
        return false;
    }
};

class damage : public component
{
public:
    int value;
};

// declare enteties here------------------------------

class entity
{
    virtual int id() = 0;
    virtual int size() = 0;
};

class aircraft :public entity, public health,public boundingbox
{
    virtual int id(){return 1;}
    virtual int size() {return sizeof(*this);};
};

class bullet :public entity, public damage, public boundingbox
{
    virtual int id(){return 2;}
    virtual int size() {return sizeof(*this);};
};

int main()
{
    entity* gameobjects[3];
    gameobjects[0] = new aircraft;
    gameobjects[1] = new bullet;
    gameobjects[2] = new bullet;
    for (int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if (dynamic_cast<boundingbox*>(gameobjects[i]) && dynamic_cast<boundingbox*>(gameobjects[j]) &&
                dynamic_cast<boundingbox*>(gameobjects[i])->collision(*dynamic_cast<boundingbox*>(gameobjects[j])))
                if (dynamic_cast<health*>(gameobjects[i]) && dynamic_cast<damage*>(gameobjects[j]))
                    dynamic_cast<health*>(gameobjects[i])->value -= dynamic_cast<damage*>(gameobjects[j])->value;
}

trong sự chấp thuận này, mọi thành phần là một cơ sở cho một thực thể vì vậy với thành phần đó, con trỏ của nó cũng là một thực thể! điều thứ hai bạn yêu cầu là có quyền truy cập trực tiếp vào một số thành phần của thực thể, vd. Khi tôi cần truy cập thiệt hại trong một trong các thực thể của mình, tôi sử dụng dynamic_cast<damage*>(entity)->value, vì vậy nếu entitycó thành phần thiệt hại, nó sẽ trả về giá trị. nếu bạn không chắc chắn entitycó thiệt hại thành phần hay không, bạn có thể dễ dàng kiểm tra if (dynamic_cast<damage*> (entity))giá trị trả về dynamic_castluôn là NULL nếu giá trị không hợp lệ và cùng một con trỏ nhưng với loại được yêu cầu nếu nó hợp lệ. Vì vậy, để làm một cái gì đó với tất cả những entitiescomponentbạn có thể làm như dưới đây

for (int i=0;i<enteties.size();i++)
    if (dynamic_cast<component*>(enteties[i]))
        //do somthing here

nếu có câu hỏi nào khác tôi sẽ vui lòng trả lời.


Tại sao tôi lại bỏ phiếu xuống? Điều gì đã sai với giải pháp của tôi?
Ali1S 232

3
Giải pháp của bạn không thực sự là một giải pháp dựa trên thành phần vì các thành phần không tách biệt với các lớp trò chơi của bạn. Tất cả các trường hợp của bạn đều dựa vào mối quan hệ IS (kế thừa) thay vì quan hệ HAS A (thành phần). Thực hiện theo cách thành phần (thực thể giải quyết một số thành phần) mang lại cho bạn rất nhiều lợi thế so với mô hình thừa kế (thường là lý do tại sao bạn sử dụng các thành phần). Giải pháp của bạn không mang lại lợi ích nào cho giải pháp dựa trên thành phần và giới thiệu một số quirks (nhiều kế thừa, v.v.). Không có dữ liệu địa phương, không có cập nhật thành phần riêng biệt. Không sửa đổi thời gian chạy của các thành phần.
khoảng trống

trước hết câu hỏi yêu cầu cấu trúc rằng mọi thể hiện thành phần chỉ liên quan đến một thực thể và bạn có thể kích hoạt và hủy kích hoạt các thành phần bằng cách chỉ thêm một bool isActivelớp vào lớp cơ sở. vẫn cần phải giới thiệu các thành phần có thể sử dụng được khi bạn xác định các mục nhập nhưng tôi không coi đó là một vấn đề, và bạn vẫn tách biệt các bản cập nhật thành phần (hãy nhớ một số thứ như dynamic_cast<componnet*>(entity)->update().
Ali1S232

và tôi đồng ý vẫn sẽ có một vấn đề khi anh ta muốn có thành phần có thể chia sẻ dữ liệu nhưng xem xét những gì anh ta yêu cầu tôi đoán sẽ không có vấn đề gì cho điều đó, và một lần nữa cũng có một số mẹo cho vấn đề đó nếu bạn muốn tôi có thể giải thích.
Ali1S 232

Mặc dù tôi đồng ý rằng có thể thực hiện theo cách này, tôi không nghĩ rằng đó là một ý tưởng tốt. Các nhà thiết kế của bạn không thể tự soạn các đối tượng, trừ khi bạn có một lớp über kế thừa tất cả các thành phần có thể. Và mặc dù bạn có thể gọi cập nhật trên chỉ một thành phần, nhưng nó sẽ không có bố cục trong bộ nhớ tốt, trong một mô hình tổng hợp, tất cả các phiên bản thành phần cùng loại có thể được giữ gần trong bộ nhớ và lặp đi lặp lại mà không có bất kỳ lỗi bộ nhớ cache nào. Bạn cũng đang dựa vào RTTI, một thứ thường bị tắt trong các trò chơi vì lý do hiệu suất. Một bố trí đối tượng được sắp xếp tốt sửa chữa mà chủ yếu.
khoảng trống
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.