การใช้ระบบเอนทิตีตามองค์ประกอบจริง


59

เมื่อวานนี้ฉันได้อ่านงานนำเสนอจาก GDC Canada เกี่ยวกับระบบเอนทิตีแอตทริบิวต์ / พฤติกรรมและฉันคิดว่ามันยอดเยี่ยมมาก อย่างไรก็ตามฉันไม่แน่ใจว่าจะใช้งานได้อย่างไรไม่ใช่ในทางทฤษฎี ก่อนอื่นฉันจะอธิบายให้คุณทราบอย่างรวดเร็วว่าระบบนี้ทำงานอย่างไร


แต่ละเอนทิตีเกม (เกมวัตถุ) ประกอบด้วยคุณลักษณะ (= ข้อมูลซึ่งสามารถเข้าถึงได้โดยพฤติกรรม แต่ยังรวมถึง 'รหัสภายนอก') และพฤติกรรม (= ตรรกะซึ่งมีOnUpdate()และOnMessage()) ดังนั้นสำหรับตัวอย่างเช่นในโคลนฝ่าวงล้อมอิฐแต่ละคนจะต้องประกอบด้วย (ตัวอย่าง): PositionAttribute , ColorAttribute , HealthAttribute , RenderableBehaviour , HitBehaviour คนสุดท้ายอาจมีลักษณะเช่นนี้ (เป็นเพียงตัวอย่างที่ไม่ทำงานเขียนใน 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);
        }
    }
}

หากคุณสนใจระบบนี้คุณสามารถอ่านเพิ่มเติมได้ที่นี่ (.ppt)


คำถามของฉันเกี่ยวข้องกับระบบนี้ แต่โดยทั่วไปทุกหน่วยงานตามระบบเอนทิตี ฉันไม่เคยเห็นว่าสิ่งเหล่านี้ใช้งานได้จริงในเกมคอมพิวเตอร์จริงเพราะฉันไม่สามารถหาตัวอย่างที่ดีได้และถ้าฉันพบมันก็ไม่มีเอกสารแสดงความคิดเห็นและฉันก็ไม่เข้าใจ

ดังนั้นฉันต้องการถามอะไร วิธีการออกแบบพฤติกรรม (ส่วนประกอบ) ฉันได้อ่านที่นี่ใน GameDev SE ว่าข้อผิดพลาดที่พบบ่อยที่สุดคือการสร้างองค์ประกอบมากมายและเพียงแค่ "ทำให้ทุกอย่างเป็นองค์ประกอบ" ฉันได้อ่านแล้วว่ามันไม่แนะนำให้ทำการเรนเดอร์ในองค์ประกอบ แต่ทำมันนอกมัน (ดังนั้นแทนที่จะเป็นRenderableBehaviourมันอาจจะเป็นRenderableAttributeและถ้าเอนทิตีมีRenderableAttributeตั้งค่าเป็นจริงแล้วRenderer(คลาสไม่เกี่ยวข้องกับ ส่วนประกอบ แต่สำหรับเครื่องยนต์เอง) ควรวาดลงบนหน้าจอหรือไม่)

แต่แล้วพฤติกรรม / องค์ประกอบล่ะ? สมมติว่าฉันมีระดับและในระดับที่มีEntity button, และEntity doors Entity playerเมื่อผู้เล่นชนกับปุ่ม (มันเป็นปุ่มตั้งพื้นซึ่งถูกสลับด้วยแรงกด) มันจะถูกกด เมื่อกดปุ่มมันจะเปิดประตู ทีนี้ทำอย่างไรดี?

ฉันคิดอะไรแบบนี้ขึ้นมา: ผู้เล่นได้รับCollisionBehaviourซึ่งตรวจสอบว่าผู้เล่นชนกับบางสิ่งหรือไม่ หากเขาชนกับปุ่มมันจะส่ง a CollisionMessageไปยังbuttonเอนทิตี ข้อความจะมีข้อมูลที่จำเป็นทั้งหมด: ใครเป็นผู้ชนกับปุ่ม ปุ่มได้มีToggleableBehaviourCollisionMessageซึ่งจะได้รับ มันจะตรวจสอบว่าใครทำมันชนกันและถ้าน้ำหนักของเอนทิตีนั้นใหญ่พอที่จะสลับปุ่มปุ่มจะถูกสลับ ตอนนี้มันตั้งค่าToggledAttributeของปุ่มเป็นจริง เอาล่ะ แต่ตอนนี้เป็นอย่างไร

ปุ่มควรส่งข้อความอื่นไปยังวัตถุอื่นทั้งหมดเพื่อบอกพวกเขาว่ามันถูกสลับ? ฉันคิดว่าถ้าฉันทำทุกอย่างเช่นนี้ฉันจะมีข้อความนับพันและมันก็ยุ่งเหยิงไปหมด ดังนั้นนี่อาจจะดีกว่า: ประตูตรวจสอบอย่างต่อเนื่องว่ามีการกดปุ่มที่เชื่อมโยงกับพวกเขาหรือไม่และเปลี่ยนแปลงOpenedAttributeตามนั้น แต่นั่นหมายความว่าOnUpdate()วิธีการของประตูจะทำอะไรบางอย่างอยู่ตลอดเวลา (มันเป็นปัญหาจริงๆเหรอ?)

และปัญหาที่สอง: ถ้าฉันมีปุ่มหลายชนิด หนึ่งถูกกดด้วยแรงดันอันที่สองถูกสลับโดยการยิงที่หนึ่งที่สามคือการสลับถ้าน้ำไหลบน ฯลฯ ซึ่งหมายความว่าฉันจะต้องมีพฤติกรรมที่แตกต่างกันดังนี้:

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

เกมนี้ใช้งานได้จริงหรือฉันแค่โง่งั้นเหรอ? บางทีฉันอาจจะมีเพียงหนึ่งToggleableBehaviourและมันจะประพฤติตามButtonTypeAttribute ดังนั้นถ้ามันคือButtonType.Pressureมันทำอย่างนี้ถ้ามันเป็นButtonType.Shotมันทำอย่างอื่น ...

แล้วฉันต้องการอะไร ฉันอยากจะถามคุณว่าฉันทำถูกหรือว่าฉันโง่และฉันไม่เข้าใจประเด็นขององค์ประกอบ ฉันไม่พบตัวอย่างที่ดีเกี่ยวกับองค์ประกอบที่ใช้งานได้จริงในเกมฉันพบบทเรียนบางบทที่อธิบายถึงวิธีการสร้างระบบคอมโพเนนต์ แต่ไม่ใช่วิธีการใช้งาน

คำตอบ:


46

ส่วนประกอบนั้นยอดเยี่ยม แต่อาจต้องใช้เวลาพอสมควรในการหาวิธีแก้ปัญหาที่รู้สึกดีกับคุณ ไม่ต้องกังวลคุณจะไปถึงที่นั่น :)

การจัดองค์ประกอบ

ฉันกำลังพูดอยู่อย่างถูกต้อง ฉันจะพยายามอธิบายวิธีการแก้ปัญหาในสิ่งที่ตรงกันข้ามเริ่มต้นที่ประตูและลงท้ายด้วยสวิทช์ การนำไปใช้ของฉันทำให้เกิดเหตุการณ์ที่หนักหน่วง ด้านล่างฉันอธิบายถึงวิธีที่คุณสามารถใช้กิจกรรมได้อย่างมีประสิทธิภาพมากขึ้นเพื่อไม่ให้เกิดปัญหา

หากคุณมีกลไกสำหรับเชื่อมต่อเอนทิตีระหว่างกันฉันจะให้สวิตช์แจ้งไปที่ประตูที่กดไว้โดยตรงจากนั้นประตูสามารถตัดสินใจได้ว่าจะทำอย่างไร

หากคุณไม่สามารถเชื่อมโยงเอนทิตี้ของคุณโซลูชันของคุณใกล้เคียงกับสิ่งที่ฉันทำ ฉันมีประตูฟังเหตุการณ์ทั่วไป ( SwitchActivatedEventอาจ) เมื่อสวิตช์เปิดใช้งานพวกเขาโพสต์เหตุการณ์นี้

หากคุณมีมากกว่าหนึ่งชนิดของสวิทช์, ฉันมีPressureToggle, WaterToggleและShotToggleพฤติกรรมเกินไป แต่ผมไม่แน่ใจว่าฐานToggleableBehaviourเป็นสิ่งที่ดีใด ๆ ดังนั้นฉันจะไปกับที่ (เว้นแต่ของหลักสูตรคุณมีดี เหตุผลในการรักษา)

Behaviour -> ToggleOnPressureBehaviour
          -> ToggleOnShotBehaviour
          -> ToggleOnWaterBehaviour

การจัดการเหตุการณ์ที่มีประสิทธิภาพ

สำหรับความกังวลที่มีเหตุการณ์มากเกินไปที่บินไปรอบ ๆ มีสิ่งหนึ่งที่คุณสามารถทำได้ แทนที่จะแจ้งให้ทุกองค์ประกอบได้รับทราบทุกเหตุการณ์ที่เกิดขึ้นจากนั้นให้ตรวจสอบองค์ประกอบว่าเป็นประเภทที่เหมาะสมของกิจกรรมหรือไม่นี่เป็นกลไกที่แตกต่างกัน ...

คุณสามารถมีEventDispatcherกับsubscribeวิธีการที่มีลักษณะบางอย่างเช่นนี้ (pseudocode):

EventDispatcher.subscribe(event_type, function)

จากนั้นเมื่อคุณโพสต์เหตุการณ์ดิสแพตเชอร์จะตรวจสอบประเภทของมันและจะแจ้งให้ฟังก์ชั่นเหล่านั้นที่สมัครเป็นสมาชิกของเหตุการณ์นั้น ๆ เท่านั้น คุณสามารถใช้สิ่งนี้เป็นแผนที่ที่เชื่อมโยงประเภทของเหตุการณ์กับรายการฟังก์ชั่น

ด้วยวิธีนี้ระบบมีประสิทธิภาพมากขึ้นอย่างมีนัยสำคัญ: มีการเรียกใช้ฟังก์ชั่นต่อเหตุการณ์น้อยลงและส่วนประกอบสามารถมั่นใจได้ว่าพวกเขาได้รับประเภทของเหตุการณ์ที่ถูกต้องและไม่ต้องตรวจสอบซ้ำ

ฉันโพสต์การใช้งานง่าย ๆ เมื่อไม่นานมานี้บน StackOverflow มันเขียนไว้ใน Python แต่ก็อาจช่วยคุณได้:
https://stackoverflow.com/a/7294148/627005

การนำไปใช้นั้นค่อนข้างทั่วไป: มันทำงานได้กับทุกชนิดของฟังก์ชั่นไม่เพียง แต่ฟังก์ชั่นจากส่วนประกอบ หากคุณไม่ต้องการแทนที่functionคุณสามารถมีbehaviorพารามิเตอร์ในsubscribeวิธีการของคุณ- ตัวอย่างพฤติกรรมที่ต้องได้รับการแจ้งเตือน

คุณสมบัติและพฤติกรรม

ฉันมาใช้คุณสมบัติและพฤติกรรมของตัวเองแทนองค์ประกอบเก่าธรรมดา อย่างไรก็ตามจากคำอธิบายของคุณว่าคุณจะใช้ระบบอย่างไรในเกมฝ่าวงล้อมฉันคิดว่าคุณทำมากเกินไป

ฉันใช้คุณลักษณะเฉพาะเมื่อพฤติกรรมสองอย่างจำเป็นต้องเข้าถึงข้อมูลเดียวกัน แอ็ตทริบิวต์ช่วยแยกพฤติกรรมและการพึ่งพาระหว่างส่วนประกอบ (ไม่ว่าจะเป็นแอตทริบิวต์หรือพฤติกรรม) ไม่พันกันเพราะพวกเขาปฏิบัติตามกฎที่ง่ายและชัดเจนมาก:

  • แอตทริบิวต์ไม่ใช้องค์ประกอบอื่น ๆ (ไม่ว่าจะเป็นคุณลักษณะหรือพฤติกรรมอื่น ๆ ) แต่ก็ไม่เพียงพอ

  • พฤติกรรมไม่ใช้หรือรู้เกี่ยวกับพฤติกรรมอื่น ๆ พวกเขารู้เกี่ยวกับคุณลักษณะบางอย่างเท่านั้น(สิ่งที่พวกเขาต้องการอย่างเคร่งครัด)

เมื่อข้อมูลบางอย่างต้องการเพียงพฤติกรรมเดียวเท่านั้นฉันไม่เห็นเหตุผลที่จะใส่ข้อมูลไว้ในแอตทริบิวต์ฉันปล่อยให้พฤติกรรมนั้นเก็บไว้


@ ความคิดเห็นของ heishe

ปัญหานั้นจะเกิดขึ้นกับองค์ประกอบปกติหรือไม่

อย่างไรก็ตามผมไม่ได้มีการตรวจสอบชนิดของเหตุการณ์เพราะฟังก์ชั่นทุกมั่นใจว่าจะได้รับประเภทสิทธิของเหตุการณ์เสมอ

นอกจากนี้การพึ่งพาของพฤติกรรม (เช่นคุณลักษณะที่ต้องการ) ได้รับการแก้ไขในการก่อสร้างดังนั้นคุณไม่ต้องไปหาแอตทริบิวต์ทุกครั้งในการอัปเดตทุกครั้ง

และสุดท้ายฉันใช้ Python สำหรับรหัสตรรกะของเกมของฉัน (เอ็นจินอยู่ใน C ++) ดังนั้นจึงไม่จำเป็นต้องทำการแคสต์ Python ทำการพิมพ์ด้วยเป็ดและทุกอย่างทำงานได้ดี แต่ถึงแม้ว่าฉันไม่ได้ใช้ภาษากับการพิมพ์เป็ด แต่ฉันก็ทำเช่นนี้ (ตัวอย่างง่าย ๆ ):

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

แอ็ตทริบิวต์ไม่มีupdateฟังก์ชั่นใด ๆ- ไม่จำเป็นต้องมีวัตถุประสงค์เพื่อเก็บข้อมูลไม่ใช่เพื่อใช้ตรรกะของเกมที่ซับซ้อน

คุณยังสามารถให้คุณสมบัติของคุณดำเนินการตรรกะง่ายๆ ในตัวอย่างนี้HealthAttributeอาจจะมั่นใจได้ว่า0 <= value <= max_healthเป็นจริงเสมอ นอกจากนี้ยังสามารถส่งHealthCriticalEventไปยังส่วนประกอบอื่น ๆ ของเอนทิตีเดียวกันเมื่อลดลงต่ำกว่าพูดร้อยละ 25 แต่มันไม่สามารถใช้ตรรกะที่ซับซ้อนกว่านั้นได้


ตัวอย่างของคลาสแอตทริบิวต์:

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

ขอขอบคุณ! นี่เป็นสิ่งที่ฉันต้องการอย่างแน่นอน ฉันชอบความคิดของคุณเกี่ยวกับ EventDispatcher ดีกว่าการส่งข้อความธรรมดาไปยังเอนทิตีทั้งหมด ตอนนี้ถึงสิ่งสุดท้ายที่คุณบอกฉัน: โดยพื้นฐานแล้วคุณบอกว่าสุขภาพและความเสียหายผลกระทบไม่จำเป็นต้องเป็นคุณลักษณะในตัวอย่างนี้ ดังนั้นแทนที่จะเป็นคุณสมบัติพวกมันเป็นเพียงตัวแปรส่วนตัวของพฤติกรรม? ซึ่งหมายความว่า "DamageImpact" จะถูกส่งผ่านเหตุการณ์หรือไม่ ตัวอย่างเช่น EventArgs.DamageImpact? ฟังดูดี ... แต่ถ้าฉันต้องการให้อิฐเปลี่ยนสีตามสุขภาพแล้วสุขภาพจะต้องเป็นคุณสมบัติใช่ไหม? ขอขอบคุณ!
TomsonTom

2
@TomsonTom ใช่แล้ว การมีเหตุการณ์เก็บข้อมูลใด ๆ ที่ผู้ฟังจำเป็นต้องรู้เป็นวิธีแก้ปัญหาที่ดีมาก
พอล Manta

3
นี่เป็นคำตอบที่ยอดเยี่ยม! (เช่นเดียวกับไฟล์ pdf ของคุณ) - เมื่อคุณมีโอกาสคุณสามารถอธิบายรายละเอียดเล็กน้อยเกี่ยวกับวิธีจัดการกับการแสดงผลด้วยระบบนี้ได้หรือไม่? โมเดลพฤติกรรม / พฤติกรรมนี้ใหม่สำหรับฉัน แต่น่าสนใจมาก
ไมเคิล

1
@TomsonTom เกี่ยวกับการแสดงผลให้ดูคำตอบที่ฉันให้กับไมเคิล สำหรับการชนฉันใช้ทางลัด ฉันใช้ห้องสมุดชื่อ Box2D ซึ่งใช้งานง่ายและจัดการการชนได้ดีกว่าที่ฉันสามารถทำได้ แต่ฉันไม่ได้ใช้ห้องสมุดโดยตรงในรหัสตรรกะเกมของฉัน ทุกคนEntityมีสิ่งEntityBodyซึ่งแยกส่วนที่น่าเกลียดออกไป พฤติกรรมสามารถอ่านตำแหน่งจากEntityBodyบังคับใช้กับมันใช้ข้อต่อและมอเตอร์ที่ร่างกายมีเป็นต้นการจำลองทางฟิสิกส์ที่มีความเที่ยงตรงสูงเช่น Box2D นำมาซึ่งความท้าทายใหม่ ๆ แต่พวกมันค่อนข้างสนุก
พอล Manta

1
@thelinuxlich ดังนั้นคุณคือผู้พัฒนาของอาร์ทิมิส! : D ฉันได้เห็นComponent/ Systemโครงการอ้างอิงสองสามครั้งบนกระดาน การใช้งานของเรามีความคล้ายคลึงกันค่อนข้างน้อย
พอล Manta
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.