Sử dụng hệ thống thực thể dựa trên thành phần thực tế


59

Hôm qua, tôi đã đọc một bài thuyết trình từ GDC Canada về hệ thống thực thể Thuộc tính / Hành vi và tôi nghĩ nó khá tuyệt. Tuy nhiên, tôi không chắc chắn làm thế nào để sử dụng nó một cách thiết thực, không chỉ trong lý thuyết. Trước hết, tôi sẽ nhanh chóng giải thích cho bạn cách hệ thống này hoạt động.


Mỗi thực thể trò chơi (đối tượng trò chơi) bao gồm các thuộc tính (= dữ liệu, có thể được truy cập bằng các hành vi, nhưng cũng bằng 'mã bên ngoài') và hành vi (= logic, có chứa OnUpdate()OnMessage()). Vì vậy, ví dụ, trong một bản sao đột phá, mỗi viên gạch sẽ bao gồm (ví dụ!): PositionAttribution , ColorAttribution , HealthAttribution , RenderableBehaviour , HitBehaviour . Cái cuối cùng có thể trông như thế này (nó chỉ là một ví dụ không hoạt động được viết bằng C #):

void OnMessage(Message m)
{
    if (m is CollisionMessage) // CollisionMessage is inherited from Message
    {
        Entity otherEntity = m.CollidedWith; // Entity CollisionMessage.CollidedWith
        if (otherEntity.Type = EntityType.Ball) // Collided with ball
        {
            int brickHealth = GetAttribute<int>(Attribute.Health); // owner's attribute
            brickHealth -= otherEntity.GetAttribute<int>(Attribute.DamageImpact);
            SetAttribute<int>(Attribute.Health, brickHealth); // owner's attribute

            // If health is <= 0, "destroy" the brick
            if (brickHealth <= 0)
                SetAttribute<bool>(Attribute.Alive, false);
        }
    }
    else if (m is AttributeChangedMessage) // Some attribute has been changed 'externally'
    {
        if (m.Attribute == Attribute.Health)
        {
            // If health is <= 0, "destroy" the brick
            if (brickHealth <= 0)
                SetAttribute<bool>(Attribute.Alive, false);
        }
    }
}

Nếu bạn quan tâm đến hệ thống này, bạn có thể đọc thêm tại đây (.ppt).


Câu hỏi của tôi liên quan đến hệ thống này, nhưng nói chung là mọi hệ thống thực thể dựa trên thành phần. Tôi chưa bao giờ thấy bất kỳ cái nào trong số này thực sự hoạt động trong các trò chơi máy tính thực sự, bởi vì tôi không thể tìm thấy bất kỳ ví dụ hay nào và nếu tôi tìm thấy nó, nó không được ghi lại, không có bình luận nào và vì vậy tôi không hiểu nó.

Vì vậy, tôi muốn hỏi gì? Làm thế nào để thiết kế các hành vi (thành phần). Tôi đã đọc ở đây, trên GameDev SE, rằng lỗi phổ biến nhất là tạo ra nhiều thành phần và đơn giản là "biến mọi thứ thành một thành phần". Tôi đã đọc rằng đề nghị không thực hiện kết xuất trong một thành phần, nhưng thực hiện nó bên ngoài nó (vì vậy thay vì RenderableBehaviour , nó có thể là RenderableAttribution và nếu một thực thể có RenderableAttribution được đặt thành true, thì Renderer(lớp không liên quan đến các thành phần, nhưng với chính động cơ) nên vẽ nó trên màn hình?).

Nhưng, làm thế nào về các hành vi / thành phần? Hãy nói rằng tôi đã có một cấp độ, và ở cấp độ, có một Entity button, Entity doorsEntity player. Khi người chơi va chạm với nút (đó là nút sàn, bị thay đổi bởi áp lực), nó sẽ bị nhấn. Khi nhấn nút, nó sẽ mở cửa. Vâng, bây giờ làm thế nào để làm điều đó?

Tôi đã nghĩ ra một thứ như thế này: người chơi đã có CollisionBehaviour , để kiểm tra xem người chơi có va chạm với thứ gì đó không. Nếu anh ta va chạm với một nút, nó sẽ gửi một CollisionMessageđến buttonthực thể. Tin nhắn sẽ chứa tất cả các thông tin cần thiết: người đã va chạm với nút. Nút đã có TogglizableBehaviour , sẽ nhận được CollisionMessage. Nó sẽ kiểm tra xem ai đã va chạm với nó và nếu trọng lượng của thực thể đó đủ lớn để bật nút, nút sẽ được bật. Bây giờ, nó đặt ToggledAttribution của nút thành true. Được rồi, nhưng bây giờ thì sao?

Nút có nên gửi một tin nhắn khác cho tất cả các đối tượng khác để nói với họ rằng nó đã được bật? Tôi nghĩ rằng nếu tôi làm mọi thứ như thế này, tôi sẽ có hàng ngàn tin nhắn và nó sẽ trở nên khá lộn xộn. Vì vậy, có lẽ điều này là tốt hơn: các cửa liên tục kiểm tra xem nút được liên kết với chúng có được nhấn hay không và thay đổi OpenedAttribution của nó cho phù hợp. Nhưng sau đó, điều đó có nghĩa là phương pháp của các cánh cửa OnUpdate()sẽ liên tục làm một cái gì đó (nó có thực sự là một vấn đề không?).

Và vấn đề thứ hai: nếu tôi có nhiều loại nút hơn. Người ta bị ép bởi áp lực, người thứ hai bị quấy rối bằng cách bắn vào nó, người thứ ba bị bật nếu nước đổ vào nó, v.v. Điều này có nghĩa là tôi sẽ phải có những hành vi khác nhau, đại loại như thế này:

Behaviour -> ToggleableBehaviour -> ToggleOnPressureBehaviour
                                 -> ToggleOnShotBehaviour
                                 -> ToggleOnWaterBehaviour

Đây là cách trò chơi thực sự hoạt động hay tôi chỉ là ngu ngốc? Có lẽ tôi chỉ có thể có một TogglizableBehaviour và nó sẽ hoạt động theo NútTypeAttribution . Vì vậy, nếu nó là ButtonType.Pressure, nó làm điều này, nếu nó là ButtonType.Shot, nó làm một cái gì đó khác ...

Vậy tôi muốn gì? Tôi muốn hỏi bạn rằng tôi đang làm đúng hay tôi chỉ ngu ngốc và tôi không hiểu điểm của các thành phần. Tôi không tìm thấy bất kỳ ví dụ hay nào về cách các thành phần thực sự hoạt động trong các trò chơi, tôi chỉ tìm thấy một số hướng dẫn mô tả cách tạo hệ thống thành phần, nhưng không sử dụng nó.

Câu trả lời:


46

Các thành phần là tuyệt vời, nhưng có thể mất một thời gian để tìm một giải pháp cảm thấy tốt cho bạn. Đừng lo lắng, bạn sẽ đến đó. :)

Tổ chức thành phần

Bạn đang nói khá nhiều về việc đi đúng hướng, tôi nói. Tôi sẽ cố gắng mô tả giải pháp ngược lại, bắt đầu với cánh cửa và kết thúc bằng các công tắc. Việc thực hiện của tôi làm cho việc sử dụng các sự kiện trở nên nặng nề; bên dưới tôi mô tả cách bạn có thể sử dụng các sự kiện hiệu quả hơn để chúng không trở thành vấn đề.

Nếu bạn có một cơ chế kết nối các thực thể giữa chúng, tôi sẽ có công tắc trực tiếp thông báo cho cửa rằng nó đã được nhấn, thì cửa có thể quyết định phải làm gì.

Nếu bạn không thể kết nối các thực thể, giải pháp của bạn khá gần với những gì tôi sẽ làm. Tôi muốn có một cánh cửa lắng nghe cho một sự kiện chung chung ( SwitchActivatedEventcó thể). Khi các công tắc được kích hoạt, họ đăng sự kiện này.

Nếu bạn có nhiều hơn một loại công tắc, tôi muốn có PressureToggle, WaterTogglevà một ShotTogglehành vi quá, nhưng tôi không chắc chắn cơ sở ToggleableBehaviourlà bất kỳ tốt, vì vậy tôi muốn làm đi với điều đó (trừ khi, tất nhiên, bạn có một tốt lý do để giữ nó).

Behaviour -> ToggleOnPressureBehaviour
          -> ToggleOnShotBehaviour
          -> ToggleOnWaterBehaviour

Xử lý sự kiện hiệu quả

Vì lo lắng rằng có quá nhiều sự kiện bay xung quanh, có một điều bạn có thể làm. Thay vì để mọi thành phần được thông báo về mọi sự kiện xảy ra, thì hãy kiểm tra thành phần xem đó có phải là loại sự kiện phù hợp hay không, đây là một cơ chế khác ...

Bạn có thể có EventDispatchermột subscribephương thức trông giống như thế này (mã giả):

EventDispatcher.subscribe(event_type, function)

Sau đó, khi bạn đăng một sự kiện, người điều phối sẽ kiểm tra loại của nó và chỉ thông báo cho các chức năng đã đăng ký loại sự kiện cụ thể đó. Bạn có thể thực hiện điều này như một bản đồ liên kết các loại sự kiện với danh sách các chức năng.

Theo cách này, hệ thống hiệu quả hơn đáng kể: có rất ít lệnh gọi chức năng cho mỗi sự kiện và các thành phần có thể chắc chắn rằng chúng đã nhận đúng loại sự kiện và không phải kiểm tra lại.

Tôi đã đăng một triển khai đơn giản về điều này một thời gian trước trên StackOverflow. Nó được viết bằng Python, nhưng có lẽ nó vẫn có thể giúp bạn:
https://stackoverflow.com/a/7294148/627005

Việc thực hiện đó khá chung chung: nó hoạt động với bất kỳ loại chức năng nào, không chỉ các chức năng từ các thành phần. Nếu bạn không cần điều đó, thay vì function, bạn có thể có một behaviortham số trong subscribephương thức của mình - trường hợp hành vi cần được thông báo.

Thuộc tính và hành vi

Tôi đã tự mình sử dụng các thuộc tính và hành vi , thay vì các thành phần cũ đơn giản. Tuy nhiên, từ mô tả của bạn về cách bạn sử dụng hệ thống trong trò chơi Breakout, tôi nghĩ bạn đang làm quá sức.

Tôi chỉ sử dụng các thuộc tính khi hai hành vi cần truy cập vào cùng một dữ liệu. Thuộc tính giúp giữ cho các hành vi tách biệt và sự phụ thuộc giữa các thành phần (có thể là thuộc tính hoặc hành vi) không bị vướng mắc, bởi vì chúng tuân theo các quy tắc rất đơn giản và rõ ràng:

  • Các thuộc tính không sử dụng bất kỳ thành phần nào khác (không thuộc tính khác, cũng không phải hành vi), chúng là tự cung cấp.

  • Các hành vi không sử dụng hoặc biết về các hành vi khác. Họ chỉ biết về một số thuộc tính (những thuộc tính mà họ thực sự cần).

Khi một số dữ liệu chỉ cần một và chỉ một trong các hành vi, tôi thấy không có lý do gì để đưa nó vào một thuộc tính, tôi để hành vi giữ nó.


@ bình luận của Heishe

Vấn đề đó có xảy ra với các thành phần bình thường không?

Dù sao, tôi không phải kiểm tra các loại sự kiện bởi vì mọi chức năng chắc chắn luôn luôn nhận được đúng loại sự kiện .

Ngoài ra, các phụ thuộc của hành vi (ví dụ: các thuộc tính mà chúng cần) được giải quyết khi xây dựng, do đó bạn không phải tìm kiếm các thuộc tính mỗi lần cập nhật.

Và cuối cùng, tôi sử dụng Python cho mã logic trò chơi của mình (mặc dù công cụ này là C ++), do đó không cần phải truyền. Python thực hiện thao tác gõ vịt và mọi thứ đều hoạt động tốt. Nhưng ngay cả khi tôi không sử dụng ngôn ngữ với cách gõ vịt, tôi sẽ làm điều này (ví dụ đơn giản hóa):

class SomeBehavior
{
  public:
    SomeBehavior(std::map<std::string, Attribute*> attribs, EventDispatcher* events)
        // For the purposes of this example, I'll assume that the attributes I
        // receive are the right ones. 
        : health_(static_cast<HealthAttribute*>(attribs["health"])),
          armor_(static_cast<ArmorAttribute*>(attribs["armor"]))
    {
        // Boost's polymorphic_downcast would probably be more secure than
        // a static_cast here, but nonetheless...
        // Also, I'd probably use some smart pointers instead of plain
        // old C pointers for the attributes.

        // This is how I'd subscribe a function to a certain type of event.
        // The dispatcher returns a `Subscription` object; the subscription 
        // is alive for as long this object is alive.
        subscription_ = events->subscribe(event::type<DamageEvent>(),
            std::bind(&SomeBehavior::onDamageEvent, this, _1));
    }

    void onDamageEvent(std::shared_ptr<Event> e)
    {
        DamageEvent* damage = boost::polymorphic_downcast<DamageEvent*>(e.get());
        // Simplistic and incorrect formula: health = health - damage + armor
        health_->value(health_->value() - damage->amount() + armor_->protection());
    }

    void update(boost::chrono::duration timePassed)
    {
        // Behaviors also have an `update` function, just like
        // traditional components.
    }

  private:
    HealthAttribute* health_;
    ArmorAttribute* armor_;
    EventDispatcher::Subscription subscription_;
};

Không giống như các hành vi, các thuộc tính không có bất kỳ updatechức năng nào - chúng không cần, mục đích của chúng là giữ dữ liệu, không thực hiện logic trò chơi phức tạp.

Bạn vẫn có thể có các thuộc tính của bạn thực hiện một số logic đơn giản. Trong ví dụ này, một điều HealthAttributecó thể đảm bảo 0 <= value <= max_healthluôn luôn đúng. Nó cũng có thể gửi một HealthCriticalEventđến các thành phần khác của cùng một thực thể khi nó giảm xuống dưới 25%, nhưng nó không thể thực hiện logic nào phức tạp hơn thế.


Ví dụ về một lớp thuộc tính:

class HealthAttribute : public EntityAttribute
{
  public:
    HealthAttribute(Entity* entity, double max, double critical)
        : max_(max), critical_(critical), current_(max)
    { }

    double value() const {
        return current_;
    }    

    void value(double val)
    {
        // Ensure that 0 <= current <= max 
        if (0 <= val && val <= max_)
            current_ = val;

        // Notify other components belonging to this entity that
        // health is too low.
        if (current_ <= critical_) {
            auto ev = std::shared_ptr<Event>(new HealthCriticalEvent())
            entity_->events().post(ev)
        }
    }

  private:
    double current_, max_, critical_;
};

Cảm ơn bạn! Đây chính xác là một asnwer tôi muốn. Tôi cũng thích ý tưởng của bạn về EventDispatcher hơn là thông điệp đơn giản truyền đến tất cả các thực thể. Bây giờ, đến điều cuối cùng bạn nói với tôi: về cơ bản bạn nói rằng Sức khỏe và Thiệt hại không phải là thuộc tính trong ví dụ này. Vì vậy, thay vì các thuộc tính, chúng chỉ là các biến riêng tư của các hành vi? Điều đó có nghĩa là, "DamageImpact" sẽ được thông qua sự kiện này? Ví dụ: EventArss.DamageImpact? Điều đó nghe có vẻ tốt ... Nhưng nếu tôi muốn viên gạch thay đổi màu sắc theo sức khỏe của nó, thì Sức khỏe sẽ phải là một thuộc tính, phải không? Cảm ơn bạn!
TomsonTom

2
@TomsonTom Vâng, đúng vậy. Có các sự kiện chứa bất kỳ dữ liệu nào người nghe cần biết là một giải pháp rất tốt.
Paul Manta

3
Đây là một câu trả lời tuyệt vời! (như pdf của bạn) - Khi bạn có cơ hội, bạn có thể giải thích một chút về cách bạn xử lý kết xuất với hệ thống này không? Mô hình thuộc tính / hành vi này hoàn toàn mới đối với tôi, nhưng rất hấp dẫn.
Michael

1
@TomsonTom Về kết xuất, hãy xem câu trả lời tôi đã đưa ra cho Michael. Đối với va chạm, cá nhân tôi đã đi một lối tắt. Tôi đã sử dụng một thư viện có tên Box2D, khá dễ sử dụng và xử lý các va chạm tốt hơn nhiều so với tôi có thể. Nhưng tôi không sử dụng thư viện trực tiếp trong mã logic trò chơi của mình. Mỗi cái đều Entitycó một EntityBodycái mà trừu tượng hóa đi tất cả các bit xấu xí. Các hành vi sau đó có thể đọc vị trí từ EntityBody, áp dụng lực cho nó, sử dụng các khớp và động cơ mà cơ thể có, v.v ... Có một mô phỏng vật lý có độ chính xác cao như Box2D chắc chắn mang đến những thách thức mới, nhưng chúng khá thú vị, imo.
Paul Manta

1
@thelinuxlich Vậy bạn là nhà phát triển của Artemis! : D Tôi đã thấy Component/ Systemlược đồ được tham chiếu một vài lần trên bảng. Việc triển khai của chúng tôi thực sự có khá nhiều điểm tương đồng.
Paul Manta
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.